├── .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~`

\n"); 51 | } 52 | 53 | 54 | public void closingCanHaveSpacesAfter() { 55 | assertRendering("```\ncode\n``` ", 56 | "
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"); 29 | assertRendering("###### test", "
test
\n"); 30 | assertRendering("####### test", "

####### test

\n"); 31 | assertRendering("#test", "

#test

\n"); 32 | assertRendering("#", "

\n"); 33 | } 34 | 35 | 36 | public void atxHeadingTrailing() { 37 | assertRendering("# test #", "

test

\n"); 38 | assertRendering("# test ###", "

test

\n"); 39 | assertRendering("# test # ", "

test

\n"); 40 | assertRendering("# test ### ", "

test

\n"); 41 | assertRendering("# test # #", "

test #

\n"); 42 | assertRendering("# test#", "

test#

\n"); 43 | } 44 | 45 | 46 | public void atxHeadingSurrogates() { 47 | assertRendering("# \u4F60\u597D #", "

\u4F60\u597D

\n"); 48 | } 49 | 50 | 51 | public void setextHeadingMarkers() { 52 | assertRendering("test\n=", "

test

\n"); 53 | assertRendering("test\n-", "

test

\n"); 54 | assertRendering("test\n====", "

test

\n"); 55 | assertRendering("test\n----", "

test

\n"); 56 | assertRendering("test\n==== ", "

test

\n"); 57 | assertRendering("test\n==== =", "

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 | "\n" ~ 31 | "\n"); 34 | } 35 | 36 | public void loose() { 37 | assertRendering("- foo\n" ~ 38 | "\n" ~ 39 | "- bar\n" ~ 40 | "\n" ~ 41 | "\n" ~ 42 | "- baz\n", 43 | "\n"); 54 | } 55 | 56 | 57 | public void looseNested() { 58 | assertRendering("- foo\n" ~ 59 | " - bar\n" ~ 60 | "\n" ~ 61 | "\n" ~ 62 | " baz", 63 | "\n"); 73 | } 74 | 75 | 76 | public void looseNested2() { 77 | assertRendering("- a\n" ~ 78 | " - b\n" ~ 79 | "\n" ~ 80 | " c\n" ~ 81 | "- d\n", 82 | "\n"); 93 | } 94 | 95 | public void looseOuter() { 96 | assertRendering("- foo\n" ~ 97 | " - bar\n" ~ 98 | "\n" ~ 99 | "\n" ~ 100 | " baz", 101 | "\n"); 110 | } 111 | 112 | public void looseListItem() { 113 | assertRendering("- one\n" ~ 114 | "\n" ~ 115 | " two\n", 116 | "\n"); 122 | } 123 | 124 | 125 | public void tightWithBlankLineAfter() { 126 | assertRendering("- foo\n" ~ 127 | "- bar\n" ~ 128 | "\n", 129 | "\n"); 133 | } 134 | 135 | 136 | public void tightListWithCodeBlock() { 137 | assertRendering("- a\n" ~ 138 | "- ```\n" ~ 139 | " b\n" ~ 140 | "\n" ~ 141 | "\n" ~ 142 | " ```\n" ~ 143 | "- c\n", 144 | "\n"); 154 | } 155 | 156 | 157 | public void tightListWithCodeBlock2() { 158 | assertRendering("* foo\n" ~ 159 | " ```\n" ~ 160 | " bar\n" ~ 161 | "\n" ~ 162 | " ```\n" ~ 163 | " baz\n", 164 | "\n"); 171 | } 172 | 173 | 174 | public void looseEmptyListItem() { 175 | assertRendering("* a\n" ~ 176 | "*\n" ~ 177 | "\n" ~ 178 | "* c", 179 | "\n"); 188 | } 189 | 190 | 191 | public void looseBlankLineAfterCodeBlock() { 192 | assertRendering("1. ```\n" ~ 193 | " foo\n" ~ 194 | " ```\n" ~ 195 | "\n" ~ 196 | " bar", 197 | "
    \n" ~ 198 | "
  1. \n" ~ 199 | "
    foo\n" ~
    200 |                         "
    \n" ~ 201 | "

    bar

    \n" ~ 202 | "
  2. \n" ~ 203 | "
\n"); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /example/source/test/ParserTest.d: -------------------------------------------------------------------------------- 1 | module test.ParserTest; 2 | 3 | import hunt.markdown.node; 4 | import hunt.markdown.parser.InlineParser; 5 | import hunt.markdown.parser.InlineParserContext; 6 | import hunt.markdown.parser.InlineParserFactory; 7 | import hunt.markdown.parser.Parser; 8 | import hunt.markdown.parser.block; 9 | import hunt.markdown.renderer.html.HtmlRenderer; 10 | import hunt.Assert; 11 | import hunt.util.StringBuilder; 12 | import hunt.Exceptions; 13 | import std.stdio; 14 | import std.file : read,write; 15 | 16 | public class ParserTest { 17 | 18 | 19 | public static void test() { 20 | Parser parser = Parser.builder().build(); 21 | 22 | auto readPath = "./resources/spec.txt"; 23 | auto writePath = "./resources/spec.html"; 24 | auto reader = File(readPath,"r"); 25 | StringBuilder sb = new StringBuilder(); 26 | try { 27 | string line; 28 | while ((line = reader.readln()) != null) { 29 | sb.append(line); 30 | sb.append("\n"); 31 | } 32 | 33 | } catch (IOException e) { 34 | throw new RuntimeException(e); 35 | } 36 | string spec = sb.toString; 37 | Node document2 = parser.parse(spec); 38 | 39 | HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).build(); 40 | write(writePath,renderer.render(document2)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/source/test/RenderingTestCase.d: -------------------------------------------------------------------------------- 1 | module test.RenderingTestCase; 2 | 3 | import hunt.Assert; 4 | import std.string; 5 | import hunt.logging; 6 | 7 | public abstract class RenderingTestCase { 8 | 9 | protected abstract string render(string source); 10 | 11 | protected void assertRendering(string source, string expectedResult) { 12 | string renderedContent = render(source); 13 | 14 | // include source for better assertion errors 15 | string expected = showTabs(expectedResult ~ "\n\n" ~ source); 16 | string actual = showTabs(renderedContent ~ "\n\n" ~ source); 17 | // logInfo("actual : ",actual); 18 | // logInfo("expected : ",expected); 19 | Assert.assertEquals(expected, actual); 20 | } 21 | 22 | private static string showTabs(string s) { 23 | // Tabs are shown as "rightwards arrow" for easier comparison 24 | return s.replace("\t", "\u2192"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/source/test/SpecialInputTest.d: -------------------------------------------------------------------------------- 1 | module test.SpecialInputTest; 2 | 3 | import test.CoreRenderingTestCase; 4 | import test.Common; 5 | 6 | public class SpecialInputTest : CoreRenderingTestCase { 7 | 8 | public void test() 9 | { 10 | empty(); 11 | nullCharacterShouldBeReplaced(); 12 | nullCharacterEntityShouldBeReplaced(); 13 | crLfAsLineSeparatorShouldBeParsed(); 14 | crLfAtEndShouldBeParsed(); 15 | mixedLineSeparators(); 16 | surrogatePair(); 17 | surrogatePairInLinkDestination(); 18 | indentedCodeBlockWithMixedTabsAndSpaces(); 19 | tightListInBlockQuote(); 20 | looseListInBlockQuote(); 21 | lineWithOnlySpacesAfterListBullet(); 22 | listWithTwoSpacesForFirstBullet(); 23 | orderedListMarkerOnly(); 24 | columnIsInTabOnPreviousLine(); 25 | linkLabelWithBracket(); 26 | linkLabelLength(); 27 | linkDestinationEscaping(); 28 | linkReferenceBackslash(); 29 | emphasisMultipleOf3Rule(); 30 | } 31 | 32 | public void empty() { 33 | assertRendering("", ""); 34 | } 35 | 36 | 37 | public void nullCharacterShouldBeReplaced() { 38 | assertRendering("foo\0bar", "

foo\uFFFDbar

\n"); 39 | } 40 | 41 | 42 | public void nullCharacterEntityShouldBeReplaced() { 43 | assertRendering("foo�bar", "

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", "\n"); 59 | assertRendering("a\n\nb\r\rc\r\n\r\nd\n\re", "

a

\n

b

\n

c

\n

d

\n

e

\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)", "

title

\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
\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"); 86 | } 87 | 88 | 89 | public void lineWithOnlySpacesAfterListBullet() { 90 | assertRendering("- \n \n foo\n", "\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", "\n"); 97 | } 98 | 99 | 100 | public void orderedListMarkerOnly() { 101 | assertRendering("2.", "
    \n
  1. \n
\n"); 102 | } 103 | 104 | 105 | public void columnIsInTabOnPreviousLine() { 106 | assertRendering("- foo\n\n\tbar\n\n# baz\n", 107 | "\n

baz

\n"); 108 | assertRendering("- foo\n\n\tbar\n# baz\n", 109 | "\n

baz

\n"); 110 | } 111 | 112 | 113 | public void linkLabelWithBracket() { 114 | assertRendering("[a[b]\n\n[a[b]: /", "

[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 ~ "]: /", "

foo

\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 ~ "]: /", "

foo

\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](\\))", "

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](\\ )", "

foo

\n"); 140 | // Backslash escapes `>`, so it's not a `(<...>)` link, but a `(...)` link instead 141 | assertRendering("[foo](<\\>)", "

foo

\n"); 142 | // Backslash is a literal, so valid 143 | assertRendering("[foo]()", "

foo

\n"); 144 | // Backslash escapes `>` but there's another `>`, valid 145 | assertRendering("[foo](>)", "

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", "

a\\b

\n"); 155 | // Backslash escapes `]` but there's another `]`, valid 156 | assertRendering("[a\\]]\n\n[a\\]]: test", "

a]

\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("![text](/url.png)"); 68 | Assert.assertEquals("

\"text\"

\n", 69 | renderer.render(document)); 70 | } 71 | 72 | 73 | public void customizeRendering() { 74 | Parser parser = Parser.builder().build(); 75 | HtmlRenderer renderer = HtmlRenderer.builder() 76 | .nodeRendererFactory(new class HtmlNodeRendererFactory { 77 | public NodeRenderer create(HtmlNodeRendererContext context) { 78 | return new IndentedCodeBlockNodeRenderer(context); 79 | } 80 | }) 81 | .build(); 82 | 83 | Node document = parser.parse("Example:\n\n code"); 84 | Assert.assertEquals("

Example:

\n
code\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 | *

53 | *

54 | * In that case, the three generated IDs will be: 55 | *

60 | *

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|>|$)", ""], 25 | ["^"], 26 | ["^<[?]", "\\?>"], 27 | ["^"], 28 | ["^"], 29 | ["^]|$)",""], 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 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 = "]"; 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 | *

20 | *

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 | *

9 | */ 10 | 11 | module hunt.markdown; 12 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/InlineParser.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.InlineParser; 2 | 3 | import hunt.markdown.node.Node; 4 | 5 | /** 6 | * Parser for inline content (text, links, emphasized text, etc). 7 | */ 8 | public interface InlineParser { 9 | 10 | /** 11 | * @param input the content to parse as inline 12 | * @param node the node to append resulting nodes to (as children) 13 | */ 14 | void parse(string input, Node node); 15 | } 16 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/InlineParserContext.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.InlineParserContext; 2 | 3 | import hunt.markdown.parser.delimiter.DelimiterProcessor; 4 | 5 | import hunt.collection.List; 6 | 7 | /** 8 | * Parameter context for custom inline parser. 9 | */ 10 | public interface InlineParserContext { 11 | List!(DelimiterProcessor) getCustomDelimiterProcessors(); 12 | } 13 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/InlineParserFactory.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.InlineParserFactory; 2 | 3 | import hunt.markdown.parser.InlineParser; 4 | import hunt.markdown.parser.InlineParserContext; 5 | 6 | /** 7 | * Factory for custom inline parser. 8 | */ 9 | public interface InlineParserFactory { 10 | InlineParser create(InlineParserContext inlineParserContext); 11 | } 12 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/PostProcessor.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.PostProcessor; 2 | 3 | import hunt.markdown.node.Node; 4 | 5 | public interface PostProcessor { 6 | 7 | /** 8 | * @param node the node to post-process 9 | * @return the result of post-processing, may be a modified {@code node} argument 10 | */ 11 | Node process(Node node); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/block/AbstractBlockParser.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.block.AbstractBlockParser; 2 | 3 | import hunt.markdown.node.Block; 4 | import hunt.markdown.parser.InlineParser; 5 | import hunt.markdown.parser.block.BlockParser; 6 | import hunt.util.Comparator; 7 | 8 | abstract class AbstractBlockParser : BlockParser { 9 | 10 | public bool isContainer() { 11 | return false; 12 | } 13 | 14 | public bool canContain(Block childBlock) { 15 | return false; 16 | } 17 | 18 | override public void addLine(string line) { 19 | } 20 | 21 | void closeBlock() { 22 | } 23 | 24 | override Block getBlock(){ 25 | return null; 26 | } 27 | 28 | public void parseInlines(InlineParser inlineParser) { 29 | } 30 | 31 | override int opCmp(BlockParser o) 32 | { 33 | auto cmp = compare(getBlock(),o.getBlock()); 34 | import hunt.logging; 35 | logDebug("------223---"); 36 | return cmp; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/block/AbstractBlockParserFactory.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.block.AbstractBlockParserFactory; 2 | 3 | import hunt.markdown.parser.block.BlockParserFactory; 4 | 5 | abstract class AbstractBlockParserFactory : BlockParserFactory { 6 | } 7 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/block/BlockContinue.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.block.BlockContinue; 2 | 3 | import hunt.markdown.internal.BlockContinueImpl; 4 | 5 | /** 6 | * Result object for continuing parsing of a block, see static methods for constructors. 7 | */ 8 | class BlockContinue { 9 | 10 | protected this() { 11 | } 12 | 13 | public static BlockContinue none() { 14 | return null; 15 | } 16 | 17 | public static BlockContinue atIndex(int newIndex) { 18 | return new BlockContinueImpl(newIndex, -1, false); 19 | } 20 | 21 | public static BlockContinue atColumn(int newColumn) { 22 | return new BlockContinueImpl(-1, newColumn, false); 23 | } 24 | 25 | public static BlockContinue finished() { 26 | return new BlockContinueImpl(-1, -1, true); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /source/hunt/markdown/parser/block/BlockParser.d: -------------------------------------------------------------------------------- 1 | module hunt.markdown.parser.block.BlockParser; 2 | 3 | import hunt.markdown.node.Block; 4 | import hunt.markdown.parser.block.ParserState; 5 | import hunt.markdown.parser.block.BlockContinue; 6 | import hunt.markdown.parser.InlineParser; 7 | 8 | /** 9 | * Parser for a specific block node. 10 | *

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 | --------------------------------------------------------------------------------