Merge pull request dart-lang/markdown#44 from srawlins/formal-extensions-situation
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,