Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* Inline HTML syntax support; This is also considered an extension (#18).
* The text `[foo] (bar)` now parses as an inline link (#53).
* The text `[foo]()` now renders as an inline link.
* Header identifier support in the HeaderWithIdSyntax and
SetextHeaderWithIdSyntax extensions.

## 0.8.0

Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,17 @@ specifying an Array of extension syntaxes in the `blockSyntaxes` or
The currently supported inline extension syntaxes are:

* `new InlineHtmlSyntax()` - approximately CommonMark's
[definition](http://spec.commonmark.org/0.22/#raw-html) of "Raw HTML".
[definition][commonmark-raw-html] of "Raw HTML".

The currently supported block extension syntaxes are:

* `const FencedCodeBlockSyntax()` - Code blocks familiar to Pandoc and PHP
Markdown Extra users.
* `const HeaderWithIdSyntax()` - ATX-style headers have generated IDs, for link
anchors (akin to Pandoc's [`auto_identifiers`][pandoc-auto_identifiers]).
* `const SetextHeaderWithIdSyntax()` - Setext-style headers have generated IDs
for link anchors (akin to Pandoc's
[`auto_identifiers`][pandoc-auto_identifiers]).

For example:

Expand Down Expand Up @@ -75,3 +80,5 @@ void main() {

[Perl Markdown]: http://daringfireball.net/projects/markdown/
[CommonMark]: http://commonmark.org/
[commonMark-raw-html]: http://spec.commonmark.org/0.22/#raw-html
[pandoc-auto_identifiers]: http://pandoc.org/README.html#extension-auto_identifiers
1 change: 1 addition & 0 deletions lib/src/ast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Element implements Node {
final String tag;
final List<Node> children;
final Map<String, String> attributes;
String generatedId;

Element(this.tag, this.children) : attributes = <String, String>{};

Expand Down
38 changes: 37 additions & 1 deletion lib/src/block_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ class BlockParser {
}

abstract class BlockSyntax {

const BlockSyntax();

/// Gets the regex used to identify the beginning of this block, if any.
Expand Down Expand Up @@ -144,6 +143,20 @@ abstract class BlockSyntax {
if (parser.isDone) return true;
return parser.blockSyntaxes.any((s) => s.canParse(parser) && s.canEndBlock);
}

/// Generates a valid HTML anchor from the inner text of [element].
static String generateAnchorHash(Element element) =>
_concatenatedText(element)
.toLowerCase()
.trim()
.replaceFirst(new RegExp(r'^[^a-z]+'), '')
.replaceAll(new RegExp(r'[^a-z0-9 _-]'), '')
.replaceAll(new RegExp(r'\s'), '-');

/// Concatenates the text found in all the children of [element].
static String _concatenatedText(Element element) => element.children
.map((child) => (child is Text) ? child.text : _concatenatedText(child))
.join('');
}

class EmptyBlockSyntax extends BlockSyntax {
Expand Down Expand Up @@ -181,6 +194,18 @@ class SetextHeaderSyntax extends BlockSyntax {
}
}

/// Parses setext-style headers, and adds generated IDs to the generated
/// elements.
class SetextHeaderWithIdSyntax extends SetextHeaderSyntax {
const SetextHeaderWithIdSyntax();

Node parse(BlockParser parser) {
var element = super.parse(parser);
element.generatedId = BlockSyntax.generateAnchorHash(element);
return element;
}
}

/// Parses atx-style headers: `## Header ##`.
class HeaderSyntax extends BlockSyntax {
RegExp get pattern => _headerPattern;
Expand All @@ -196,6 +221,17 @@ class HeaderSyntax extends BlockSyntax {
}
}

/// Parses atx-style headers, and adds generated IDs to the generated elements.
class HeaderWithIdSyntax extends HeaderSyntax {
const HeaderWithIdSyntax();

Node parse(BlockParser parser) {
var element = super.parse(parser);
element.generatedId = BlockSyntax.generateAnchorHash(element);
return element;
}
}

/// Parses email-style blockquotes: `> quote`.
class BlockquoteSyntax extends BlockSyntax {
RegExp get pattern => _blockquotePattern;
Expand Down
25 changes: 25 additions & 0 deletions lib/src/html_renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

library markdown.src.html_renderer;

import 'dart:collection';

import 'ast.dart';
import 'document.dart';
import 'extension_set.dart';
Expand Down Expand Up @@ -40,11 +42,13 @@ class HtmlRenderer implements NodeVisitor {
static final _blockTags = new RegExp('blockquote|h1|h2|h3|h4|h5|h6|hr|p|pre');

StringBuffer buffer;
Set<String> uniqueIds;

HtmlRenderer();

String render(List<Node> nodes) {
buffer = new StringBuffer();
uniqueIds = new LinkedHashSet<String>();

for (final node in nodes) node.accept(this);

Expand All @@ -71,6 +75,11 @@ class HtmlRenderer implements NodeVisitor {
buffer.write(' $name="${element.attributes[name]}"');
}

// attach header anchor ids generated from text
if (element.generatedId != null) {
buffer.write(' id="${uniquifyId(element.generatedId)}"');
}

if (element.isEmpty) {
// Empty element like <hr/>.
buffer.write(' />');
Expand All @@ -84,4 +93,20 @@ class HtmlRenderer implements NodeVisitor {
void visitElementAfter(Element element) {
buffer.write('</${element.tag}>');
}

/// Uniquifies an id generated from text.
String uniquifyId(String id) {
if (!uniqueIds.contains(id)) {
uniqueIds.add(id);
return id;
}

int suffix = 2;
String suffixedId = '$id-$suffix';
while (uniqueIds.contains(suffixedId)) {
suffixedId = '$id-${suffix++}';
}
uniqueIds.add(suffixedId);
return suffixedId;
}
}
27 changes: 27 additions & 0 deletions test/extensions/headers_with_ids.unit
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
>>> simple header
# header

<<<
<h1 id="header">header</h1>
>>> header that starts with garbage
## 2. header again

<<<
<h2 id="header-again">2. header again</h2>
>>> header with inline syntaxes
### headers **rock** `etc.`

<<<
<h3 id="headers-rock-etc">headers <strong>rock</strong> <code>etc.</code></h3>
>>> non-unique headers
# header

## header

<<<
<h1 id="header">header</h1>
<h2 id="header-2">header</h2>
>>> header starts with inline syntax
# *headers* etc.
<<<
<h1 id="headers-etc"><em>headers</em> etc.</h1>
18 changes: 18 additions & 0 deletions test/extensions/setext_headers_with_ids.unit
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
>>> h1
text
===

<<<
<h1 id="text">text</h1>
>>> h2
text
---

<<<
<h2 id="text">text</h2>
>>> header with inline syntax
header *emphasised*
===

<<<
<h1 id="header-emphasised">header <em>emphasised</em></h1>
6 changes: 6 additions & 0 deletions test/markdown_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,10 @@ nyan''', '''<p>~=[,,_,,]:3</p>

testFile('extensions/inline_html.unit',
inlineSyntaxes: [new InlineHtmlSyntax()]);

testFile('extensions/headers_with_ids.unit',
blockSyntaxes: [const HeaderWithIdSyntax()]);

testFile('extensions/setext_headers_with_ids.unit',
blockSyntaxes: [const SetextHeaderWithIdSyntax()]);
}