├── .gitignore ├── README.md ├── dub.sdl ├── example ├── .gitignore ├── dub.sdl ├── resources │ ├── entities.properties │ ├── spec.html │ ├── spec.md │ └── spec.txt └── source │ ├── app.d │ └── test │ ├── Common.d │ ├── CoreRenderingTestCase.d │ ├── DelimitedTest.d │ ├── FencedCodeBlockParserTest.d │ ├── HeadingParserTest.d │ ├── HtmlRenderTest.d │ ├── ListTightLooseTest.d │ ├── ParserTest.d │ ├── RenderingTestCase.d │ ├── SpecialInputTest.d │ ├── TextContentRendererTest.d │ └── UsageExampleTest.d ├── resources └── entities.properties └── source └── hunt └── markdown ├── Extension.d ├── ext ├── heading │ └── anchor │ │ ├── HeadingAnchorExtension.d │ │ ├── IdGenerator.d │ │ └── internal │ │ └── HeadingIdAttributeProvider.d ├── ins │ ├── Ins.d │ ├── InsExtension.d │ └── internal │ │ ├── InsDelimiterProcessor.d │ │ └── InsNodeRenderer.d ├── matter │ ├── YamlFrontMatterBlock.d │ ├── YamlFrontMatterExtension.d │ ├── YamlFrontMatterNode.d │ ├── YamlFrontMatterVisitor.d │ └── internal │ │ └── YamlFrontMatterBlockParser.d ├── strikethrough │ ├── Strikethrough.d │ ├── StrikethroughExtension.d │ └── internal │ │ ├── StrikethroughDelimiterProcessor.d │ │ ├── StrikethroughHtmlNodeRenderer.d │ │ ├── StrikethroughNodeRenderer.d │ │ └── StrikethroughTextContentNodeRenderer.d └── table │ ├── TableBlock.d │ ├── TableBody.d │ ├── TableCell.d │ ├── TableExtension.d │ ├── TableHead.d │ ├── TableRow.d │ ├── internal │ ├── TableBlockParser.d │ ├── TableHtmlNodeRenderer.d │ ├── TableNodeRenderer.d │ └── TableTextContentNodeRenderer.d │ └── package.d ├── internal ├── BlockContent.d ├── BlockContinueImpl.d ├── BlockQuoteParser.d ├── BlockStartImpl.d ├── Bracket.d ├── Delimiter.d ├── DocumentBlockParser.d ├── DocumentParser.d ├── FencedCodeBlockParser.d ├── HeadingParser.d ├── HtmlBlockParser.d ├── IndentedCodeBlockParser.d ├── InlineParserImpl.d ├── ListBlockParser.d ├── ListItemParser.d ├── ParagraphParser.d ├── ReferenceParser.d ├── StaggeredDelimiterProcessor.d ├── ThematicBreakParser.d ├── inline │ ├── AsteriskDelimiterProcessor.d │ ├── EmphasisDelimiterProcessor.d │ └── UnderscoreDelimiterProcessor.d ├── renderer │ ├── NodeRendererMap.d │ └── text │ │ ├── BulletListHolder.d │ │ ├── ListHolder.d │ │ └── OrderedListHolder.d └── util │ ├── Common.d │ ├── Escaping.d │ ├── Html5Entities.d │ └── Parsing.d ├── node ├── AbstractVisitor.d ├── Block.d ├── BlockQuote.d ├── BulletList.d ├── Code.d ├── CustomBlock.d ├── CustomNode.d ├── Delimited.d ├── Document.d ├── Emphasis.d ├── FencedCodeBlock.d ├── HardLineBreak.d ├── Heading.d ├── HtmlBlock.d ├── HtmlInline.d ├── Image.d ├── IndentedCodeBlock.d ├── Link.d ├── ListBlock.d ├── ListItem.d ├── Node.d ├── OrderedList.d ├── Paragraph.d ├── SoftLineBreak.d ├── StrongEmphasis.d ├── Text.d ├── ThematicBreak.d ├── Visitor.d └── package.d ├── package.d ├── parser ├── InlineParser.d ├── InlineParserContext.d ├── InlineParserFactory.d ├── Parser.d ├── PostProcessor.d ├── block │ ├── AbstractBlockParser.d │ ├── AbstractBlockParserFactory.d │ ├── BlockContinue.d │ ├── BlockParser.d │ ├── BlockParserFactory.d │ ├── BlockStart.d │ ├── MatchedBlockParser.d │ ├── ParserState.d │ └── package.d ├── delimiter │ ├── DelimiterProcessor.d │ └── DelimiterRun.d └── package.d └── renderer ├── NodeRenderer.d ├── Renderer.d ├── html ├── AttributeProvider.d ├── AttributeProviderContext.d ├── AttributeProviderFactory.d ├── CoreHtmlNodeRenderer.d ├── HtmlNodeRendererContext.d ├── HtmlNodeRendererFactory.d ├── HtmlRenderer.d ├── HtmlWriter.d └── package.d └── text ├── CoreTextContentNodeRenderer.d ├── TextContentNodeRendererContext.d ├── TextContentNodeRendererFactory.d ├── TextContentRenderer.d ├── TextContentWriter.d └── package.d /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio Code 2 | .vscode/ 3 | .suo 4 | 5 | # Compiled Object files 6 | *.o 7 | *.obj 8 | 9 | # Compiled Dynamic libraries 10 | *.so 11 | *.dylib 12 | *.dll 13 | 14 | # Compiled Static libraries 15 | *.a 16 | *.lib 17 | 18 | # Executables 19 | *.exe 20 | 21 | # DUB 22 | .dub 23 | dub.*.json 24 | dub.*.sdl 25 | docs.json 26 | __dummy.html 27 | 28 | 29 | # Code coverage 30 | *.lst 31 | 32 | # Examples 33 | example/example 34 | 35 | # others 36 | .DS_Store 37 | *.zip 38 | core 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hunt-markdown 2 | A markdown parsing and rendering library for D programming language. 3 | The project use [commonmark](https://spec.commonmark.org/0.28/) spec, ported from [commonmark-java](https://github.com/atlassian/commonmark-java). 4 | 5 | ## Parse and render 6 | 7 | ```D 8 | import hunt.markdown.node.Node; 9 | import hunt.markdown.parser.Parser; 10 | import hunt.markdown.renderer.html.HtmlRenderer; 11 | 12 | Parser parser = Parser.builder().build(); 13 | Node document = parser.parse("This is *New*"); 14 | HtmlRenderer renderer = HtmlRenderer.builder().build(); 15 | renderer.render(document); // "
This is New
\n" 16 | ``` 17 | 18 | ## How to use Tables extension? 19 | 20 | ```D 21 | string markdown = ` 22 | ## Test for tables 23 | | head 1 | head 2 | head 3 | 24 | |--------|--------|--------| 25 | | row 1.1 | row 1.2 | row 1.3 | 26 | | row 2.1 | row 2.2 | row 2.3 | 27 | `; 28 | 29 | auto extensions = Collections.singleton(TableExtension.create()); 30 | 31 | Parser parser = Parser.builder().extensions(extensions).build(); 32 | Node document = parser.parse(markdown); 33 | HtmlRenderer renderer = HtmlRenderer.builder().extensions(extensions).build(); 34 | 35 | renderer.render(document); 36 | ``` 37 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "hunt-markdown" 2 | description "A markdown parsing and rendering library for D programming language." 3 | homepage "https://www.huntLabs.net" 4 | copyright "Copyright © 2019-2020, HuntLabs" 5 | targetType "library" 6 | dependency "hunt-extra" version="~>1.2.0" 7 | license "Apache-2.0" 8 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | docs/ 5 | example.so 6 | example.dylib 7 | example.dll 8 | example.a 9 | example.lib 10 | example-test-* 11 | *.exe 12 | *.o 13 | *.obj 14 | *.lst 15 | core 16 | *.vscode 17 | -------------------------------------------------------------------------------- /example/dub.sdl: -------------------------------------------------------------------------------- 1 | name "example" 2 | description "a simple example" 3 | authors "gaoxincheng" 4 | copyright "Copyright © 2019, gaoxincheng" 5 | license "proprietary" 6 | targetType "executable" 7 | dependency "hunt-markdown" path="../" -------------------------------------------------------------------------------- /example/source/app.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | 3 | import hunt.markdown.node.Node; 4 | import hunt.markdown.parser.Parser; 5 | import hunt.markdown.renderer.html.HtmlRenderer; 6 | import test.ParserTest; 7 | import test.UsageExampleTest; 8 | import test.HtmlRenderTest; 9 | import test.DelimitedTest; 10 | import test.FencedCodeBlockParserTest; 11 | import test.HeadingParserTest; 12 | import test.ListTightLooseTest; 13 | import test.SpecialInputTest; 14 | import test.TextContentRendererTest; 15 | 16 | void main() 17 | { 18 | writeln("Running ..."); 19 | 20 | new UsageExampleTest().test(); 21 | 22 | new HtmlRendererTest().test(); 23 | 24 | ParserTest.test(); 25 | 26 | new DelimitedTest().test(); 27 | 28 | new FencedCodeBlockParserTest().test(); 29 | 30 | new HeadingParserTest().test(); 31 | 32 | new ListTightLooseTest().test(); 33 | 34 | new SpecialInputTest().test(); 35 | 36 | new TextContentRendererTest().test(); 37 | 38 | writeln("All unit tests have been run successfully."); 39 | } 40 | -------------------------------------------------------------------------------- /example/source/test/Common.d: -------------------------------------------------------------------------------- 1 | module test.Common; 2 | 3 | import hunt.util.StringBuilder; 4 | 5 | string repeat(char ch, int count) 6 | { 7 | StringBuilder buffer = new StringBuilder(); 8 | 9 | for (int i = 0; i < count; ++i) 10 | buffer.append(ch); 11 | 12 | return buffer.toString(); 13 | } 14 | 15 | string repeat(string s, int count) 16 | { 17 | StringBuilder sb = new StringBuilder(s.length * count); 18 | for (int i = 0; i < count; i++) 19 | { 20 | sb.append(s); 21 | } 22 | return sb.toString(); 23 | } 24 | -------------------------------------------------------------------------------- /example/source/test/CoreRenderingTestCase.d: -------------------------------------------------------------------------------- 1 | module test.CoreRenderingTestCase; 2 | 3 | import hunt.markdown.parser.Parser; 4 | import hunt.markdown.renderer.html.HtmlRenderer; 5 | import test.RenderingTestCase; 6 | 7 | public class CoreRenderingTestCase : RenderingTestCase { 8 | 9 | private static Parser PARSER; 10 | private static HtmlRenderer RENDERER; 11 | 12 | static this() 13 | { 14 | PARSER = Parser.builder().build(); 15 | RENDERER = HtmlRenderer.builder().build(); 16 | } 17 | 18 | override 19 | protected string render(string source) { 20 | return RENDERER.render(PARSER.parse(source)); 21 | } 22 | } -------------------------------------------------------------------------------- /example/source/test/DelimitedTest.d: -------------------------------------------------------------------------------- 1 | module test.DelimitedTest; 2 | 3 | import hunt.markdown.node; 4 | import hunt.markdown.node.Delimited; 5 | import hunt.markdown.parser.Parser; 6 | import hunt.markdown.node.Visitor; 7 | import hunt.markdown.node.AbstractVisitor; 8 | 9 | import hunt.Assert; 10 | 11 | import hunt.collection.ArrayList; 12 | import hunt.collection.List; 13 | 14 | 15 | public class DelimitedTest { 16 | 17 | public void test() 18 | { 19 | emphasisDelimiters(); 20 | } 21 | 22 | public void emphasisDelimiters() { 23 | string input = "* *emphasis* \n" 24 | ~ "* **strong** \n" 25 | ~ "* _important_ \n" 26 | ~ "* __CRITICAL__ \n"; 27 | 28 | Parser parser = Parser.builder().build(); 29 | Node document = parser.parse(input); 30 | 31 | List!(Delimited) list = new ArrayList!(Delimited)(); 32 | Visitor visitor = new class AbstractVisitor { 33 | override 34 | public void visit(Emphasis node) { 35 | list.add(node); 36 | } 37 | 38 | override 39 | public void visit(StrongEmphasis node) { 40 | list.add(node); 41 | } 42 | }; 43 | document.accept(visitor); 44 | 45 | Assert.assertEquals(4, list.size()); 46 | 47 | Delimited emphasis = list.get(0); 48 | Delimited strong = list.get(1); 49 | Delimited important = list.get(2); 50 | Delimited critical = list.get(3); 51 | 52 | Assert.assertEquals("*", emphasis.getOpeningDelimiter()); 53 | Assert.assertEquals("*", emphasis.getClosingDelimiter()); 54 | Assert.assertEquals("**", strong.getOpeningDelimiter()); 55 | Assert.assertEquals("**", strong.getClosingDelimiter()); 56 | Assert.assertEquals("_", important.getOpeningDelimiter()); 57 | Assert.assertEquals("_", important.getClosingDelimiter()); 58 | Assert.assertEquals("__", critical.getOpeningDelimiter()); 59 | Assert.assertEquals("__", critical.getClosingDelimiter()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /example/source/test/FencedCodeBlockParserTest.d: -------------------------------------------------------------------------------- 1 | module test.FencedCodeBlockParserTest; 2 | 3 | 4 | import hunt.markdown.node.FencedCodeBlock; 5 | import hunt.markdown.node.Node; 6 | import hunt.markdown.parser.Parser; 7 | import hunt.markdown.renderer.html.HtmlRenderer; 8 | import test.RenderingTestCase; 9 | import hunt.Assert; 10 | 11 | 12 | public class FencedCodeBlockParserTest : RenderingTestCase { 13 | 14 | private static Parser PARSER ; 15 | private static HtmlRenderer RENDERER; 16 | 17 | static this() 18 | { 19 | PARSER = Parser.builder().build(); 20 | RENDERER = HtmlRenderer.builder().build(); 21 | } 22 | 23 | public void test() 24 | { 25 | backtickInfo(); 26 | backtickInfoDoesntAllowBacktick(); 27 | backtickAndTildeCantBeMixed(); 28 | closingCanHaveSpacesAfter(); 29 | closingCanNotHaveNonSpaces(); 30 | } 31 | 32 | public void backtickInfo() { 33 | Node document = PARSER.parse("```info ~ test\ncode\n```"); 34 | FencedCodeBlock codeBlock = cast(FencedCodeBlock) (document.getFirstChild()); 35 | Assert.assertEquals("info ~ test", codeBlock.getInfo()); 36 | Assert.assertEquals("code\n", codeBlock.getLiteral()); 37 | } 38 | 39 | 40 | public void backtickInfoDoesntAllowBacktick() { 41 | assertRendering("```info ` test\ncode\n```", 42 | "```info ` test\ncode
\n
\n");
43 | // Note, it's unclear in the spec whether a ~~~ code block can contain ` in info or not, see:
44 | // https://github.com/commonmark/CommonMark/issues/119
45 | }
46 |
47 |
48 | public void backtickAndTildeCantBeMixed() {
49 | assertRendering("``~`\ncode\n``~`",
50 | "~` code
~`
code\n
\n");
57 | }
58 |
59 |
60 | public void closingCanNotHaveNonSpaces() {
61 | assertRendering("```\ncode\n``` a",
62 | "code\n``` a\n
\n");
63 | }
64 |
65 | override
66 | protected string render(string source) {
67 | return RENDERER.render(PARSER.parse(source));
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/example/source/test/HeadingParserTest.d:
--------------------------------------------------------------------------------
1 | module test.HeadingParserTest;
2 |
3 | import hunt.markdown.parser.Parser;
4 | import hunt.markdown.renderer.html.HtmlRenderer;
5 | import test.RenderingTestCase;
6 | import hunt.Assert;
7 |
8 | public class HeadingParserTest : RenderingTestCase {
9 |
10 | private static Parser PARSER;
11 | private static HtmlRenderer RENDERER;
12 |
13 | static this()
14 | {
15 | PARSER = Parser.builder().build();
16 | RENDERER = HtmlRenderer.builder().build();
17 | }
18 |
19 | public void test()
20 | {
21 | atxHeadingStart();
22 | atxHeadingTrailing();
23 | atxHeadingSurrogates();
24 | setextHeadingMarkers();
25 | }
26 |
27 | public void atxHeadingStart() {
28 | assertRendering("# test", "####### test
\n"); 31 | assertRendering("#test", "#test
\n"); 32 | assertRendering("#", "\n"); 33 | } 34 | 35 | 36 | public void atxHeadingTrailing() { 37 | assertRendering("# test #", "test\n==== =
\n"); 58 | assertRendering("test\n=-=", "test\n=-=
\n"); 59 | assertRendering("test\n=a", "test\n=a
\n"); 60 | } 61 | 62 | override 63 | protected string render(string source) { 64 | return RENDERER.render(PARSER.parse(source)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /example/source/test/ListTightLooseTest.d: -------------------------------------------------------------------------------- 1 | module test.ListTightLooseTest; 2 | 3 | import hunt.Assert; 4 | import test.CoreRenderingTestCase; 5 | 6 | public class ListTightLooseTest : CoreRenderingTestCase { 7 | 8 | public void test() 9 | { 10 | tight(); 11 | loose(); 12 | looseNested(); 13 | looseNested2(); 14 | looseOuter(); 15 | looseListItem(); 16 | tightWithBlankLineAfter(); 17 | tightListWithCodeBlock(); 18 | tightListWithCodeBlock2(); 19 | looseEmptyListItem(); 20 | looseBlankLineAfterCodeBlock(); 21 | } 22 | 23 | public void tight() { 24 | assertRendering("- foo\n" ~ 25 | "- bar\n" ~ 26 | "+ baz\n", 27 | "foo
\n" ~ 46 | "bar
\n" ~ 49 | "baz
\n" ~ 52 | "bar
\n" ~ 68 | "baz
\n" ~ 69 | "b
\n" ~ 87 | "c
\n" ~ 88 | "foo
\n" ~ 104 | "baz
\n" ~ 108 | "one
\n" ~ 119 | "two
\n" ~ 120 | "b\n" ~
148 | "\n" ~
149 | "\n" ~
150 | "
\n" ~
151 | "bar\n" ~
167 | "\n" ~
168 | "
\n" ~
169 | "baza
\n" ~ 182 | "c
\n" ~ 186 | "foo\n" ~
200 | "
\n" ~
201 | "bar
\n" ~ 202 | "foo\uFFFDbar
\n"); 39 | } 40 | 41 | 42 | public void nullCharacterEntityShouldBeReplaced() { 43 | assertRendering("foobar", "foo\uFFFDbar
\n"); 44 | } 45 | 46 | 47 | public void crLfAsLineSeparatorShouldBeParsed() { 48 | assertRendering("foo\r\nbar", "foo\nbar
\n"); 49 | } 50 | 51 | 52 | public void crLfAtEndShouldBeParsed() { 53 | assertRendering("foo\r\n", "foo
\n"); 54 | } 55 | 56 | 57 | public void mixedLineSeparators() { 58 | assertRendering("- a\n- b\r- c\r\n- d", "a
\nb
\nc
\nd
\ne
\n"); 60 | } 61 | 62 | 63 | public void surrogatePair() { 64 | assertRendering("surrogate pair: \u4F60\u597D", "surrogate pair: \u4F60\u597D
\n"); 65 | } 66 | 67 | 68 | public void surrogatePairInLinkDestination() { 69 | assertRendering("[title](\u4F60\u597D)", "\n"); 70 | } 71 | 72 | 73 | public void indentedCodeBlockWithMixedTabsAndSpaces() { 74 | assertRendering(" foo\n\tbar", "foo\nbar\n
\n");
75 | }
76 |
77 |
78 | public void tightListInBlockQuote() {
79 | assertRendering("> *\n> * a", "\n\n"); 80 | } 81 | 82 | 83 | public void looseListInBlockQuote() { 84 | // Second line in block quote is considered blank for purpose of loose list 85 | assertRendering("> *\n>\n> * a", "\n\n
\n- a
\n
\n\n"); 86 | } 87 | 88 | 89 | public void lineWithOnlySpacesAfterListBullet() { 90 | assertRendering("- \n \n foo\n", "\n\n
\n- \n
\na
\n
foo
\n"); 91 | } 92 | 93 | 94 | public void listWithTwoSpacesForFirstBullet() { 95 | // We have two spaces after the bullet, but no content. With content, the next line would be required 96 | assertRendering("* \n foo\n", "foo
\nbar
\nfoo
\nbar
\n[a[b]
\n[a[b]: /
\n"); 115 | assertRendering("[a]b]\n\n[a]b]: /", "[a]b]
\n[a]b]: /
\n"); 116 | assertRendering("[a[b]]\n\n[a[b]]: /", "[a[b]]
\n[a[b]]: /
\n"); 117 | } 118 | 119 | 120 | public void linkLabelLength() { 121 | string label1 = repeat("a", 999); 122 | assertRendering("[foo][" ~ label1 ~ "]\n\n[" ~ label1 ~ "]: /", "\n"); 123 | assertRendering("[foo][x" ~ label1 ~ "]\n\n[x" ~ label1 ~ "]: /", 124 | "[foo][x" ~ label1 ~ "]
\n[x" ~ label1 ~ "]: /
\n"); 125 | assertRendering("[foo][\n" ~ label1 ~ "]\n\n[\n" ~ label1 ~ "]: /", 126 | "[foo][\n" ~ label1 ~ "]
\n[\n" ~ label1 ~ "]: /
\n"); 127 | 128 | string label2 = repeat("a\n", 499); 129 | assertRendering("[foo][" ~ label2 ~ "]\n\n[" ~ label2 ~ "]: /", "\n"); 130 | assertRendering("[foo][12" ~ label2 ~ "]\n\n[12" ~ label2 ~ "]: /", 131 | "[foo][12" ~ label2 ~ "]
\n[12" ~ label2 ~ "]: /
\n"); 132 | } 133 | 134 | 135 | public void linkDestinationEscaping() { 136 | // Backslash escapes `)` 137 | assertRendering("[foo](\\))", "\n"); 138 | // ` ` is not escapable, so the backslash is a literal backslash and there's an optional space at the end 139 | assertRendering("[foo](\\ )", "\n"); 140 | // Backslash escapes `>`, so it's not a `(<...>)` link, but a `(...)` link instead 141 | assertRendering("[foo](<\\>)", "\n"); 142 | // Backslash is a literal, so valid 143 | assertRendering("[foo]()", "\n"); 144 | // Backslash escapes `>` but there's another `>`, valid 145 | assertRendering("[foo](>)", "\n"); 146 | } 147 | 148 | // commonmark/CommonMark#468 149 | 150 | public void linkReferenceBackslash() { 151 | // Backslash escapes ']', so not a valid link label 152 | assertRendering("[\\]: test", "[]: test
\n"); 153 | // Backslash is a literal, so valid 154 | assertRendering("[a\\b]\n\n[a\\b]: test", "\n"); 155 | // Backslash escapes `]` but there's another `]`, valid 156 | assertRendering("[a\\]]\n\n[a\\]]: test", "\n"); 157 | } 158 | 159 | // commonmark/cmark#177 160 | 161 | public void emphasisMultipleOf3Rule() { 162 | // assertRendering("a***b* c*", "a*b c
\n"); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /example/source/test/UsageExampleTest.d: -------------------------------------------------------------------------------- 1 | module test.UsageExampleTest; 2 | 3 | import hunt.markdown.node; 4 | import hunt.markdown.parser.Parser; 5 | import hunt.markdown.renderer.NodeRenderer; 6 | import hunt.markdown.renderer.html; 7 | import hunt.markdown.node.AbstractVisitor; 8 | import hunt.markdown.renderer.html.AttributeProvider; 9 | import hunt.markdown.renderer.html.HtmlWriter; 10 | import hunt.markdown.renderer.html.HtmlNodeRendererContext; 11 | import hunt.markdown.renderer.html.HtmlRenderer; 12 | import hunt.markdown.renderer.html.AttributeProviderContext; 13 | import hunt.markdown.renderer.html.AttributeProviderFactory; 14 | import hunt.markdown.renderer.html.HtmlNodeRendererFactory; 15 | 16 | import hunt.collection.Map; 17 | import hunt.collection.Set; 18 | import hunt.collection.HashSet; 19 | import hunt.Assert; 20 | 21 | public class UsageExampleTest { 22 | 23 | public void test() 24 | { 25 | parseAndRender(); 26 | visitor(); 27 | addAttributes(); 28 | customizeRendering(); 29 | } 30 | public void parseAndRender() { 31 | Parser parser = Parser.builder().build(); 32 | Node document = parser.parse("This is *Sparta*"); 33 | HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).build(); 34 | Assert.assertEquals("This is Sparta
\n", renderer.render(document)); 35 | } 36 | 37 | 38 | 39 | public void parseReaderRender() { 40 | Parser parser = Parser.builder().build(); 41 | // try (InputStreamReader reader = new InputStreamReader(new FileInputStream("file.md"), StandardCharsets.UTF_8)) { 42 | // Node document = parser.parseReader(reader); 43 | // // ... 44 | // } 45 | } 46 | 47 | 48 | public void visitor() { 49 | Parser parser = Parser.builder().build(); 50 | Node node = parser.parse("Example\n=======\n\nSome more text"); 51 | WordCountVisitor visitor = new WordCountVisitor(); 52 | node.accept(visitor); 53 | Assert.assertEquals(4, visitor.wordCount); 54 | } 55 | 56 | 57 | public void addAttributes() { 58 | Parser parser = Parser.builder().build(); 59 | HtmlRenderer renderer = HtmlRenderer.builder() 60 | .attributeProviderFactory(new class AttributeProviderFactory { 61 | public AttributeProvider create(AttributeProviderContext context) { 62 | return new ImageAttributeProvider(); 63 | } 64 | }) 65 | .build(); 66 | 67 | Node document = parser.parse(""); 68 | Assert.assertEquals("Example:
\ncode\n\n", renderer.render(document)); 85 | } 86 | 87 | class WordCountVisitor : AbstractVisitor { 88 | 89 | int wordCount = 0; 90 | 91 | override 92 | public void visit(Text text) { 93 | // This is called for all Text nodes. Override other visit methods for other node types. 94 | 95 | // Count words (this is just an example, don't actually do it this way for various reasons). 96 | import std.regex; 97 | wordCount += text.getLiteral().split(regex("\\W+")).length; 98 | 99 | // Descend into children (could be omitted in this case because Text nodes don't have children). 100 | visitChildren(text); 101 | } 102 | } 103 | 104 | class ImageAttributeProvider : AttributeProvider { 105 | override 106 | public void setAttributes(Node node, string tagName, Map!(string,string) attributes) { 107 | if (cast(Image)node !is null) { 108 | attributes.put("class", "border"); 109 | } 110 | } 111 | } 112 | 113 | class IndentedCodeBlockNodeRenderer : NodeRenderer { 114 | 115 | private HtmlWriter html; 116 | 117 | this(HtmlNodeRendererContext context) { 118 | this.html = context.getWriter(); 119 | } 120 | 121 | override 122 | public Set!(TypeInfo_Class) getNodeTypes() { 123 | // Return the node types we want to use this renderer for. 124 | return new HashSet!(TypeInfo_Class)([typeid(IndentedCodeBlock)]); 125 | } 126 | 127 | override 128 | public void render(Node node) { 129 | // We only handle one type as per getNodeTypes, so we can just cast it here. 130 | IndentedCodeBlock codeBlock = cast(IndentedCodeBlock) node; 131 | html.line(); 132 | html.tag("pre"); 133 | html.text(codeBlock.getLiteral()); 134 | html.tag("/pre"); 135 | html.line(); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /source/hunt/markdown/Extension.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.Extension; 2 | 3 | /** 4 | * Base interface for a parser/renderer extension. 5 | *
6 | * Doesn't have any methods itself, but has specific sub interfaces to 7 | * configure parser/renderer. This base interface is for convenience, so that a list of extensions can be built and then 8 | * used for configuring both the parser and renderer in the same way. 9 | */ 10 | public interface Extension { 11 | } 12 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/heading/anchor/HeadingAnchorExtension.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.heading.anchor.HeadingAnchorExtension; 2 | 3 | import hunt.markdown.Extension; 4 | import hunt.markdown.ext.heading.anchor.internal.HeadingIdAttributeProvider; 5 | import hunt.markdown.renderer.html.AttributeProvider; 6 | import hunt.markdown.renderer.html.AttributeProviderContext; 7 | import hunt.markdown.renderer.html.AttributeProviderFactory; 8 | import hunt.markdown.renderer.html.HtmlRenderer; 9 | 10 | /** 11 | * Extension for adding auto generated IDs to headings. 12 | *
13 | * Create it with {@link #create()} or {@link #builder()} and then configure it on the 14 | * renderer builder ({@link HtmlRenderer.Builder#extensions(Iterable)}). 15 | *
16 | * The heading text will be used to create the id. Multiple headings with the 17 | * same text will result in appending a hyphen and number. For example: 18 | *
19 | * # Heading
20 | * # Heading
21 | *
22 | * will result in
23 | *
24 | * <h1 id="heading">Heading</h1>
25 | * <h1 id="heading-1">Heading</h1>
26 | *
27 | *
28 | * @see IdGenerator the IdGenerator class if just the ID generation part is needed
29 | */
30 | class HeadingAnchorExtension : HtmlRenderer.HtmlRendererExtension {
31 |
32 | private string defaultId;
33 | private string idPrefix;
34 | private string idSuffix;
35 |
36 | private this(Builder builder) {
37 | this.defaultId = builder._defaultId;
38 | this.idPrefix = builder._idPrefix;
39 | this.idSuffix = builder._idSuffix;
40 | }
41 |
42 | /**
43 | * @return the extension built with default settings
44 | */
45 | public static Extension create() {
46 | return new HeadingAnchorExtension(builder());
47 | }
48 |
49 | /**
50 | * @return a builder to configure the extension settings
51 | */
52 | public static Builder builder() {
53 | return new Builder();
54 | }
55 |
56 | override public void extend(HtmlRenderer.Builder rendererBuilder) {
57 | rendererBuilder.attributeProviderFactory(new class AttributeProviderFactory {
58 | override public AttributeProvider create(AttributeProviderContext context) {
59 | return HeadingIdAttributeProvider.create(defaultId, idPrefix, idSuffix);
60 | }
61 | });
62 | }
63 |
64 | public static class Builder {
65 | private string _defaultId = "id";
66 | private string _idPrefix = "";
67 | private string _idSuffix = "";
68 |
69 | /**
70 | * @param value Default value for the id to take if no generated id can be extracted. Default "id"
71 | * @return {@code this}
72 | */
73 | public Builder defaultId(string value) {
74 | this._defaultId = value;
75 | return this;
76 | }
77 |
78 | /**
79 | * @param value Set the value to be prepended to every id generated. Default ""
80 | * @return {@code this}
81 | */
82 | public Builder idPrefix(string value) {
83 | this._idPrefix = value;
84 | return this;
85 | }
86 |
87 | /**
88 | * @param value Set the value to be appended to every id generated. Default ""
89 | * @return {@code this}
90 | */
91 | public Builder idSuffix(string value) {
92 | this._idSuffix = value;
93 | return this;
94 | }
95 |
96 | /**
97 | * @return a configured extension
98 | */
99 | public Extension build() {
100 | return new HeadingAnchorExtension(this);
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/source/hunt/markdown/ext/heading/anchor/IdGenerator.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.ext.heading.anchor.IdGenerator;
2 |
3 | import hunt.collection.HashMap;
4 | import hunt.collection.Map;
5 | import hunt.Integer;
6 |
7 | import std.string;
8 | import std.regex;
9 | import std.conv : to;
10 |
11 | import hunt.text;
12 | import hunt.util.StringBuilder;
13 | /**
14 | * Generates strings to be used as identifiers.
15 | * 16 | * Use {@link #builder()} to create an instance. 17 | */ 18 | class IdGenerator { 19 | private Regex!char allowedCharacters; 20 | private Map!(string, int) identityMap; 21 | private string prefix; 22 | private string suffix; 23 | private string defaultIdentifier; 24 | 25 | private this(Builder builder) { 26 | this.allowedCharacters = compileAllowedCharactersPattern(); 27 | this.defaultIdentifier = builder._defaultIdentifier; 28 | this.prefix = builder._prefix; 29 | this.suffix = builder._suffix; 30 | this.identityMap = new HashMap!(string, int)(); 31 | } 32 | 33 | /** 34 | * @return a new builder with default arguments 35 | */ 36 | public static Builder builder() { 37 | return new Builder(); 38 | } 39 | 40 | /** 41 | *
42 | * Generate an ID based on the provided text and previously generated IDs. 43 | *
44 | * This method is not thread safe, concurrent calls can end up 45 | * with non-unique identifiers. 46 | *
47 | * Note that collision can occur in the case that 48 | *
54 | * In that case, the three generated IDs will be: 55 | *
61 | * Therefore if collisions are unacceptable you should ensure that 62 | * numbers are stripped from end of {@code text}. 63 | * 64 | * @param text Text that the identifier should be based on. Will be normalised, then used to generate the 65 | * identifier. 66 | * @return {@code text} if this is the first instance that the {@code text} has been passed 67 | * to the method. Otherwise, {@code text ~ "-" ~ X} will be returned, where X is the number of times 68 | * that {@code text} has previously been passed in. If {@code text} is empty, the default 69 | * identifier given in the constructor will be used. 70 | */ 71 | public string generateId(string text) { 72 | string normalizedIdentity = text !is null ? normalizeText(text) : defaultIdentifier; 73 | 74 | if (normalizedIdentity.length == 0) { 75 | normalizedIdentity = defaultIdentifier; 76 | } 77 | 78 | if (!identityMap.containsKey(normalizedIdentity)) { 79 | identityMap.put(normalizedIdentity, 1); 80 | return prefix ~ normalizedIdentity ~ suffix; 81 | } else { 82 | int currentCount = identityMap.get(normalizedIdentity); 83 | identityMap.put(normalizedIdentity, currentCount + 1); 84 | return prefix ~ normalizedIdentity ~ "-" ~ currentCount.to!string() ~ suffix; 85 | } 86 | } 87 | 88 | private static Regex!char compileAllowedCharactersPattern() { 89 | return regex("[\\w\\-_]+"); 90 | } 91 | 92 | /** 93 | * Assume we've been given a space separated text. 94 | * 95 | * @param text Text to normalize to an ID 96 | */ 97 | // private string normalizeText(string text) { 98 | // string firstPassNormalising = text.toLower().replace(" ", "-"); 99 | 100 | // StringBuilder sb = new StringBuilder(); 101 | // Matcher matcher = allowedCharacters.matcher(firstPassNormalising); 102 | 103 | // while (matcher.find()) { 104 | // sb.append(matcher.group()); 105 | // } 106 | 107 | // return sb.toString(); 108 | // } 109 | 110 | private string normalizeText(string text) { 111 | string firstPassNormalising = text.toLower().replace(" ", "-"); 112 | 113 | StringBuilder sb = new StringBuilder(); 114 | 115 | foreach (c ; matchAll(firstPassNormalising, allowedCharacters)) { 116 | sb.append(c[0]); 117 | } 118 | 119 | return sb.toString(); 120 | } 121 | 122 | public static class Builder { 123 | private string _defaultIdentifier = "id"; 124 | private string _prefix = ""; 125 | private string _suffix = ""; 126 | 127 | public IdGenerator build() { 128 | return new IdGenerator(this); 129 | } 130 | 131 | /** 132 | * @param defaultId the default identifier to use in case the provided text is empty or only contains unusable characters 133 | * @return {@code this} 134 | */ 135 | public Builder defaultId(string defaultId) { 136 | this._defaultIdentifier = defaultId; 137 | return this; 138 | } 139 | 140 | /** 141 | * @param prefix the text to place before the generated identity 142 | * @return {@code this} 143 | */ 144 | public Builder prefix(string prefix) { 145 | this._prefix = prefix; 146 | return this; 147 | } 148 | 149 | /** 150 | * @param suffix the text to place after the generated identity 151 | * @return {@code this} 152 | */ 153 | public Builder suffix(string suffix) { 154 | this._suffix = suffix; 155 | return this; 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/heading/anchor/internal/HeadingIdAttributeProvider.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.heading.anchor.internal.HeadingIdAttributeProvider; 2 | 3 | import hunt.markdown.ext.heading.anchor.IdGenerator; 4 | import hunt.markdown.renderer.html.AttributeProvider; 5 | import hunt.markdown.node.Node; 6 | import hunt.markdown.node.Heading; 7 | import hunt.markdown.node.Code; 8 | import hunt.markdown.node.Text; 9 | import hunt.markdown.node.AbstractVisitor; 10 | 11 | import hunt.collection.ArrayList; 12 | import hunt.collection.List; 13 | import hunt.collection.Map; 14 | 15 | import std.string; 16 | 17 | class HeadingIdAttributeProvider : AttributeProvider { 18 | 19 | private IdGenerator idGenerator; 20 | 21 | private this(string defaultId, string prefix, string suffix) { 22 | idGenerator = IdGenerator.builder() 23 | .defaultId(defaultId) 24 | .prefix(prefix) 25 | .suffix(suffix) 26 | .build(); 27 | } 28 | 29 | public static HeadingIdAttributeProvider create(string defaultId, string prefix, string suffix) { 30 | return new HeadingIdAttributeProvider(defaultId, prefix, suffix); 31 | } 32 | 33 | override public void setAttributes(Node node, string tagName, Map!(string, string) attributes) { 34 | 35 | if (cast(Heading)node !is null) { 36 | 37 | List!(string) wordList = new ArrayList!(string)(); 38 | 39 | node.accept(new class AbstractVisitor { 40 | override public void visit(Text text) { 41 | wordList.add(text.getLiteral()); 42 | } 43 | 44 | override public void visit(Code code) { 45 | wordList.add(code.getLiteral()); 46 | } 47 | }); 48 | 49 | string finalstring = ""; 50 | foreach (string word ; wordList) { 51 | finalstring ~= word; 52 | } 53 | finalstring = strip(finalstring).toLower(); 54 | 55 | attributes.put("id", idGenerator.generateId(finalstring)); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/ins/Ins.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.ins.Ins; 2 | 3 | import hunt.markdown.node.CustomNode; 4 | import hunt.markdown.node.Delimited; 5 | 6 | /** 7 | * An ins node containing text and other inline nodes as children. 8 | */ 9 | class Ins : CustomNode, Delimited { 10 | 11 | private enum string DELIMITER = "++"; 12 | 13 | override public string getOpeningDelimiter() { 14 | return DELIMITER; 15 | } 16 | 17 | override public string getClosingDelimiter() { 18 | return DELIMITER; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/ins/InsExtension.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.ins.InsExtension; 2 | 3 | import hunt.markdown.Extension; 4 | import hunt.markdown.ext.ins.internal.InsDelimiterProcessor; 5 | import hunt.markdown.ext.ins.internal.InsNodeRenderer; 6 | import hunt.markdown.renderer.html.HtmlNodeRendererContext; 7 | import hunt.markdown.renderer.html.HtmlNodeRendererFactory; 8 | import hunt.markdown.parser.Parser; 9 | import hunt.markdown.renderer.html.HtmlRenderer; 10 | import hunt.markdown.renderer.NodeRenderer; 11 | 12 | /** 13 | * Extension for ins using ++ 14 | *
15 | * Create it with {@link #create()} and then configure it on the builders 16 | * ({@link hunt.markdown.parser.Parser.Builder#extensions(Iterable)}, 17 | * {@link HtmlRenderer.Builder#extensions(Iterable)}). 18 | *
19 | *20 | * The parsed ins text regions are turned into {@link Ins} nodes. 21 | *
22 | */ 23 | class InsExtension : Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension { 24 | 25 | private this() { 26 | } 27 | 28 | public static Extension create() { 29 | return new InsExtension(); 30 | } 31 | 32 | override public void extend(Parser.Builder parserBuilder) { 33 | parserBuilder.customDelimiterProcessor(new InsDelimiterProcessor()); 34 | } 35 | 36 | override public void extend(HtmlRenderer.Builder rendererBuilder) { 37 | rendererBuilder.nodeRendererFactory(new class HtmlNodeRendererFactory { 38 | override public NodeRenderer create(HtmlNodeRendererContext context) { 39 | return new InsNodeRenderer(context); 40 | } 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/ins/internal/InsDelimiterProcessor.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.ins.internal.InsDelimiterProcessor; 2 | 3 | import hunt.markdown.ext.ins.Ins; 4 | import hunt.markdown.node.Node; 5 | import hunt.markdown.node.Text; 6 | import hunt.markdown.parser.delimiter.DelimiterProcessor; 7 | import hunt.markdown.parser.delimiter.DelimiterRun; 8 | 9 | class InsDelimiterProcessor : DelimiterProcessor { 10 | 11 | override public char getOpeningCharacter() { 12 | return '+'; 13 | } 14 | 15 | override public char getClosingCharacter() { 16 | return '+'; 17 | } 18 | 19 | override public int getMinLength() { 20 | return 2; 21 | } 22 | 23 | override public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) { 24 | if (opener.length >= 2 && closer.length >= 2) { 25 | // Use exactly two delimiters even if we have more, and don't care about internal openers/closers. 26 | return 2; 27 | } else { 28 | return 0; 29 | } 30 | } 31 | 32 | override public void process(Text opener, Text closer, int delimiterCount) { 33 | // Wrap nodes between delimiters in ins. 34 | Node ins = new Ins(); 35 | 36 | Node tmp = opener.getNext(); 37 | while (tmp !is null && tmp != closer) { 38 | Node next = tmp.getNext(); 39 | ins.appendChild(tmp); 40 | tmp = next; 41 | } 42 | 43 | opener.insertAfter(ins); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/ins/internal/InsNodeRenderer.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.ins.internal.InsNodeRenderer; 2 | 3 | import hunt.markdown.ext.ins.Ins; 4 | import hunt.markdown.renderer.html.HtmlWriter; 5 | import hunt.markdown.renderer.html.HtmlNodeRendererContext; 6 | import hunt.markdown.node.Node; 7 | import hunt.markdown.renderer.NodeRenderer; 8 | 9 | import hunt.collection.Collections; 10 | import hunt.collection.Map; 11 | import hunt.collection.Set; 12 | 13 | class InsNodeRenderer : NodeRenderer { 14 | 15 | private HtmlNodeRendererContext context; 16 | private HtmlWriter html; 17 | 18 | public this(HtmlNodeRendererContext context) { 19 | this.context = context; 20 | this.html = context.getWriter(); 21 | } 22 | 23 | public Set!(TypeInfo_Class) getNodeTypes() { 24 | return Collections.singleton!(TypeInfo_Class)(typeid(Ins)); 25 | } 26 | 27 | public void render(Node node) { 28 | Map!(string, string) attributes = context.extendAttributes(node, "ins", Collections.emptyMap!(string, string)()); 29 | html.tag("ins", attributes); 30 | renderChildren(node); 31 | html.tag("/ins"); 32 | } 33 | 34 | private void renderChildren(Node parent) { 35 | Node node = parent.getFirstChild(); 36 | while (node !is null) { 37 | Node next = node.getNext(); 38 | context.render(node); 39 | node = next; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/matter/YamlFrontMatterBlock.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.front.matter.YamlFrontMatterBlock; 2 | 3 | import hunt.markdown.node.CustomBlock; 4 | 5 | class YamlFrontMatterBlock : CustomBlock { 6 | } 7 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/matter/YamlFrontMatterExtension.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.front.matter.YamlFrontMatterExtension; 2 | 3 | import hunt.markdown.Extension; 4 | import hunt.markdown.ext.matter.internal.YamlFrontMatterBlockParser; 5 | import hunt.markdown.parser.Parser; 6 | import hunt.markdown.renderer.html.HtmlRenderer; 7 | 8 | /** 9 | * Extension for YAML-like metadata. 10 | *11 | * Create it with {@link #create()} and then configure it on the builders 12 | * ({@link hunt.markdown.parser.Parser.Builder#extensions(Iterable)}, 13 | * {@link HtmlRenderer.Builder#extensions(Iterable)}). 14 | *
15 | *16 | * The parsed metadata is turned into {@link YamlFrontMatterNode}. You can access the metadata using {@link YamlFrontMatterVisitor}. 17 | *
18 | */ 19 | class YamlFrontMatterExtension : Parser.ParserExtension { 20 | 21 | private this() { 22 | } 23 | 24 | override public void extend(Parser.Builder parserBuilder) { 25 | parserBuilder.customBlockParserFactory(new YamlFrontMatterBlockParser.Factory()); 26 | } 27 | 28 | public static Extension create() { 29 | return new YamlFrontMatterExtension(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/matter/YamlFrontMatterNode.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.front.matter.YamlFrontMatterNode; 2 | 3 | import hunt.markdown.node.CustomNode; 4 | 5 | import hunt.collection.List; 6 | 7 | class YamlFrontMatterNode : CustomNode { 8 | private string key; 9 | private List!(string) values; 10 | 11 | public this(string key, List!(string) values) { 12 | this.key = key; 13 | this.values = values; 14 | } 15 | 16 | public string getKey() { 17 | return key; 18 | } 19 | 20 | public void setKey(string key) { 21 | this.key = key; 22 | } 23 | 24 | public List!(string) getValues() { 25 | return values; 26 | } 27 | 28 | public void setValues(List!(string) values) { 29 | this.values = values; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/matter/YamlFrontMatterVisitor.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.matter.YamlFrontMatterVisitor; 2 | 3 | import hunt.markdown.node.AbstractVisitor; 4 | import hunt.markdown.node.CustomNode; 5 | 6 | import hunt.markdown.ext.front.matter.YamlFrontMatterNode; 7 | import hunt.collection.LinkedHashMap; 8 | import hunt.collection.List; 9 | import hunt.collection.Map; 10 | 11 | class YamlFrontMatterVisitor : AbstractVisitor { 12 | private Map!(string, List!(string)) data; 13 | 14 | public this() { 15 | data = new LinkedHashMap!(string, List!(string))(); 16 | } 17 | 18 | override public void visit(CustomNode customNode) { 19 | if (cast(YamlFrontMatterNode)customNode !is null) { 20 | data.put((cast(YamlFrontMatterNode) customNode).getKey(), (cast(YamlFrontMatterNode) customNode).getValues()); 21 | } else { 22 | super.visit(customNode); 23 | } 24 | } 25 | 26 | public Map!(string, List!(string)) getData() { 27 | return data; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/matter/internal/YamlFrontMatterBlockParser.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.matter.internal.YamlFrontMatterBlockParser; 2 | 3 | import hunt.markdown.ext.front.matter.YamlFrontMatterBlock; 4 | import hunt.markdown.ext.front.matter.YamlFrontMatterNode; 5 | import hunt.markdown.internal.DocumentBlockParser; 6 | import hunt.markdown.node.Block; 7 | import hunt.markdown.parser.InlineParser; 8 | import hunt.markdown.parser.block.AbstractBlockParser; 9 | import hunt.markdown.parser.block.AbstractBlockParserFactory; 10 | import hunt.markdown.parser.block.BlockContinue; 11 | import hunt.markdown.parser.block.ParserState; 12 | import hunt.markdown.parser.block.BlockStart; 13 | import hunt.markdown.parser.block.MatchedBlockParser; 14 | import hunt.markdown.parser.block.BlockParser; 15 | 16 | import hunt.collection.ArrayList; 17 | import hunt.collection.List; 18 | 19 | import std.string; 20 | import std.regex; 21 | import hunt.util.Comparator; 22 | 23 | class YamlFrontMatterBlockParser : AbstractBlockParser { 24 | private __gshared Regex!char REGEX_METADATA = regex("^[ ]{0,3}([A-Za-z0-9_-]+):\\s*(.*)"); 25 | private __gshared Regex!char REGEX_METADATA_LIST = regex("^[ ]+-\\s*(.*)"); 26 | private __gshared Regex!char REGEX_METADATA_LITERAL = regex("^\\s*(.*)"); 27 | private __gshared Regex!char REGEX_BEGIN = regex("^-{3}(\\s.*)?"); 28 | private __gshared Regex!char REGEX_END = regex("^(-{3}|\\.{3})(\\s.*)?"); 29 | 30 | private bool inLiteral; 31 | private string currentKey; 32 | private List!(string) currentValues; 33 | private YamlFrontMatterBlock block; 34 | 35 | public this() { 36 | inLiteral = false; 37 | currentKey = null; 38 | currentValues = new ArrayList!(string)(); 39 | block = new YamlFrontMatterBlock(); 40 | } 41 | 42 | // /* override */ int opCmp(BlockParser o) 43 | // { 44 | // auto cmp = compare(this.currentKey,(cast(YamlFrontMatterBlockParser)o).currentKey); 45 | // if(cmp == 0) 46 | // { 47 | // cmp = compare(this.inLiteral,(cast(YamlFrontMatterBlockParser)o).inLiteral); 48 | // } 49 | // return cmp; 50 | // } 51 | 52 | 53 | override public Block getBlock() { 54 | return block; 55 | } 56 | 57 | override public void addLine(string line) { 58 | } 59 | 60 | public BlockContinue tryContinue(ParserState parserState) { 61 | string line = parserState.getLine(); 62 | 63 | if (match(line, REGEX_END)) { 64 | if (currentKey !is null) { 65 | block.appendChild(new YamlFrontMatterNode(currentKey, currentValues)); 66 | } 67 | return BlockContinue.finished(); 68 | } 69 | 70 | auto matches = match(line, REGEX_METADATA); 71 | if (matches) { 72 | if (currentKey !is null) { 73 | block.appendChild(new YamlFrontMatterNode(currentKey, currentValues)); 74 | } 75 | 76 | inLiteral = false; 77 | currentKey = matches.front[1]; 78 | currentValues = new ArrayList!(string)(); 79 | if ("|" == matches.front[2]) { 80 | inLiteral = true; 81 | } else if ("" != matches.front[2]) { 82 | currentValues.add(matches.front[2]); 83 | } 84 | 85 | return BlockContinue.atIndex(parserState.getIndex()); 86 | } else { 87 | if (inLiteral) { 88 | matches = match(line, REGEX_METADATA_LITERAL); 89 | if (matches) { 90 | if (currentValues.size() == 1) { 91 | currentValues.set(0, currentValues.get(0) ~ "\n" ~ matches.front[1].strip()); 92 | } else { 93 | currentValues.add(matches.front[1].strip()); 94 | } 95 | } 96 | } else { 97 | matches = match(line, REGEX_METADATA_LIST); 98 | if (matches) { 99 | currentValues.add(matches.front[1]); 100 | } 101 | } 102 | 103 | return BlockContinue.atIndex(parserState.getIndex()); 104 | } 105 | } 106 | 107 | override public void parseInlines(InlineParser inlineParser) { 108 | } 109 | 110 | public static class Factory : AbstractBlockParserFactory { 111 | public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { 112 | string line = state.getLine(); 113 | BlockParser parentParser = matchedBlockParser.getMatchedBlockParser(); 114 | // check whether this line is the first line of whole document or not 115 | if (cast(DocumentBlockParser)parentParser !is null && parentParser.getBlock().getFirstChild() is null && match(line, REGEX_BEGIN)) { 116 | return BlockStart.of(new YamlFrontMatterBlockParser()).atIndex(state.getNextNonSpaceIndex()); 117 | } 118 | 119 | return BlockStart.none(); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/strikethrough/Strikethrough.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.gfm.strikethrough.Strikethrough; 2 | 3 | import hunt.markdown.node.CustomNode; 4 | import hunt.markdown.node.Delimited; 5 | 6 | /** 7 | * A strikethrough node containing text and other inline nodes nodes as children. 8 | */ 9 | class Strikethrough : CustomNode, Delimited { 10 | 11 | private __gshared string DELIMITER = "~~"; 12 | 13 | override public string getOpeningDelimiter() { 14 | return DELIMITER; 15 | } 16 | 17 | override public string getClosingDelimiter() { 18 | return DELIMITER; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/strikethrough/StrikethroughExtension.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.strikethrough.StrikethroughExtension; 2 | 3 | import hunt.markdown.Extension; 4 | import hunt.markdown.renderer.text.TextContentRenderer; 5 | import hunt.markdown.renderer.text.TextContentNodeRendererContext; 6 | import hunt.markdown.renderer.text.TextContentNodeRendererFactory; 7 | import hunt.markdown.ext.gfm.strikethrough.internal.StrikethroughDelimiterProcessor; 8 | import hunt.markdown.ext.gfm.strikethrough.internal.StrikethroughHtmlNodeRenderer; 9 | import hunt.markdown.ext.gfm.strikethrough.internal.StrikethroughTextContentNodeRenderer; 10 | import hunt.markdown.renderer.html.HtmlRenderer; 11 | import hunt.markdown.renderer.html.HtmlNodeRendererContext; 12 | import hunt.markdown.renderer.html.HtmlNodeRendererFactory; 13 | import hunt.markdown.parser.Parser; 14 | import hunt.markdown.renderer.NodeRenderer; 15 | 16 | /** 17 | * Extension for GFM strikethrough using ~~ (GitHub Flavored Markdown). 18 | *19 | * Create it with {@link #create()} and then configure it on the builders 20 | * ({@link hunt.markdown.parser.Parser.Builder#extensions(Iterable)}, 21 | * {@link HtmlRenderer.Builder#extensions(Iterable)}). 22 | *
23 | *24 | * The parsed strikethrough text regions are turned into {@link Strikethrough} nodes. 25 | *
26 | */ 27 | class StrikethroughExtension : Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension, 28 | TextContentRenderer.TextContentRendererExtension { 29 | 30 | private this() { 31 | } 32 | 33 | public static Extension create() { 34 | return new StrikethroughExtension(); 35 | } 36 | 37 | override public void extend(Parser.Builder parserBuilder) { 38 | parserBuilder.customDelimiterProcessor(new StrikethroughDelimiterProcessor()); 39 | } 40 | 41 | override public void extend(HtmlRenderer.Builder rendererBuilder) { 42 | rendererBuilder.nodeRendererFactory(new class HtmlNodeRendererFactory { 43 | override public NodeRenderer create(HtmlNodeRendererContext context) { 44 | return new StrikethroughHtmlNodeRenderer(context); 45 | } 46 | }); 47 | } 48 | 49 | override public void extend(TextContentRenderer.Builder rendererBuilder) { 50 | rendererBuilder.nodeRendererFactory(new class TextContentNodeRendererFactory { 51 | override public NodeRenderer create(TextContentNodeRendererContext context) { 52 | return new StrikethroughTextContentNodeRenderer(context); 53 | } 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/strikethrough/internal/StrikethroughDelimiterProcessor.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.gfm.strikethrough.internal.StrikethroughDelimiterProcessor; 2 | 3 | import hunt.markdown.ext.gfm.strikethrough.Strikethrough; 4 | import hunt.markdown.node.Node; 5 | import hunt.markdown.node.Text; 6 | import hunt.markdown.parser.delimiter.DelimiterProcessor; 7 | import hunt.markdown.parser.delimiter.DelimiterRun; 8 | 9 | class StrikethroughDelimiterProcessor : DelimiterProcessor { 10 | 11 | override public char getOpeningCharacter() { 12 | return '~'; 13 | } 14 | 15 | override public char getClosingCharacter() { 16 | return '~'; 17 | } 18 | 19 | override public int getMinLength() { 20 | return 2; 21 | } 22 | 23 | override public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) { 24 | if (opener.length >= 2 && closer.length >= 2) { 25 | // Use exactly two delimiters even if we have more, and don't care about internal openers/closers. 26 | return 2; 27 | } else { 28 | return 0; 29 | } 30 | } 31 | 32 | override public void process(Text opener, Text closer, int delimiterCount) { 33 | // Wrap nodes between delimiters in strikethrough. 34 | Node strikethrough = new Strikethrough(); 35 | 36 | Node tmp = opener.getNext(); 37 | while (tmp !is null && tmp != closer) { 38 | Node next = tmp.getNext(); 39 | strikethrough.appendChild(tmp); 40 | tmp = next; 41 | } 42 | 43 | opener.insertAfter(strikethrough); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/strikethrough/internal/StrikethroughHtmlNodeRenderer.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.gfm.strikethrough.internal.StrikethroughHtmlNodeRenderer; 2 | 3 | import hunt.markdown.renderer.html.HtmlWriter; 4 | import hunt.markdown.renderer.html.HtmlNodeRendererContext; 5 | import hunt.markdown.node.Node; 6 | import hunt.markdown.ext.gfm.strikethrough.internal.StrikethroughNodeRenderer; 7 | 8 | import hunt.collection.Collections; 9 | import hunt.collection.Map; 10 | 11 | class StrikethroughHtmlNodeRenderer : StrikethroughNodeRenderer { 12 | 13 | private HtmlNodeRendererContext context; 14 | private HtmlWriter html; 15 | 16 | public this(HtmlNodeRendererContext context) { 17 | this.context = context; 18 | this.html = context.getWriter(); 19 | } 20 | 21 | public void render(Node node) { 22 | Map!(string, string) attributes = context.extendAttributes(node, "del", Collections.emptyMap!(string, string)()); 23 | html.tag("del", attributes); 24 | renderChildren(node); 25 | html.tag("/del"); 26 | } 27 | 28 | private void renderChildren(Node parent) { 29 | Node node = parent.getFirstChild(); 30 | while (node !is null) { 31 | Node next = node.getNext(); 32 | context.render(node); 33 | node = next; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/strikethrough/internal/StrikethroughNodeRenderer.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.gfm.strikethrough.internal.StrikethroughNodeRenderer; 2 | 3 | import hunt.markdown.ext.gfm.strikethrough.Strikethrough; 4 | import hunt.markdown.node.Node; 5 | import hunt.markdown.renderer.NodeRenderer; 6 | 7 | import hunt.collection.Collections; 8 | import hunt.collection.Set; 9 | 10 | abstract class StrikethroughNodeRenderer : NodeRenderer { 11 | 12 | public Set!(TypeInfo_Class) getNodeTypes() { 13 | return Collections.singleton!(TypeInfo_Class)(typeid(Strikethrough)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/strikethrough/internal/StrikethroughTextContentNodeRenderer.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.gfm.strikethrough.internal.StrikethroughTextContentNodeRenderer; 2 | 3 | import hunt.markdown.renderer.text.TextContentWriter; 4 | import hunt.markdown.renderer.text.TextContentNodeRendererContext; 5 | import hunt.markdown.node.Node; 6 | import hunt.markdown.ext.gfm.strikethrough.internal.StrikethroughNodeRenderer; 7 | 8 | class StrikethroughTextContentNodeRenderer : StrikethroughNodeRenderer { 9 | 10 | private TextContentNodeRendererContext context; 11 | private TextContentWriter textContent; 12 | 13 | public this(TextContentNodeRendererContext context) { 14 | this.context = context; 15 | this.textContent = context.getWriter(); 16 | } 17 | 18 | public void render(Node node) { 19 | textContent.write('/'); 20 | renderChildren(node); 21 | textContent.write('/'); 22 | } 23 | 24 | private void renderChildren(Node parent) { 25 | Node node = parent.getFirstChild(); 26 | while (node !is null) { 27 | Node next = node.getNext(); 28 | context.render(node); 29 | node = next; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/table/TableBlock.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.table.TableBlock; 2 | 3 | import hunt.markdown.node.CustomBlock; 4 | 5 | /** 6 | * Table block containing a {@link TableHead} and optionally a {@link TableBody}. 7 | */ 8 | class TableBlock : CustomBlock { 9 | } 10 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/table/TableBody.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.table.TableBody; 2 | 3 | import hunt.markdown.node.CustomNode; 4 | 5 | /** 6 | * Body part of a {@link TableBlock} containing {@link TableRow TableRows}. 7 | */ 8 | class TableBody : CustomNode { 9 | } 10 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/table/TableCell.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.table.TableCell; 2 | 3 | import hunt.markdown.node.CustomNode; 4 | 5 | /** 6 | * Table cell of a {@link TableRow} containing inline nodes. 7 | */ 8 | class TableCell : CustomNode { 9 | 10 | private bool header; 11 | private Alignment alignment; 12 | 13 | /** 14 | * @return whether the cell is a header or not 15 | */ 16 | public bool isHeader() { 17 | return header; 18 | } 19 | 20 | public void setHeader(bool header) { 21 | this.header = header; 22 | } 23 | 24 | /** 25 | * @return the cell alignment 26 | */ 27 | public Alignment getAlignment() { 28 | return alignment; 29 | } 30 | 31 | public void setAlignment(Alignment alignment) { 32 | this.alignment = alignment; 33 | } 34 | 35 | /** 36 | * How the cell is aligned horizontally. 37 | */ 38 | public enum Alignment { 39 | NONE, LEFT, CENTER, RIGHT 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/table/TableExtension.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.table.TableExtension; 2 | 3 | import hunt.markdown.Extension; 4 | import hunt.markdown.ext.table.internal.TableBlockParser; 5 | import hunt.markdown.ext.table.internal.TableHtmlNodeRenderer; 6 | import hunt.markdown.ext.table.internal.TableTextContentNodeRenderer; 7 | import hunt.markdown.renderer.html.HtmlRenderer; 8 | import hunt.markdown.renderer.html.HtmlNodeRendererContext; 9 | import hunt.markdown.renderer.html.HtmlNodeRendererFactory; 10 | import hunt.markdown.parser.Parser; 11 | import hunt.markdown.renderer.NodeRenderer; 12 | import hunt.markdown.renderer.text.TextContentNodeRendererContext; 13 | import hunt.markdown.renderer.text.TextContentNodeRendererFactory; 14 | import hunt.markdown.renderer.text.TextContentRenderer; 15 | 16 | /** 17 | * Extension for GFM tables using "|" pipes (GitHub Flavored Markdown). 18 | *19 | * Create it with {@link #create()} and then configure it on the builders 20 | * ({@link hunt.markdown.parser.Parser.Builder#extensions(Iterable)}, 21 | * {@link HtmlRenderer.Builder#extensions(Iterable)}). 22 | *
23 | *24 | * The parsed tables are turned into {@link TableBlock} blocks. 25 | *
26 | */ 27 | class TableExtension : Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension, 28 | TextContentRenderer.TextContentRendererExtension { 29 | 30 | private this() { 31 | } 32 | 33 | public static Extension create() { 34 | return new TableExtension(); 35 | } 36 | 37 | override public void extend(Parser.Builder parserBuilder) { 38 | parserBuilder.customBlockParserFactory(new TableBlockParser.Factory()); 39 | } 40 | 41 | override public void extend(HtmlRenderer.Builder rendererBuilder) { 42 | rendererBuilder.nodeRendererFactory(new class HtmlNodeRendererFactory { 43 | override public NodeRenderer create(HtmlNodeRendererContext context) { 44 | return new TableHtmlNodeRenderer(context); 45 | } 46 | }); 47 | } 48 | 49 | override public void extend(TextContentRenderer.Builder rendererBuilder) { 50 | rendererBuilder.nodeRendererFactory(new class TextContentNodeRendererFactory { 51 | override public NodeRenderer create(TextContentNodeRendererContext context) { 52 | return new TableTextContentNodeRenderer(context); 53 | } 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/table/TableHead.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.table.TableHead; 2 | 3 | import hunt.markdown.node.CustomNode; 4 | 5 | /** 6 | * Head part of a {@link TableBlock} containing {@link TableRow TableRows}. 7 | */ 8 | class TableHead : CustomNode { 9 | } 10 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/table/TableRow.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.table.TableRow; 2 | 3 | import hunt.markdown.node.CustomNode; 4 | 5 | /** 6 | * Table row of a {@link TableHead} or {@link TableBody} containing {@link TableCell TableCells}. 7 | */ 8 | class TableRow : CustomNode { 9 | } 10 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/table/internal/TableBlockParser.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.table.internal.TableBlockParser; 2 | 3 | import hunt.markdown.ext.table; 4 | import hunt.markdown.node.Block; 5 | import hunt.markdown.node.Node; 6 | import hunt.markdown.parser.InlineParser; 7 | import hunt.markdown.parser.block.AbstractBlockParser; 8 | import hunt.markdown.parser.block.BlockContinue; 9 | import hunt.markdown.parser.block.ParserState; 10 | import hunt.markdown.parser.block.AbstractBlockParserFactory; 11 | import hunt.markdown.parser.block.BlockStart; 12 | import hunt.markdown.parser.block.MatchedBlockParser; 13 | 14 | import hunt.collection.ArrayList; 15 | import hunt.collection.List; 16 | 17 | import std.string; 18 | import std.regex; 19 | 20 | import hunt.text; 21 | 22 | class TableBlockParser : AbstractBlockParser { 23 | 24 | private enum string COL = "\\s*:?-{1,}:?\\s*"; 25 | private enum string TABLE_HEADER_SEPARATOR = "\\|" ~ COL ~ "\\|?\\s*" ~ "|" ~ 26 | COL ~ "\\|\\s*" ~ "|" ~ 27 | "\\|?" ~ "(?:" ~ COL ~ "\\|)+" ~ COL ~ "\\|?\\s*"; 28 | 29 | private TableBlock block; 30 | private List!(string) rowLines; 31 | 32 | private bool nextIsSeparatorLine = true; 33 | private string separatorLine = ""; 34 | 35 | // static this() 36 | // { 37 | // TABLE_HEADER_SEPARATOR = regex( 38 | // // For single column, require at least one pipe, otherwise it's ambiguous with setext headers 39 | // "\\|" ~ COL ~ "\\|?\\s*" ~ "|" ~ 40 | // COL ~ "\\|\\s*" ~ "|" ~ 41 | // "\\|?" ~ "(?:" ~ COL ~ "\\|)+" ~ COL ~ "\\|?\\s*"); 42 | // } 43 | 44 | private this(string headerLine) { 45 | block = new TableBlock(); 46 | rowLines = new ArrayList!(string)(); 47 | rowLines.add(headerLine); 48 | } 49 | 50 | override public Block getBlock() { 51 | return block; 52 | } 53 | 54 | public BlockContinue tryContinue(ParserState state) { 55 | import std.algorithm; 56 | 57 | if (state.getLine().find("|").empty) { 58 | return BlockContinue.none(); 59 | } else { 60 | return BlockContinue.atIndex(state.getIndex()); 61 | } 62 | } 63 | 64 | override public void addLine(string line) { 65 | if (nextIsSeparatorLine) { 66 | nextIsSeparatorLine = false; 67 | separatorLine = line; 68 | } else { 69 | rowLines.add(line); 70 | } 71 | } 72 | 73 | override public void parseInlines(InlineParser inlineParser) { 74 | Node section = new TableHead(); 75 | block.appendChild(section); 76 | 77 | List!(TableCell.Alignment) alignments = parseAlignment(separatorLine); 78 | 79 | int headerColumns = -1; 80 | bool header = true; 81 | foreach (string rowLine ; rowLines) { 82 | List!(string) cells = split(rowLine); 83 | TableRow tableRow = new TableRow(); 84 | 85 | if (headerColumns == -1) { 86 | headerColumns = cells.size(); 87 | } 88 | 89 | // Body can not have more columns than head 90 | for (int i = 0; i < headerColumns; i++) { 91 | string cell = i < cells.size() ? cells.get(i) : ""; 92 | TableCell.Alignment alignment = alignments.get(i); 93 | TableCell tableCell = new TableCell(); 94 | tableCell.setHeader(header); 95 | tableCell.setAlignment(alignment); 96 | inlineParser.parse(cell.strip(), tableCell); 97 | tableRow.appendChild(tableCell); 98 | } 99 | 100 | section.appendChild(tableRow); 101 | 102 | if (header) { 103 | // Format allows only one row in head 104 | header = false; 105 | section = new TableBody(); 106 | block.appendChild(section); 107 | } 108 | } 109 | } 110 | 111 | private static List!(TableCell.Alignment) parseAlignment(string separatorLine) { 112 | List!(string) parts = split(separatorLine); 113 | List!(TableCell.Alignment) alignments = new ArrayList!(TableCell.Alignment)(); 114 | foreach (string part ; parts) { 115 | string trimmed = part.strip(); 116 | bool left = trimmed.startsWith(":"); 117 | bool right = trimmed.endsWith(":"); 118 | TableCell.Alignment alignment = getAlignment(left, right); 119 | alignments.add(alignment); 120 | } 121 | return alignments; 122 | } 123 | 124 | private static List!(string) split(string input) { 125 | string line = input.strip(); 126 | if (line.startsWith("|")) { 127 | line = line.substring(1); 128 | } 129 | List!(string) cells = new ArrayList!(string)(); 130 | StringBuilder sb = new StringBuilder(); 131 | bool escape = false; 132 | for (int i = 0; i < line.length; i++) { 133 | char c = line[i]; 134 | if (escape) { 135 | escape = false; 136 | sb.append(c); 137 | } else { 138 | switch (c) { 139 | case '\\': 140 | escape = true; 141 | // Removing the escaping '\' is handled by the inline parser later, so add it to cell 142 | sb.append(c); 143 | break; 144 | case '|': 145 | cells.add(sb.toString()); 146 | sb.setLength(0); 147 | break; 148 | default: 149 | sb.append(c); 150 | } 151 | } 152 | } 153 | if (sb.length > 0) { 154 | cells.add(sb.toString()); 155 | } 156 | return cells; 157 | } 158 | 159 | private static TableCell.Alignment getAlignment(bool left, bool right) { 160 | if (left && right) { 161 | return TableCell.Alignment.CENTER; 162 | } else if (right) { 163 | return TableCell.Alignment.RIGHT; 164 | } else if (left) { 165 | return TableCell.Alignment.LEFT; 166 | } else { 167 | return TableCell.Alignment.NONE; 168 | } 169 | } 170 | 171 | public static class Factory : AbstractBlockParserFactory { 172 | 173 | public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { 174 | string line = state.getLine(); 175 | string paragraph = matchedBlockParser.getParagraphContent(); 176 | if (paragraph != null && paragraph.contains("|") && !paragraph.contains("\n")) { 177 | string separatorLine = line[state.getIndex()..line.length]; 178 | if (match(separatorLine, regex(TABLE_HEADER_SEPARATOR))) { 179 | List!(string) headParts = split(paragraph); 180 | List!(string) separatorParts = split(separatorLine); 181 | if (separatorParts.size() >= headParts.size()) { 182 | return BlockStart.of(new TableBlockParser(paragraph)) 183 | .atIndex(state.getIndex()) 184 | .replaceActiveBlockParser(); 185 | } 186 | } 187 | } 188 | return BlockStart.none(); 189 | } 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/table/internal/TableHtmlNodeRenderer.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.table.internal.TableHtmlNodeRenderer; 2 | 3 | import hunt.collection.Collections; 4 | import hunt.collection.Map; 5 | import hunt.collection.HashMap; 6 | 7 | import hunt.markdown.ext.table.TableBlock; 8 | import hunt.markdown.ext.table.TableBody; 9 | import hunt.markdown.ext.table.TableCell; 10 | import hunt.markdown.ext.table.TableHead; 11 | import hunt.markdown.ext.table.TableRow; 12 | import hunt.markdown.ext.table.internal.TableNodeRenderer; 13 | import hunt.markdown.node.Node; 14 | import hunt.markdown.renderer.html.HtmlNodeRendererContext; 15 | import hunt.markdown.renderer.html.HtmlWriter; 16 | 17 | import std.conv : to; 18 | 19 | class TableHtmlNodeRenderer : TableNodeRenderer { 20 | 21 | private HtmlWriter htmlWriter; 22 | private HtmlNodeRendererContext context; 23 | 24 | public this(HtmlNodeRendererContext context) { 25 | this.htmlWriter = context.getWriter(); 26 | this.context = context; 27 | } 28 | 29 | override protected void renderBlock(TableBlock tableBlock) { 30 | htmlWriter.line(); 31 | htmlWriter.tag("table", getAttributes(tableBlock, "table")); 32 | renderChildren(tableBlock); 33 | htmlWriter.tag("/table"); 34 | htmlWriter.line(); 35 | } 36 | 37 | override protected void renderHead(TableHead tableHead) { 38 | htmlWriter.line(); 39 | htmlWriter.tag("thead", getAttributes(tableHead, "thead")); 40 | renderChildren(tableHead); 41 | htmlWriter.tag("/thead"); 42 | htmlWriter.line(); 43 | } 44 | 45 | override protected void renderBody(TableBody tableBody) { 46 | htmlWriter.line(); 47 | htmlWriter.tag("tbody", getAttributes(tableBody, "tbody")); 48 | renderChildren(tableBody); 49 | htmlWriter.tag("/tbody"); 50 | htmlWriter.line(); 51 | } 52 | 53 | override protected void renderRow(TableRow tableRow) { 54 | htmlWriter.line(); 55 | htmlWriter.tag("tr", getAttributes(tableRow, "tr")); 56 | renderChildren(tableRow); 57 | htmlWriter.tag("/tr"); 58 | htmlWriter.line(); 59 | } 60 | 61 | override protected void renderCell(TableCell tableCell) { 62 | string tagName = tableCell.isHeader() ? "th" : "td"; 63 | htmlWriter.tag(tagName, getCellAttributes(tableCell, tagName)); 64 | renderChildren(tableCell); 65 | htmlWriter.tag("/" ~ tagName); 66 | } 67 | 68 | private Map!(string, string) getAttributes(Node node, string tagName) { 69 | return context.extendAttributes(node, tagName, Collections.emptyMap!(string, string)()); 70 | } 71 | 72 | private Map!(string, string) getCellAttributes(TableCell tableCell, string tagName) 73 | { 74 | auto attributes = new HashMap!(string, string); 75 | 76 | if (tableCell.getAlignment() != TableCell.Alignment.NONE) { 77 | attributes.put("align", getAlignValue(tableCell.getAlignment())); 78 | } 79 | 80 | return context.extendAttributes(tableCell, tagName, attributes); 81 | } 82 | 83 | private static string getAlignValue(TableCell.Alignment alignment) { 84 | switch (alignment) { 85 | case TableCell.Alignment.LEFT: 86 | return "left"; 87 | case TableCell.Alignment.CENTER: 88 | return "center"; 89 | case TableCell.Alignment.RIGHT: 90 | return "right"; 91 | default: 92 | return null; 93 | } 94 | } 95 | 96 | private void renderChildren(Node parent) { 97 | Node node = parent.getFirstChild(); 98 | while (node !is null) { 99 | Node next = node.getNext(); 100 | context.render(node); 101 | node = next; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/table/internal/TableNodeRenderer.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.table.internal.TableNodeRenderer; 2 | 3 | // import hunt.collection.Arrays; 4 | import hunt.collection.HashSet; 5 | import hunt.collection.Set; 6 | 7 | import hunt.markdown.ext.table.TableBlock; 8 | import hunt.markdown.ext.table.TableBody; 9 | import hunt.markdown.ext.table.TableCell; 10 | import hunt.markdown.ext.table.TableHead; 11 | import hunt.markdown.ext.table.TableRow; 12 | import hunt.markdown.node.Node; 13 | import hunt.markdown.renderer.NodeRenderer; 14 | 15 | abstract class TableNodeRenderer : NodeRenderer { 16 | 17 | // TypeInfo for D 18 | override public Set!TypeInfo_Class getNodeTypes() { 19 | return new HashSet!TypeInfo_Class([ 20 | typeid(TableBlock), 21 | typeid(TableHead), 22 | typeid(TableBody), 23 | typeid(TableRow), 24 | typeid(TableCell) 25 | ]); 26 | } 27 | 28 | public void render(Node node) { 29 | if (cast(TableBlock)node !is null) { 30 | renderBlock(cast(TableBlock) node); 31 | } else if (cast(TableHead)node !is null ) { 32 | renderHead(cast(TableHead) node); 33 | } else if (cast(TableBody)node !is null) { 34 | renderBody(cast(TableBody) node); 35 | } else if (cast(TableRow)node !is null) { 36 | renderRow(cast(TableRow) node); 37 | } else if (cast(TableCell)node !is null) { 38 | renderCell(cast(TableCell) node); 39 | } 40 | } 41 | 42 | protected abstract void renderBlock(TableBlock node); 43 | 44 | protected abstract void renderHead(TableHead node); 45 | 46 | protected abstract void renderBody(TableBody node); 47 | 48 | protected abstract void renderRow(TableRow node); 49 | 50 | protected abstract void renderCell(TableCell node); 51 | } 52 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/table/internal/TableTextContentNodeRenderer.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.table.internal.TableTextContentNodeRenderer; 2 | 3 | import hunt.markdown.ext.table.internal.TableNodeRenderer; 4 | import hunt.markdown.ext.table.TableBlock; 5 | import hunt.markdown.ext.table.TableBody; 6 | import hunt.markdown.ext.table.TableCell; 7 | import hunt.markdown.ext.table.TableHead; 8 | import hunt.markdown.ext.table.TableRow; 9 | import hunt.markdown.node.Node; 10 | import hunt.markdown.renderer.text.TextContentNodeRendererContext; 11 | import hunt.markdown.renderer.text.TextContentWriter; 12 | 13 | /** 14 | * The Table node renderer that is needed for rendering GFM tables (GitHub Flavored Markdown) to text content. 15 | */ 16 | class TableTextContentNodeRenderer : TableNodeRenderer { 17 | 18 | private TextContentWriter textContentWriter; 19 | private TextContentNodeRendererContext context; 20 | 21 | public this(TextContentNodeRendererContext context) { 22 | this.textContentWriter = context.getWriter(); 23 | this.context = context; 24 | } 25 | 26 | override protected void renderBlock(TableBlock tableBlock) { 27 | renderChildren(tableBlock); 28 | if (tableBlock.getNext() !is null) { 29 | textContentWriter.write("\n"); 30 | } 31 | } 32 | 33 | override protected void renderHead(TableHead tableHead) { 34 | renderChildren(tableHead); 35 | } 36 | 37 | override protected void renderBody(TableBody tableBody) { 38 | renderChildren(tableBody); 39 | } 40 | 41 | override protected void renderRow(TableRow tableRow) { 42 | textContentWriter.line(); 43 | renderChildren(tableRow); 44 | textContentWriter.line(); 45 | } 46 | 47 | override protected void renderCell(TableCell tableCell) { 48 | renderChildren(tableCell); 49 | textContentWriter.write('|'); 50 | textContentWriter.whitespace(); 51 | } 52 | 53 | private void renderLastCell(TableCell tableCell) { 54 | renderChildren(tableCell); 55 | } 56 | 57 | private void renderChildren(Node parent) { 58 | Node node = parent.getFirstChild(); 59 | while (node !is null) { 60 | Node next = node.getNext(); 61 | // For last cell in row, we dont render the delimiter. 62 | if (cast(TableCell)node !is null && next is null) { 63 | renderLastCell(cast(TableCell) node); 64 | } else { 65 | context.render(node); 66 | } 67 | 68 | node = next; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /source/hunt/markdown/ext/table/package.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.ext.table; 2 | 3 | public import hunt.markdown.ext.table.TableBlock; 4 | public import hunt.markdown.ext.table.TableBody; 5 | public import hunt.markdown.ext.table.TableCell; 6 | public import hunt.markdown.ext.table.TableExtension; 7 | public import hunt.markdown.ext.table.TableHead; 8 | public import hunt.markdown.ext.table.TableRow; 9 | -------------------------------------------------------------------------------- /source/hunt/markdown/internal/BlockContent.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.internal.BlockContent; 2 | 3 | import hunt.text; 4 | import hunt.util.StringBuilder; 5 | 6 | class BlockContent { 7 | 8 | private StringBuilder sb; 9 | 10 | private int lineCount = 0; 11 | 12 | public this() { 13 | sb = new StringBuilder(); 14 | } 15 | 16 | public this(string content) { 17 | sb = new StringBuilder(content); 18 | } 19 | 20 | public void add(string line) { 21 | if (lineCount != 0) { 22 | sb.append('\n'); 23 | } 24 | sb.append(line); 25 | lineCount++; 26 | } 27 | 28 | public string getString() { 29 | return sb.toString(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /source/hunt/markdown/internal/BlockContinueImpl.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.internal.BlockContinueImpl; 2 | 3 | import hunt.markdown.parser.block.BlockContinue; 4 | 5 | class BlockContinueImpl : BlockContinue { 6 | 7 | private int newIndex; 8 | private int newColumn; 9 | private bool finalize; 10 | 11 | public this(int newIndex, int newColumn, bool finalize) { 12 | this.newIndex = newIndex; 13 | this.newColumn = newColumn; 14 | this.finalize = finalize; 15 | } 16 | 17 | public int getNewIndex() { 18 | return newIndex; 19 | } 20 | 21 | public int getNewColumn() { 22 | return newColumn; 23 | } 24 | 25 | public bool isFinalize() { 26 | return finalize; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /source/hunt/markdown/internal/BlockQuoteParser.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.internal.BlockQuoteParser; 2 | 3 | import hunt.markdown.internal.util.Parsing; 4 | import hunt.markdown.node.Block; 5 | import hunt.markdown.node.BlockQuote; 6 | import hunt.markdown.parser.block.AbstractBlockParser; 7 | import hunt.markdown.parser.block.BlockContinue; 8 | import hunt.markdown.parser.block.ParserState; 9 | import hunt.markdown.parser.block.AbstractBlockParserFactory; 10 | import hunt.markdown.parser.block.BlockStart; 11 | import hunt.markdown.parser.block.MatchedBlockParser; 12 | 13 | class BlockQuoteParser : AbstractBlockParser { 14 | 15 | private BlockQuote block; 16 | 17 | this() 18 | { 19 | block = new BlockQuote(); 20 | } 21 | 22 | override public bool isContainer() { 23 | return true; 24 | } 25 | 26 | override public bool canContain(Block block) { 27 | return true; 28 | } 29 | 30 | override 31 | public Block getBlock() { 32 | return block; 33 | } 34 | 35 | public BlockContinue tryContinue(ParserState state) { 36 | int nextNonSpace = state.getNextNonSpaceIndex(); 37 | if (isMarker(state, nextNonSpace)) { 38 | int newColumn = state.getColumn() + state.getIndent() + 1; 39 | // optional following space or tab 40 | if (Parsing.isSpaceOrTab(state.getLine(), nextNonSpace + 1)) { 41 | newColumn++; 42 | } 43 | return BlockContinue.atColumn(newColumn); 44 | } else { 45 | return BlockContinue.none(); 46 | } 47 | } 48 | 49 | private static bool isMarker(ParserState state, int index) { 50 | string line = state.getLine(); 51 | return state.getIndent() < Parsing.CODE_BLOCK_INDENT && index < line.length && line[index] == '>'; 52 | } 53 | 54 | public static class Factory : AbstractBlockParserFactory { 55 | public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { 56 | int nextNonSpace = state.getNextNonSpaceIndex(); 57 | if (isMarker(state, nextNonSpace)) { 58 | int newColumn = state.getColumn() + state.getIndent() + 1; 59 | // optional following space or tab 60 | if (Parsing.isSpaceOrTab(state.getLine(), nextNonSpace + 1)) { 61 | newColumn++; 62 | } 63 | return BlockStart.of(new BlockQuoteParser()).atColumn(newColumn); 64 | } else { 65 | return BlockStart.none(); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /source/hunt/markdown/internal/BlockStartImpl.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.internal.BlockStartImpl; 2 | 3 | import hunt.markdown.parser.block.BlockParser; 4 | import hunt.markdown.parser.block.BlockStart; 5 | import hunt.logging; 6 | 7 | class BlockStartImpl : BlockStart { 8 | 9 | private BlockParser[] blockParsers; 10 | private int newIndex = -1; 11 | private int newColumn = -1; 12 | private bool _replaceActiveBlockParser = false; 13 | 14 | public this(BlockParser[] blockParsers...) { 15 | foreach(bl; blockParsers) { 16 | this.blockParsers ~= bl; 17 | } 18 | } 19 | 20 | public BlockParser[] getBlockParsers() { 21 | return blockParsers; 22 | } 23 | 24 | public int getNewIndex() { 25 | return newIndex; 26 | } 27 | 28 | public int getNewColumn() { 29 | return newColumn; 30 | } 31 | 32 | public bool isReplaceActiveBlockParser() { 33 | return _replaceActiveBlockParser; 34 | } 35 | 36 | override public BlockStart atIndex(int newIndex) { 37 | this.newIndex = newIndex; 38 | return this; 39 | } 40 | 41 | override public BlockStart atColumn(int newColumn) { 42 | this.newColumn = newColumn; 43 | return this; 44 | } 45 | 46 | override public BlockStart replaceActiveBlockParser() { 47 | this._replaceActiveBlockParser = true; 48 | return this; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /source/hunt/markdown/internal/Bracket.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.internal.Bracket; 2 | 3 | import hunt.markdown.node.Text; 4 | import hunt.markdown.internal.Delimiter; 5 | 6 | /** 7 | * Opening bracket for links ([
) or images (![
).
8 | */
9 | class Bracket {
10 |
11 | public Text node;
12 | public int index;
13 | public bool _image;
14 |
15 | /**
16 | * Previous bracket.
17 | */
18 | public Bracket previous;
19 |
20 | /**
21 | * Previous delimiter (emphasis, etc) before this bracket.
22 | */
23 | public Delimiter previousDelimiter;
24 |
25 | /**
26 | * Whether this bracket is allowed to form a link/image (also known as "active").
27 | */
28 | public bool allowed = true;
29 |
30 | /**
31 | * Whether there is an unescaped bracket (opening or closing) anywhere after this opening bracket.
32 | */
33 | public bool bracketAfter = false;
34 |
35 | static public Bracket link(Text node, int index, Bracket previous, Delimiter previousDelimiter) {
36 | return new Bracket(node, index, previous, previousDelimiter, false);
37 | }
38 |
39 | static public Bracket image(Text node, int index, Bracket previous, Delimiter previousDelimiter) {
40 | return new Bracket(node, index, previous, previousDelimiter, true);
41 | }
42 |
43 | private this(Text node, int index, Bracket previous, Delimiter previousDelimiter, bool image) {
44 | this.node = node;
45 | this.index = index;
46 | this._image = image;
47 | this.previous = previous;
48 | this.previousDelimiter = previousDelimiter;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/Delimiter.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.Delimiter;
2 |
3 | import hunt.markdown.node.Text;
4 | import hunt.markdown.parser.delimiter.DelimiterRun;
5 |
6 | /**
7 | * Delimiter (emphasis, strong emphasis or custom emphasis).
8 | */
9 | class Delimiter : DelimiterRun {
10 |
11 | public Text node;
12 | public char delimiterChar;
13 |
14 | /**
15 | * Can open emphasis, see spec.
16 | */
17 | public bool _canOpen;
18 |
19 | /**
20 | * Can close emphasis, see spec.
21 | */
22 | public bool _canClose;
23 |
24 | public Delimiter previous;
25 | public Delimiter next;
26 |
27 | public int _length = 1;
28 | public int _originalLength = 1;
29 |
30 | public this(Text node, char delimiterChar, bool canOpen, bool canClose, Delimiter previous) {
31 | this.node = node;
32 | this.delimiterChar = delimiterChar;
33 | this._canOpen = canOpen;
34 | this._canClose = canClose;
35 | this.previous = previous;
36 | }
37 |
38 | public bool canOpen() {
39 | return _canOpen;
40 | }
41 |
42 | public bool canClose() {
43 | return _canClose;
44 | }
45 |
46 | @property public int length() {
47 | return _length;
48 | }
49 |
50 | public void setLength(int len) {
51 | _length = len;
52 | }
53 |
54 | @property public int originalLength() {
55 | return _originalLength;
56 | }
57 |
58 | public void setOriginalLength(int len) {
59 | _originalLength = len;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/DocumentBlockParser.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.DocumentBlockParser;
2 |
3 | import hunt.markdown.node.Block;
4 | import hunt.markdown.node.Document;
5 | import hunt.markdown.parser.block.AbstractBlockParser;
6 | import hunt.markdown.parser.block.BlockContinue;
7 | import hunt.markdown.parser.block.ParserState;
8 |
9 | class DocumentBlockParser : AbstractBlockParser {
10 |
11 | private Document document;
12 |
13 | this()
14 | {
15 | document = new Document();
16 | }
17 |
18 | override public bool isContainer() {
19 | return true;
20 | }
21 |
22 | override public bool canContain(Block block) {
23 | return true;
24 | }
25 |
26 | override public Document getBlock() {
27 | return document;
28 | }
29 |
30 | public BlockContinue tryContinue(ParserState state) {
31 | return BlockContinue.atIndex(state.getIndex());
32 | }
33 |
34 | override public void addLine(string line) {
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/FencedCodeBlockParser.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.FencedCodeBlockParser;
2 |
3 | import hunt.markdown.internal.util.Parsing;
4 | import hunt.markdown.internal.util.Escaping;
5 | import hunt.markdown.node.Block;
6 | import hunt.markdown.node.FencedCodeBlock;
7 | import hunt.markdown.parser.block.AbstractBlockParser;
8 | import hunt.markdown.parser.block.BlockContinue;
9 | import hunt.markdown.parser.block.ParserState;
10 | import hunt.markdown.parser.block.AbstractBlockParserFactory;
11 | import hunt.markdown.parser.block.BlockStart;
12 | import hunt.markdown.parser.block.MatchedBlockParser;
13 |
14 | import hunt.text;
15 | import std.string;
16 | import hunt.util.StringBuilder;
17 |
18 | class FencedCodeBlockParser : AbstractBlockParser {
19 |
20 | private FencedCodeBlock block;
21 |
22 | private string firstLine;
23 |
24 | private StringBuilder otherLines;
25 |
26 | public this(char fenceChar, int fenceLength, int fenceIndent) {
27 |
28 | block = new FencedCodeBlock();
29 | otherLines = new StringBuilder();
30 |
31 | block.setFenceChar(fenceChar);
32 | block.setFenceLength(fenceLength);
33 | block.setFenceIndent(fenceIndent);
34 | }
35 |
36 | override public Block getBlock() {
37 | return block;
38 | }
39 |
40 | public BlockContinue tryContinue(ParserState state) {
41 | int nextNonSpace = state.getNextNonSpaceIndex();
42 | int newIndex = state.getIndex();
43 | string line = state.getLine();
44 | bool closing = state.getIndent() < Parsing.CODE_BLOCK_INDENT && isClosing(line, nextNonSpace);
45 | if (closing) {
46 | // closing fence - we're at end of line, so we can finalize now
47 | return BlockContinue.finished();
48 | } else {
49 | // skip optional spaces of fence indent
50 | int i = block.getFenceIndent();
51 | int length = cast(int)line.length;
52 | while (i > 0 && newIndex < length && line[newIndex] == ' ') {
53 | newIndex++;
54 | i--;
55 | }
56 | }
57 | return BlockContinue.atIndex(newIndex);
58 | }
59 |
60 | override public void addLine(string line) {
61 | if (firstLine is null) {
62 | firstLine = line;
63 | } else {
64 | otherLines.append(line);
65 | otherLines.append('\n');
66 | }
67 | }
68 |
69 | override public void closeBlock() {
70 | // first line becomes info string
71 | block.setInfo(Escaping.unescapeString(firstLine.strip()));
72 | block.setLiteral(otherLines.toString());
73 | }
74 |
75 | public static class Factory : AbstractBlockParserFactory {
76 |
77 | public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
78 | int indent = state.getIndent();
79 | if (indent >= Parsing.CODE_BLOCK_INDENT) {
80 | return BlockStart.none();
81 | }
82 |
83 | int nextNonSpace = state.getNextNonSpaceIndex();
84 | FencedCodeBlockParser blockParser = checkOpener(state.getLine(), nextNonSpace, indent);
85 | if (blockParser !is null) {
86 | return BlockStart.of(blockParser).atIndex(nextNonSpace + blockParser.block.getFenceLength());
87 | } else {
88 | return BlockStart.none();
89 | }
90 | }
91 | }
92 |
93 | // spec: A code fence is a sequence of at least three consecutive backtick characters (`) or tildes (~). (Tildes and
94 | // backticks cannot be mixed.)
95 | private static FencedCodeBlockParser checkOpener(string line, int index, int indent) {
96 | int backticks = 0;
97 | int tildes = 0;
98 | int length = cast(int)line.length;
99 | loop:
100 | for (int i = index; i < length; i++) {
101 | switch (line[i]) {
102 | case '`':
103 | backticks++;
104 | break;
105 | case '~':
106 | tildes++;
107 | break;
108 | default:
109 | break loop;
110 | }
111 | }
112 | if (backticks >= 3 && tildes == 0) {
113 | // spec: The info string may not contain any backtick characters.
114 | if (Parsing.find('`', line, index + backticks) != -1) {
115 | return null;
116 | }
117 | return new FencedCodeBlockParser('`', backticks, indent);
118 | } else if (tildes >= 3 && backticks == 0) {
119 | if (Parsing.find('~', line, index + tildes) != -1) {
120 | return null;
121 | }
122 | return new FencedCodeBlockParser('~', tildes, indent);
123 | } else {
124 | return null;
125 | }
126 | }
127 |
128 | // spec: The content of the code block consists of all subsequent lines, until a closing code fence of the same type
129 | // as the code block began with (backticks or tildes), and with at least as many backticks or tildes as the opening
130 | // code fence.
131 | private bool isClosing(string line, int index) {
132 | char fenceChar = block.getFenceChar();
133 | int fenceLength = block.getFenceLength();
134 | int fences = Parsing.skip(fenceChar, line, index, cast(int)line.length) - index;
135 | if (fences < fenceLength) {
136 | return false;
137 | }
138 | // spec: The closing code fence [...] may be followed only by spaces, which are ignored.
139 | int after = Parsing.skipSpaceTab(line, index + fences, cast(int)line.length);
140 | return after == line.length;
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/HeadingParser.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.HeadingParser;
2 |
3 | import hunt.markdown.internal.util.Parsing;
4 | import hunt.markdown.node.Block;
5 | import hunt.markdown.node.Heading;
6 | import hunt.markdown.parser.InlineParser;
7 | import hunt.markdown.parser.block.AbstractBlockParser;
8 | import hunt.markdown.parser.block.BlockContinue;
9 | import hunt.markdown.parser.block.ParserState;
10 | import hunt.markdown.parser.block.AbstractBlockParserFactory;
11 | import hunt.markdown.parser.block.BlockStart;
12 | import hunt.markdown.parser.block.MatchedBlockParser;
13 |
14 | import hunt.text.Common;
15 |
16 | class HeadingParser : AbstractBlockParser {
17 |
18 | private Heading block;
19 | private string content;
20 |
21 | public this(int level, string content) {
22 | block = new Heading();
23 | block.setLevel(level);
24 | this.content = content;
25 | }
26 |
27 | override public Block getBlock() {
28 | return block;
29 | }
30 |
31 | public BlockContinue tryContinue(ParserState parserState) {
32 | // In both ATX and Setext headings, once we have the heading markup, there's nothing more to parse.
33 | return BlockContinue.none();
34 | }
35 |
36 | override public void parseInlines(InlineParser inlineParser) {
37 | inlineParser.parse(content, block);
38 | }
39 |
40 | public static class Factory : AbstractBlockParserFactory {
41 |
42 | public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
43 | if (state.getIndent() >= Parsing.CODE_BLOCK_INDENT) {
44 | return BlockStart.none();
45 | }
46 |
47 | string line = state.getLine();
48 | int nextNonSpace = state.getNextNonSpaceIndex();
49 | HeadingParser atxHeading = getAtxHeading(line, nextNonSpace);
50 | if (atxHeading !is null) {
51 | return BlockStart.of(atxHeading).atIndex(cast(int)line.length);
52 | }
53 |
54 | int setextHeadingLevel = getSetextHeadingLevel(line, nextNonSpace);
55 | if (setextHeadingLevel > 0) {
56 | string paragraph = matchedBlockParser.getParagraphContent();
57 | if (paragraph !is null) {
58 | string content = paragraph;
59 | return BlockStart.of(new HeadingParser(setextHeadingLevel, content))
60 | .atIndex(cast(int)line.length)
61 | .replaceActiveBlockParser();
62 | }
63 | }
64 |
65 | return BlockStart.none();
66 | }
67 | }
68 |
69 | // spec: An ATX heading consists of a string of characters, parsed as inline content, between an opening sequence of
70 | // 1–6 unescaped # characters and an optional closing sequence of any number of unescaped # characters. The opening
71 | // sequence of # characters must be followed by a space or by the end of line. The optional closing sequence of #s
72 | // must be preceded by a space and may be followed by spaces only.
73 | private static HeadingParser getAtxHeading(string line, int index) {
74 | int level = Parsing.skip('#', line, index, cast(int)line.length) - index;
75 |
76 | if (level == 0 || level > 6) {
77 | return null;
78 | }
79 |
80 | int start = index + level;
81 | if (start >= line.length) {
82 | // End of line after markers is an empty heading
83 | return new HeadingParser(level, "");
84 | }
85 |
86 | char next = line[start];
87 | if (!(next == ' ' || next == '\t')) {
88 | return null;
89 | }
90 |
91 | int beforeSpace = Parsing.skipSpaceTabBackwards(line, cast(int)line.length - 1, start);
92 | int beforeHash = Parsing.skipBackwards('#', line, beforeSpace, start);
93 | int beforeTrailer = Parsing.skipSpaceTabBackwards(line, beforeHash, start);
94 | if (beforeTrailer != beforeHash) {
95 | return new HeadingParser(level, line.substring(start, beforeTrailer + 1));
96 | } else {
97 | return new HeadingParser(level, line.substring(start, beforeSpace + 1));
98 | }
99 | }
100 |
101 | // spec: A setext heading underline is a sequence of = characters or a sequence of - characters, with no more than
102 | // 3 spaces indentation and any number of trailing spaces.
103 | private static int getSetextHeadingLevel(string line, int index) {
104 | switch (line[index]) {
105 | case '=':
106 | if (isSetextHeadingRest(line, index + 1, '=')) {
107 | return 1;
108 | }
109 | else
110 | {
111 | return 0;
112 | }
113 | case '-':
114 | if (isSetextHeadingRest(line, index + 1, '-')) {
115 | return 2;
116 | }
117 | else
118 | {
119 | return 0;
120 | }
121 | default:
122 | break;
123 | }
124 |
125 | return 0;
126 | }
127 |
128 | private static bool isSetextHeadingRest(string line, int index, char marker) {
129 | int afterMarker = Parsing.skip(marker, line, index, cast(int)line.length);
130 | int afterSpace = Parsing.skipSpaceTab(line, afterMarker, cast(int)line.length);
131 | return afterSpace >= line.length;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/HtmlBlockParser.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.HtmlBlockParser;
2 |
3 | import hunt.markdown.internal.util.Parsing;
4 | import hunt.markdown.internal.BlockContent;
5 | import hunt.markdown.node.Block;
6 | import hunt.markdown.node.HtmlBlock;
7 | import hunt.markdown.node.Paragraph;
8 | import hunt.markdown.parser.block.AbstractBlockParser;
9 | import hunt.markdown.parser.block.BlockContinue;
10 | import hunt.markdown.parser.block.ParserState;
11 | import hunt.markdown.parser.block.BlockStart;
12 | import hunt.markdown.parser.block.AbstractBlockParserFactory;
13 | import hunt.markdown.parser.block.MatchedBlockParser;
14 |
15 | import hunt.text.Common;
16 |
17 | import std.regex;
18 | import hunt.logging;
19 |
20 | class HtmlBlockParser : AbstractBlockParser {
21 |
22 | private static string[][] BLOCK_PATTERNS = [
23 | ["", ""],
24 | ["^<(?:script|pre|style)(?:\\s|>|$)", "(?:script|pre|style)>"],
25 | ["^"],
26 | ["^<[?]", "\\?>"],
27 | ["^"],
28 | ["^"],
29 | ["^?(?:" ~
30 | "address|article|aside|" ~
31 | "base|basefont|blockquote|body|" ~
32 | "caption|center|col|colgroup|" ~
33 | "dd|details|dialog|dir|div|dl|dt|" ~
34 | "fieldset|figcaption|figure|footer|form|frame|frameset|" ~
35 | "h1|h2|h3|h4|h5|h6|head|header|hr|html|" ~
36 | "iframe|" ~
37 | "legend|li|link|" ~
38 | "main|menu|menuitem|meta|" ~
39 | "nav|noframes|" ~
40 | "ol|optgroup|option|" ~
41 | "p|param|" ~
42 | "section|source|summary|" ~
43 | "table|tbody|td|tfoot|th|thead|title|tr|track|" ~
44 | "ul" ~
45 | ")(?:\\s|[/]?[>]|$)",""],
46 | ["^(?:" ~ Parsing.OPENTAG ~ '|' ~ Parsing.CLOSETAG ~ ")\\s*$", null]
47 | ];
48 |
49 | private HtmlBlock block;
50 | private Regex!char closingPattern;
51 |
52 | private bool finished = false;
53 | private BlockContent content;
54 |
55 | private this(Regex!char closingPattern) {
56 | block = new HtmlBlock();
57 | content = new BlockContent();
58 | this.closingPattern = closingPattern;
59 | }
60 |
61 | override public Block getBlock() {
62 | return block;
63 | }
64 |
65 | public BlockContinue tryContinue(ParserState state) {
66 | if (finished) {
67 | return BlockContinue.none();
68 | }
69 |
70 | // Blank line ends type 6 and type 7 blocks
71 | if (state.isBlank() && closingPattern.empty()) {
72 | return BlockContinue.none();
73 | } else {
74 | return BlockContinue.atIndex(state.getIndex());
75 | }
76 | }
77 |
78 | override public void addLine(string line) {
79 | content.add(line);
80 |
81 | if (!closingPattern.empty() && !matchAll(line,closingPattern).empty()) {
82 | finished = true;
83 | }
84 | }
85 |
86 | override public void closeBlock() {
87 | block.setLiteral(content.getString());
88 | content = null;
89 | }
90 |
91 | public static class Factory : AbstractBlockParserFactory {
92 |
93 | public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
94 | int nextNonSpace = state.getNextNonSpaceIndex();
95 | string line = state.getLine();
96 |
97 | if (state.getIndent() < 4 && line[nextNonSpace] == '<') {
98 | for (int blockType = 1; blockType <= 7; blockType++) {
99 | // Type 7 can not interrupt a paragraph
100 | if (blockType == 7 && cast(Paragraph)matchedBlockParser.getMatchedBlockParser().getBlock() !is null) {
101 | continue;
102 | }
103 | Regex!char opener = regex(BLOCK_PATTERNS[blockType][0]);
104 | Regex!char closer = regex(BLOCK_PATTERNS[blockType][1]);
105 | bool matches = matchAll(line.substring(nextNonSpace, cast(int)line.length),opener).empty();
106 | if (!matches) {
107 | return BlockStart.of(new HtmlBlockParser(closer)).atIndex(state.getIndex());
108 | }
109 | }
110 | }
111 | return BlockStart.none();
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/IndentedCodeBlockParser.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.IndentedCodeBlockParser;
2 |
3 | import hunt.markdown.internal.util.Parsing;
4 | import hunt.markdown.node.Block;
5 | import hunt.markdown.node.IndentedCodeBlock;
6 | import hunt.markdown.node.Paragraph;
7 | import hunt.markdown.parser.block.AbstractBlockParser;
8 | import hunt.markdown.parser.block.BlockContinue;
9 | import hunt.markdown.parser.block.ParserState;
10 | import hunt.markdown.parser.block.BlockStart;
11 | import hunt.markdown.parser.block.AbstractBlockParserFactory;
12 | import hunt.markdown.parser.block.MatchedBlockParser;
13 |
14 | import hunt.collection.ArrayList;
15 | import hunt.collection.List;
16 |
17 | import hunt.text;
18 |
19 | class IndentedCodeBlockParser : AbstractBlockParser {
20 |
21 | private IndentedCodeBlock block;
22 | private List!(string) lines;
23 |
24 | this()
25 | {
26 | block = new IndentedCodeBlock();
27 | lines = new ArrayList!(string)();
28 | }
29 |
30 | override public Block getBlock() {
31 | return block;
32 | }
33 |
34 | public BlockContinue tryContinue(ParserState state) {
35 | if (state.getIndent() >= Parsing.CODE_BLOCK_INDENT) {
36 | return BlockContinue.atColumn(state.getColumn() + Parsing.CODE_BLOCK_INDENT);
37 | } else if (state.isBlank()) {
38 | return BlockContinue.atIndex(state.getNextNonSpaceIndex());
39 | } else {
40 | return BlockContinue.none();
41 | }
42 | }
43 |
44 | override public void addLine(string line) {
45 | lines.add(line);
46 | }
47 |
48 | override public void closeBlock() {
49 | int lastNonBlank = lines.size() - 1;
50 | while (lastNonBlank >= 0) {
51 | if (!Parsing.isBlank(lines.get(lastNonBlank))) {
52 | break;
53 | }
54 | lastNonBlank--;
55 | }
56 |
57 | StringBuilder sb = new StringBuilder();
58 | for (int i = 0; i < lastNonBlank + 1; i++) {
59 | sb.append(lines.get(i));
60 | sb.append("\n");
61 | }
62 |
63 | string literal = sb.toString();
64 | block.setLiteral(literal);
65 | }
66 |
67 | public static class Factory : AbstractBlockParserFactory {
68 |
69 | public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
70 | // An indented code block cannot interrupt a paragraph.
71 | if (state.getIndent() >= Parsing.CODE_BLOCK_INDENT && !state.isBlank() && cast(Paragraph)state.getActiveBlockParser().getBlock() is null) {
72 | return BlockStart.of(new IndentedCodeBlockParser()).atColumn(state.getColumn() + Parsing.CODE_BLOCK_INDENT);
73 | } else {
74 | return BlockStart.none();
75 | }
76 | }
77 | }
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/ListItemParser.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.ListItemParser;
2 |
3 | import hunt.markdown.node.Block;
4 | import hunt.markdown.node.ListBlock;
5 | import hunt.markdown.node.ListItem;
6 | import hunt.markdown.node.Paragraph;
7 | import hunt.markdown.parser.block.AbstractBlockParser;
8 | import hunt.markdown.parser.block.BlockContinue;
9 | import hunt.markdown.parser.block.ParserState;
10 |
11 | class ListItemParser : AbstractBlockParser {
12 |
13 | private ListItem block;
14 |
15 | /**
16 | * Minimum number of columns that the content has to be indented (relative to the containing block) to be part of
17 | * this list item.
18 | */
19 | private int contentIndent;
20 |
21 | private bool hadBlankLine;
22 |
23 | public this(int contentIndent) {
24 | block = new ListItem();
25 | this.contentIndent = contentIndent;
26 | }
27 |
28 | override public bool isContainer() {
29 | return true;
30 | }
31 |
32 | override public bool canContain(Block childBlock) {
33 | if (hadBlankLine) {
34 | // We saw a blank line in this list item, that means the list block is loose.
35 | //
36 | // spec: if any of its constituent list items directly contain two block-level elements with a blank line
37 | // between them
38 | assert(block !is null);
39 | Block parent = block.getParent();
40 | if (cast(ListBlock)parent !is null) {
41 | (cast(ListBlock) parent).setTight(false);
42 | }
43 | }
44 | return true;
45 | }
46 |
47 | override public Block getBlock() {
48 | return block;
49 | }
50 |
51 | public BlockContinue tryContinue(ParserState state) {
52 | if (state.isBlank()) {
53 | if (block.getFirstChild() is null) {
54 | // Blank line after empty list item
55 | return BlockContinue.none();
56 | } else {
57 | Block activeBlock = state.getActiveBlockParser().getBlock();
58 | // If the active block is a code block, blank lines in it should not affect if the list is tight.
59 | hadBlankLine = cast(Paragraph)activeBlock !is null || cast(ListItem)activeBlock !is null;
60 | return BlockContinue.atIndex(state.getNextNonSpaceIndex());
61 | }
62 | }
63 |
64 | if (state.getIndent() >= contentIndent) {
65 | return BlockContinue.atColumn(state.getColumn() + contentIndent);
66 | } else {
67 | return BlockContinue.none();
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/ParagraphParser.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.ParagraphParser;
2 |
3 | import hunt.markdown.internal.ReferenceParser;
4 | import hunt.markdown.internal.util.Parsing;
5 | import hunt.markdown.internal.BlockContent;
6 | import hunt.markdown.node.Block;
7 | import hunt.markdown.node.Paragraph;
8 | import hunt.markdown.parser.block.AbstractBlockParser;
9 | import hunt.markdown.parser.block.BlockContinue;
10 | import hunt.markdown.parser.InlineParser;
11 | import hunt.markdown.parser.block.ParserState;
12 | import hunt.markdown.parser.block.BlockParser;
13 | import hunt.util.Comparator;
14 | import hunt.text.Common;
15 |
16 | class ParagraphParser : AbstractBlockParser {
17 |
18 | private Paragraph block;
19 | private BlockContent content;
20 |
21 | this()
22 | {
23 | block = new Paragraph();
24 | content = new BlockContent();
25 | }
26 |
27 | override public Block getBlock() {
28 | return block;
29 | }
30 |
31 | public BlockContinue tryContinue(ParserState state) {
32 | if (!state.isBlank()) {
33 | return BlockContinue.atIndex(state.getIndex());
34 | } else {
35 | return BlockContinue.none();
36 | }
37 | }
38 |
39 | override public void addLine(string line) {
40 | content.add(line);
41 | }
42 |
43 | override public void closeBlock() {
44 | }
45 |
46 | override int opCmp(BlockParser o)
47 | {
48 | auto cmp = compare(getBlock(),o.getBlock());
49 | import hunt.logging;
50 | logDebug("------223-2--");
51 | return cmp;
52 | }
53 |
54 | public void closeBlock(ReferenceParser inlineParser) {
55 | string contentString = content.getString();
56 | bool hasReferenceDefs = false;
57 |
58 | int pos;
59 | // try parsing the beginning as link reference definitions:
60 | while (contentString.length > 3 && contentString[0] == '[' &&
61 | (pos = inlineParser.parseReference(contentString)) != 0) {
62 | contentString = contentString.substring(pos);
63 | hasReferenceDefs = true;
64 | }
65 | if (hasReferenceDefs && Parsing.isBlank(contentString)) {
66 | block.unlink();
67 | content = null;
68 | } else {
69 | content = new BlockContent(contentString);
70 | }
71 | }
72 |
73 | override public void parseInlines(InlineParser inlineParser) {
74 | if (content !is null) {
75 | inlineParser.parse(content.getString(), block);
76 | }
77 | }
78 |
79 | public string getContentString() {
80 | return content.getString();
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/ReferenceParser.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.ReferenceParser;
2 |
3 | /**
4 | * Parser for inline references
5 | */
6 | public interface ReferenceParser {
7 | /**
8 | * @return how many characters were parsed as a reference, {@code 0} if none
9 | */
10 | int parseReference(string s);
11 | }
12 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/StaggeredDelimiterProcessor.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.StaggeredDelimiterProcessor;
2 |
3 | import hunt.markdown.node.Text;
4 | import hunt.markdown.parser.delimiter.DelimiterProcessor;
5 | import hunt.markdown.parser.delimiter.DelimiterRun;
6 |
7 | import hunt.collection.LinkedList;
8 | // import hunt.collection.ListIterator;
9 | import hunt.Exceptions;
10 | import std.conv;
11 | /**
12 | * An implementation of DelimiterProcessor that dispatches all calls to two or more other DelimiterProcessors
13 | * depending on the length of the delimiter run. All child DelimiterProcessors must have different minimum
14 | * lengths. A given delimiter run is dispatched to the child with the largest acceptable minimum length. If no
15 | * child is applicable, the one with the largest minimum length is chosen.
16 | */
17 | class StaggeredDelimiterProcessor : DelimiterProcessor {
18 |
19 | private char delim;
20 | private int minLength = 0;
21 | private LinkedList!(DelimiterProcessor) processors; // in reverse getMinLength order
22 |
23 | this(char delim) {
24 | processors = new LinkedList!(DelimiterProcessor)();
25 | this.delim = delim;
26 | }
27 |
28 |
29 | override public char getOpeningCharacter() {
30 | return delim;
31 | }
32 |
33 | override public char getClosingCharacter() {
34 | return delim;
35 | }
36 |
37 | override public int getMinLength() {
38 | return minLength;
39 | }
40 |
41 | void add(DelimiterProcessor dp) {
42 | int len = dp.getMinLength();
43 | // ListIterator!(DelimiterProcessor) it = processors.listIterator();
44 | bool added = false;
45 | // while (it.hasNext()) {
46 | // DelimiterProcessor p = it.next();
47 | // int pLen = p.getMinLength();
48 | // if (len > pLen) {
49 | // it.previous();
50 | // it.add(dp);
51 | // added = true;
52 | // break;
53 | // } else if (len == pLen) {
54 | // throw new IllegalArgumentException("Cannot add two delimiter processors for char '" ~ delim ~ "' and minimum length " ~ len);
55 | // }
56 | // }
57 | int idx = 0;
58 | foreach(p; processors) {
59 | int pLen = p.getMinLength();
60 | if (len > pLen) {
61 | // it.previous();
62 | // it.add(dp);
63 | processors.add(idx,dp);
64 | added = true;
65 | break;
66 | } else if (len == pLen) {
67 | throw new IllegalArgumentException("Cannot add two delimiter processors for char '" ~ delim ~ "' and minimum length " ~ len.to!string);
68 | }
69 | idx++;
70 | }
71 |
72 | if (!added) {
73 | processors.add(dp);
74 | this.minLength = len;
75 | }
76 | }
77 |
78 | private DelimiterProcessor findProcessor(int len) {
79 | foreach (DelimiterProcessor p ; processors) {
80 | if (p.getMinLength() <= len) {
81 | return p;
82 | }
83 | }
84 | return processors.getFirst();
85 | }
86 |
87 | override public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) {
88 | return findProcessor(opener.length).getDelimiterUse(opener, closer);
89 | }
90 |
91 | override public void process(Text opener, Text closer, int delimiterUse) {
92 | findProcessor(delimiterUse).process(opener, closer, delimiterUse);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/ThematicBreakParser.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.ThematicBreakParser;
2 |
3 | import hunt.markdown.node.Block;
4 | import hunt.markdown.node.ThematicBreak;
5 | import hunt.markdown.parser.block.AbstractBlockParser;
6 | import hunt.markdown.parser.block.BlockContinue;
7 | import hunt.markdown.parser.block.ParserState;
8 | import hunt.markdown.parser.block.BlockStart;
9 | import hunt.markdown.parser.block.MatchedBlockParser;
10 | import hunt.markdown.parser.block.AbstractBlockParserFactory;
11 |
12 | class ThematicBreakParser : AbstractBlockParser {
13 |
14 | private ThematicBreak block;
15 |
16 | this()
17 | {
18 | block = new ThematicBreak();
19 | }
20 |
21 | override public Block getBlock() {
22 | return block;
23 | }
24 |
25 | public BlockContinue tryContinue(ParserState state) {
26 | // a horizontal rule can never container > 1 line, so fail to match
27 | return BlockContinue.none();
28 | }
29 |
30 | public static class Factory : AbstractBlockParserFactory {
31 |
32 | public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
33 | if (state.getIndent() >= 4) {
34 | return BlockStart.none();
35 | }
36 | int nextNonSpace = state.getNextNonSpaceIndex();
37 | string line = state.getLine();
38 | if (isThematicBreak(line, nextNonSpace)) {
39 | return BlockStart.of(new ThematicBreakParser()).atIndex(cast(int)(line.length));
40 | } else {
41 | return BlockStart.none();
42 | }
43 | }
44 | }
45 |
46 | // spec: A line consisting of 0-3 spaces of indentation, followed by a sequence of three or more matching -, _, or *
47 | // characters, each followed optionally by any number of spaces, forms a thematic break.
48 | private static bool isThematicBreak(string line, int index) {
49 | int dashes = 0;
50 | int underscores = 0;
51 | int asterisks = 0;
52 | int length =cast(int)(line.length);
53 | for (int i = index; i < length; i++) {
54 | switch (line[i]) {
55 | case '-':
56 | dashes++;
57 | break;
58 | case '_':
59 | underscores++;
60 | break;
61 | case '*':
62 | asterisks++;
63 | break;
64 | case ' ':
65 | case '\t':
66 | // Allowed, even between markers
67 | break;
68 | default:
69 | return false;
70 | }
71 | }
72 |
73 | return ((dashes >= 3 && underscores == 0 && asterisks == 0) ||
74 | (underscores >= 3 && dashes == 0 && asterisks == 0) ||
75 | (asterisks >= 3 && dashes == 0 && underscores == 0));
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/inline/AsteriskDelimiterProcessor.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.inline.AsteriskDelimiterProcessor;
2 |
3 | import hunt.markdown.internal.inline.EmphasisDelimiterProcessor;
4 |
5 | class AsteriskDelimiterProcessor : EmphasisDelimiterProcessor {
6 |
7 | public this() {
8 | super('*');
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/inline/EmphasisDelimiterProcessor.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.inline.EmphasisDelimiterProcessor;
2 |
3 | import hunt.markdown.node.Emphasis;
4 | import hunt.markdown.node.Node;
5 | import hunt.markdown.node.StrongEmphasis;
6 | import hunt.markdown.node.Text;
7 | import hunt.markdown.parser.delimiter.DelimiterProcessor;
8 | import hunt.markdown.parser.delimiter.DelimiterRun;
9 |
10 | abstract class EmphasisDelimiterProcessor : DelimiterProcessor {
11 |
12 | private char delimiterChar;
13 |
14 | protected this(char delimiterChar) {
15 | this.delimiterChar = delimiterChar;
16 | }
17 |
18 | override public char getOpeningCharacter() {
19 | return delimiterChar;
20 | }
21 |
22 | override public char getClosingCharacter() {
23 | return delimiterChar;
24 | }
25 |
26 | override public int getMinLength() {
27 | return 1;
28 | }
29 |
30 | override public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) {
31 | // "multiple of 3" rule for internal delimiter runs
32 | if ((opener.canClose() || closer.canOpen()) && (opener.originalLength() + closer.originalLength()) % 3 == 0) {
33 | return 0;
34 | }
35 | // calculate actual number of delimiters used from this closer
36 | if (opener.length >= 2 && closer.length >= 2) {
37 | return 2;
38 | } else {
39 | return 1;
40 | }
41 | }
42 |
43 | override public void process(Text opener, Text closer, int delimiterUse) {
44 | string singleDelimiter = "" ~ (getOpeningCharacter());
45 | Node emphasis = delimiterUse == 1
46 | ? new Emphasis(singleDelimiter)
47 | : new StrongEmphasis(singleDelimiter ~ singleDelimiter);
48 |
49 | Node tmp = opener.getNext();
50 | while (tmp !is null && tmp != closer) {
51 | Node next = tmp.getNext();
52 | emphasis.appendChild(tmp);
53 | tmp = next;
54 | }
55 |
56 | opener.insertAfter(emphasis);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/inline/UnderscoreDelimiterProcessor.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.inline.UnderscoreDelimiterProcessor;
2 |
3 | import hunt.markdown.internal.inline.EmphasisDelimiterProcessor;
4 |
5 | class UnderscoreDelimiterProcessor : EmphasisDelimiterProcessor {
6 |
7 | public this() {
8 | super('_');
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/renderer/NodeRendererMap.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.renderer.NodeRendererMap;
2 |
3 | import hunt.markdown.node.Node;
4 | import hunt.markdown.renderer.NodeRenderer;
5 |
6 | import hunt.collection.HashMap;
7 | import hunt.collection.Map;
8 |
9 | class NodeRendererMap {
10 |
11 | private Map!(TypeInfo_Class, NodeRenderer) renderers;
12 |
13 | this()
14 | {
15 | renderers = new HashMap!(TypeInfo_Class, NodeRenderer)(32);
16 | }
17 |
18 | // public void add(NodeRenderer nodeRenderer) {
19 | // for (Class : Node> nodeType : nodeRenderer.getNodeTypes()) {
20 | // // Overwrite existing renderer
21 | // renderers.put(nodeType, nodeRenderer);
22 | // }
23 | // }
24 |
25 | public void add(NodeRenderer nodeRenderer) {
26 | foreach (nodeType ; nodeRenderer.getNodeTypes()) {
27 | // Overwrite existing renderer
28 | renderers.put(nodeType, nodeRenderer);
29 | }
30 | }
31 |
32 | public void render(Node node) {
33 | NodeRenderer nodeRenderer = renderers.get(typeid(node));
34 | if (nodeRenderer !is null) {
35 | nodeRenderer.render(node);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/renderer/text/BulletListHolder.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.renderer.text.BulletListHolder;
2 |
3 | import hunt.markdown.node.BulletList;
4 | import hunt.markdown.internal.renderer.text.ListHolder;
5 |
6 | class BulletListHolder : ListHolder {
7 | private char marker;
8 |
9 | public this(ListHolder parent, BulletList list) {
10 | super(parent);
11 | marker = list.getBulletMarker();
12 | }
13 |
14 | public char getMarker() {
15 | return marker;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/renderer/text/ListHolder.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.renderer.text.ListHolder;
2 |
3 | abstract class ListHolder {
4 | private __gshared string INDENT_DEFAULT = " ";
5 | private __gshared string INDENT_EMPTY = "";
6 |
7 | private ListHolder parent;
8 | private string indent;
9 |
10 | this(ListHolder parent) {
11 | this.parent = parent;
12 |
13 | if (parent !is null) {
14 | indent = parent.indent ~ INDENT_DEFAULT;
15 | } else {
16 | indent = INDENT_EMPTY;
17 | }
18 | }
19 |
20 | public ListHolder getParent() {
21 | return parent;
22 | }
23 |
24 | public string getIndent() {
25 | return indent;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/renderer/text/OrderedListHolder.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.renderer.text.OrderedListHolder;
2 |
3 | import hunt.markdown.node.OrderedList;
4 | import hunt.markdown.internal.renderer.text.ListHolder;
5 |
6 | class OrderedListHolder : ListHolder {
7 | private char delimiter;
8 | private int counter;
9 |
10 | public this(ListHolder parent, OrderedList list) {
11 | super(parent);
12 | delimiter = list.getDelimiter();
13 | counter = list.getStartNumber();
14 | }
15 |
16 | public char getDelimiter() {
17 | return delimiter;
18 | }
19 |
20 | public int getCounter() {
21 | return counter;
22 | }
23 |
24 | public void increaseCounter() {
25 | counter++;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/util/Common.d:
--------------------------------------------------------------------------------
1 | /*
2 | * hunt-time: A time library for D programming language.
3 | *
4 | * Copyright (C) 2015-2018 HuntLabs
5 | *
6 | * Website: https://www.huntlabs.net/
7 | *
8 | * Licensed under the Apache-2.0 License.
9 | *
10 | */
11 | module hunt.markdown.internal.util.Common;
12 |
13 | public import std.traits;
14 | public import std.array;
15 |
16 | string MakeGlobalVar(T)(string var, string init = null)
17 | {
18 | string str;
19 | str ~= `__gshared ` ~ T.stringof ~ ` _` ~ var ~ `;`;
20 | str ~= "\r\n";
21 | if (init is null)
22 | {
23 | str ~= `public static ref ` ~ T.stringof ~ ` ` ~ var ~ `()
24 | {
25 | static if(isAggregateType!(`~ T.stringof ~`))
26 | {
27 | if(_` ~ var ~ ` is null)
28 | {
29 | _`~ var ~ `= new ` ~ T.stringof ~ `();
30 | }
31 | }
32 | else static if(isArray!(`~ T.stringof ~ `))
33 | {
34 | if(_` ~ var ~ `.length == 0 )
35 | {
36 | _`~ var ~ `= new ` ~ T.stringof ~ `;
37 | }
38 | }
39 | else
40 | {
41 | if(_` ~ var ~ ` == `~ T.stringof.replace("[]","") ~`.init )
42 | {
43 | _`~ var ~ `= new ` ~ T.stringof ~ `();
44 | }
45 | }
46 |
47 | return _` ~ var ~ `;
48 | }`;
49 | }
50 | else
51 | {
52 | str ~= `public static ref ` ~ T.stringof ~ ` ` ~ var ~ `()
53 | {
54 | static if(isAggregateType!(`~ T.stringof ~`))
55 | {
56 | if(_` ~ var ~ ` is null)
57 | {
58 | _`~ var ~ `= ` ~ init ~ `;
59 | }
60 | }
61 | else static if(isArray!(`~ T.stringof ~ `))
62 | {
63 | if(_` ~ var ~ `.length == 0 )
64 | {
65 | _`~ var ~ `= ` ~ init ~ `;
66 | }
67 | }
68 | else
69 | {
70 | if(_` ~ var ~ ` == `~ T.stringof.replace("[]","") ~`.init )
71 | {
72 | _`~ var ~ `= ` ~ init ~ `;
73 | }
74 | }
75 |
76 | return _` ~ var ~ `;
77 | }`;
78 | }
79 |
80 | return str;
81 | }
82 |
83 |
84 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/util/Escaping.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.util.Escaping;
2 |
3 | import hunt.markdown.internal.util.Html5Entities;
4 |
5 | // import java.nio.charset.Charset;
6 | import std.algorithm.searching;
7 | // import hunt.time.util.Locale;
8 | import hunt.text;
9 | import hunt.util.StringBuilder;
10 | import std.regex;
11 | import std.string;
12 | import hunt.markdown.internal.util.Common;
13 |
14 | class Escaping {
15 |
16 | public enum string ESCAPABLE = "[!\"#$%&\'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~-]";
17 |
18 | private enum string ENTITY = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});";
19 |
20 | private enum string BACKSLASH_OR_AMP = /* Pattern.compile */"[\\\\&]";
21 |
22 | private enum string ENTITY_OR_ESCAPED_CHAR =
23 | /* Pattern.compile */"\\\\" ~ ESCAPABLE ~ '|' ~ ENTITY;
24 |
25 | private enum string XML_SPECIAL = "[&<>\"]";
26 |
27 | private enum string XML_SPECIAL_RE = /* Pattern.compile */XML_SPECIAL;
28 |
29 | private enum string XML_SPECIAL_OR_ENTITY =
30 | /* Pattern.compile */ENTITY ~ '|' ~ XML_SPECIAL;
31 |
32 | // From RFC 3986 (see "reserved", "unreserved") except don't escape '[' or ']' to be compatible with JS encodeURI
33 | private enum string ESCAPE_IN_URI =
34 | /* Pattern.compile */"(%[a-fA-F0-9]{0,2}|[^:/?#@!$&'()*+,;=a-zA-Z0-9\\-._~])";
35 |
36 | private enum char[] HEX_DIGITS =['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
37 |
38 | private enum string WHITESPACE = /* Pattern.compile */"[ \t\r\n]+";
39 |
40 | // private __gshared Replacer UNSAFE_CHAR_REPLACER;
41 |
42 | // private __gshared Replacer UNESCAPE_REPLACER;
43 |
44 | // private __gshared Replacer URI_REPLACER;
45 | mixin(MakeGlobalVar!(Replacer)("UNSAFE_CHAR_REPLACER",`new class Replacer {
46 | override public void replace(string input, StringBuilder sb) {
47 | switch (input) {
48 | case "&":
49 | sb.append("&");
50 | break;
51 | case "<":
52 | sb.append("<");
53 | break;
54 | case ">":
55 | sb.append(">");
56 | break;
57 | case "\"":
58 | sb.append(""");
59 | break;
60 | default:
61 | sb.append(input);
62 | }
63 | }
64 | }`));
65 | mixin(MakeGlobalVar!(Replacer)("UNESCAPE_REPLACER",`new class Replacer {
66 | override public void replace(string input, StringBuilder sb) {
67 | if (input[0] == '\\') {
68 | sb.append(input, 1, cast(int)input.length);
69 | } else {
70 | sb.append(Html5Entities.entityToString(input));
71 | }
72 | }
73 | }`));
74 | mixin(MakeGlobalVar!(Replacer)("URI_REPLACER",`new class Replacer {
75 | override public void replace(string input, StringBuilder sb) {
76 | if (input.startsWith("%")) {
77 | if (input.length == 3) {
78 | // Already percent-encoded, preserve
79 | sb.append(input);
80 | } else {
81 | // %25 is the percent-encoding for %
82 | sb.append("%25");
83 | sb.append(input, 1, cast(int)input.length);
84 | }
85 | } else {
86 | byte[] bytes = cast(byte[])input/* .getBytes(Charset.forName("UTF-8")) */;
87 | foreach (byte b ; bytes) {
88 | sb.append('%');
89 | sb.append(HEX_DIGITS[(b >> 4) & 0xF]);
90 | sb.append(HEX_DIGITS[b & 0xF]);
91 | }
92 | }
93 | }
94 | }`));
95 |
96 | public static string escapeHtml(string input, bool preserveEntities) {
97 | Regex!char p = preserveEntities ? regex(XML_SPECIAL_OR_ENTITY,"i") : regex(XML_SPECIAL_RE);
98 | return replaceAll(p, input, UNSAFE_CHAR_REPLACER);
99 | }
100 |
101 | /**
102 | * Replace entities and backslash escapes with literal characters.
103 | */
104 | public static string unescapeString(string s) {
105 | if (!matchAll(s,BACKSLASH_OR_AMP).empty()) {
106 | return replaceAll(regex(ENTITY_OR_ESCAPED_CHAR,"i"), s, UNESCAPE_REPLACER);
107 | } else {
108 | return s;
109 | }
110 | }
111 |
112 | public static string percentEncodeUrl(string s) {
113 | return replaceAll(regex(ESCAPE_IN_URI), s, URI_REPLACER);
114 | }
115 |
116 | public static string normalizeReference(string input) {
117 | // Strip '[' and ']', then strip
118 | string stripped = input.substring(1, cast(int)input.length - 1).strip();
119 | string lowercase = stripped.toLower(/* Locale.ROOT */);
120 | return std.regex.replaceAll(lowercase,regex(WHITESPACE)," ");
121 | }
122 |
123 | private static string replaceAll(Regex!char p, string s, Replacer replacer) {
124 | auto matchers = matchAll(s,p);
125 |
126 | if (matchers.empty()) {
127 | return s;
128 | }
129 |
130 | StringBuilder sb = new StringBuilder(s.length + 16);
131 | int lastEnd = 0;
132 | // do {
133 | // sb.append(s, lastEnd, matcher.start());
134 | // replacer.replace(matcher.group(), sb);
135 | // lastEnd = matcher.end();
136 | // } while (matcher.find());
137 | int offset = 0;
138 | foreach(matcher; matchers) {
139 | auto cap = matcher.captures[0];
140 | auto start =cast(int)(s[offset..$].indexOf(cap)) + offset;
141 | sb.append(s, lastEnd, start);
142 | replacer.replace(cap, sb);
143 | lastEnd = start + cast(int)(cap.length);
144 | offset = lastEnd;
145 | }
146 |
147 | if (lastEnd != s.length) {
148 | sb.append(s, lastEnd, cast(int)s.length);
149 | }
150 | return sb.toString();
151 | }
152 |
153 | private interface Replacer {
154 | void replace(string input, StringBuilder sb);
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/util/Html5Entities.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.util.Html5Entities;
2 |
3 | // import java.io.BufferedReader;
4 | // import hunt.Exceptions;
5 | // import java.io.InputStream;
6 | // import java.io.InputStreamReader;
7 | // import java.nio.charset.Charset;
8 |
9 | // import hunt.io.Common;
10 | import hunt.collection.HashMap;
11 | import hunt.collection.Map;
12 | import hunt.Exceptions;
13 | import hunt.Integer;
14 | import hunt.text.Common;
15 | import hunt.Char;
16 | import hunt.Exceptions;
17 | import hunt.markdown.internal.util.Common;
18 |
19 | import std.regex;
20 | import std.string;
21 | import std.stdio;
22 |
23 | class Html5Entities
24 | {
25 |
26 | mixin(MakeGlobalVar!(Map!(string, string))("NAMED_CHARACTER_REFERENCES",`readEntities()`));
27 | private __gshared string NUMERIC_PATTERN = "^[Xx]?";
28 | private __gshared string ENTITY_PATH = "resources/entities.properties";
29 |
30 | public static string entityToString(string input)
31 | {
32 | auto matcher = matchAll(input, NUMERIC_PATTERN);
33 |
34 | if (!matcher.empty())
35 | {
36 | auto group = matcher.front.captures[0];
37 | auto end = input.indexOf(group) + group.length;
38 | int base = end == 2 ? 10 : 16;
39 | try
40 | {
41 | int codePoint = Integer.parseInt(input.substring(end,
42 | cast(int) input.length - 1), base);
43 | if (codePoint == 0)
44 | {
45 | return "\uFFFD";
46 | }
47 | return cast(string)(Char.toChars(codePoint));
48 | }
49 | catch (IllegalArgumentException e)
50 | {
51 | return "\uFFFD";
52 | }
53 | }
54 | else
55 | {
56 | string name = input.substring(1, cast(int) input.length - 1);
57 | string s = NAMED_CHARACTER_REFERENCES.get(name);
58 | if (s !is null)
59 | {
60 | return s;
61 | }
62 | else
63 | {
64 | return input;
65 | }
66 | }
67 | }
68 |
69 | private static Map!(string, string) readEntities()
70 | {
71 |
72 | Map!(string, string) entities = new HashMap!(string, string)();
73 |
74 | auto f = File(ENTITY_PATH, "r");
75 | try
76 | {
77 | string line;
78 | while ((line = f.readln()) !is null)
79 | {
80 | if (line.length == 0)
81 | {
82 | continue;
83 | }
84 | int equal = cast(int)(line.indexOf("="));
85 | string key = line.substring(0, equal);
86 | string value = line.substring(equal + 1);
87 | entities.put(key, value);
88 | }
89 | }
90 | catch (IOException e)
91 | {
92 | throw new IllegalStateException(
93 | "Failed reading data for HTML named character references", e);
94 | }
95 |
96 | entities.put("NewLine", "\n");
97 |
98 | return entities;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/source/hunt/markdown/internal/util/Parsing.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.internal.util.Parsing;
2 | import hunt.Char;
3 | import hunt.util.StringBuilder;
4 | import hunt.text.Common;
5 | import hunt.logging;
6 | alias Character = Char;
7 |
8 | class Parsing {
9 |
10 | private const string TAGNAME = "[A-Za-z][A-Za-z0-9-]*";
11 | private const string ATTRIBUTENAME = "[a-zA-Z_:][a-zA-Z0-9:._-]*";
12 | private const string UNQUOTEDVALUE = "[^\"'=<>`\\x00-\\x20]+";
13 | private const string SINGLEQUOTEDVALUE = "'[^']*'";
14 | private const string DOUBLEQUOTEDVALUE = "\"[^\"]*\"";
15 |
16 | private const string ATTRIBUTEVALUE = "(?:" ~ UNQUOTEDVALUE ~ "|" ~ SINGLEQUOTEDVALUE ~ "|" ~ DOUBLEQUOTEDVALUE ~ ")";
17 | private const string ATTRIBUTEVALUESPEC = "(?:" ~ "\\s*=" ~ "\\s*" ~ ATTRIBUTEVALUE ~ ")";
18 | private const string ATTRIBUTE = "(?:" ~ "\\s+" ~ ATTRIBUTENAME ~ ATTRIBUTEVALUESPEC ~ "?)";
19 |
20 | public enum string OPENTAG = "<" ~ TAGNAME ~ ATTRIBUTE ~ "*" ~ "\\s*/?>";
21 | public enum string CLOSETAG = "" ~ TAGNAME ~ "\\s*[>]";
22 |
23 | public enum int CODE_BLOCK_INDENT = 4;
24 |
25 | public static int columnsToNextTabStop(int column) {
26 | // Tab stop is 4
27 | return 4 - (column % 4);
28 | }
29 |
30 | public static int find(char c, string s, int startIndex) {
31 | int length = cast(int)(s.length);
32 | for (int i = startIndex; i < length; i++) {
33 | if (s[i] == c) {
34 | return i;
35 | }
36 | }
37 | return -1;
38 | }
39 |
40 | public static int findLineBreak(string s, int startIndex) {
41 | int length = cast(int)(s.length);
42 | for (int i = startIndex; i < length; i++) {
43 | switch (s[i]) {
44 | case '\n':
45 | case '\r':
46 | return i;
47 | default:break;
48 | }
49 | }
50 | return -1;
51 | }
52 |
53 | public static bool isBlank(string s) {
54 | return findNonSpace(s, 0) == -1;
55 | }
56 |
57 | public static bool isLetter(string s, int index) {
58 | // int codePoint = Char.codePointAt(s, index);
59 | // return Char.isLetter(codePoint);
60 | import std.ascii;
61 | auto b = isAlpha(s.charAt(index));
62 | return b;
63 | }
64 |
65 | public static bool isSpaceOrTab(string s, int index) {
66 | if (index < s.length) {
67 | switch (s[index]) {
68 | case ' ':
69 | case '\t':
70 | return true;
71 | default: break;
72 | }
73 | }
74 | return false;
75 | }
76 |
77 | /**
78 | * Prepares the input line replacing {@code \0}
79 | */
80 | public static string prepareLine(string line) {
81 | // Avoid building a new string in the majority of cases (no \0)
82 | StringBuilder sb = null;
83 | int length = cast(int)(line.length);
84 | for (int i = 0; i < length; i++) {
85 | char c = line[i];
86 | switch (c) {
87 | case '\0':
88 | if (sb is null) {
89 | sb = new StringBuilder(length);
90 | sb.append(line, 0, i);
91 | }
92 | sb.append("\uFFFD");
93 | break;
94 | default:
95 | if (sb !is null) {
96 | sb.append(c);
97 | }
98 | }
99 | }
100 |
101 | if (sb !is null) {
102 | return sb.toString();
103 | } else {
104 | return line;
105 | }
106 | }
107 |
108 | public static int skip(char skip, string s, int startIndex, int endIndex) {
109 | for (int i = startIndex; i < endIndex; i++) {
110 | if (s[i] != skip) {
111 | return i;
112 | }
113 | }
114 | return endIndex;
115 | }
116 |
117 | public static int skipBackwards(char skip, string s, int startIndex, int lastIndex) {
118 | for (int i = startIndex; i >= lastIndex; i--) {
119 | if (s[i] != skip) {
120 | return i;
121 | }
122 | }
123 | return lastIndex - 1;
124 | }
125 |
126 | public static int skipSpaceTab(string s, int startIndex, int endIndex) {
127 | for (int i = startIndex; i < endIndex; i++) {
128 | switch (s[i]) {
129 | case ' ':
130 | case '\t':
131 | break;
132 | default:
133 | return i;
134 | }
135 | }
136 | return endIndex;
137 | }
138 |
139 | public static int skipSpaceTabBackwards(string s, int startIndex, int lastIndex) {
140 | for (int i = startIndex; i >= lastIndex; i--) {
141 | switch (s[i]) {
142 | case ' ':
143 | case '\t':
144 | break;
145 | default:
146 | return i;
147 | }
148 | }
149 | return lastIndex - 1;
150 | }
151 |
152 | private static int findNonSpace(string s, int startIndex) {
153 | int length = cast(int)(s.length);
154 | for (int i = startIndex; i < length; i++) {
155 | switch (s[i]) {
156 | case ' ':
157 | case '\t':
158 | case '\n':
159 | case '\u000B':
160 | case '\f':
161 | case '\r':
162 | break;
163 | default:
164 | return i;
165 | }
166 | }
167 | return -1;
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/AbstractVisitor.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.AbstractVisitor;
2 |
3 | import hunt.markdown.node.Node;
4 | import hunt.markdown.node.BlockQuote;
5 | import hunt.markdown.node.BulletList;
6 | import hunt.markdown.node.Code;
7 | import hunt.markdown.node.Document;
8 | import hunt.markdown.node.Emphasis;
9 | import hunt.markdown.node.FencedCodeBlock;
10 | import hunt.markdown.node.HardLineBreak;
11 | import hunt.markdown.node.Heading;
12 | import hunt.markdown.node.ThematicBreak;
13 | import hunt.markdown.node.HtmlInline;
14 | import hunt.markdown.node.HtmlBlock;
15 | import hunt.markdown.node.Image;
16 | import hunt.markdown.node.IndentedCodeBlock;
17 | import hunt.markdown.node.Link;
18 | import hunt.markdown.node.ListItem;
19 | import hunt.markdown.node.OrderedList;
20 | import hunt.markdown.node.Paragraph;
21 | import hunt.markdown.node.SoftLineBreak;
22 | import hunt.markdown.node.StrongEmphasis;
23 | import hunt.markdown.node.Text;
24 | import hunt.markdown.node.CustomBlock;
25 | import hunt.markdown.node.CustomNode;
26 | import hunt.markdown.node.Visitor;
27 |
28 | /**
29 | * Abstract visitor that visits all children by default.
30 | *
31 | * Can be used to only process certain nodes. If you override a method and want visiting to descend into children,
32 | * call {@link #visitChildren}.
33 | */
34 | abstract class AbstractVisitor : Visitor {
35 |
36 | override public void visit(BlockQuote blockQuote) {
37 | visitChildren(blockQuote);
38 | }
39 |
40 | override public void visit(BulletList bulletList) {
41 | visitChildren(bulletList);
42 | }
43 |
44 | override public void visit(Code code) {
45 | visitChildren(code);
46 | }
47 |
48 | override public void visit(Document document) {
49 | visitChildren(document);
50 | }
51 |
52 | override public void visit(Emphasis emphasis) {
53 | visitChildren(emphasis);
54 | }
55 |
56 | override public void visit(FencedCodeBlock fencedCodeBlock) {
57 | visitChildren(fencedCodeBlock);
58 | }
59 |
60 | override public void visit(HardLineBreak hardLineBreak) {
61 | visitChildren(hardLineBreak);
62 | }
63 |
64 | override public void visit(Heading heading) {
65 | visitChildren(heading);
66 | }
67 |
68 | override public void visit(ThematicBreak thematicBreak) {
69 | visitChildren(thematicBreak);
70 | }
71 |
72 | override public void visit(HtmlInline htmlInline) {
73 | visitChildren(htmlInline);
74 | }
75 |
76 | override public void visit(HtmlBlock htmlBlock) {
77 | visitChildren(htmlBlock);
78 | }
79 |
80 | override public void visit(Image image) {
81 | visitChildren(image);
82 | }
83 |
84 | override public void visit(IndentedCodeBlock indentedCodeBlock) {
85 | visitChildren(indentedCodeBlock);
86 | }
87 |
88 | override public void visit(Link link) {
89 | visitChildren(link);
90 | }
91 |
92 | override public void visit(ListItem listItem) {
93 | visitChildren(listItem);
94 | }
95 |
96 | override public void visit(OrderedList orderedList) {
97 | visitChildren(orderedList);
98 | }
99 |
100 | override public void visit(Paragraph paragraph) {
101 | visitChildren(paragraph);
102 | }
103 |
104 | override public void visit(SoftLineBreak softLineBreak) {
105 | visitChildren(softLineBreak);
106 | }
107 |
108 | override public void visit(StrongEmphasis strongEmphasis) {
109 | visitChildren(strongEmphasis);
110 | }
111 |
112 | override public void visit(Text text) {
113 | visitChildren(text);
114 | }
115 |
116 | override public void visit(CustomBlock customBlock) {
117 | visitChildren(customBlock);
118 | }
119 |
120 | override public void visit(CustomNode customNode) {
121 | visitChildren(customNode);
122 | }
123 |
124 | /**
125 | * Visit the child nodes.
126 | *
127 | * @param parent the parent node whose children should be visited
128 | */
129 | protected void visitChildren(Node parent) {
130 | Node node = parent.getFirstChild();
131 | while (node !is null) {
132 | // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no
133 | // node after visiting it. So get the next node before visiting.
134 | Node next = node.getNext();
135 | node.accept(this);
136 | node = next;
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/Block.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.Block;
2 |
3 | import hunt.markdown.node.Node;
4 | import hunt.Exceptions;
5 |
6 | abstract class Block : Node {
7 |
8 | override public Block getParent() {
9 | return cast(Block) super.getParent();
10 | }
11 |
12 | override protected void setParent(Node parent) {
13 | if (!(cast(Block)parent !is null)) {
14 | throw new IllegalArgumentException("Parent of block must also be block (can not be inline)");
15 | }
16 | super.setParent(parent);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/BlockQuote.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.BlockQuote;
2 |
3 | import hunt.markdown.node.Block;
4 | import hunt.markdown.node.Visitor;
5 |
6 | class BlockQuote : Block {
7 |
8 | override public void accept(Visitor visitor) {
9 | visitor.visit(this);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/BulletList.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.BulletList;
2 |
3 | import hunt.markdown.node.ListBlock;
4 | import hunt.markdown.node.Visitor;
5 |
6 | class BulletList : ListBlock {
7 |
8 | private char bulletMarker;
9 |
10 | override public void accept(Visitor visitor) {
11 | visitor.visit(this);
12 | }
13 |
14 | public char getBulletMarker() {
15 | return bulletMarker;
16 | }
17 |
18 | public void setBulletMarker(char bulletMarker) {
19 | this.bulletMarker = bulletMarker;
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/Code.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.Code;
2 |
3 | import hunt.markdown.node.Node;
4 | import hunt.markdown.node.Visitor;
5 |
6 | class Code : Node {
7 |
8 | private string literal;
9 |
10 | public this() {
11 | }
12 |
13 | public this(string literal) {
14 | this.literal = literal;
15 | }
16 |
17 | override public void accept(Visitor visitor) {
18 | visitor.visit(this);
19 | }
20 |
21 | public string getLiteral() {
22 | return literal;
23 | }
24 |
25 | public void setLiteral(string literal) {
26 | this.literal = literal;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/CustomBlock.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.CustomBlock;
2 |
3 | import hunt.markdown.node.Block;
4 | import hunt.markdown.node.Visitor;
5 |
6 | abstract class CustomBlock : Block {
7 |
8 | override public void accept(Visitor visitor) {
9 | visitor.visit(this);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/CustomNode.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.CustomNode;
2 |
3 | import hunt.markdown.node.Node;
4 | import hunt.markdown.node.Visitor;
5 |
6 | abstract class CustomNode : Node {
7 | override public void accept(Visitor visitor) {
8 | visitor.visit(this);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/Delimited.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.Delimited;
2 |
3 | /**
4 | * A node that uses delimiters in the source form (e.g. *bold*
).
5 | */
6 | public interface Delimited {
7 |
8 | /**
9 | * @return the opening (beginning) delimiter, e.g. *
10 | */
11 | string getOpeningDelimiter();
12 |
13 | /**
14 | * @return the closing (ending) delimiter, e.g. *
15 | */
16 | string getClosingDelimiter();
17 | }
18 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/Document.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.Document;
2 |
3 | import hunt.markdown.node.Block;
4 | import hunt.markdown.node.Visitor;
5 |
6 | class Document : Block {
7 |
8 | override public void accept(Visitor visitor) {
9 | visitor.visit(this);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/Emphasis.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.Emphasis;
2 |
3 | import hunt.markdown.node.Node;
4 | import hunt.markdown.node.Visitor;
5 | import hunt.markdown.node.Delimited;
6 |
7 | class Emphasis : Node, Delimited {
8 |
9 | private string delimiter;
10 |
11 | public this() {
12 | }
13 |
14 | public this(string delimiter) {
15 | this.delimiter = delimiter;
16 | }
17 |
18 | public void setDelimiter(string delimiter) {
19 | this.delimiter = delimiter;
20 | }
21 |
22 | override public string getOpeningDelimiter() {
23 | return delimiter;
24 | }
25 |
26 | override public string getClosingDelimiter() {
27 | return delimiter;
28 | }
29 |
30 | override public void accept(Visitor visitor) {
31 | visitor.visit(this);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/FencedCodeBlock.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.FencedCodeBlock;
2 |
3 |
4 | import hunt.markdown.node.Block;
5 | import hunt.markdown.node.Visitor;
6 |
7 | class FencedCodeBlock : Block {
8 |
9 | private char fenceChar;
10 | private int fenceLength;
11 | private int fenceIndent;
12 |
13 | private string info;
14 | private string literal;
15 |
16 | override public void accept(Visitor visitor) {
17 | visitor.visit(this);
18 | }
19 |
20 | public char getFenceChar() {
21 | return fenceChar;
22 | }
23 |
24 | public void setFenceChar(char fenceChar) {
25 | this.fenceChar = fenceChar;
26 | }
27 |
28 | public int getFenceLength() {
29 | return fenceLength;
30 | }
31 |
32 | public void setFenceLength(int fenceLength) {
33 | this.fenceLength = fenceLength;
34 | }
35 |
36 | public int getFenceIndent() {
37 | return fenceIndent;
38 | }
39 |
40 | public void setFenceIndent(int fenceIndent) {
41 | this.fenceIndent = fenceIndent;
42 | }
43 |
44 | public string getInfo() {
45 | return info;
46 | }
47 |
48 | public void setInfo(string info) {
49 | this.info = info;
50 | }
51 |
52 | public string getLiteral() {
53 | return literal;
54 | }
55 |
56 | public void setLiteral(string literal) {
57 | this.literal = literal;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/HardLineBreak.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.HardLineBreak;
2 |
3 | import hunt.markdown.node.Node;
4 | import hunt.markdown.node.Visitor;
5 |
6 | class HardLineBreak : Node {
7 |
8 | override public void accept(Visitor visitor) {
9 | visitor.visit(this);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/Heading.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.Heading;
2 |
3 | import hunt.markdown.node.Block;
4 | import hunt.markdown.node.Visitor;
5 |
6 | class Heading : Block {
7 |
8 | private int level;
9 |
10 | override public void accept(Visitor visitor) {
11 | visitor.visit(this);
12 | }
13 |
14 | public int getLevel() {
15 | return level;
16 | }
17 |
18 | public void setLevel(int level) {
19 | this.level = level;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/HtmlBlock.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.HtmlBlock;
2 |
3 | import hunt.markdown.node.Block;
4 | import hunt.markdown.node.Visitor;
5 |
6 | /**
7 | * HTML block
8 | */
9 | class HtmlBlock : Block {
10 |
11 | private string literal;
12 |
13 | override public void accept(Visitor visitor) {
14 | visitor.visit(this);
15 | }
16 |
17 | public string getLiteral() {
18 | return literal;
19 | }
20 |
21 | public void setLiteral(string literal) {
22 | this.literal = literal;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/HtmlInline.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.HtmlInline;
2 |
3 |
4 | import hunt.markdown.node.Node;
5 | import hunt.markdown.node.Visitor;
6 |
7 | /**
8 | * Inline HTML element.
9 | */
10 | class HtmlInline : Node {
11 |
12 | private string literal;
13 |
14 | override public void accept(Visitor visitor) {
15 | visitor.visit(this);
16 | }
17 |
18 | public string getLiteral() {
19 | return literal;
20 | }
21 |
22 | public void setLiteral(string literal) {
23 | this.literal = literal;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/Image.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.Image;
2 |
3 | import hunt.markdown.node.Node;
4 | import hunt.markdown.node.Visitor;
5 |
6 | class Image : Node {
7 |
8 | private string destination;
9 | private string title;
10 |
11 | public this() {
12 | }
13 |
14 | public this(string destination, string title) {
15 | this.destination = destination;
16 | this.title = title;
17 | }
18 |
19 | override public void accept(Visitor visitor) {
20 | visitor.visit(this);
21 | }
22 |
23 | public string getDestination() {
24 | return destination;
25 | }
26 |
27 | public void setDestination(string destination) {
28 | this.destination = destination;
29 | }
30 |
31 | public string getTitle() {
32 | return title;
33 | }
34 |
35 | public void setTitle(string title) {
36 | this.title = title;
37 | }
38 |
39 | override protected string toStringAttributes() {
40 | return "destination=" ~ destination ~ ", title=" ~ title;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/IndentedCodeBlock.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.IndentedCodeBlock;
2 |
3 | import hunt.markdown.node.Block;
4 | import hunt.markdown.node.Visitor;
5 |
6 | class IndentedCodeBlock : Block {
7 |
8 | private string literal;
9 |
10 | override public void accept(Visitor visitor) {
11 | visitor.visit(this);
12 | }
13 |
14 | public string getLiteral() {
15 | return literal;
16 | }
17 |
18 | public void setLiteral(string literal) {
19 | this.literal = literal;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/source/hunt/markdown/node/Link.d:
--------------------------------------------------------------------------------
1 | module hunt.markdown.node.Link;
2 |
3 | import hunt.markdown.node.Node;
4 | import hunt.markdown.node.Visitor;
5 |
6 | /**
7 | * A link with a destination and an optional title; the link text is in child nodes.
8 | *
9 | * Example for an inline link in a CommonMark document: 10 | *
11 | * [link](/uri "title")
12 | *
13 | * 14 | * The corresponding Link node would look like this: 15 | *
21 | * Note that the text in the link can contain inline formatting, so it could also contain an {@link Image} or 22 | * {@link Emphasis}, etc. 23 | */ 24 | class Link : Node { 25 | 26 | private string destination; 27 | private string title; 28 | 29 | public this() { 30 | } 31 | 32 | public this(string destination, string title) { 33 | this.destination = destination; 34 | this.title = title; 35 | } 36 | 37 | override public void accept(Visitor visitor) { 38 | visitor.visit(this); 39 | } 40 | 41 | public string getDestination() { 42 | return destination; 43 | } 44 | 45 | public void setDestination(string destination) { 46 | this.destination = destination; 47 | } 48 | 49 | public string getTitle() { 50 | return title; 51 | } 52 | 53 | public void setTitle(string title) { 54 | this.title = title; 55 | } 56 | 57 | override protected string toStringAttributes() { 58 | return "destination=" ~ destination ~ ", title=" ~ title; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /source/hunt/markdown/node/ListBlock.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.node.ListBlock; 2 | 3 | import hunt.markdown.node.Block; 4 | import hunt.markdown.node.Visitor; 5 | 6 | abstract class ListBlock : Block { 7 | 8 | private bool tight; 9 | 10 | /** 11 | * @return whether this list is tight or loose 12 | */ 13 | public bool isTight() { 14 | return tight; 15 | } 16 | 17 | public void setTight(bool tight) { 18 | this.tight = tight; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /source/hunt/markdown/node/ListItem.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.node.ListItem; 2 | 3 | import hunt.markdown.node.Block; 4 | import hunt.markdown.node.Visitor; 5 | 6 | class ListItem : Block { 7 | 8 | override public void accept(Visitor visitor) { 9 | visitor.visit(this); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /source/hunt/markdown/node/Node.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.node.Node; 2 | 3 | import hunt.markdown.node.Visitor; 4 | 5 | abstract class Node { 6 | 7 | private Node parent = null; 8 | private Node firstChild = null; 9 | private Node lastChild = null; 10 | private Node prev = null; 11 | private Node next = null; 12 | 13 | public abstract void accept(Visitor visitor); 14 | 15 | public Node getNext() { 16 | return next; 17 | } 18 | 19 | public Node getPrevious() { 20 | return prev; 21 | } 22 | 23 | public Node getFirstChild() { 24 | return firstChild; 25 | } 26 | 27 | public Node getLastChild() { 28 | return lastChild; 29 | } 30 | 31 | public Node getParent() { 32 | return parent; 33 | } 34 | 35 | protected void setParent(Node parent) { 36 | this.parent = parent; 37 | } 38 | 39 | public void appendChild(Node child) { 40 | child.unlink(); 41 | child.setParent(this); 42 | if (this.lastChild !is null) { 43 | this.lastChild.next = child; 44 | child.prev = this.lastChild; 45 | this.lastChild = child; 46 | } else { 47 | this.firstChild = child; 48 | this.lastChild = child; 49 | } 50 | } 51 | 52 | public void prependChild(Node child) { 53 | child.unlink(); 54 | child.setParent(this); 55 | if (this.firstChild !is null) { 56 | this.firstChild.prev = child; 57 | child.next = this.firstChild; 58 | this.firstChild = child; 59 | } else { 60 | this.firstChild = child; 61 | this.lastChild = child; 62 | } 63 | } 64 | 65 | public void unlink() { 66 | if (this.prev !is null) { 67 | this.prev.next = this.next; 68 | } else if (this.parent !is null) { 69 | this.parent.firstChild = this.next; 70 | } 71 | if (this.next !is null) { 72 | this.next.prev = this.prev; 73 | } else if (this.parent !is null) { 74 | this.parent.lastChild = this.prev; 75 | } 76 | this.parent = null; 77 | this.next = null; 78 | this.prev = null; 79 | } 80 | 81 | public void insertAfter(Node sibling) { 82 | sibling.unlink(); 83 | sibling.next = this.next; 84 | if (sibling.next !is null) { 85 | sibling.next.prev = sibling; 86 | } 87 | sibling.prev = this; 88 | this.next = sibling; 89 | sibling.parent = this.parent; 90 | if (sibling.next is null) { 91 | sibling.parent.lastChild = sibling; 92 | } 93 | } 94 | 95 | public void insertBefore(Node sibling) { 96 | sibling.unlink(); 97 | sibling.prev = this.prev; 98 | if (sibling.prev !is null) { 99 | sibling.prev.next = sibling; 100 | } 101 | sibling.next = this; 102 | this.prev = sibling; 103 | sibling.parent = this.parent; 104 | if (sibling.prev is null) { 105 | sibling.parent.firstChild = sibling; 106 | } 107 | } 108 | 109 | override public string toString() { 110 | return typeid(this).name ~ "{" ~ toStringAttributes() ~ "}"; 111 | } 112 | 113 | protected string toStringAttributes() { 114 | return ""; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /source/hunt/markdown/node/OrderedList.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.node.OrderedList; 2 | 3 | import hunt.markdown.node.ListBlock; 4 | import hunt.markdown.node.Visitor; 5 | 6 | class OrderedList : ListBlock { 7 | 8 | private int startNumber; 9 | private char delimiter; 10 | 11 | override public void accept(Visitor visitor) { 12 | visitor.visit(this); 13 | } 14 | 15 | public int getStartNumber() { 16 | return startNumber; 17 | } 18 | 19 | public void setStartNumber(int startNumber) { 20 | this.startNumber = startNumber; 21 | } 22 | 23 | public char getDelimiter() { 24 | return delimiter; 25 | } 26 | 27 | public void setDelimiter(char delimiter) { 28 | this.delimiter = delimiter; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /source/hunt/markdown/node/Paragraph.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.node.Paragraph; 2 | 3 | import hunt.markdown.node.Block; 4 | import hunt.markdown.node.Visitor; 5 | 6 | class Paragraph : Block { 7 | 8 | override public void accept(Visitor visitor) { 9 | visitor.visit(this); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /source/hunt/markdown/node/SoftLineBreak.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.node.SoftLineBreak; 2 | 3 | import hunt.markdown.node.Node; 4 | import hunt.markdown.node.Visitor; 5 | 6 | class SoftLineBreak : Node { 7 | 8 | override public void accept(Visitor visitor) { 9 | visitor.visit(this); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /source/hunt/markdown/node/StrongEmphasis.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.node.StrongEmphasis; 2 | 3 | import hunt.markdown.node.Node; 4 | import hunt.markdown.node.Delimited; 5 | import hunt.markdown.node.Visitor; 6 | 7 | 8 | class StrongEmphasis : Node, Delimited { 9 | 10 | private string delimiter; 11 | 12 | public this() { 13 | } 14 | 15 | public this(string delimiter) { 16 | this.delimiter = delimiter; 17 | } 18 | 19 | public void setDelimiter(string delimiter) { 20 | this.delimiter = delimiter; 21 | } 22 | 23 | override public string getOpeningDelimiter() { 24 | return delimiter; 25 | } 26 | 27 | override public string getClosingDelimiter() { 28 | return delimiter; 29 | } 30 | 31 | override public void accept(Visitor visitor) { 32 | visitor.visit(this); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /source/hunt/markdown/node/Text.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.node.Text; 2 | 3 | import hunt.markdown.node.Node; 4 | import hunt.markdown.node.Visitor; 5 | 6 | class Text : Node { 7 | 8 | private string literal; 9 | 10 | public this() { 11 | } 12 | 13 | public this(string literal) { 14 | this.literal = literal; 15 | } 16 | 17 | override public void accept(Visitor visitor) { 18 | visitor.visit(this); 19 | } 20 | 21 | public string getLiteral() { 22 | return literal; 23 | } 24 | 25 | public void setLiteral(string literal) { 26 | this.literal = literal; 27 | } 28 | 29 | override protected string toStringAttributes() { 30 | return "literal=" ~ literal; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /source/hunt/markdown/node/ThematicBreak.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.node.ThematicBreak; 2 | 3 | import hunt.markdown.node.Block; 4 | import hunt.markdown.node.Visitor; 5 | 6 | class ThematicBreak : Block { 7 | 8 | override public void accept(Visitor visitor) { 9 | visitor.visit(this); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /source/hunt/markdown/node/Visitor.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.node.Visitor; 2 | 3 | import hunt.markdown.node.BlockQuote; 4 | import hunt.markdown.node.BulletList; 5 | import hunt.markdown.node.Code; 6 | import hunt.markdown.node.Document; 7 | import hunt.markdown.node.Emphasis; 8 | import hunt.markdown.node.FencedCodeBlock; 9 | import hunt.markdown.node.HardLineBreak; 10 | import hunt.markdown.node.Heading; 11 | import hunt.markdown.node.ThematicBreak; 12 | import hunt.markdown.node.HtmlInline; 13 | import hunt.markdown.node.HtmlBlock; 14 | import hunt.markdown.node.Image; 15 | import hunt.markdown.node.IndentedCodeBlock; 16 | import hunt.markdown.node.Link; 17 | import hunt.markdown.node.ListItem; 18 | import hunt.markdown.node.OrderedList; 19 | import hunt.markdown.node.Paragraph; 20 | import hunt.markdown.node.SoftLineBreak; 21 | import hunt.markdown.node.StrongEmphasis; 22 | import hunt.markdown.node.Text; 23 | import hunt.markdown.node.CustomBlock; 24 | import hunt.markdown.node.CustomNode; 25 | 26 | /** 27 | * Node visitor. 28 | *
29 | * See {@link AbstractVisitor} for a base class that can be extended. 30 | */ 31 | public interface Visitor { 32 | 33 | void visit(BlockQuote blockQuote); 34 | 35 | void visit(BulletList bulletList); 36 | 37 | void visit(Code code); 38 | 39 | void visit(Document document); 40 | 41 | void visit(Emphasis emphasis); 42 | 43 | void visit(FencedCodeBlock fencedCodeBlock); 44 | 45 | void visit(HardLineBreak hardLineBreak); 46 | 47 | void visit(Heading heading); 48 | 49 | void visit(ThematicBreak thematicBreak); 50 | 51 | void visit(HtmlInline htmlInline); 52 | 53 | void visit(HtmlBlock htmlBlock); 54 | 55 | void visit(Image image); 56 | 57 | void visit(IndentedCodeBlock indentedCodeBlock); 58 | 59 | void visit(Link link); 60 | 61 | void visit(ListItem listItem); 62 | 63 | void visit(OrderedList orderedList); 64 | 65 | void visit(Paragraph paragraph); 66 | 67 | void visit(SoftLineBreak softLineBreak); 68 | 69 | void visit(StrongEmphasis strongEmphasis); 70 | 71 | void visit(Text text); 72 | 73 | void visit(CustomBlock customBlock); 74 | 75 | void visit(CustomNode customNode); 76 | } 77 | -------------------------------------------------------------------------------- /source/hunt/markdown/node/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * AST node types (see {@link hunt.markdown.node.Node}) and visitors (see {@link hunt.markdown.node.AbstractVisitor}) 3 | */ 4 | module hunt.markdown.node; 5 | 6 | public import hunt.markdown.node.Node; 7 | public import hunt.markdown.node.Block; 8 | public import hunt.markdown.node.ListBlock; 9 | public import hunt.markdown.node.BlockQuote; 10 | public import hunt.markdown.node.BulletList; 11 | public import hunt.markdown.node.Code; 12 | public import hunt.markdown.node.Document; 13 | public import hunt.markdown.node.Emphasis; 14 | public import hunt.markdown.node.FencedCodeBlock; 15 | public import hunt.markdown.node.HardLineBreak; 16 | public import hunt.markdown.node.Heading; 17 | public import hunt.markdown.node.ThematicBreak; 18 | public import hunt.markdown.node.HtmlInline; 19 | public import hunt.markdown.node.HtmlBlock; 20 | public import hunt.markdown.node.Image; 21 | public import hunt.markdown.node.IndentedCodeBlock; 22 | public import hunt.markdown.node.Link; 23 | public import hunt.markdown.node.ListItem; 24 | public import hunt.markdown.node.OrderedList; 25 | public import hunt.markdown.node.Paragraph; 26 | public import hunt.markdown.node.SoftLineBreak; 27 | public import hunt.markdown.node.StrongEmphasis; 28 | public import hunt.markdown.node.Text; 29 | public import hunt.markdown.node.CustomBlock; 30 | public import hunt.markdown.node.CustomNode; 31 | -------------------------------------------------------------------------------- /source/hunt/markdown/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Root package of hunt-markdown 3 | *
4 | *
11 | * Implementations should subclass {@link AbstractBlockParser} instead of implementing this directly. 12 | */ 13 | public interface BlockParser { 14 | 15 | /** 16 | * Return true if the block that is parsed is a container (contains other blocks), or false if it's a leaf. 17 | */ 18 | bool isContainer(); 19 | 20 | bool canContain(Block childBlock); 21 | 22 | Block getBlock(); 23 | 24 | BlockContinue tryContinue(ParserState parserState); 25 | 26 | void addLine(string line); 27 | 28 | void closeBlock(); 29 | 30 | void parseInlines(InlineParser inlineParser); 31 | 32 | int opCmp(BlockParser o); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/block/BlockParserFactory.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.block.BlockParserFactory; 2 | 3 | import hunt.markdown.parser.block.ParserState; 4 | import hunt.markdown.parser.block.BlockStart; 5 | import hunt.markdown.parser.block.MatchedBlockParser; 6 | 7 | /** 8 | * Parser factory for a block node for determining when a block starts. 9 | *
10 | * Implementations should subclass {@link AbstractBlockParserFactory} instead of implementing this directly. 11 | */ 12 | public interface BlockParserFactory { 13 | 14 | BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/block/BlockStart.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.block.BlockStart; 2 | 3 | import hunt.markdown.internal.BlockStartImpl; 4 | 5 | import hunt.markdown.parser.block.BlockParser; 6 | 7 | /** 8 | * Result object for starting parsing of a block, see static methods for constructors. 9 | */ 10 | abstract class BlockStart { 11 | 12 | protected this() { 13 | } 14 | 15 | public static BlockStart none() { 16 | return null; 17 | } 18 | 19 | public static BlockStart of(BlockParser[] blockParsers...) { 20 | return new BlockStartImpl(blockParsers); 21 | } 22 | 23 | public abstract BlockStart atIndex(int newIndex); 24 | 25 | public abstract BlockStart atColumn(int newColumn); 26 | 27 | public abstract BlockStart replaceActiveBlockParser(); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/block/MatchedBlockParser.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.block.MatchedBlockParser; 2 | 3 | import hunt.markdown.parser.block.BlockParser; 4 | 5 | /** 6 | * Open block parser that was last matched during the continue phase. This is different from the currently active 7 | * block parser, as an unmatched block is only closed when a new block is started. 8 | *
This interface is not intended to be implemented by clients.
9 | */ 10 | public interface MatchedBlockParser { 11 | 12 | BlockParser getMatchedBlockParser(); 13 | 14 | /** 15 | * Returns the current content of the paragraph if the matched block is a paragraph. The content can be multiple 16 | * lines separated by {@code '\n'}. 17 | * 18 | * @return paragraph content or {@code null} 19 | */ 20 | string getParagraphContent(); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/block/ParserState.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.block.ParserState; 2 | 3 | import hunt.markdown.parser.block.BlockParser; 4 | 5 | /** 6 | * State of the parser that is used in block parsers. 7 | *This interface is not intended to be implemented by clients.
8 | */ 9 | public interface ParserState { 10 | 11 | /** 12 | * @return the current line 13 | */ 14 | string getLine(); 15 | 16 | /** 17 | * @return the current index within the line (0-based) 18 | */ 19 | int getIndex(); 20 | 21 | /** 22 | * @return the index of the next non-space character starting from {@link #getIndex()} (may be the same) (0-based) 23 | */ 24 | int getNextNonSpaceIndex(); 25 | 26 | /** 27 | * The column is the position within the line after tab characters have been processed as 4-space tab stops. 28 | * If the line doesn't contain any tabs, it's the same as the {@link #getIndex()}. If the line starts with a tab, 29 | * followed by text, then the column for the first character of the text is 4 (the index is 1). 30 | * 31 | * @return the current column within the line (0-based) 32 | */ 33 | int getColumn(); 34 | 35 | /** 36 | * @return the indentation in columns (either by spaces or tab stop of 4), starting from {@link #getColumn()} 37 | */ 38 | int getIndent(); 39 | 40 | /** 41 | * @return true if the current line is blank starting from the index 42 | */ 43 | bool isBlank(); 44 | 45 | /** 46 | * @return the deepest open block parser 47 | */ 48 | BlockParser getActiveBlockParser(); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/block/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Types for extending block parsing 3 | */ 4 | module hunt.markdown.parser.block; 5 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/delimiter/DelimiterProcessor.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.delimiter.DelimiterProcessor; 2 | 3 | import hunt.markdown.node.Text; 4 | import hunt.markdown.parser.delimiter.DelimiterRun; 5 | 6 | /** 7 | * Custom delimiter processor for additional delimiters besides {@code _} and {@code *}. 8 | *9 | * Note that implementations of this need to be thread-safe, the same instance may be used by multiple parsers. 10 | */ 11 | public interface DelimiterProcessor { 12 | 13 | /** 14 | * @return the character that marks the beginning of a delimited node, must not clash with any built-in special 15 | * characters 16 | */ 17 | char getOpeningCharacter(); 18 | 19 | /** 20 | * @return the character that marks the the ending of a delimited node, must not clash with any built-in special 21 | * characters. Note that for a symmetric delimiter such as "*", this is the same as the opening. 22 | */ 23 | char getClosingCharacter(); 24 | 25 | /** 26 | * Minimum number of delimiter characters that are needed to activate this. Must be at least 1. 27 | */ 28 | int getMinLength(); 29 | 30 | /** 31 | * Determine how many (if any) of the delimiter characters should be used. 32 | *
33 | * This allows implementations to decide how many characters to use based on the properties of the delimiter runs. 34 | * An implementation can also return 0 when it doesn't want to allow this particular combination of delimiter runs. 35 | * 36 | * @param opener the opening delimiter run 37 | * @param closer the closing delimiter run 38 | * @return how many delimiters should be used; must not be greater than length of either opener or closer 39 | */ 40 | int getDelimiterUse(DelimiterRun opener, DelimiterRun closer); 41 | 42 | /** 43 | * Process the matched delimiters, e.g. by wrapping the nodes between opener and closer in a new node, or appending 44 | * a new node after the opener. 45 | *
46 | * Note that removal of the delimiter from the delimiter nodes and unlinking them is done by the caller. 47 | * 48 | * @param opener the text node that contained the opening delimiter 49 | * @param closer the text node that contained the closing delimiter 50 | * @param delimiterUse the number of delimiters that were used 51 | */ 52 | void process(Text opener, Text closer, int delimiterUse); 53 | 54 | } 55 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/delimiter/DelimiterRun.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.delimiter.DelimiterRun; 2 | 3 | /** 4 | * A delimiter run is one or more of the same delimiter character. 5 | */ 6 | public interface DelimiterRun { 7 | 8 | /** 9 | * @return whether this can open a delimiter 10 | */ 11 | bool canOpen(); 12 | 13 | /** 14 | * @return whether this can close a delimiter 15 | */ 16 | bool canClose(); 17 | 18 | /** 19 | * @return the number of characters in this delimiter run (that are left for processing) 20 | */ 21 | int length(); 22 | 23 | /** 24 | * @return the number of characters originally in this delimiter run; at the start of processing, this is the same 25 | * as {{@link #length()}} 26 | */ 27 | int originalLength(); 28 | } 29 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Parsing input text to AST nodes (see {@link hunt.markdown.parser.Parser}) 3 | */ 4 | module hunt.markdown.parser; 5 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/NodeRenderer.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.renderer.NodeRenderer; 2 | 3 | import hunt.markdown.node.Node; 4 | 5 | import hunt.collection.Set; 6 | 7 | /** 8 | * A renderer for a set of node types. 9 | */ 10 | public interface NodeRenderer { 11 | 12 | /** 13 | * @return the types of nodes that this renderer handles 14 | */ 15 | Set!TypeInfo_Class getNodeTypes(); 16 | 17 | /** 18 | * Render the specified node. 19 | * 20 | * @param node the node to render, will be an instance of one of {@link #getNodeTypes()} 21 | */ 22 | void render(Node node); 23 | } 24 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/Renderer.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.renderer.Renderer; 2 | 3 | import hunt.markdown.node.Node; 4 | 5 | import hunt.util.Appendable; 6 | 7 | public interface Renderer { 8 | 9 | /** 10 | * Render the tree of nodes to output. 11 | * 12 | * @param node the root node 13 | * @param output output for rendering 14 | */ 15 | void render(Node node, Appendable output); 16 | 17 | /** 18 | * Render the tree of nodes to string. 19 | * 20 | * @param node the root node 21 | * @return the rendered string 22 | */ 23 | string render(Node node); 24 | } 25 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/html/AttributeProvider.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.renderer.html.AttributeProvider; 2 | 3 | import hunt.markdown.node.Node; 4 | 5 | import hunt.collection.Map; 6 | 7 | /** 8 | * Extension point for adding/changing attributes on HTML tags for a node. 9 | */ 10 | public interface AttributeProvider { 11 | 12 | /** 13 | * Set the attributes for a HTML tag of the specified node by modifying the provided map. 14 | *
15 | * This allows to change or even remove default attributes. With great power comes great responsibility. 16 | *
17 | * The attribute key and values will be escaped (preserving character entities), so don't escape them here, 18 | * otherwise they will be double-escaped. 19 | *
20 | * This method may be called multiple times for the same node, if the node is rendered using multiple nested 21 | * tags (e.g. code blocks). 22 | * 23 | * @param node the node to set attributes for 24 | * @param tagName the HTML tag name that these attributes are for (e.g. {@code h1}, {@code pre}, {@code code}). 25 | * @param attributes the attributes, with any default attributes already set in the map 26 | */ 27 | void setAttributes(Node node, string tagName, Map!(string, string) attributes); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/html/AttributeProviderContext.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.renderer.html.AttributeProviderContext; 2 | 3 | /** 4 | * The context for attribute providers. 5 | *
Note: There are currently no methods here, this is for future extensibility.
6 | *This interface is not intended to be implemented by clients.
7 | */ 8 | public interface AttributeProviderContext { 9 | } 10 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/html/AttributeProviderFactory.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.renderer.html.AttributeProviderFactory; 2 | 3 | import hunt.markdown.renderer.html.AttributeProvider; 4 | import hunt.markdown.renderer.html.AttributeProviderContext; 5 | 6 | /** 7 | * Factory for instantiating new attribute providers when rendering is done. 8 | */ 9 | public interface AttributeProviderFactory { 10 | 11 | /** 12 | * Create a new attribute provider. 13 | * 14 | * @param context for this attribute provider 15 | * @return an AttributeProvider 16 | */ 17 | AttributeProvider create(AttributeProviderContext context); 18 | } 19 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/html/HtmlNodeRendererContext.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.renderer.html.HtmlNodeRendererContext; 2 | 3 | import hunt.markdown.node.Node; 4 | import hunt.markdown.renderer.html.HtmlWriter; 5 | 6 | import hunt.collection.Map; 7 | 8 | public interface HtmlNodeRendererContext { 9 | 10 | /** 11 | * @param url to be encoded 12 | * @return an encoded URL (depending on the configuration) 13 | */ 14 | string encodeUrl(string url); 15 | 16 | /** 17 | * Let extensions modify the HTML tag attributes. 18 | * 19 | * @param node the node for which the attributes are applied 20 | * @param tagName the HTML tag name that these attributes are for (e.g. {@code h1}, {@code pre}, {@code code}). 21 | * @param attributes the attributes that were calculated by the renderer 22 | * @return the extended attributes with added/updated/removed entries 23 | */ 24 | Map!(string, string) extendAttributes(Node node, string tagName, Map!(string, string) attributes); 25 | 26 | /** 27 | * @return the HTML writer to use 28 | */ 29 | HtmlWriter getWriter(); 30 | 31 | /** 32 | * @return HTML that should be rendered for a soft line break 33 | */ 34 | string getSoftbreak(); 35 | 36 | /** 37 | * Render the specified node and its children using the configured renderers. This should be used to render child 38 | * nodes; be careful not to pass the node that is being rendered, that would result in an endless loop. 39 | * 40 | * @param node the node to render 41 | */ 42 | void render(Node node); 43 | 44 | /** 45 | * @return whether HTML blocks and tags should be escaped or not 46 | */ 47 | bool shouldEscapeHtml(); 48 | } 49 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/html/HtmlNodeRendererFactory.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.renderer.html.HtmlNodeRendererFactory; 2 | 3 | import hunt.markdown.renderer.NodeRenderer; 4 | 5 | import hunt.markdown.renderer.html.HtmlNodeRendererContext; 6 | 7 | /** 8 | * Factory for instantiating new node renderers when rendering is done. 9 | */ 10 | public interface HtmlNodeRendererFactory { 11 | 12 | /** 13 | * Create a new node renderer for the specified rendering context. 14 | * 15 | * @param context the context for rendering (normally passed on to the node renderer) 16 | * @return a node renderer 17 | */ 18 | NodeRenderer create(HtmlNodeRendererContext context); 19 | } 20 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/html/HtmlWriter.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.renderer.html.HtmlWriter; 2 | 3 | import hunt.markdown.internal.util.Escaping; 4 | 5 | import hunt.Exceptions; 6 | import hunt.util.Common; 7 | import hunt.text.Common; 8 | import hunt.collection.Collections; 9 | import hunt.collection.Map; 10 | import hunt.util.Appendable; 11 | import hunt.markdown.internal.util.Common; 12 | 13 | class HtmlWriter { 14 | 15 | mixin(MakeGlobalVar!(Map!(string, string))("NO_ATTRIBUTES",`Collections.emptyMap!(string, string)()`)); 16 | 17 | 18 | private Appendable buffer; 19 | private char lastChar = 0; 20 | 21 | this(Appendable o) { 22 | this.buffer = o; 23 | } 24 | 25 | public void raw(string s) { 26 | append(s); 27 | } 28 | 29 | public void text(string text) { 30 | append(Escaping.escapeHtml(text, false)); 31 | } 32 | 33 | public void tag(string name) { 34 | tag(name, NO_ATTRIBUTES); 35 | } 36 | 37 | public void tag(string name, Map!(string, string) attrs) { 38 | tag(name, attrs, false); 39 | } 40 | 41 | public void tag(string name, Map!(string, string) attrs, bool voidElement) { 42 | append("<"); 43 | append(name); 44 | if (attrs !is null && !attrs.isEmpty()) { 45 | foreach (string k ,string v ; attrs) { 46 | append(" "); 47 | append(Escaping.escapeHtml(k, true)); 48 | append("=\""); 49 | append(Escaping.escapeHtml(v, true)); 50 | append("\""); 51 | } 52 | } 53 | if (voidElement) { 54 | append(" /"); 55 | } 56 | 57 | append(">"); 58 | } 59 | 60 | public void line() { 61 | if (lastChar != 0 && lastChar != '\n') { 62 | append("\n"); 63 | } 64 | } 65 | 66 | protected void append(string s) { 67 | try { 68 | buffer.append(s); 69 | } catch (IOException e) { 70 | throw new RuntimeException(e); 71 | } 72 | int length = cast(int)(s.length); 73 | if (length != 0) { 74 | lastChar = s.charAt(length - 1); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/html/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * HTML rendering (see {@link hunt.markdown.renderer.html.HtmlRenderer}) 3 | */ 4 | module hunt.markdown.renderer.html; 5 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/text/TextContentNodeRendererContext.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.renderer.text.TextContentNodeRendererContext; 2 | 3 | import hunt.markdown.node.Node; 4 | import hunt.markdown.renderer.text.TextContentWriter; 5 | 6 | public interface TextContentNodeRendererContext { 7 | 8 | /** 9 | * @return true for stripping new lines and render text as "single line", 10 | * false for keeping all line breaks. 11 | */ 12 | bool stripNewlines(); 13 | 14 | /** 15 | * @return the writer to use 16 | */ 17 | TextContentWriter getWriter(); 18 | 19 | /** 20 | * Render the specified node and its children using the configured renderers. This should be used to render child 21 | * nodes; be careful not to pass the node that is being rendered, that would result in an endless loop. 22 | * 23 | * @param node the node to render 24 | */ 25 | void render(Node node); 26 | } 27 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/text/TextContentNodeRendererFactory.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.renderer.text.TextContentNodeRendererFactory; 2 | 3 | import hunt.markdown.renderer.NodeRenderer; 4 | import hunt.markdown.renderer.text.TextContentNodeRendererContext; 5 | 6 | /** 7 | * Factory for instantiating new node renderers when rendering is done. 8 | */ 9 | public interface TextContentNodeRendererFactory { 10 | 11 | /** 12 | * Create a new node renderer for the specified rendering context. 13 | * 14 | * @param context the context for rendering (normally passed on to the node renderer) 15 | * @return a node renderer 16 | */ 17 | NodeRenderer create(TextContentNodeRendererContext context); 18 | } 19 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/text/TextContentRenderer.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.renderer.text.TextContentRenderer; 2 | 3 | import hunt.markdown.Extension; 4 | import hunt.markdown.internal.renderer.NodeRendererMap; 5 | import hunt.markdown.node.Node; 6 | import hunt.markdown.renderer.NodeRenderer; 7 | import hunt.markdown.renderer.Renderer; 8 | import hunt.markdown.renderer.text.TextContentWriter; 9 | import hunt.markdown.renderer.text.TextContentNodeRendererFactory; 10 | import hunt.markdown.renderer.text.TextContentNodeRendererContext; 11 | import hunt.markdown.renderer.text.CoreTextContentNodeRenderer; 12 | 13 | import hunt.collection.ArrayList; 14 | import hunt.collection.List; 15 | 16 | import hunt.util.Appendable; 17 | import hunt.util.Common; 18 | import hunt.util.StringBuilder; 19 | 20 | class TextContentRenderer : Renderer { 21 | 22 | private bool _stripNewlines; 23 | 24 | private List!(TextContentNodeRendererFactory) nodeRendererFactories; 25 | 26 | private this(Builder builder) { 27 | this._stripNewlines = builder._stripNewlines; 28 | 29 | this.nodeRendererFactories = new ArrayList!TextContentNodeRendererFactory(builder.nodeRendererFactories.size() + 1); 30 | this.nodeRendererFactories.addAll(builder.nodeRendererFactories); 31 | // Add as last. This means clients can override the rendering of core nodes if they want. 32 | this.nodeRendererFactories.add(new class TextContentNodeRendererFactory { 33 | override public NodeRenderer create(TextContentNodeRendererContext context) { 34 | return new CoreTextContentNodeRenderer(context); 35 | } 36 | }); 37 | } 38 | 39 | /** 40 | * Create a new builder for configuring an {@link TextContentRenderer}. 41 | * 42 | * @return a builder 43 | */ 44 | public static Builder builder() { 45 | return new Builder(); 46 | } 47 | 48 | public void render(Node node, Appendable output) { 49 | RendererContext context = new RendererContext(new TextContentWriter(output)); 50 | context.render(node); 51 | } 52 | 53 | override public string render(Node node) { 54 | StringBuilder sb = new StringBuilder(); 55 | render(node, sb); 56 | return sb.toString(); 57 | } 58 | 59 | /** 60 | * Builder for configuring an {@link TextContentRenderer}. See methods for default configuration. 61 | */ 62 | public static class Builder { 63 | 64 | private bool _stripNewlines = false; 65 | private List!(TextContentNodeRendererFactory) nodeRendererFactories; 66 | 67 | this() 68 | { 69 | nodeRendererFactories = new ArrayList!TextContentNodeRendererFactory(); 70 | } 71 | 72 | /** 73 | * @return the configured {@link TextContentRenderer} 74 | */ 75 | public TextContentRenderer build() { 76 | return new TextContentRenderer(this); 77 | } 78 | 79 | /** 80 | * Set the value of flag for stripping new lines. 81 | * 82 | * @param stripNewlines true for stripping new lines and render text as "single line", 83 | * false for keeping all line breaks 84 | * @return {@code this} 85 | */ 86 | public Builder stripNewlines(bool stripNewlines) { 87 | this._stripNewlines = stripNewlines; 88 | return this; 89 | } 90 | 91 | /** 92 | * Add a factory for instantiating a node renderer (done when rendering). This allows to override the rendering 93 | * of node types or define rendering for custom node types. 94 | *95 | * If multiple node renderers for the same node type are created, the one from the factory that was added first 96 | * "wins". (This is how the rendering for core node types can be overridden; the default rendering comes last.) 97 | * 98 | * @param nodeRendererFactory the factory for creating a node renderer 99 | * @return {@code this} 100 | */ 101 | public Builder nodeRendererFactory(TextContentNodeRendererFactory nodeRendererFactory) { 102 | this.nodeRendererFactories.add(nodeRendererFactory); 103 | return this; 104 | } 105 | 106 | /** 107 | * @param extensions extensions to use on this text content renderer 108 | * @return {@code this} 109 | */ 110 | public Builder extensions(Iterable!Extension extensions) { 111 | foreach (Extension extension ; extensions) { 112 | if (cast(TextContentRenderer.TextContentRendererExtension)extension !is null) { 113 | TextContentRenderer.TextContentRendererExtension htmlRendererExtension = 114 | cast(TextContentRenderer.TextContentRendererExtension) extension; 115 | htmlRendererExtension.extend(this); 116 | } 117 | } 118 | return this; 119 | } 120 | } 121 | 122 | /** 123 | * Extension for {@link TextContentRenderer}. 124 | */ 125 | public interface TextContentRendererExtension : Extension { 126 | void extend(TextContentRenderer.Builder rendererBuilder); 127 | } 128 | 129 | private class RendererContext : TextContentNodeRendererContext { 130 | private TextContentWriter textContentWriter; 131 | private NodeRendererMap nodeRendererMap; 132 | 133 | private this(TextContentWriter textContentWriter) { 134 | nodeRendererMap = new NodeRendererMap(); 135 | this.textContentWriter = textContentWriter; 136 | 137 | // The first node renderer for a node type "wins". 138 | for (int i = nodeRendererFactories.size() - 1; i >= 0; i--) { 139 | TextContentNodeRendererFactory nodeRendererFactory = nodeRendererFactories.get(i); 140 | NodeRenderer nodeRenderer = nodeRendererFactory.create(this); 141 | nodeRendererMap.add(nodeRenderer); 142 | } 143 | } 144 | 145 | override public bool stripNewlines() { 146 | return _stripNewlines; 147 | } 148 | 149 | override public TextContentWriter getWriter() { 150 | return textContentWriter; 151 | } 152 | 153 | public void render(Node node) { 154 | nodeRendererMap.render(node); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/text/TextContentWriter.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.renderer.text.TextContentWriter; 2 | 3 | import hunt.Exceptions; 4 | 5 | import hunt.util.Appendable; 6 | import hunt.util.Common; 7 | import hunt.text.Common; 8 | import std.regex; 9 | 10 | class TextContentWriter { 11 | 12 | private Appendable buffer; 13 | 14 | private char lastChar; 15 | 16 | public this(Appendable o) { 17 | buffer = o; 18 | } 19 | 20 | public void whitespace() { 21 | if (lastChar != 0 && lastChar != ' ') { 22 | append(' '); 23 | } 24 | } 25 | 26 | public void colon() { 27 | if (lastChar != 0 && lastChar != ':') { 28 | append(':'); 29 | } 30 | } 31 | 32 | public void line() { 33 | if (lastChar != 0 && lastChar != '\n') { 34 | append('\n'); 35 | } 36 | } 37 | 38 | public void writeStripped(string s) { 39 | append(s.replaceAll(regex("[\\r\\n\\s]+"), " ")); 40 | } 41 | 42 | public void write(string s) { 43 | append(s); 44 | } 45 | 46 | public void write(char c) { 47 | append(c); 48 | } 49 | 50 | private void append(string s) { 51 | try { 52 | buffer.append(s); 53 | } catch (IOException e) { 54 | throw new RuntimeException(e); 55 | } 56 | 57 | int length = cast(int)(s.length); 58 | if (length != 0) { 59 | lastChar = s.charAt(length - 1); 60 | } 61 | } 62 | 63 | private void append(char c) { 64 | try { 65 | buffer.append(c); 66 | } catch (IOException e) { 67 | throw new RuntimeException(e); 68 | } 69 | 70 | lastChar = c; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /source/hunt/markdown/renderer/text/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Text content rendering (see {@link hunt.markdown.renderer.text.TextContentRenderer}) 3 | */ 4 | module hunt.markdown.renderer.text; 5 | --------------------------------------------------------------------------------