Formalize extensions support; add inline HTML support
diff --git a/pkgs/markdown/CHANGELOG.md b/pkgs/markdown/CHANGELOG.md index d7d580c..47a307c 100644 --- a/pkgs/markdown/CHANGELOG.md +++ b/pkgs/markdown/CHANGELOG.md
@@ -1,3 +1,10 @@ +## 0.9.0 + +* Formalize an API for Markdown extensions (#43). +* **Breaking:** Fenced code blocks are now considered an extension, as + they are not part of Markdown.pl. +* Inline HTML syntax supported. This is also considered an extension (#18). + ## 0.8.0 * **Breaking:** Remove (probably unused) fields: `LinkSyntax.resolved`,
diff --git a/pkgs/markdown/README.md b/pkgs/markdown/README.md index 6237377..0cbd748 100644 --- a/pkgs/markdown/README.md +++ b/pkgs/markdown/README.md
@@ -1,5 +1,5 @@ -A portable markdown library written in Dart. It can parse markdown into -html on both the client and server. +A portable Markdown library written in Dart. It can parse Markdown into +HTML on both the client and server. Usage ----- @@ -13,6 +13,37 @@ } ``` +Syntax extensions +----------------- + +A few Markdown extensions are supported. They are all disabled by default, and +can be enabled by specifying an Array of extension syntaxes in the `blockSyntaxes` or `inlineSyntaxes` +argument of `markdownToHtml`. + +The currently supported inline extension syntaxes are: + +* `new InlineHtmlSyntax()` - approximately CommonMark's + [definition](http://spec.commonmark.org/0.22/#raw-html) of "Raw HTML". + +The currently supported block extension syntaxes are: + +* `const FencedCodeBlockSyntax()` - Code blocks familiar to Pandoc and PHP + Markdown Extra users. + +For example: + +```dart +import 'package:markdown/markdown.dart'; + +void main() { + print(markdownToHtml('Hello <span class="green">Markdown</span>', + inlineSyntaxes: [new InlineHtmlSyntax()])); + //=> <p>Hello <span class="green">Markdown</span></p> +} +``` + +### Custom syntax extensions + You can create and use your own syntaxes. ```dart
diff --git a/pkgs/markdown/lib/src/block_parser.dart b/pkgs/markdown/lib/src/block_parser.dart index 67e4bbd..13c5b68 100644 --- a/pkgs/markdown/lib/src/block_parser.dart +++ b/pkgs/markdown/lib/src/block_parser.dart
@@ -55,10 +55,31 @@ /// The markdown document this parser is parsing. final Document document; - /// Index of the current line. - int _pos; + /// The enabled block syntaxes. To turn a series of lines into blocks, each of + /// these will be tried in turn. Order matters here. + final List<BlockSyntax> blockSyntaxes = []; - BlockParser(this.lines, this.document) : _pos = 0; + /// Index of the current line. + int _pos = 0; + + /// The collection of built-in block parsers. + final List<BlockSyntax> standardBlockSyntaxes = const [ + const EmptyBlockSyntax(), + const BlockHtmlSyntax(), + const SetextHeaderSyntax(), + const HeaderSyntax(), + const CodeBlockSyntax(), + const BlockquoteSyntax(), + const HorizontalRuleSyntax(), + const UnorderedListSyntax(), + const OrderedListSyntax(), + const ParagraphSyntax() + ]; + + BlockParser(this.lines, this.document) { + blockSyntaxes.addAll(document.blockSyntaxes); + blockSyntaxes.addAll(standardBlockSyntaxes); + } /// Gets the current line. String get current => lines[_pos]; @@ -90,21 +111,6 @@ } abstract class BlockSyntax { - /// Gets the collection of built-in block parsers. To turn a series of lines - /// into blocks, each of these will be tried in turn. Order matters here. - static const List<BlockSyntax> syntaxes = const [ - const EmptyBlockSyntax(), - const BlockHtmlSyntax(), - const SetextHeaderSyntax(), - const HeaderSyntax(), - const CodeBlockSyntax(), - const FencedCodeBlockSyntax(), - const BlockquoteSyntax(), - const HorizontalRuleSyntax(), - const UnorderedListSyntax(), - const OrderedListSyntax(), - const ParagraphSyntax() - ]; const BlockSyntax(); @@ -136,7 +142,7 @@ /// Gets whether or not [parser]'s current line should end the previous block. static bool isAtBlockEnd(BlockParser parser) { if (parser.isDone) return true; - return syntaxes.any((s) => s.canParse(parser) && s.canEndBlock); + return parser.blockSyntaxes.any((s) => s.canParse(parser) && s.canEndBlock); } }
diff --git a/pkgs/markdown/lib/src/document.dart b/pkgs/markdown/lib/src/document.dart index 71a29d9..1190b96 100644 --- a/pkgs/markdown/lib/src/document.dart +++ b/pkgs/markdown/lib/src/document.dart
@@ -4,14 +4,19 @@ import 'block_parser.dart'; import 'inline_parser.dart'; -/// Maintains the context needed to parse a markdown document. +/// Maintains the context needed to parse a Markdown document. class Document { final Map<String, Link> refLinks; + List<BlockSyntax> blockSyntaxes; List<InlineSyntax> inlineSyntaxes; Resolver linkResolver; Resolver imageLinkResolver; - Document({this.inlineSyntaxes, this.linkResolver, this.imageLinkResolver}) + Document( + {this.blockSyntaxes: const [], + this.inlineSyntaxes: const [], + this.linkResolver, + this.imageLinkResolver}) : refLinks = <String, Link>{}; parseRefLinks(List<String> lines) { @@ -61,7 +66,7 @@ var blocks = <Node>[]; while (!parser.isDone) { - for (var syntax in BlockSyntax.syntaxes) { + for (var syntax in parser.blockSyntaxes) { if (syntax.canParse(parser)) { var block = syntax.parse(parser); if (block != null) blocks.add(block);
diff --git a/pkgs/markdown/lib/src/html_renderer.dart b/pkgs/markdown/lib/src/html_renderer.dart index 87fb05c..e477721 100644 --- a/pkgs/markdown/lib/src/html_renderer.dart +++ b/pkgs/markdown/lib/src/html_renderer.dart
@@ -10,11 +10,13 @@ /// Converts the given string of markdown to HTML. String markdownToHtml(String markdown, - {List<InlineSyntax> inlineSyntaxes, + {List<BlockSyntax> blockSyntaxes: const [], + List<InlineSyntax> inlineSyntaxes: const [], Resolver linkResolver, Resolver imageLinkResolver, bool inlineOnly: false}) { var document = new Document( + blockSyntaxes: blockSyntaxes, inlineSyntaxes: inlineSyntaxes, imageLinkResolver: imageLinkResolver, linkResolver: linkResolver);
diff --git a/pkgs/markdown/lib/src/inline_parser.dart b/pkgs/markdown/lib/src/inline_parser.dart index 9f0db08..c549067 100644 --- a/pkgs/markdown/lib/src/inline_parser.dart +++ b/pkgs/markdown/lib/src/inline_parser.dart
@@ -70,13 +70,10 @@ InlineParser(this.source, this.document) : _stack = <TagState>[] { // User specified syntaxes are the first syntaxes to be evaluated. - if (document.inlineSyntaxes != null) { - syntaxes.addAll(document.inlineSyntaxes); - } - + syntaxes.addAll(document.inlineSyntaxes); syntaxes.addAll(_defaultSyntaxes); - // Custom link resolvers goes after the generic text syntax. + // Custom link resolvers go after the generic text syntax. syntaxes.insertAll(1, [ new LinkSyntax(linkResolver: document.linkResolver), new ImageLinkSyntax(linkResolver: document.imageLinkResolver) @@ -202,6 +199,19 @@ } } +/// Leave inline HTML tags alone, from +/// [CommonMark 0.22](http://spec.commonmark.org/0.22/#raw-html). +/// +/// This is not actually a good definition (nor CommonMark's) of an HTML tag, +/// but it is fast. It will leave text like <a href='hi"> alone, which is +/// incorrect. +/// +/// TODO(srawlins): improve accuracy while ensuring performance, once +/// Markdown benchmarking is more mature. +class InlineHtmlSyntax extends TextSyntax { + InlineHtmlSyntax() : super(r'</?[A-Za-z][^>]*>'); +} + /// Matches autolinks like `<http://foo.com>`. class AutolinkSyntax extends InlineSyntax { AutolinkSyntax() : super(r'<((http|https|ftp)://[^>]*)>');
diff --git a/pkgs/markdown/pubspec.yaml b/pkgs/markdown/pubspec.yaml index e459456..9525f32 100644 --- a/pkgs/markdown/pubspec.yaml +++ b/pkgs/markdown/pubspec.yaml
@@ -1,5 +1,5 @@ name: markdown -version: 0.8.0 +version: 0.9.0-dev author: Dart Team <misc@dartlang.org> description: A library for converting markdown to HTML. homepage: https://github.com/dart-lang/markdown
diff --git a/pkgs/markdown/test/original/fenced_code_blocks.unit b/pkgs/markdown/test/extensions/fenced_code_blocks.unit similarity index 100% rename from pkgs/markdown/test/original/fenced_code_blocks.unit rename to pkgs/markdown/test/extensions/fenced_code_blocks.unit
diff --git a/pkgs/markdown/test/extensions/inline_html.unit b/pkgs/markdown/test/extensions/inline_html.unit new file mode 100644 index 0000000..4aa260b --- /dev/null +++ b/pkgs/markdown/test/extensions/inline_html.unit
@@ -0,0 +1,17 @@ +>>> within a paragraph +Within a <em class="x">paragraph</EM>. + +<<< +<p>Within a <em class="x">paragraph</EM>.</p> +>>> not HTML +Obviously, 3 < 5 and 7 > 2. +Not HTML: <3>, <_a>, <> + +<<< +<p>Obviously, 3 < 5 and 7 > 2. +Not HTML: <3>, <_a>, <></p> +>>> "markdown" within a tag is not parsed +Text <a href="_foo_">And "_foo_"</a>. + +<<< +<p>Text <a href="_foo_">And "<em>foo</em>"</a>.</p>
diff --git a/pkgs/markdown/test/markdown_test.dart b/pkgs/markdown/test/markdown_test.dart index c2804c4..8162f0d 100644 --- a/pkgs/markdown/test/markdown_test.dart +++ b/pkgs/markdown/test/markdown_test.dart
@@ -99,4 +99,10 @@ 1. This will not be an <ol>. ''', inlineOnly: true); }); + + testFile('extensions/fenced_code_blocks.unit', + blockSyntaxes: [const FencedCodeBlockSyntax()]); + + testFile('extensions/inline_html.unit', + inlineSyntaxes: [new InlineHtmlSyntax()]); }
diff --git a/pkgs/markdown/test/util.dart b/pkgs/markdown/test/util.dart index 2680a28..22d1935 100644 --- a/pkgs/markdown/test/util.dart +++ b/pkgs/markdown/test/util.dart
@@ -15,60 +15,90 @@ /// Run tests defined in "*.unit" files inside directory [name]. void testDirectory(String name) { - // Locate the "test" directory. Use mirrors so that this works with the test - // package, which loads this suite into an isolate. - var testDir = p.dirname(currentMirrorSystem() - .findLibrary(#markdown.test.util) - .uri - .path); - - var dir = p.join(testDir, name); + var dir = p.join(_testDir, name); var entries = new Directory(dir).listSync().where((e) => e.path.endsWith('.unit')); for (var entry in entries) { - group("$name ${p.basename(entry.path)}", () { - var lines = (entry as File).readAsLinesSync(); - - var i = 0; - while (i < lines.length) { - var description = lines[i++].replaceAll(">>>", "").trim(); - - // Let the test specify a leading indentation. This is handy for - // regression tests which often come from a chunk of nested code. - var indentMatch = _indentPattern.firstMatch(description); - if (indentMatch != null) { - // The test specifies it in spaces, but the formatter expects levels. - description = description.substring(indentMatch.end); - } - - if (description == "") { - description = "line ${i + 1}"; - } else { - description = "line ${i + 1}: $description"; - } - - var input = ""; - while (!lines[i].startsWith("<<<")) { - input += lines[i++] + "\n"; - } - - var expectedOutput = ""; - while (++i < lines.length && !lines[i].startsWith(">>>")) { - expectedOutput += lines[i] + "\n"; - } - - validateCore(description, input, expectedOutput); - } - }); + testUnitFile(name, entry); } } -void validateCore(String description, String markdown, String html, - {List<InlineSyntax> inlineSyntaxes, Resolver linkResolver, - Resolver imageLinkResolver, bool inlineOnly: false}) { +// Locate the "test" directory. Use mirrors so that this works with the test +// package, which loads this suite into an isolate. +String get _testDir => p.dirname(currentMirrorSystem() + .findLibrary(#markdown.test.util) + .uri + .path); + +void testFile(String file, + {List<BlockSyntax> blockSyntaxes: const [], + List<InlineSyntax> inlineSyntaxes: const []}) => + testUnitFile( + file, + new File(p.join(_testDir, file)), + blockSyntaxes: blockSyntaxes, + inlineSyntaxes: inlineSyntaxes); + +void testUnitFile( + String directory, + File entry, + {List<BlockSyntax> blockSyntaxes: const [], + List<InlineSyntax> inlineSyntaxes: const []}) { + group('$directory ${p.basename(entry.path)}', () { + var lines = entry.readAsLinesSync(); + + var i = 0; + while (i < lines.length) { + var description = lines[i++].replaceAll(">>>", "").trim(); + + // Let the test specify a leading indentation. This is handy for + // regression tests which often come from a chunk of nested code. + var indentMatch = _indentPattern.firstMatch(description); + if (indentMatch != null) { + // The test specifies it in spaces, but the formatter expects levels. + description = description.substring(indentMatch.end); + } + + if (description == "") { + description = "line ${i + 1}"; + } else { + description = "line ${i + 1}: $description"; + } + + var input = ""; + while (!lines[i].startsWith("<<<")) { + input += lines[i++] + "\n"; + } + + var expectedOutput = ""; + while (++i < lines.length && !lines[i].startsWith(">>>")) { + expectedOutput += lines[i] + "\n"; + } + + validateCore( + description, + input, + expectedOutput, + blockSyntaxes: blockSyntaxes, + inlineSyntaxes: inlineSyntaxes + ); + } + }); +} + +void validateCore( + String description, + String markdown, + String html, + {List<BlockSyntax> blockSyntaxes: const [], + List<InlineSyntax> inlineSyntaxes: const [], + Resolver linkResolver, + Resolver imageLinkResolver, + bool inlineOnly: false}) { test(description, () { var result = markdownToHtml(markdown, + blockSyntaxes: blockSyntaxes, inlineSyntaxes: inlineSyntaxes, linkResolver: linkResolver, imageLinkResolver: imageLinkResolver,