Skip to content

Commit 3ad1dfd

Browse files
committed
Formalize extensions support; add inline HTML support
1 parent 306e689 commit 3ad1dfd

File tree

11 files changed

+188
-74
lines changed

11 files changed

+188
-74
lines changed

pkgs/markdown/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.9.0
2+
3+
* Formalize an API for Markdown extensions (#43).
4+
* **Breaking:** Fenced code blocks are now considered an extension, as
5+
they are not part of Markdown.pl.
6+
* Inline HTML syntax supported. This is also considered an extension (#18).
7+
18
## 0.8.0
29

310
* **Breaking:** Remove (probably unused) fields: `LinkSyntax.resolved`,

pkgs/markdown/README.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
A portable markdown library written in Dart. It can parse markdown into
2-
html on both the client and server.
1+
A portable Markdown library written in Dart. It can parse Markdown into
2+
HTML on both the client and server.
33

44
Usage
55
-----
@@ -13,6 +13,37 @@ void main() {
1313
}
1414
```
1515

16+
Syntax extensions
17+
-----------------
18+
19+
A few Markdown extensions are supported. They are all disabled by default, and
20+
can be enabled by specifying an Array of extension syntaxes in the `blockSyntaxes` or `inlineSyntaxes`
21+
argument of `markdownToHtml`.
22+
23+
The currently supported inline extension syntaxes are:
24+
25+
* `new InlineHtmlSyntax()` - approximately CommonMark's
26+
[definition](http://spec.commonmark.org/0.22/#raw-html) of "Raw HTML".
27+
28+
The currently supported block extension syntaxes are:
29+
30+
* `const FencedCodeBlockSyntax()` - Code blocks familiar to Pandoc and PHP
31+
Markdown Extra users.
32+
33+
For example:
34+
35+
```dart
36+
import 'package:markdown/markdown.dart';
37+
38+
void main() {
39+
print(markdownToHtml('Hello <span class="green">Markdown</span>',
40+
inlineSyntaxes: [new InlineHtmlSyntax()]));
41+
//=> <p>Hello <span class="green">Markdown</span></p>
42+
}
43+
```
44+
45+
### Custom syntax extensions
46+
1647
You can create and use your own syntaxes.
1748

1849
```dart

pkgs/markdown/lib/src/block_parser.dart

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,31 @@ class BlockParser {
5555
/// The markdown document this parser is parsing.
5656
final Document document;
5757

58+
/// The enabled block syntaxes. To turn a series of lines into blocks, each of
59+
/// these will be tried in turn. Order matters here.
60+
final List<BlockSyntax> blockSyntaxes = [];
61+
5862
/// Index of the current line.
59-
int _pos;
63+
int _pos = 0;
6064

61-
BlockParser(this.lines, this.document) : _pos = 0;
65+
/// The collection of built-in block parsers.
66+
final List<BlockSyntax> standardBlockSyntaxes = const [
67+
const EmptyBlockSyntax(),
68+
const BlockHtmlSyntax(),
69+
const SetextHeaderSyntax(),
70+
const HeaderSyntax(),
71+
const CodeBlockSyntax(),
72+
const BlockquoteSyntax(),
73+
const HorizontalRuleSyntax(),
74+
const UnorderedListSyntax(),
75+
const OrderedListSyntax(),
76+
const ParagraphSyntax()
77+
];
78+
79+
BlockParser(this.lines, this.document) {
80+
blockSyntaxes.addAll(document.blockSyntaxes);
81+
blockSyntaxes.addAll(standardBlockSyntaxes);
82+
}
6283

6384
/// Gets the current line.
6485
String get current => lines[_pos];
@@ -90,21 +111,6 @@ class BlockParser {
90111
}
91112

92113
abstract class BlockSyntax {
93-
/// Gets the collection of built-in block parsers. To turn a series of lines
94-
/// into blocks, each of these will be tried in turn. Order matters here.
95-
static const List<BlockSyntax> syntaxes = const [
96-
const EmptyBlockSyntax(),
97-
const BlockHtmlSyntax(),
98-
const SetextHeaderSyntax(),
99-
const HeaderSyntax(),
100-
const CodeBlockSyntax(),
101-
const FencedCodeBlockSyntax(),
102-
const BlockquoteSyntax(),
103-
const HorizontalRuleSyntax(),
104-
const UnorderedListSyntax(),
105-
const OrderedListSyntax(),
106-
const ParagraphSyntax()
107-
];
108114

109115
const BlockSyntax();
110116

@@ -136,7 +142,7 @@ abstract class BlockSyntax {
136142
/// Gets whether or not [parser]'s current line should end the previous block.
137143
static bool isAtBlockEnd(BlockParser parser) {
138144
if (parser.isDone) return true;
139-
return syntaxes.any((s) => s.canParse(parser) && s.canEndBlock);
145+
return parser.blockSyntaxes.any((s) => s.canParse(parser) && s.canEndBlock);
140146
}
141147
}
142148

pkgs/markdown/lib/src/document.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ import 'ast.dart';
44
import 'block_parser.dart';
55
import 'inline_parser.dart';
66

7-
/// Maintains the context needed to parse a markdown document.
7+
/// Maintains the context needed to parse a Markdown document.
88
class Document {
99
final Map<String, Link> refLinks;
10+
List<BlockSyntax> blockSyntaxes;
1011
List<InlineSyntax> inlineSyntaxes;
1112
Resolver linkResolver;
1213
Resolver imageLinkResolver;
1314

14-
Document({this.inlineSyntaxes, this.linkResolver, this.imageLinkResolver})
15+
Document(
16+
{this.blockSyntaxes: const [],
17+
this.inlineSyntaxes: const [],
18+
this.linkResolver,
19+
this.imageLinkResolver})
1520
: refLinks = <String, Link>{};
1621

1722
parseRefLinks(List<String> lines) {
@@ -61,7 +66,7 @@ class Document {
6166

6267
var blocks = <Node>[];
6368
while (!parser.isDone) {
64-
for (var syntax in BlockSyntax.syntaxes) {
69+
for (var syntax in parser.blockSyntaxes) {
6570
if (syntax.canParse(parser)) {
6671
var block = syntax.parse(parser);
6772
if (block != null) blocks.add(block);

pkgs/markdown/lib/src/html_renderer.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import 'inline_parser.dart';
1010

1111
/// Converts the given string of markdown to HTML.
1212
String markdownToHtml(String markdown,
13-
{List<InlineSyntax> inlineSyntaxes,
13+
{List<BlockSyntax> blockSyntaxes: const [],
14+
List<InlineSyntax> inlineSyntaxes: const [],
1415
Resolver linkResolver,
1516
Resolver imageLinkResolver,
1617
bool inlineOnly: false}) {
1718
var document = new Document(
19+
blockSyntaxes: blockSyntaxes,
1820
inlineSyntaxes: inlineSyntaxes,
1921
imageLinkResolver: imageLinkResolver,
2022
linkResolver: linkResolver);

pkgs/markdown/lib/src/inline_parser.dart

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,10 @@ class InlineParser {
7070

7171
InlineParser(this.source, this.document) : _stack = <TagState>[] {
7272
// User specified syntaxes are the first syntaxes to be evaluated.
73-
if (document.inlineSyntaxes != null) {
74-
syntaxes.addAll(document.inlineSyntaxes);
75-
}
76-
73+
syntaxes.addAll(document.inlineSyntaxes);
7774
syntaxes.addAll(_defaultSyntaxes);
7875

79-
// Custom link resolvers goes after the generic text syntax.
76+
// Custom link resolvers go after the generic text syntax.
8077
syntaxes.insertAll(1, [
8178
new LinkSyntax(linkResolver: document.linkResolver),
8279
new ImageLinkSyntax(linkResolver: document.imageLinkResolver)
@@ -202,6 +199,19 @@ class TextSyntax extends InlineSyntax {
202199
}
203200
}
204201

202+
/// Leave inline HTML tags alone, from
203+
/// [CommonMark 0.22](http://spec.commonmark.org/0.22/#raw-html).
204+
///
205+
/// This is not actually a good definition (nor CommonMark's) of an HTML tag,
206+
/// but it is fast. It will leave text like <a href='hi"> alone, which is
207+
/// incorrect.
208+
///
209+
/// TODO(srawlins): improve accuracy while ensuring performance, once
210+
/// Markdown benchmarking is more mature.
211+
class InlineHtmlSyntax extends TextSyntax {
212+
InlineHtmlSyntax() : super(r'</?[A-Za-z][^>]*>');
213+
}
214+
205215
/// Matches autolinks like `<http://foo.com>`.
206216
class AutolinkSyntax extends InlineSyntax {
207217
AutolinkSyntax() : super(r'<((http|https|ftp)://[^>]*)>');

pkgs/markdown/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: markdown
2-
version: 0.8.0
2+
version: 0.9.0-dev
33
author: Dart Team <misc@dartlang.org>
44
description: A library for converting markdown to HTML.
55
homepage: https://github.com/dart-lang/markdown
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
>>> within a paragraph
2+
Within a <em class="x">paragraph</EM>.
3+
4+
<<<
5+
<p>Within a <em class="x">paragraph</EM>.</p>
6+
>>> not HTML
7+
Obviously, 3 < 5 and 7 > 2.
8+
Not HTML: <3>, <_a>, <>
9+
10+
<<<
11+
<p>Obviously, 3 &lt; 5 and 7 > 2.
12+
Not HTML: &lt;3>, &lt;_a>, &lt;></p>
13+
>>> "markdown" within a tag is not parsed
14+
Text <a href="_foo_">And "_foo_"</a>.
15+
16+
<<<
17+
<p>Text <a href="_foo_">And "<em>foo</em>"</a>.</p>

pkgs/markdown/test/markdown_test.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,10 @@ nyan''', '''<p>~=[,,_,,]:3</p>
9999
1. This will not be an &lt;ol>.
100100
''', inlineOnly: true);
101101
});
102+
103+
testFile('extensions/fenced_code_blocks.unit',
104+
blockSyntaxes: [const FencedCodeBlockSyntax()]);
105+
106+
testFile('extensions/inline_html.unit',
107+
inlineSyntaxes: [new InlineHtmlSyntax()]);
102108
}

pkgs/markdown/test/util.dart

Lines changed: 74 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,60 +15,90 @@ final _indentPattern = new RegExp(r"^\(indent (\d+)\)\s*");
1515

1616
/// Run tests defined in "*.unit" files inside directory [name].
1717
void testDirectory(String name) {
18-
// Locate the "test" directory. Use mirrors so that this works with the test
19-
// package, which loads this suite into an isolate.
20-
var testDir = p.dirname(currentMirrorSystem()
18+
var dir = p.join(_testDir, name);
19+
var entries =
20+
new Directory(dir).listSync().where((e) => e.path.endsWith('.unit'));
21+
22+
for (var entry in entries) {
23+
testUnitFile(name, entry);
24+
}
25+
}
26+
27+
// Locate the "test" directory. Use mirrors so that this works with the test
28+
// package, which loads this suite into an isolate.
29+
String get _testDir => p.dirname(currentMirrorSystem()
2130
.findLibrary(#markdown.test.util)
2231
.uri
2332
.path);
2433

25-
var dir = p.join(testDir, name);
26-
var entries =
27-
new Directory(dir).listSync().where((e) => e.path.endsWith('.unit'));
34+
void testFile(String file,
35+
{List<BlockSyntax> blockSyntaxes: const [],
36+
List<InlineSyntax> inlineSyntaxes: const []}) =>
37+
testUnitFile(
38+
file,
39+
new File(p.join(_testDir, file)),
40+
blockSyntaxes: blockSyntaxes,
41+
inlineSyntaxes: inlineSyntaxes);
2842

29-
for (var entry in entries) {
30-
group("$name ${p.basename(entry.path)}", () {
31-
var lines = (entry as File).readAsLinesSync();
32-
33-
var i = 0;
34-
while (i < lines.length) {
35-
var description = lines[i++].replaceAll(">>>", "").trim();
36-
37-
// Let the test specify a leading indentation. This is handy for
38-
// regression tests which often come from a chunk of nested code.
39-
var indentMatch = _indentPattern.firstMatch(description);
40-
if (indentMatch != null) {
41-
// The test specifies it in spaces, but the formatter expects levels.
42-
description = description.substring(indentMatch.end);
43-
}
44-
45-
if (description == "") {
46-
description = "line ${i + 1}";
47-
} else {
48-
description = "line ${i + 1}: $description";
49-
}
50-
51-
var input = "";
52-
while (!lines[i].startsWith("<<<")) {
53-
input += lines[i++] + "\n";
54-
}
55-
56-
var expectedOutput = "";
57-
while (++i < lines.length && !lines[i].startsWith(">>>")) {
58-
expectedOutput += lines[i] + "\n";
59-
}
60-
61-
validateCore(description, input, expectedOutput);
43+
void testUnitFile(
44+
String directory,
45+
File entry,
46+
{List<BlockSyntax> blockSyntaxes: const [],
47+
List<InlineSyntax> inlineSyntaxes: const []}) {
48+
group('$directory ${p.basename(entry.path)}', () {
49+
var lines = entry.readAsLinesSync();
50+
51+
var i = 0;
52+
while (i < lines.length) {
53+
var description = lines[i++].replaceAll(">>>", "").trim();
54+
55+
// Let the test specify a leading indentation. This is handy for
56+
// regression tests which often come from a chunk of nested code.
57+
var indentMatch = _indentPattern.firstMatch(description);
58+
if (indentMatch != null) {
59+
// The test specifies it in spaces, but the formatter expects levels.
60+
description = description.substring(indentMatch.end);
6261
}
63-
});
64-
}
62+
63+
if (description == "") {
64+
description = "line ${i + 1}";
65+
} else {
66+
description = "line ${i + 1}: $description";
67+
}
68+
69+
var input = "";
70+
while (!lines[i].startsWith("<<<")) {
71+
input += lines[i++] + "\n";
72+
}
73+
74+
var expectedOutput = "";
75+
while (++i < lines.length && !lines[i].startsWith(">>>")) {
76+
expectedOutput += lines[i] + "\n";
77+
}
78+
79+
validateCore(
80+
description,
81+
input,
82+
expectedOutput,
83+
blockSyntaxes: blockSyntaxes,
84+
inlineSyntaxes: inlineSyntaxes
85+
);
86+
}
87+
});
6588
}
6689

67-
void validateCore(String description, String markdown, String html,
68-
{List<InlineSyntax> inlineSyntaxes, Resolver linkResolver,
69-
Resolver imageLinkResolver, bool inlineOnly: false}) {
90+
void validateCore(
91+
String description,
92+
String markdown,
93+
String html,
94+
{List<BlockSyntax> blockSyntaxes: const [],
95+
List<InlineSyntax> inlineSyntaxes: const [],
96+
Resolver linkResolver,
97+
Resolver imageLinkResolver,
98+
bool inlineOnly: false}) {
7099
test(description, () {
71100
var result = markdownToHtml(markdown,
101+
blockSyntaxes: blockSyntaxes,
72102
inlineSyntaxes: inlineSyntaxes,
73103
linkResolver: linkResolver,
74104
imageLinkResolver: imageLinkResolver,

0 commit comments

Comments
 (0)