zefyr

attributes.dart 15KB

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