Add support for github task lists (aka checkboxes) actually fixes dart-lang/markdown#268 add color swatch syntax support (dart-lang/markdown#428)
* change source of emojis to github api
* add github checkboxes and color syntax
* update change log
* update extension sets
* correct change log
* clean up, rename color_syntax.dart to color_swatch_syntax.dart
* remove unintentional emoji changes
* restore to master version
* restore to master version
* move ColorSwatchSyntax to gitHubWeb extension set
* add exports for new syntaxes
* made requested changes
* clean up so variables and comments as code review suggested
diff --git a/pkgs/markdown/CHANGELOG.md b/pkgs/markdown/CHANGELOG.md
index 4c8ad47..1172756 100644
--- a/pkgs/markdown/CHANGELOG.md
+++ b/pkgs/markdown/CHANGELOG.md
@@ -1,6 +1,14 @@
## 6.0.0-dev
-* Change emoji list do be derived from the GitHub API. The only two emoji that visually change are `:cricket:` and `:beetle:`. There are alternate emoji `:cricket_game:` and `:lady_beetle:` which can be used to access the previous emoji. `update_github_emoji.dart` now pulls all emoji info directly from GitHub API and as a result we have now support the entire GitHub emoji set (excluding the 19 custom GitHub specific emoji which have no Unicode support).
+* Add support to GFM extension for GitHub task lists (aka checkboxes). These
+ are only active in the `gitHubFlavored` and `gitHubWeb` extension sets.
+* Add support for `#ff0000` color swatches.
+* Change emoji list do be derived from the GitHub API. The only two emoji that
+ visually change are `:cricket:` and `:beetle:`. There are alternate emoji
+ `:cricket_game:` and `:lady_beetle:` which can be used to access the previous
+ emoji. `update_github_emoji.dart` now pulls all emoji info directly from
+ GitHub API and as a result we have now support the entire GitHub emoji set
+ (excluding the 19 custom GitHub specific emoji which have no Unicode support).
* **Breaking change**: The `TagSyntax` is _deprecated_.
* Add new syntax `DelimiterSyntax`.
* **Breaking change**: `StrikethroughSyntax` now extends `DelimiterSyntax`
diff --git a/pkgs/markdown/example/index.html b/pkgs/markdown/example/index.html
index 97515c6..82650a6 100644
--- a/pkgs/markdown/example/index.html
+++ b/pkgs/markdown/example/index.html
@@ -10,6 +10,34 @@
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/languages/dart.min.js" integrity="sha512-14QR6tzX5xTNeMJKXzSK+xCquDvtNEr1jM5NlKy/149BBY50Kv70qqxHtzo6zClbtc1gIG7G0CGWXuMgPIMt0g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script defer src="app.dart.js"></script>
<title>Dart Markdown Live Editor</title>
+ <style>
+.gfm-color_chip span {
+ display: inline-block;
+ line-height: 1;
+ margin: 0 0 2px 4px;
+ vertical-align: middle;
+ border-radius: 3px;
+ width: 0.9em;
+ height: 0.9em;
+ background: #fff;
+}
+
+code.gfm-color_chip {
+ padding: 2px 4px;
+ color: #1f1f1f;
+ background-color: #f0f0f0;
+ border-radius: 4px;
+}
+
+ul.contains-task-list, ol.contains-task-list {
+ margin-left: -1em;
+ list-style-type: none;
+}
+
+.task-list-item {
+ list-style-type: none;
+}
+ </style>
</head>
<body>
<div class="container">
diff --git a/pkgs/markdown/lib/markdown.dart b/pkgs/markdown/lib/markdown.dart
index 205ba38..6155242 100644
--- a/pkgs/markdown/lib/markdown.dart
+++ b/pkgs/markdown/lib/markdown.dart
@@ -53,12 +53,14 @@
export 'src/block_syntaxes/list_syntax.dart';
export 'src/block_syntaxes/long_block_html_syntax.dart';
export 'src/block_syntaxes/ordered_list_syntax.dart';
+export 'src/block_syntaxes/ordered_list_with_checkbox_syntax.dart';
export 'src/block_syntaxes/other_tag_block_html_syntax.dart';
export 'src/block_syntaxes/paragraph_syntax.dart';
export 'src/block_syntaxes/setext_header_syntax.dart';
export 'src/block_syntaxes/setext_header_with_id_syntax.dart';
export 'src/block_syntaxes/table_syntax.dart';
export 'src/block_syntaxes/unordered_list_syntax.dart';
+export 'src/block_syntaxes/unordered_list_with_checkbox_syntax.dart';
export 'src/document.dart';
export 'src/emojis.dart';
export 'src/extension_set.dart';
@@ -67,6 +69,7 @@
export 'src/inline_syntaxes/autolink_extension_syntax.dart';
export 'src/inline_syntaxes/autolink_syntax.dart';
export 'src/inline_syntaxes/code_syntax.dart';
+export 'src/inline_syntaxes/color_swatch_syntax.dart';
export 'src/inline_syntaxes/delimiter_syntax.dart';
export 'src/inline_syntaxes/email_autolink_syntax.dart';
export 'src/inline_syntaxes/emoji_syntax.dart';
diff --git a/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart
index 06b1cf9..483a0d8 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart
@@ -14,6 +14,14 @@
final List<String> lines;
}
+/// Invisible string used to placehold for an *unchecked* CheckBox.
+/// The character is Unicode Zero Width Space (U+200B).
+const indicatorForUncheckedCheckBox = '\u{200B}';
+
+/// Invisible string used to placehold for a *checked* CheckBox.
+/// This is 2 Unicode Zero Width Space (U+200B) characters.
+const indicatorForCheckedCheckBox = '\u{200B}\u{200B}';
+
/// Base class for both ordered and unordered lists.
abstract class ListSyntax extends BlockSyntax {
@override
@@ -49,6 +57,8 @@
Node parse(BlockParser parser) {
final items = <ListItem>[];
var childLines = <String>[];
+ final isCheckboxListSubclass =
+ (listTag == 'ol_with_checkbox' || listTag == 'ul_with_checkbox');
void endItem() {
if (childLines.isNotEmpty) {
@@ -57,10 +67,10 @@
}
}
- late Match? match;
+ late Match? possibleMatch;
bool tryMatch(RegExp pattern) {
- match = pattern.firstMatch(parser.current);
- return match != null;
+ possibleMatch = pattern.firstMatch(parser.current);
+ return possibleMatch != null;
}
String? listMarker;
@@ -89,16 +99,43 @@
} else if (tryMatch(hrPattern)) {
// Horizontal rule takes precedence to a new list item.
break;
- } else if (tryMatch(ulPattern) || tryMatch(olPattern)) {
- final precedingWhitespace = match![1]!;
- final digits = match![2] ?? '';
+ } else if ((isCheckboxListSubclass &&
+ (tryMatch(ulWithCheckBoxPattern) ||
+ tryMatch(olWithCheckBoxPattern))) ||
+ tryMatch(ulPattern) ||
+ tryMatch(olPattern)) {
+ // We know we have a valid [possibleMatch] now, so capture it.
+ final successfulMatch = possibleMatch!;
+ // The checkbox "subclass" patterns ([ulWithCheckBoxPattern] and
+ // [olWithCheckBoxPattern]) have 2 extra capturing groups at the 5
+ // position to capture the checkbox. These shift the other groups
+ // forward by 2 slots.
+ final cbGroupOffset = isCheckboxListSubclass ? 2 : 0;
+ final precedingWhitespace = successfulMatch[1]!;
+ final digits = successfulMatch[2] ?? '';
if (startNumber == null && digits.isNotEmpty) {
startNumber = int.parse(digits);
}
- final marker = match![3]!;
- final firstWhitespace = match![5] ?? '';
- final restWhitespace = match![6] ?? '';
- final content = match![7] ?? '';
+ final marker = successfulMatch[3]!;
+ // [checkBoxIndicator] is always empty unless a checkbox was found.
+ String checkBoxIndicator = '';
+ if (isCheckboxListSubclass) {
+ // Look at checkbox capture group and get checkbox state.
+ // If we find a checked or unchecked checkbox then we will
+ // set [checkBoxIndicator] to one of our invisible
+ // codes that we can later detect to know if we need to insert
+ // a check or unchecked checkbox when we are inserting the
+ // listitem li node.
+ final String checkboxGroup = successfulMatch[5]!.toLowerCase();
+ if (checkboxGroup == '[ ]') {
+ checkBoxIndicator = indicatorForUncheckedCheckBox;
+ } else if (checkboxGroup == '[x]') {
+ checkBoxIndicator = indicatorForCheckedCheckBox;
+ }
+ }
+ final firstWhitespace = successfulMatch[5 + cbGroupOffset] ?? '';
+ final restWhitespace = successfulMatch[6 + cbGroupOffset] ?? '';
+ final content = successfulMatch[7 + cbGroupOffset] ?? '';
final isBlank = content.isEmpty;
if (listMarker != null && listMarker != marker) {
// Changing the bullet or ordered list delimiter starts a new list.
@@ -128,7 +165,7 @@
}
// End the current list item and start a new one.
endItem();
- childLines.add(restWhitespace + content);
+ childLines.add('$checkBoxIndicator$restWhitespace$content');
} else if (BlockSyntax.isAtBlockEnd(parser)) {
// Done with the list.
break;
@@ -156,7 +193,30 @@
for (final item in items) {
final itemParser = BlockParser(item.lines, parser.document);
final children = itemParser.parseLines();
- itemNodes.add(Element('li', children));
+ // If this is a checkbox sublass of ListSyntax then we must check
+ // for possible invisible checkbox placeholder characters at
+ // the start of first node's text to see if we need to insert a checkbox.
+ Element? checkboxToInsert;
+ if (isCheckboxListSubclass) {
+ if (children.isNotEmpty) {
+ if (children.first.textContent
+ .startsWith(indicatorForCheckedCheckBox)) {
+ checkboxToInsert = Element.withTag('input')
+ ..attributes['type'] = 'checkbox'
+ ..attributes['checked'] = 'true';
+ } else if (children.first.textContent
+ .startsWith(indicatorForUncheckedCheckBox)) {
+ checkboxToInsert = Element.withTag('input')
+ ..attributes['type'] = 'checkbox';
+ }
+ }
+ }
+ if (checkboxToInsert != null) {
+ itemNodes.add(Element('li', [checkboxToInsert, ...children])
+ ..attributes['class'] = 'task-list-item');
+ } else {
+ itemNodes.add(Element('li', children));
+ }
anyEmptyLinesBetweenBlocks =
anyEmptyLinesBetweenBlocks || itemParser.encounteredBlankLine;
}
@@ -182,7 +242,17 @@
}
}
- if (listTag == 'ol' && startNumber != 1) {
+ if (listTag == 'ol_with_checkbox') {
+ final olWithCheckbox = Element('ol', itemNodes)
+ ..attributes['class'] = 'contains-task-list';
+ if (startNumber != 1) {
+ olWithCheckbox.attributes['start'] = '$startNumber';
+ }
+ return olWithCheckbox;
+ } else if (listTag == 'ul_with_checkbox') {
+ return Element('ul', itemNodes)
+ ..attributes['class'] = 'contains-task-list';
+ } else if (listTag == 'ol' && startNumber != 1) {
return Element(listTag, itemNodes)..attributes['start'] = '$startNumber';
} else {
return Element(listTag, itemNodes);
diff --git a/pkgs/markdown/lib/src/block_syntaxes/ordered_list_with_checkbox_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/ordered_list_with_checkbox_syntax.dart
new file mode 100644
index 0000000..2f8a66a
--- /dev/null
+++ b/pkgs/markdown/lib/src/block_syntaxes/ordered_list_with_checkbox_syntax.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import '../patterns.dart';
+import 'list_syntax.dart';
+
+/// Parses ordered lists.
+class OrderedListWithCheckBoxSyntax extends ListSyntax {
+ @override
+ RegExp get pattern => olWithCheckBoxPattern;
+
+ @override
+ String get listTag => 'ol_with_checkbox';
+
+ const OrderedListWithCheckBoxSyntax();
+}
diff --git a/pkgs/markdown/lib/src/block_syntaxes/unordered_list_with_checkbox_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/unordered_list_with_checkbox_syntax.dart
new file mode 100644
index 0000000..795d8a7
--- /dev/null
+++ b/pkgs/markdown/lib/src/block_syntaxes/unordered_list_with_checkbox_syntax.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import '../patterns.dart';
+import 'list_syntax.dart';
+
+/// Parses unordered lists.
+class UnorderedListWithCheckBoxSyntax extends ListSyntax {
+ @override
+ RegExp get pattern => ulWithCheckBoxPattern;
+
+ @override
+ String get listTag => 'ul_with_checkbox';
+
+ const UnorderedListWithCheckBoxSyntax();
+}
diff --git a/pkgs/markdown/lib/src/extension_set.dart b/pkgs/markdown/lib/src/extension_set.dart
index c0e0b13..7f2be48 100644
--- a/pkgs/markdown/lib/src/extension_set.dart
+++ b/pkgs/markdown/lib/src/extension_set.dart
@@ -1,9 +1,13 @@
+import 'package:markdown/src/block_syntaxes/ordered_list_with_checkbox_syntax.dart';
+import 'package:markdown/src/block_syntaxes/unordered_list_with_checkbox_syntax.dart';
+
import 'block_syntaxes/block_syntax.dart';
import 'block_syntaxes/fenced_code_block_syntax.dart';
import 'block_syntaxes/header_with_id_syntax.dart';
import 'block_syntaxes/setext_header_with_id_syntax.dart';
import 'block_syntaxes/table_syntax.dart';
import 'inline_syntaxes/autolink_extension_syntax.dart';
+import 'inline_syntaxes/color_swatch_syntax.dart';
import 'inline_syntaxes/emoji_syntax.dart';
import 'inline_syntaxes/inline_html_syntax.dart';
import 'inline_syntaxes/inline_syntax.dart';
@@ -56,6 +60,8 @@
const HeaderWithIdSyntax(),
const SetextHeaderWithIdSyntax(),
const TableSyntax(),
+ const UnorderedListWithCheckBoxSyntax(),
+ const OrderedListWithCheckBoxSyntax(),
],
),
List<InlineSyntax>.unmodifiable(
@@ -63,6 +69,7 @@
InlineHtmlSyntax(),
StrikethroughSyntax(),
EmojiSyntax(),
+ ColorSwatchSyntax(),
AutolinkExtensionSyntax()
],
),
@@ -75,6 +82,8 @@
<BlockSyntax>[
const FencedCodeBlockSyntax(),
const TableSyntax(),
+ const UnorderedListWithCheckBoxSyntax(),
+ const OrderedListWithCheckBoxSyntax(),
],
),
List<InlineSyntax>.unmodifiable(
diff --git a/pkgs/markdown/lib/src/inline_syntaxes/color_swatch_syntax.dart b/pkgs/markdown/lib/src/inline_syntaxes/color_swatch_syntax.dart
new file mode 100644
index 0000000..0998d74
--- /dev/null
+++ b/pkgs/markdown/lib/src/inline_syntaxes/color_swatch_syntax.dart
@@ -0,0 +1,79 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import '../ast.dart';
+import '../charcode.dart';
+import '../inline_parser.dart';
+import '../util.dart';
+import 'inline_syntax.dart';
+
+/// Matches code blocks containing a subset of CSS color syntax.
+class ColorSwatchSyntax extends InlineSyntax {
+ /// This pattern matches:
+ /// * GitHub Flavored Markup supports fewer of these options, GitLab Flavored
+ /// Markup supports all of these. Presumably GitHub will be more complete at
+ /// some point.
+ /// * CSS style '#' prefixed color hex codes in 3,4,6 or 8 digits in length.
+ /// * CSS style RGB()/RgbA()/Hsl()/HSLA() style color declarations, of any
+ /// capitalization.
+ /// EXAMPLES:
+ /// * `#f00`
+ /// * `#BA` (2 digit hex, regex will not match)
+ /// * `#F00a`
+ /// * `#F0BAD` (5 digit hex, regex will not match)
+ /// * `#FF0000`
+ /// * `#F000BAD` (7 digit hex, regex will not match)
+ /// * `#FF0000aA` (GitHub supports only this style)
+ /// * `RGB(0,255,0)`
+ /// * `rgb(0,255,0)`
+ /// * `RGB(0%,100%,0%)`
+ /// * `rgb(0%,100%,0%)`
+ /// * `RGBA(0,255,0,0.3)`
+ /// * `rgba(0,255,0,0.3)`
+ /// * `HSL(540,70%,50%)`
+ /// * `hsl(540,70%,50%)`
+ /// * `HSLA(540,70%,50%,0.3)`
+ /// * `Hsla(540,70%,50%,0.3)`
+ static final String _pattern =
+ '`((#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8}))|'
+ r'([Rr][Gg][Bb][Aa]?\((\d+[%]?),(\d+[%]?),(\d+[%]?),?(\d+\.?\d+[%]?)?\))|'
+ r'([Hh][Ss][Ll][Aa]?\((\d+[%]?),(\d+[%]?),(\d+[%]?),?(\d+\.?\d+[%]?)?\)))`';
+
+ ColorSwatchSyntax() : super(_pattern);
+
+ @override
+ bool tryMatch(InlineParser parser, [int? startMatchPos]) {
+ if (parser.pos > 0 && parser.charAt(parser.pos - 1) == $backquote) {
+ // Not really a match! We can't just sneak past one backtick to try the
+ // next character. An example of this situation would be:
+ //
+ // before ``` and `` after.
+ // ^--parser.pos
+ return false;
+ }
+
+ final match = pattern.matchAsPrefix(parser.source, parser.pos);
+ if (match == null) {
+ return false;
+ }
+ parser.writeText();
+ if (onMatch(parser, match)) parser.consume(match.match.length);
+ return true;
+ }
+
+ @override
+ bool onMatch(InlineParser parser, Match match) {
+ var code = match[1]!.trim().replaceAll('\n', ' ');
+
+ if (parser.encodeHtml) code = escapeHtml(code);
+
+ parser.addNode(Element('code', [
+ Text(code),
+ Element.withTag('span')..attributes['style'] = 'background-color:$code;',
+ ])
+ ..attributes['class'] = 'gfm-color_chip');
+
+ return true;
+ }
+}
diff --git a/pkgs/markdown/lib/src/patterns.dart b/pkgs/markdown/lib/src/patterns.dart
index 4ddc184..f2aa7d8 100644
--- a/pkgs/markdown/lib/src/patterns.dart
+++ b/pkgs/markdown/lib/src/patterns.dart
@@ -35,15 +35,29 @@
/// three leading spaces before the marker and any number of spaces or tabs
/// after.
///
-/// Contains a dummy group at [2], so that the groups in [ulPattern] and
-/// [olPattern] match up; in both, [2] is the length of the number that begins
+/// Contains a dummy group at `[2]`, so that the groups in [ulPattern] and
+/// [olPattern] match up; in both, `[2]` is the length of the number that begins
/// the list marker.
final ulPattern = RegExp(r'^([ ]{0,3})()([*+-])(([ \t])([ \t]*)(.*))?$');
+/// Similar to [ulPattern] but with a GitHub style checkbox
+/// `'[ ]'|'[x]'|'[X]'` following the number. The checkbox will
+/// be grabbed by group `[5]` and [ulPattern]'s groups `[5,6,7]` are all
+/// shifted 2 places to be `[7,8,9]`
+final ulWithCheckBoxPattern = RegExp(
+ r'^([ ]{0,3})()([*+-])([ \t]{0,4})(\[[ xX]{1}\])(([ \t])([ \t]*)(.*))?$');
+
/// A line starting with a number like `123.`. May have up to three leading
/// spaces before the marker and any number of spaces or tabs after.
final olPattern = RegExp(r'^([ ]{0,3})(\d{1,9})([\.)])(([ \t])([ \t]*)(.*))?$');
+/// Similar to [olPattern] but with a GitHub style checkbox
+/// `'[ ]'|'[x]'|'[X]'` following the number. The checkbox will
+/// be grabbed by group `[5]` and [olPattern]'s groups `[5,6,7]` are all
+/// shifted 2 places to be `[7,8,9]`
+final olWithCheckBoxPattern = RegExp(
+ r'^([ ]{0,3})(\d{1,9})([\.)])([ \t]{0,4})(\[[ xX]{1}\])(([ \t])([ \t]*)(.*))?$');
+
/// A line of hyphens separated by at least one pipe.
final tablePattern = RegExp(
r'^[ ]{0,3}\|?([ \t]*:?\-+:?[ \t]*\|)+([ \t]|[ \t]*:?\-+:?[ \t]*)?$');