zefyr

attributes.dart 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. // Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file
  2. // for details. All rights reserved. Use of this source code is governed by a
  3. // BSD-style license that can be found in the LICENSE file.
  4. import 'package:collection/collection.dart';
  5. import 'package:quiver_hashcode/hashcode.dart';
  6. /// Scope of a style attribute, defines context in which an attribute can be
  7. /// applied.
  8. enum NotusAttributeScope {
  9. /// Inline-scoped attributes are applicable to all characters within a line.
  10. ///
  11. /// Inline attributes cannot be applied to the line itself.
  12. inline,
  13. /// Line-scoped attributes are only applicable to a line of text as a whole.
  14. ///
  15. /// Line attributes do not have any effect on any character within the line.
  16. line,
  17. }
  18. /// Interface for objects which provide access to an attribute key.
  19. ///
  20. /// Implemented by [NotusAttribute] and [NotusAttributeBuilder].
  21. abstract class NotusAttributeKey<T> {
  22. /// Unique key of this attribute.
  23. String get key;
  24. }
  25. /// Builder for style attributes.
  26. ///
  27. /// Useful in scenarios when an attribute value is not known upfront, for
  28. /// instance, link attribute.
  29. ///
  30. /// See also:
  31. /// * [LinkAttributeBuilder]
  32. /// * [BlockAttributeBuilder]
  33. /// * [HeadingAttributeBuilder]
  34. abstract class NotusAttributeBuilder<T> implements NotusAttributeKey<T> {
  35. const NotusAttributeBuilder._(this.key, this.scope);
  36. final String key;
  37. final NotusAttributeScope scope;
  38. NotusAttribute<T> get unset => new NotusAttribute<T>._(key, scope, null);
  39. NotusAttribute<T> withValue(T value) =>
  40. new NotusAttribute<T>._(key, scope, value);
  41. }
  42. /// Style attribute applicable to a segment of a Notus document.
  43. ///
  44. /// All supported attributes are available via static fields on this class.
  45. /// Here is an example of applying styles to a document:
  46. ///
  47. /// void makeItPretty(Notus document) {
  48. /// // Format 5 characters at position 0 as bold
  49. /// document.format(0, 5, NotusAttribute.bold);
  50. /// // Similarly for italic
  51. /// document.format(0, 5, NotusAttribute.italic);
  52. /// // Format first line as a heading (h1)
  53. /// // Note that there is no need to specify character range of the whole
  54. /// // line. Simply set index position to anywhere within the line and
  55. /// // length to 0.
  56. /// document.format(0, 0, NotusAttribute.h1);
  57. /// }
  58. ///
  59. /// List of supported attributes:
  60. ///
  61. /// * [NotusAttribute.bold]
  62. /// * [NotusAttribute.italic]
  63. /// * [NotusAttribute.link]
  64. /// * [NotusAttribute.heading]
  65. /// * [NotusAttribute.block]
  66. class NotusAttribute<T> implements NotusAttributeBuilder<T> {
  67. static final Map<String, NotusAttributeBuilder> _registry = {
  68. NotusAttribute.bold.key: NotusAttribute.bold,
  69. NotusAttribute.italic.key: NotusAttribute.italic,
  70. NotusAttribute.link.key: NotusAttribute.link,
  71. NotusAttribute.heading.key: NotusAttribute.heading,
  72. NotusAttribute.block.key: NotusAttribute.block,
  73. NotusAttribute.embed.key: NotusAttribute.embed,
  74. };
  75. // Inline attributes
  76. /// Bold style attribute.
  77. static const bold = const _BoldAttribute();
  78. /// Italic style attribute.
  79. static const italic = const _ItalicAttribute();
  80. /// Link style attribute.
  81. static const link = const LinkAttributeBuilder._();
  82. // Line attributes
  83. /// Heading style attribute.
  84. static const heading = const HeadingAttributeBuilder._();
  85. /// Alias for [NotusAttribute.heading.level1].
  86. static NotusAttribute<int> get h1 => heading.level1;
  87. /// Alias for [NotusAttribute.heading.level2].
  88. static NotusAttribute<int> get h2 => heading.level2;
  89. /// Alias for [NotusAttribute.heading.level3].
  90. static NotusAttribute<int> get h3 => heading.level3;
  91. /// Block attribute
  92. static const block = const BlockAttributeBuilder._();
  93. /// Alias for [NotusAttribute.block.bulletList].
  94. static NotusAttribute<String> get ul => block.bulletList;
  95. /// Alias for [NotusAttribute.block.numberList].
  96. static NotusAttribute<String> get ol => block.numberList;
  97. /// Alias for [NotusAttribute.block.quote].
  98. static NotusAttribute<String> get bq => block.quote;
  99. /// Alias for [NotusAttribute.block.code].
  100. static NotusAttribute<String> get code => block.code;
  101. /// Embed style attribute.
  102. static const embed = const EmbedAttributeBuilder._();
  103. static NotusAttribute _fromKeyValue(String key, dynamic value) {
  104. if (!_registry.containsKey(key))
  105. throw new ArgumentError.value(
  106. key, 'No attribute with key "$key" registered.');
  107. final builder = _registry[key];
  108. return builder.withValue(value);
  109. }
  110. const NotusAttribute._(this.key, this.scope, this.value);
  111. /// Unique key of this attribute.
  112. final String key;
  113. /// Scope of this attribute.
  114. final NotusAttributeScope scope;
  115. /// Value of this attribute.
  116. ///
  117. /// If value is `null` then this attribute represents a transient action
  118. /// of removing associated style and is never persisted in a resulting
  119. /// document.
  120. ///
  121. /// See also [unset], [NotusStyle.merge] and [NotusStyle.put]
  122. /// for details.
  123. final T value;
  124. /// Returns special "unset" version of this attribute.
  125. ///
  126. /// Unset attribute's [value] is always `null`.
  127. ///
  128. /// When composed into a rich text document, unset attributes remove
  129. /// associated style.
  130. NotusAttribute<T> get unset => new NotusAttribute<T>._(key, scope, null);
  131. /// Returns `true` if this attribute is an unset attribute.
  132. bool get isUnset => value == null;
  133. /// Returns `true` if this is an inline-scoped attribute.
  134. bool get isInline => scope == NotusAttributeScope.inline;
  135. NotusAttribute<T> withValue(T value) =>
  136. new NotusAttribute<T>._(key, scope, value);
  137. @override
  138. bool operator ==(Object other) {
  139. if (identical(this, other)) return true;
  140. if (other is! NotusAttribute<T>) return false;
  141. NotusAttribute<T> typedOther = other;
  142. return key == typedOther.key &&
  143. scope == typedOther.scope &&
  144. value == typedOther.value;
  145. }
  146. @override
  147. int get hashCode => hash3(key, scope, value);
  148. @override
  149. String toString() => '$key: $value';
  150. Map<String, dynamic> toJson() => <String, dynamic>{key: value};
  151. }
  152. /// Collection of style attributes.
  153. class NotusStyle {
  154. NotusStyle._(this._data);
  155. final Map<String, NotusAttribute> _data;
  156. static NotusStyle fromJson(Map<String, dynamic> data) {
  157. if (data == null) return new NotusStyle();
  158. final result = data.map((String key, dynamic value) {
  159. var attr = NotusAttribute._fromKeyValue(key, value);
  160. return new MapEntry<String, NotusAttribute>(key, attr);
  161. });
  162. return new NotusStyle._(result);
  163. }
  164. NotusStyle() : _data = new Map<String, NotusAttribute>();
  165. /// Returns `true` if this attribute set is empty.
  166. bool get isEmpty => _data.isEmpty;
  167. /// Returns `true` if this attribute set is note empty.
  168. bool get isNotEmpty => _data.isNotEmpty;
  169. /// Returns `true` if this style is not empty and contains only inline-scoped
  170. /// attributes and is not empty.
  171. bool get isInline => isNotEmpty && values.every((item) => item.isInline);
  172. /// Checks that this style has only one attribute, and returns that attribute.
  173. NotusAttribute get single => _data.values.single;
  174. /// Returns `true` if attribute with [key] is present in this set.
  175. ///
  176. /// Only checks for presence of specified [key] regardless of the associated
  177. /// value.
  178. ///
  179. /// To test if this set contains an attribute with specific value consider
  180. /// using [containsSame].
  181. bool contains(NotusAttributeKey key) => _data.containsKey(key.key);
  182. /// Returns `true` if this set contains attribute with the same value as
  183. /// [attribute].
  184. bool containsSame(NotusAttribute attribute) {
  185. assert(attribute != null);
  186. return get<dynamic>(attribute) == attribute;
  187. }
  188. /// Returns value of specified attribute [key] in this set.
  189. T value<T>(NotusAttributeKey<T> key) => get(key).value;
  190. /// Returns [NotusAttribute] from this set by specified [key].
  191. NotusAttribute<T> get<T>(NotusAttributeKey<T> key) =>
  192. _data[key.key] as NotusAttribute<T>;
  193. /// Returns collection of all attribute keys in this set.
  194. Iterable<String> get keys => _data.keys;
  195. /// Returns collection of all attributes in this set.
  196. Iterable<NotusAttribute> get values => _data.values;
  197. /// Puts [attribute] into this attribute set and returns result as a new set.
  198. NotusStyle put(NotusAttribute attribute) {
  199. final result = new Map<String, NotusAttribute>.from(_data);
  200. result[attribute.key] = attribute;
  201. return new NotusStyle._(result);
  202. }
  203. /// Merges this attribute set with [attribute] and returns result as a new
  204. /// attribute set.
  205. ///
  206. /// Performs compaction if [attribute] is an "unset" value, e.g. removes
  207. /// corresponding attribute from this set completely.
  208. ///
  209. /// See also [put] method which does not perform compaction and allows
  210. /// constructing styles with "unset" values.
  211. NotusStyle merge(NotusAttribute attribute) {
  212. final merged = new Map<String, NotusAttribute>.from(_data);
  213. if (attribute.isUnset) {
  214. merged.remove(attribute.key);
  215. } else {
  216. merged[attribute.key] = attribute;
  217. }
  218. return new NotusStyle._(merged);
  219. }
  220. /// Merges all attributes from [other] into this style and returns result
  221. /// as a new instance of [NotusStyle].
  222. NotusStyle mergeAll(NotusStyle other) {
  223. var result = new NotusStyle._(_data);
  224. for (var value in other.values) {
  225. result = result.merge(value);
  226. }
  227. return result;
  228. }
  229. /// Removes [attributes] from this style and returns new instance of
  230. /// [NotusStyle] containing result.
  231. NotusStyle removeAll(Iterable<NotusAttribute> attributes) {
  232. final merged = new Map<String, NotusAttribute>.from(_data);
  233. attributes.map((item) => item.key).forEach(merged.remove);
  234. return new NotusStyle._(merged);
  235. }
  236. /// Returns JSON-serializable representation of this style.
  237. Map<String, dynamic> toJson() => _data.isEmpty
  238. ? null
  239. : _data.map<String, dynamic>((String _, NotusAttribute value) =>
  240. new MapEntry<String, dynamic>(value.key, value.value));
  241. @override
  242. bool operator ==(Object other) {
  243. if (identical(this, other)) return true;
  244. if (other is! NotusStyle) return false;
  245. NotusStyle typedOther = other;
  246. final eq = const MapEquality<String, NotusAttribute>();
  247. return eq.equals(_data, typedOther._data);
  248. }
  249. @override
  250. int get hashCode {
  251. final hashes = _data.entries.map((entry) => hash2(entry.key, entry.value));
  252. return hashObjects(hashes);
  253. }
  254. @override
  255. String toString() => "{${_data.values.join(', ')}}";
  256. }
  257. /// Applies bold style to a text segment.
  258. class _BoldAttribute extends NotusAttribute<bool> {
  259. const _BoldAttribute() : super._('b', NotusAttributeScope.inline, true);
  260. }
  261. /// Applies italic style to a text segment.
  262. class _ItalicAttribute extends NotusAttribute<bool> {
  263. const _ItalicAttribute() : super._('i', NotusAttributeScope.inline, true);
  264. }
  265. /// Builder for link attribute values.
  266. ///
  267. /// There is no need to use this class directly, consider using
  268. /// [NotusAttribute.link] instead.
  269. class LinkAttributeBuilder extends NotusAttributeBuilder<String> {
  270. static const _kLink = 'a';
  271. const LinkAttributeBuilder._() : super._(_kLink, NotusAttributeScope.inline);
  272. /// Creates a link attribute with specified link [value].
  273. NotusAttribute<String> fromString(String value) =>
  274. new NotusAttribute<String>._(key, scope, value);
  275. }
  276. /// Builder for heading attribute styles.
  277. ///
  278. /// There is no need to use this class directly, consider using
  279. /// [NotusAttribute.heading] instead.
  280. class HeadingAttributeBuilder extends NotusAttributeBuilder<int> {
  281. static const _kHeading = 'heading';
  282. const HeadingAttributeBuilder._()
  283. : super._(_kHeading, NotusAttributeScope.line);
  284. /// Level 1 heading, equivalent of `H1` in HTML.
  285. NotusAttribute<int> get level1 => new NotusAttribute<int>._(key, scope, 1);
  286. /// Level 2 heading, equivalent of `H2` in HTML.
  287. NotusAttribute<int> get level2 => new NotusAttribute<int>._(key, scope, 2);
  288. /// Level 3 heading, equivalent of `H3` in HTML.
  289. NotusAttribute<int> get level3 => new NotusAttribute<int>._(key, scope, 3);
  290. }
  291. /// Builder for block attribute styles (number/bullet lists, code and quote).
  292. ///
  293. /// There is no need to use this class directly, consider using
  294. /// [NotusAttribute.block] instead.
  295. class BlockAttributeBuilder extends NotusAttributeBuilder<String> {
  296. static const _kBlock = 'block';
  297. const BlockAttributeBuilder._() : super._(_kBlock, NotusAttributeScope.line);
  298. /// Formats a block of lines as a bullet list.
  299. NotusAttribute<String> get bulletList =>
  300. new NotusAttribute<String>._(key, scope, 'ul');
  301. /// Formats a block of lines as a number list.
  302. NotusAttribute<String> get numberList =>
  303. new NotusAttribute<String>._(key, scope, 'ol');
  304. /// Formats a block of lines as a code snippet, using monospace font.
  305. NotusAttribute<String> get code =>
  306. new NotusAttribute<String>._(key, scope, 'code');
  307. /// Formats a block of lines as a quote.
  308. NotusAttribute<String> get quote =>
  309. new NotusAttribute<String>._(key, scope, 'quote');
  310. }
  311. class EmbedAttributeBuilder
  312. extends NotusAttributeBuilder<Map<String, dynamic>> {
  313. const EmbedAttributeBuilder._()
  314. : super._(EmbedAttribute._kEmbed, NotusAttributeScope.inline);
  315. NotusAttribute<Map<String, dynamic>> get horizontalRule =>
  316. EmbedAttribute.horizontalRule();
  317. NotusAttribute<Map<String, dynamic>> image(String source) =>
  318. EmbedAttribute.image(source);
  319. @override
  320. NotusAttribute<Map<String, dynamic>> get unset => EmbedAttribute._(null);
  321. NotusAttribute<Map<String, dynamic>> withValue(Map<String, dynamic> value) =>
  322. EmbedAttribute._(value);
  323. }
  324. /// Type of embedded content.
  325. enum EmbedType { horizontalRule, image }
  326. class EmbedAttribute extends NotusAttribute<Map<String, dynamic>> {
  327. static const _kValueEquality = const MapEquality<String, dynamic>();
  328. static const _kEmbed = 'embed';
  329. static const _kHorizontalRuleEmbed = 'hr';
  330. static const _kImageEmbed = 'image';
  331. EmbedAttribute._(Map<String, dynamic> value)
  332. : super._(_kEmbed, NotusAttributeScope.inline, value);
  333. EmbedAttribute.horizontalRule()
  334. : this._(<String, dynamic>{'type': _kHorizontalRuleEmbed});
  335. EmbedAttribute.image(String source)
  336. : this._(<String, dynamic>{'type': _kImageEmbed, 'source': source});
  337. /// Type of this embed.
  338. EmbedType get type {
  339. if (value['type'] == _kHorizontalRuleEmbed) return EmbedType.horizontalRule;
  340. if (value['type'] == _kImageEmbed) return EmbedType.image;
  341. assert(false, 'Unknown embed attribute value $value.');
  342. return null;
  343. }
  344. @override
  345. NotusAttribute<Map<String, dynamic>> get unset => new EmbedAttribute._(null);
  346. @override
  347. bool operator ==(other) {
  348. if (identical(this, other)) return true;
  349. if (other is! EmbedAttribute) return false;
  350. EmbedAttribute typedOther = other;
  351. return key == typedOther.key &&
  352. scope == typedOther.scope &&
  353. _kValueEquality.equals(value, typedOther.value);
  354. }
  355. @override
  356. int get hashCode {
  357. final objects = [key, scope];
  358. if (value != null) {
  359. final valueHashes =
  360. value.entries.map((entry) => hash2(entry.key, entry.value));
  361. objects.addAll(valueHashes);
  362. } else {
  363. objects.add(value);
  364. }
  365. return hashObjects(objects);
  366. }
  367. }