├── .gitignore
├── Makefile
├── README.md
├── bin
└── xmd.js
├── examples
├── simple.xmd
├── structure.xmd
└── structure.xmd.json
├── grammar.md
├── package.json
├── src
├── Ast.ts
├── BlockParser.ts
├── BlockParserTest.ts
├── Line.ts
├── LineType.ts
├── Main.ts
├── Node.ts
├── Reader.ts
├── ReaderTest.ts
├── Tag.ts
├── TagInfo.ts
├── TagTest.ts
├── TextParser.ts
├── TextParserTest.ts
├── Xmd.ts
├── XmdTest.ts
├── index.d.ts
├── test.d.ts
└── test.ts
└── tsd.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | scratch
3 | typings
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | tsc=tsc --module commonjs --outDir lib src/*.ts
2 | mocha=mocha -r lib/test.js lib/*Test.js
3 | gendts=dts-generator --baseDir src/ --name xmd --main xmd/Xmd --out xmd.d.ts src/Xmd.ts
4 |
5 | .PHONY: js
6 | js:
7 | $(tsc)
8 |
9 | .PHONY: jsw
10 | jsw:
11 | $(tsc) -w
12 |
13 | .PHONY: test
14 | test: js
15 | $(mocha)
16 |
17 | .PHONY: testw
18 | testw:
19 | $(mocha) -w --reporter min
20 |
21 | .PHONY: wc
22 | wc:
23 | find src -name \*.ts | xargs wc
24 |
25 | .PHONY: release
26 | release: js xmd.d.ts
27 | echo run: npm publish
28 |
29 | .PHONY: xmd.d.ts
30 | xmd.d.ts:
31 | @echo Generate module definition file: xmd.d.ts
32 | $(gendts)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Markdown is great if you can find a dialect that suits your needs precisely. But if you need some feature that it doesn't support, you are tempted to enter into a state of sin. You might try to:
2 |
3 | 1. Embed XML in your Markdown.
4 | 2. Have a fancy pre/post-process chain.
5 | 3. Find another markdown dialect.
6 |
7 | Either of these makes me feel dirty.
8 |
9 | Nor do I want to add more ad-hoc, nilly-willy extensions to Markdown.
10 |
11 | One way to think about Markdown is that it is a dialect of XML that makes writing in it more pleasant. To create an extensible Markdown dialect, all you need to do is to make its syntax regular enough that it can express arbitrary XML in it.
12 |
13 | I want a Markdown dialect to have equivalent expressive power as XML. Something like a hybrid between HAML and Markdown.
14 |
15 | ## Example
16 |
17 | A simple document looks very similar to markdown.
18 |
19 | ```
20 | # The Title
21 |
22 | The _first_ paragraph of text
23 | spans two lines.
24 |
25 | The *second* paragraph of text
26 | spans
27 | three lines.
28 |
29 | ## A `Subtitle`
30 |
31 | #aside
32 | Marginally interesting aside.
33 |
34 | Also, see [> http://example.com][external link]
35 |
36 | A code snippet:
37 |
38 | ```[javascript theme=dark]
39 | function() {
40 | console.log("hello world");
41 | }
42 | ```
43 |
44 | Consecutive lines are joined together to form a paragraph.
45 |
46 | # Install
47 |
48 | From NPM,
49 |
50 | ```
51 | npm install xmd
52 | ```
53 |
54 | ## Command line
55 |
56 | The command line tool can render an `.xmd` file to XML or JSON.
57 |
58 | ```
59 | $ xmd --help
60 | Renders extensible markdown to xml (defualt) or json.
61 |
62 | --ast output parsed document in JSON
63 | -j, --json output JSON
64 | -p, --pretty pretty print the output
65 | -h, --help show help
66 | ```
67 |
68 | By default it renders to XML:
69 |
70 | ```
71 | $ xmd --pretty example.xmd
72 | The first paragraph of text spans two lines. The second paragraph of text spans three lines. A code snippet:The Title
74 |
77 | A
78 |
79 |
82 | Subtitle
function() {
84 | console.log("hello world");
85 | }
hello world
The first text block has a bolded word.
247 |The second text block has an italic word.
248 | ``` 249 | 250 | This works in the indented body of a tag. 251 | 252 | ``` 253 | #tag1 254 | first paragraph of tag 1 255 | 256 | #tag2 257 | content of tag 2 258 | 259 | second paragraph of tag 1 260 | ``` 261 | 262 | is translated to: 263 | 264 | ``` 265 |first paragraph of tag 1
267 |content of tag 2
second paragraph of tag 1
a paragraph of content
function foo() {
361 | console.log("foo");
362 | }
363 | ```
364 |
365 | xmd has builtin syntax for this purpose:
366 |
367 | ```[javascript]
368 | function foo() {
369 | console.log("foo");
370 | }
371 | ```
372 |
373 | Aside from the language, you might also want to specify the theme:
374 |
375 | ```[javascript theme=dark]
376 | function foo() {
377 | console.log("foo");
378 | }
379 | ```
380 |
381 | We can omit the tag arguments if we want to:
382 |
383 | ```
384 | function foo() {
385 | console.log("foo");
386 | }
387 | ```
388 |
389 | We can use a heredoc to indicate the end of the snippet:
390 |
391 | ```HERE
392 | function foo() {
393 | console.log("foo");
394 | }
395 | ```HERE
396 |
397 | Finally, to combine heredoc and tag argument:
398 |
399 | ```[javascript theme=dark]HERE
400 | function foo() {
401 | console.log("foo");
402 | }
403 | ```HERE
404 |
405 | ## Text Formatting
406 |
407 | Text formatting looks as you'd expect:
408 |
409 | ```
410 | A piece of *bolded text*.
411 | Followed by some _italic text_.
412 | Then it ends with a bit of `code`!
413 | ```
414 |
415 | ## Inline Tag
416 |
417 | Aside from "*", "_" and "`", markdown has special syntax for links and images. Some dialects support footnotes. We want to be able to express all of these in a generic way.
418 |
419 | While the `#tag` syntax is good to express structure, it is not so nice for inline text. It'd be tedious to write something like:
420 |
421 | ```
422 | #a[href="http://google.com"]
423 | #b The
424 | #i Google
425 | ```
426 |
427 | Borrowing markdown's link syntax, we can write an inline tag like this:
428 |
429 | ```
430 | [tag a b c k1=1 k2=2 k3=3][*content* of tag]
431 | ```
432 |
433 | which gets translated to:
434 |
435 | ```
436 | boldedAndItalicCode
457 | ```
458 |
459 | ### Link
460 |
461 | We use the inline-tag syntax for links.
462 |
463 | ```
464 | [> http://google.com][*The* Google]
465 | ```
466 |
467 | Or omitting the content:
468 |
469 | ```
470 | [> http://google.com]
471 | ```
472 |
473 | ### No Nesting for * _ `
474 |
475 | It is not allowed to nesting the followings:
476 |
477 | + `_`
478 | + `
479 | + `*`
480 |
481 | The rule for interpreting these is simple. The parser reads everything until it finds the matching special character.
482 |
483 | ```
484 | *_bold_*
485 | ```
486 |
487 | get translated to:
488 |
489 | ```
490 | _bold_
491 | ```
492 |
493 | If you really want italic bold, use inline-tag syntax:
494 |
495 | ```
496 | [*][_italic bold_]
497 | ```
498 |
499 | `*` and `_` can be terribly confusing when they are nested.
500 |
501 | ## Escape
502 |
503 | The only special characters are:
504 |
505 | + `#`
506 | + `*`
507 | + `_`
508 | + `
509 | + `[` and `]`
510 | + `\`
511 |
512 | The backslash is used for escaping. A character that follows `\` is interpreted as itself:
513 |
514 | ```
515 | \# => #
516 | \* => *
517 | \\ => \
518 | \a => a
519 | \n => n
520 | etc.
521 | ```
522 |
523 | ## Quotation
524 |
525 | It should be easy to copy and paste an arbitrary chunk of text into the document without having to massage it into proper syntax.
526 |
527 | Suppose we want to embed a LaTeX equation into the document;
528 |
529 | ```
530 | E &= \frac{mc^2}{\sqrt{1-\frac{v^2}{c^2}}}
531 | ```
532 |
533 | It'd be terribly tedious (and error prone) to escape it manually with backslashes:
534 |
535 | ```
536 | #latex
537 | E &= \\frac{mc^2}{\\sqrt{1-\\frac{v^2}{c^2}}}
538 | ```
539 |
540 | For this purpose, we use heredoc but with 4 backticks:
541 |
542 | #latex
543 | ````
544 | E &= \frac{mc^2}{\sqrt{1-\frac{v^2}{c^2}}}
545 | ````
546 | It gets translated to:
547 |
548 | ```
549 | "
730 | ">" => a
731 | ```
732 |
733 | You can implement your own AST transformer instead of using the default.
734 |
735 | # Other Tools
736 |
737 | XMD isn't compatible with markdown, but it's quite close. I have written a simple converter from markdown to xmarkdown: [hayeah/md2xmd](https://github.com/hayeah/md2xmd)
738 |
--------------------------------------------------------------------------------
/bin/xmd.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | require("../lib/Main.js")();
--------------------------------------------------------------------------------
/examples/simple.xmd:
--------------------------------------------------------------------------------
1 | # The Title
2 |
3 | The _first_ paragraph of text
4 | spans two lines.
5 |
6 | The *second* paragraph of text
7 | spans
8 | three lines.
9 |
10 | ## A `Subtitle`
11 |
12 | #aside
13 | Marginally interesting aside.
14 |
15 | Also, see [> http://example.com][external link]
16 |
17 | A code snippet:
18 |
19 | ```[javascript theme=dark]
20 | function() {
21 | console.log("hello world");
22 | }
23 | ```
--------------------------------------------------------------------------------
/examples/structure.xmd:
--------------------------------------------------------------------------------
1 | text block
2 | numero 1
3 |
4 | ```
5 | piece of
6 | code
7 | yo
8 | ```
9 |
10 | ````
11 | piece of text yo
12 | ````
13 |
14 | # content of this tag
15 |
16 | #tag[] fofo
17 |
18 | #tag1[]
19 | content of tag 1
20 |
21 | #tag2[a b=b c]
22 | content of tag 2
23 |
24 | #tag2a
25 | nested tag
26 |
27 | ```[foolang b=b c]HERE
28 | ```
29 | HERE
30 | ```
31 | ```HERE
32 | #tag3
33 | final text
34 | block
--------------------------------------------------------------------------------
/examples/structure.xmd.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "document",
3 | "children": [
4 | {
5 | "name": "p",
6 | "children": [
7 | "text block numero 1"
8 | ]
9 | },
10 | {
11 | "name": "```",
12 | "children": [
13 | "piece of\n code\n yo"
14 | ]
15 | },
16 | {
17 | "name": "````",
18 | "children": [
19 | "piece of text yo"
20 | ]
21 | },
22 | {
23 | "name": "",
24 | "children": [
25 | "content of this tag"
26 | ]
27 | },
28 | {
29 | "name": "tag",
30 | "children": [
31 | "fofo"
32 | ]
33 | },
34 | {
35 | "name": "tag1",
36 | "children": [
37 | {
38 | "name": "p",
39 | "children": [
40 | "content of tag 1"
41 | ]
42 | }
43 | ]
44 | },
45 | {
46 | "name": "tag2",
47 | "children": [
48 | {
49 | "name": "p",
50 | "children": [
51 | "content of tag 2"
52 | ]
53 | },
54 | {
55 | "name": "tag2a",
56 | "children": [
57 | {
58 | "name": "p",
59 | "children": [
60 | "nested tag"
61 | ]
62 | }
63 | ]
64 | },
65 | {
66 | "name": "```",
67 | "children": [
68 | "```\n HERE\n```"
69 | ],
70 | "opts": {
71 | "b": "b"
72 | },
73 | "args": [
74 | "foolang",
75 | "c"
76 | ]
77 | }
78 | ],
79 | "opts": {
80 | "b": "b"
81 | },
82 | "args": [
83 | "a",
84 | "c"
85 | ]
86 | },
87 | {
88 | "name": "tag3",
89 | "children": []
90 | },
91 | {
92 | "name": "p",
93 | "children": [
94 | "final text block"
95 | ]
96 | }
97 | ]
98 | }
--------------------------------------------------------------------------------
/grammar.md:
--------------------------------------------------------------------------------
1 |
2 | ```
3 | := * *
4 | ```
5 |
6 | ```
7 | :=
8 | |
9 | |
10 | |
11 | |
12 | |
13 |
14 | ```
15 |
16 | ```
17 | := "#"
18 | := ? ?
19 | ?
20 | :=
21 | := "[" "]"
22 | ```
23 |
24 | A list item ends with anything that's not a list item. The items may not have empty lines between them. The `?!` notation means "it's not a list item", and it's a non-consuming look-ahead.
25 |
26 | ```
27 | := ( | )+ ?!
28 | := "+"
29 | ```
30 |
31 | ```
32 | :=
33 | ```
34 |
35 | ```
36 | :=
37 | |
38 |
39 | ```
40 |
41 | ```
42 | := (" "* ( | ) " "*)*
43 | :=
44 | := =
45 | ```
46 |
47 | ```
48 | := |
49 | :=
50 | := not('[' ']' '"' "=")+
51 | ```
52 |
53 | ```
54 | :=
55 | "```" ? ?
56 |
57 | "```" ?
58 |
59 | :=
60 | "````" ? ?
61 |
62 | "````" ?
63 |
64 | ```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xmd",
3 | "version": "0.0.3",
4 | "description": "extensible markdown",
5 | "main": "lib/Xmd.js",
6 | "scripts": {
7 | "test": "make test"
8 | },
9 | "bin": {
10 | "xmd": "./bin/xmd.js"
11 | },
12 | "author": "Howard Yeh",
13 | "license": "MIT",
14 | "devDependencies": {
15 | "chai": "^2.1.1",
16 | "mocha": "^2.2.1",
17 | "dts-generator": "git://github.com/SitePen/dts-generator"
18 | },
19 | "dependencies": {
20 | "minimist": "^1.1.1"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Ast.ts:
--------------------------------------------------------------------------------
1 |
2 | import TagInfo = require("./TagInfo");
3 |
4 | export type Node = Tag | string;
5 |
6 | export class Tag {
7 | name: string;
8 | children: Array;
9 | opts: {[key:string]: string};
10 | args: string[];
11 |
12 | constructor(name: string, children?: Node[]) {
13 | this.name = name;
14 | this.children = children || [];
15 | }
16 |
17 | setInfo(info: TagInfo) {
18 | if(info.opts) {
19 | this.opts = info.opts;
20 | }
21 |
22 | if(info.args) {
23 | this.args = info.args;
24 | }
25 | }
26 |
27 | add(child: Node) {
28 | this.children.push(child);
29 | }
30 |
31 | transform(f: (node: Node, recur?: () => Array) => Node): Node {
32 | return f(this,() => {
33 | if(this.children.length == 0) {
34 | return this.children;
35 | }
36 |
37 | var newChildren: Node[] = [];
38 | for (var i = 0; i < this.children.length; i++){
39 | var node = this.children[i];
40 | if(typeof node === "string") {
41 | newChildren[i] = f(node);
42 | } else {
43 | newChildren[i] = node.transform(f);
44 | }
45 | }
46 |
47 | return newChildren;
48 | });
49 | }
50 |
51 | walk(f: (node: Node, recur?: () => void) => void) {
52 | f(this,() => {
53 | var nodes = this.children;
54 | for (var i = 0; i < nodes.length; i++) {
55 | var node = nodes[i];
56 | if(typeof node === 'string') {
57 | f(node);
58 | } else {
59 | node.walk(f);
60 | }
61 | }
62 | });
63 | }
64 |
65 | json(opts?: {indent?: number}): string {
66 | if(opts == null) {
67 | opts = {};
68 | }
69 | var indent = opts.indent || 0;
70 | return JSON.stringify(this,null,indent);
71 | }
72 |
73 | xml(opts?: {indent?: number}): string {
74 | var xml = "";
75 | if(opts == null) {
76 | opts = {};
77 | }
78 |
79 | var pretty = opts.indent != null;
80 | var indentSpaces = opts.indent || 2;
81 | var indent = 0;
82 | var stopindent = false;
83 | this.walk((node,recur) => {
84 | if(typeof node === 'string') {
85 | if(pretty) {
86 | output("\n");
87 | }
88 |
89 | outputIndent(indent);
90 | output(node);
91 | } else {
92 | if(!pretty) {
93 | tagOpen(node);
94 | recur();
95 | output(`${node.name}>`);
96 | return;
97 | }
98 |
99 | // pretty mode
100 | if(node.children.length == 0) {
101 | output("\n");
102 | outputIndent(indent);
103 | tagOpen(node);
104 | output(`${node.name}>`)
105 | return;
106 | }
107 | if(node.children.length == 1 && typeof node.children[0] === "string") {
108 | output("\n");
109 | outputIndent(indent);
110 | tagOpen(node);
111 | output(node.children);
112 | output(`${node.name}>`);
113 | } else {
114 | var oldpretty = pretty;
115 | output("\n");
116 | outputIndent(indent);
117 | tagOpen(node);
118 | indent += indentSpaces;
119 | if(node.name === "p" || node.name === "pre") {
120 | pretty = false;
121 | }
122 | recur();
123 | if(node.name === "p" || node.name === "pre") {
124 | pretty = oldpretty;
125 | }
126 | indent -= indentSpaces;
127 | output(`${node.name}>`)
128 | }
129 | }
130 | });
131 |
132 | function tagOpen(tag: Tag) {
133 | output(`<${tag.name}`);
134 | if(tag.opts != null) {
135 | for (var k in tag.opts){
136 | output(` ${k}=${JSON.stringify(tag.opts[k])}`);
137 | }
138 | }
139 | output('>');
140 | }
141 |
142 | function outputIndent(indent:number) {
143 | if(pretty && !stopindent && indent > 0) {
144 | xml += spaces.substr(0,indent);
145 | }
146 | }
147 |
148 | function output(str) {
149 | xml += str;
150 | }
151 | return xml;
152 | }
153 | }
154 |
155 | interface XMLOutputOptions {
156 | indent?: number;
157 | }
158 | var spaces = " ";
--------------------------------------------------------------------------------
/src/BlockParser.ts:
--------------------------------------------------------------------------------
1 | import Tag = require("./Tag");
2 | import Reader = require("./Reader");
3 | import LineType = require("./LineType");
4 | import TextParser = require("./TextParser");
5 |
6 | type Node = Tag | string;
7 | interface LineInfo {
8 | type: LineType;
9 | indent?: number
10 | }
11 |
12 | export = BlockParser;
13 |
14 | class BlockParser extends Reader {
15 | /*
16 | * Detect indentation and type for current line.
17 | */
18 | lineInfo(): LineInfo {
19 | if(this.eof) {
20 | return null;
21 | }
22 |
23 | var i = this.at;
24 | var blank = true;
25 | var indent = 0;
26 | var c: string;
27 | while(true) {
28 | c = this.src[i];
29 |
30 | if(c == null) {
31 | break;
32 | }
33 |
34 | if(c == "\n") {
35 | break;
36 | }
37 |
38 | if(c == " ") {
39 | indent++;
40 | } else {
41 | blank = false;
42 | break;
43 | }
44 |
45 | i++;
46 | }
47 |
48 | var type: LineType;
49 | if(blank) {
50 | type = LineType.empty;
51 | indent = null;
52 | } else {
53 | type = LineType.text;
54 |
55 | if(c == "#") {
56 | type = LineType.tag;
57 | }
58 |
59 | if(c == "+") {
60 | type = LineType.listItem;
61 | }
62 |
63 | if(c == "`") {
64 | if(this.src.substr(i,3) == "```") {
65 | if(this.src[i+3] === "`") { // ````
66 | type = LineType.hereString;
67 | } else {
68 | type = LineType.hereCode;
69 | }
70 | }
71 | }
72 | }
73 |
74 | return {type: type, indent: indent};
75 | }
76 |
77 | parse(): Tag {
78 | return new Tag("document",this._parse(0));
79 | }
80 |
81 | _parse(indent:number=0): Node[] {
82 | var nodes: Node[] = [];
83 |
84 | while(true) {
85 | this.skipEmptyLines();
86 | if(this.eof) {
87 | break;
88 | }
89 | var info = this.lineInfo();
90 | if(info.indent < indent) { // outdent
91 | break;
92 | }
93 |
94 | if(info.indent > indent) {
95 | throw "Unexpected indentation";
96 | }
97 |
98 | switch(info.type) {
99 | case LineType.text:
100 | nodes.push(this.readTextBlock(indent));
101 | break;
102 | case LineType.tag:
103 | nodes.push(this.parseTag(indent));
104 | break;
105 | case LineType.listItem:
106 | nodes.push(this.parseList(indent));
107 | break;
108 | case LineType.hereString:
109 | nodes.push(this.parseStringHeredoc(indent));
110 | break;
111 | case LineType.hereCode:
112 | nodes.push(this.parseCodeHeredoc(indent));
113 | break;
114 | case LineType.empty:
115 | // do nothing
116 | break;
117 | default:
118 | throw "unknown line type";
119 | }
120 | }
121 |
122 | return nodes;
123 | }
124 |
125 | skipEmptyLines() {
126 | while(true) {
127 | if(this.eof) {
128 | return;
129 | }
130 |
131 | var info = this.lineInfo();
132 | if(info.type != LineType.empty) {
133 | break;
134 | }
135 | this.readLine(0); // FIXME: could do it more efficiently...
136 | }
137 | }
138 |
139 | /**
140 | * Parse a list of items. A list is a sequence of tags started
141 | * with `+`, and not broken by anything that's not a list item.
142 | *
143 | * Grammar:
144 | */
145 | parseList(indent: number=0): Tag {
146 | var items: Tag[] = [];
147 |
148 | while(true) {
149 | var info = this.lineInfo();
150 | if(this.eof) {
151 | break;
152 | }
153 |
154 | if(info.indent != indent) {
155 | break;
156 | }
157 |
158 | if(info.type != LineType.listItem) {
159 | break;
160 | }
161 |
162 | this.wantIndent(indent);
163 | this.want("+");
164 |
165 | var item = this.parseTagDef(indent);
166 | item.name = "+" + item.name;
167 | items.push(item);
168 |
169 | this.skipEmptyLines();
170 | }
171 |
172 | var list = new Tag("list",items)
173 |
174 | return list;
175 | }
176 |
177 | /**
178 | * Parses the tag name, argument, and content. A grammar shared by tag and list-item.
179 | * Grammar:
180 | */
181 | parseTagDef(indent: number=0): Tag {
182 | // Grammar:
183 | var tagName = this.readSymbol();
184 | var tag = new Tag(tagName);
185 |
186 | // Grammar: ?
187 | if(this.ch == "[") {
188 | this.want("[");
189 | var args = this.parseArguments();
190 | tag.setInfo(args);
191 | this.want("]");
192 | }
193 |
194 | // Grammar: ?
195 | var textLine = this.readIf((c) => {return c != "\n"}).trim();
196 | if(textLine != "") {
197 | var tp = new TextParser(textLine);
198 | tag.children = tp.parse();
199 | }
200 |
201 | if(this.ch == "\n") {
202 | this.read();
203 | }
204 |
205 |
206 | // Grammar: ?
207 | this.skipEmptyLines();
208 | if(this.eof) {
209 | return tag;
210 | }
211 |
212 | var info = this.lineInfo();
213 |
214 |
215 | if(info.indent != indent + 2) {
216 | // Detected indentation not belonging to this tag.
217 | return tag;
218 | }
219 |
220 | // parse recursively
221 | var body = this._parse(indent + 2);
222 | if(body.length > 0) {
223 | Array.prototype.push.apply(tag.children,body);
224 | }
225 |
226 | return tag;
227 | }
228 |
229 | /**
230 | * Parse and return a tag at specified indentation level.
231 | *.
232 | * Grammar: :=
233 | * "#" ? ?
234 | * ?
235 | */
236 | parseTag(indent: number=0): Tag {
237 | this.wantIndent(indent); // indent(n)
238 | this.want("#");
239 |
240 | return this.parseTagDef(indent);
241 | }
242 |
243 | /**
244 | * Read up to the next non-whitespace char.
245 | * Tabs ("\t") are not allowed.
246 | * @return The indentation level.
247 | */
248 | readUptoNextIndent(): number {
249 | var indent = 0;
250 | return indent;
251 | }
252 |
253 | readTextBlock(indent:number): Tag {
254 | var lines = [];
255 | while(true) {
256 | // Consume a line.
257 | var line = this.readLine(indent);
258 | lines.push(line);
259 | // concat line
260 |
261 | // Decide if the new line is part of the text block.
262 | var info = this.lineInfo();
263 |
264 | if(info == null || info.type != LineType.text) {
265 | break;
266 | }
267 |
268 | if(info.indent != indent) {
269 | break;
270 | }
271 |
272 | continue;
273 | }
274 |
275 | var content = lines.join(" ");
276 | var tp = new TextParser(content);
277 |
278 | return new Tag("p",tp.parse());
279 | }
280 |
281 | /*
282 | * Read a line at a specified indentation level.
283 | */
284 | readLine(indent:number=0): string {
285 | // consume indentation
286 | this.wantIndent(indent);
287 |
288 | var line = this.readIf((c) => { return c != "\n" });
289 |
290 | // consume newline
291 | if(this.ch == "\n") {
292 | this.read();
293 | }
294 |
295 | return line;
296 | }
297 |
298 | wantIndent(indent:number=0) {
299 | for(;indent > 0; indent--) {
300 | if(this.ch != " ") {
301 | throw `Expects indent level: ${indent}`
302 | }
303 | this.read();
304 | }
305 | }
306 |
307 | /*
308 | * Heredoc for quoted Code.
309 | *
310 | * Grammar:
311 | */
312 | parseCodeHeredoc(indent:number=0): Tag {
313 | return this.parseHeredoc("```",indent);
314 | }
315 |
316 | /*
317 | * Heredoc for quoted Code.
318 | *
319 | * Grammar:
320 | */
321 | parseStringHeredoc(indent:number=0): Tag {
322 | return this.parseHeredoc('````',indent);
323 | }
324 |
325 | parseHeredoc(delimiter:string,indent:number=0): Tag {
326 | var tag = new Tag(delimiter);
327 | this.wantIndent(indent);
328 | this.wantAll(delimiter);
329 |
330 | if(this.ch == "[") {
331 | this.want("[");
332 | var args = this.parseArguments();
333 | tag.setInfo(args);
334 | this.want("]");
335 | }
336 |
337 | var symbol = this.readSymbol();
338 | this.want("\n");
339 |
340 | var heretoken = delimiter + symbol;
341 |
342 | var lines = [];
343 | while(true) {
344 | if(this.eof) {
345 | throw "EOF while parsing heredoc";
346 | }
347 |
348 | var line = this.readLine(indent);
349 | if(line.indexOf(heretoken) === 0 &&
350 | // allow spaces followinwg there heredoc close
351 | line.substr(heretoken.length).trim() === "") {
352 | break;
353 | }
354 |
355 | lines.push(line);
356 | }
357 |
358 | var content = lines.join("\n");
359 | if(content != "") {
360 | tag.children = [content];
361 | }
362 |
363 | return tag;
364 | }
365 |
366 |
367 | }
--------------------------------------------------------------------------------
/src/BlockParserTest.ts:
--------------------------------------------------------------------------------
1 | import BlockParser = require("./BlockParser");
2 | import LineType = require("./LineType");
3 | import fs = require("fs");
4 |
5 | function assertParse(a,b) {
6 | assert.deepEqual(JSON.parse(JSON.stringify(a)),b);
7 | }
8 |
9 | interface TestOpt {
10 | assert?: any;
11 | show?: boolean;
12 | }
13 |
14 | function parserTest(parser,method,opts?:TestOpt) {
15 | if(opts == null) {
16 | opts = {};
17 | }
18 |
19 | var assertFn = opts.assert || assert.deepEqual;
20 |
21 |
22 | return function(src,expect,...args): string {
23 | var p = new parser(src);
24 | var result = p[method].apply(p,args);
25 | if(opts && opts.show == true) {
26 | console.log(JSON.stringify(result,null,2));
27 | }
28 |
29 | if(expect !== undefined) {
30 | assertFn(result,expect);
31 | }
32 | return p.residue();
33 | }
34 | }
35 |
36 | describe("BlockParser",() => {
37 | describe("#lineInfo",() => {
38 | function lineInfo(src:string) {
39 | var p = new BlockParser(src)
40 | return p.lineInfo();
41 | }
42 |
43 | it("detects empty line",() => {
44 | var info = lineInfo(" ");
45 | assert.equal(info.type, LineType.empty);
46 | assert.isNull(info.indent);
47 | });
48 |
49 | it("detects tag",() => {
50 | var info = lineInfo(" #");
51 | assert.equal(info.type, LineType.tag);
52 | assert.equal(info.indent,2);
53 | });
54 |
55 | it("detects text",() => {
56 | var info = lineInfo(" hello");
57 | assert.equal(info.type, LineType.text);
58 | assert.equal(info.indent,4);
59 |
60 | var info = lineInfo(" `hello");
61 | assert.equal(info.type, LineType.text);
62 | assert.equal(info.indent,2);
63 |
64 | var info = lineInfo("``hello");
65 | assert.equal(info.type, LineType.text);
66 | assert.equal(info.indent,0);
67 | });
68 |
69 | it("detects code heredoc",() => {
70 | var info = lineInfo(" ```");
71 | assert.equal(info.type, LineType.hereCode);
72 | assert.equal(info.indent,4);
73 | });
74 |
75 | it("detects string heredoc",() => {
76 | var info = lineInfo(' ````');
77 | assert.equal(info.type, LineType.hereString);
78 | assert.equal(info.indent,2);
79 | });
80 |
81 | it("returns null for eof",() => {
82 | assert.isNull(lineInfo(""));
83 | });
84 | });
85 |
86 | describe("#readLine", () => {
87 | var readLine = parserTest(BlockParser,"readLine");
88 |
89 | it("reads an indented line",() => {
90 | var residue = readLine(" abcd\nmore","abcd",2);
91 | assert.equal(residue,"more");
92 | });
93 |
94 | it("throws error if line is not indented enough",() => {
95 | assert.throw(() => {
96 | readLine(" abcd",undefined,4);
97 | });
98 | });
99 | });
100 |
101 | describe("#readTextBlock",() => {
102 | var readTextBlock = parserTest(BlockParser,"readTextBlock",{
103 | assert: assertParse,
104 | // show: true
105 | });
106 |
107 | it("collect consecutive text lines together",() => {
108 | var text = " abc\n def\n ghi";
109 | var _ = readTextBlock(text,{
110 | "name": "p",
111 | "children": [
112 | "abc def ghi"
113 | ]
114 | },2);
115 | assert.equal(_,"");
116 | });
117 |
118 | it("ends a text block on outdent",() => {
119 | var text = " abc\n def\nghi";
120 | var _ = readTextBlock(text,{
121 | "name": "p",
122 | "children": [
123 | "abc def",
124 | ]
125 | },2);
126 | assert.equal(_,"ghi");
127 | });
128 |
129 | it("ends a text block on empty line",() => {
130 | var text = " abc\n def\n \nghi";
131 | var _ = readTextBlock(text,{
132 | "name": "p",
133 | "children": [
134 | "abc def",
135 | ]
136 | },2);
137 | assert.equal(_," \nghi");
138 | });
139 |
140 | it("ends a text block on non-text line",() => {
141 | var text;
142 | var _;
143 |
144 | text = " abc\n def\n #tag";
145 | _ = readTextBlock(text,undefined,2);
146 | assert.equal(_," #tag");
147 |
148 | text = " abc\n def\n ```";
149 | _ = readTextBlock(text,undefined,2);
150 | assert.equal(_," ```");
151 |
152 | text = ' abc\n def\n ````';
153 | _ = readTextBlock(text,undefined,2);
154 | assert.equal(_,' ````');
155 | });
156 | });
157 |
158 | describe("#parseArguments",() => {
159 | var parseArguments = parserTest(BlockParser,"parseArguments",{
160 | // show: true
161 | });
162 |
163 | it("throws error if = appears on its own");
164 |
165 | it("parses empty arguments",() => {
166 | parseArguments("",{
167 | "opts": null,
168 | "args": null
169 | });
170 | });
171 |
172 | it("parses arguments",() => {
173 | var _ = parseArguments(" a b=bar c ]more",{
174 | "opts": {
175 | "b": "bar"
176 | },
177 | "args": [
178 | "a",
179 | "c"
180 | ]
181 | });
182 |
183 | assert.equal(_,"]more")
184 | });
185 | });
186 |
187 | describe("#parseCodeHeredoc",() => {
188 | var parseCodeHeredoc = parserTest(BlockParser,"parseCodeHeredoc",{
189 | assert: assertParse,
190 | // show:true
191 | });
192 |
193 | it("throws error if heredoc is not closed")
194 |
195 | it("parses empty heredoc",() => {
196 | var doc;
197 | doc = "```\n```";
198 | parseCodeHeredoc(doc,{
199 | "name": "```",
200 | "children": []
201 | });
202 | });
203 |
204 | it("parses multiple lines of quoted text",() => {
205 | var doc = "```\n \n\n a\n b\n c\n```";
206 | parseCodeHeredoc(doc,{
207 | "name": "```",
208 | "children": [
209 | " \n\n a\n b\n c"
210 | ]
211 | });
212 | });
213 |
214 | it("allows closing to be trailed with space",() => {
215 | var doc;
216 | doc = "```\ncontent\n``` \n";
217 | parseCodeHeredoc(doc,{
218 | "name": "```",
219 | "children": [
220 | "content"
221 | ]
222 | });
223 |
224 | doc = "```HERE\ncontent\n```HERE \n";
225 | parseCodeHeredoc(doc,{
226 | "name": "```",
227 | "children": [
228 | "content"
229 | ]
230 | });
231 | });
232 |
233 | it("closes with user defined heredoc token",() => {
234 | var doc = "```FOOBAR\ncontent\n```\n```FOOBAR\n";
235 | parseCodeHeredoc(doc,{
236 | "name": "```",
237 | "children": [
238 | "content\n```"
239 | ]
240 | });
241 | });
242 |
243 | it("parses heredoc arguments",() => {
244 | var doc = "```[a b=b c]HERE\ncontent\n```HERE\nmore";
245 | var _ = parseCodeHeredoc(doc,{
246 | "name": "```",
247 | "children": [
248 | "content"
249 | ],
250 | "opts": {
251 | "b": "b"
252 | },
253 | "args": [
254 | "a",
255 | "c"
256 | ]
257 | });
258 | });
259 |
260 | it("parses indented heredoc",() => {
261 | var doc =
262 | ` \`\`\`
263 | hello
264 | world
265 | \`\`\`
266 | `
267 | parseCodeHeredoc(doc,{
268 | "name": "```",
269 | "children": [
270 | "hello\nworld"
271 | ]
272 | },2);
273 | });
274 |
275 | it("advances reader position",() => {
276 | var doc = "```HERE\ncontent\n```HERE\nmore";
277 | var _ = parseCodeHeredoc(doc,undefined);
278 | assert.equal(_,"more");
279 | });
280 | });
281 |
282 | describe("#parseStringHeredoc", () => {
283 | var parseStringHeredoc = parserTest(BlockParser,"parseStringHeredoc",{
284 | assert: assertParse,
285 | // show: true
286 | });
287 | it("parses string quoted by heredoc",() => {
288 | var doc = '````HERE\ncontent\n````HERE\nmore';
289 | var _ = parseStringHeredoc(doc,{
290 | "name": "````",
291 | "children": [
292 | "content"
293 | ]
294 | });
295 | assert.equal(_,"more");
296 | });
297 | });
298 |
299 | describe("#parseTag",() => {
300 | var parseTag = parserTest(BlockParser,"parseTag",{
301 | assert: assertParse,
302 | // show: true
303 | });
304 |
305 | it("parses tag that has no content",() => {
306 | parseTag("#foo",{
307 | "name": "foo",
308 | "children": []
309 | });
310 | });
311 |
312 | it("parses tag with inline content",() => {
313 | parseTag("#foo *hello* _world_",{
314 | "name": "foo",
315 | "children": [
316 | {
317 | "name": "*",
318 | "children": [
319 | "hello"
320 | ]
321 | },
322 | " ",
323 | {
324 | "name": "_",
325 | "children": [
326 | "world"
327 | ]
328 | }
329 | ]
330 | });
331 | });
332 |
333 |
334 | it("parses tag that has content",() => {
335 | var doc =
336 | `
337 | #foo
338 | first block
339 | of foo
340 |
341 | second block
342 | of foo
343 | `.trim();
344 |
345 | parseTag(doc,{
346 | "name": "foo",
347 | "children": [
348 | {
349 | "name": "p",
350 | "children": [
351 | "first block of foo"
352 | ]
353 | },
354 | {
355 | "name": "p",
356 | "children": [
357 | "second block of foo"
358 | ]
359 | }
360 | ]
361 | },0);
362 | });
363 |
364 | it("parses tag recursively",() => {
365 | var doc =
366 | `
367 | #foo
368 | first block of foo
369 | #bar
370 | content of bar
371 | second block of foo
372 | more content
373 | `.trim();
374 |
375 | var _ = parseTag(doc,{
376 | "name": "foo",
377 | "children": [
378 | {
379 | "name": "p",
380 | "children": [
381 | "first block of foo"
382 | ]
383 | },
384 | {
385 | "name": "bar",
386 | "children": [
387 | {
388 | "name": "p",
389 | "children": [
390 | "content of bar"
391 | ]
392 | }
393 | ]
394 | },
395 | {
396 | "name": "p",
397 | "children": [
398 | "second block of foo"
399 | ]
400 | }
401 | ]
402 | },0);
403 |
404 | assert.equal(_,"more content");
405 | });
406 |
407 | it("ignores empty lines",() => {
408 | var doc =
409 | `
410 | #foo
411 |
412 | first block of foo
413 |
414 | #bar
415 |
416 | content of bar
417 |
418 | second block of foo
419 |
420 |
421 | more content
422 | `.trim();
423 |
424 | var _ = parseTag(doc,{
425 | "name": "foo",
426 | "children": [
427 | {
428 | "name": "p",
429 | "children": [
430 | "first block of foo"
431 | ]
432 | },
433 | {
434 | "name": "bar",
435 | "children": [
436 | {
437 | "name": "p",
438 | "children": [
439 | "content of bar"
440 | ]
441 | }
442 | ]
443 | },
444 | {
445 | "name": "p",
446 | "children": [
447 | "second block of foo"
448 | ]
449 | }
450 | ]
451 | },0);
452 |
453 | assert.equal(_,"more content");
454 | });
455 |
456 | it("parses tag arguments",() => {
457 | parseTag("#tag[a b=b c]",{
458 | "name": "tag",
459 | "children": [],
460 | "opts": {
461 | "b": "b"
462 | },
463 | "args": [
464 | "a",
465 | "c"
466 | ]
467 | });
468 | });
469 |
470 | it("parses tag that has both inline content and indented body",() =>{
471 | var doc =
472 | `#foo inline text
473 | first block
474 | of foo
475 |
476 | second block
477 | of foo
478 | `
479 | parseTag(doc,{
480 | "name": "foo",
481 | "children": [
482 | "inline text",
483 | {
484 | "name": "p",
485 | "children": [
486 | "first block of foo"
487 | ]
488 | },
489 | {
490 | "name": "p",
491 | "children": [
492 | "second block of foo"
493 | ]
494 | }
495 | ]
496 | });
497 |
498 | });
499 | });
500 |
501 | describe("#parseList",() => {
502 | var parseList = parserTest(BlockParser,"parseList",{
503 | // show: true,
504 | assert: assertParse,
505 | });
506 |
507 | it("parses list of items",() => {
508 | var doc =
509 | `
510 | +1 a
511 | +2 b
512 | foo
513 | `.trim()
514 | var _ = parseList(doc,{
515 | "name": "list",
516 | "children": [
517 | {
518 | "name": "+1",
519 | "children": [
520 | "a"
521 | ]
522 | },
523 | {
524 | "name": "+2",
525 | "children": [
526 | "b"
527 | ]
528 | }
529 | ]
530 | });
531 | assert.equal(_,"foo");
532 | });
533 |
534 | it("parses nested list",() => {
535 | var doc =
536 | `+ a
537 | +1 a1
538 | +2 a2
539 | + b
540 | + bb
541 | + b b
542 | `
543 | parseList(doc,{
544 | "name": "list",
545 | "children": [
546 | {
547 | "name": "+",
548 | "children": [
549 | "a",
550 | {
551 | "name": "list",
552 | "children": [
553 | {
554 | "name": "+1",
555 | "children": [
556 | "a1"
557 | ]
558 | },
559 | {
560 | "name": "+2",
561 | "children": [
562 | "a2"
563 | ]
564 | }
565 | ]
566 | }
567 | ]
568 | },
569 | {
570 | "name": "+",
571 | "children": [
572 | "b",
573 | {
574 | "name": "list",
575 | "children": [
576 | {
577 | "name": "+",
578 | "children": [
579 | "bb"
580 | ]
581 | },
582 | {
583 | "name": "+",
584 | "children": [
585 | "b b"
586 | ]
587 | }
588 | ]
589 | }
590 | ]
591 | }
592 | ]
593 | });
594 | });
595 | });
596 |
597 | describe("#parse",() => {
598 | function parse(srcFile,astFile) {
599 | var src = fs.readFileSync(srcFile,"utf8");
600 | var ast = fs.readFileSync(astFile,"utf8");
601 | var p = new BlockParser(src);
602 | var result = p.parse();
603 | assert.deepEqual(JSON.parse(result.json()),JSON.parse(ast))
604 | }
605 |
606 | it("parses a complete document",() => {
607 | parse("./examples/structure.xmd","./examples/structure.xmd.json")
608 | });
609 | });
610 | });
--------------------------------------------------------------------------------
/src/Line.ts:
--------------------------------------------------------------------------------
1 | class Line {
2 | indent: number;
3 | // stripped of indent and newline
4 | content: string;
5 | lineno: number;
6 |
7 | constructor(content: string, indent: number, lineno: number) {
8 | this.indent = indent;
9 | this.content = content;
10 | this.lineno = lineno;
11 | }
12 | }
13 |
14 | export = Line;
--------------------------------------------------------------------------------
/src/LineType.ts:
--------------------------------------------------------------------------------
1 | export = LineType;
2 | enum LineType {
3 | empty,
4 | hereString,
5 | hereCode,
6 | text,
7 | tag,
8 | listItem,
9 | }
--------------------------------------------------------------------------------
/src/Main.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import minimist = require("minimist");
3 | import fs = require("fs");
4 |
5 | import xmd = require("./Xmd");
6 |
7 | // xmd [--json | -j] [xmdfile]
8 | export = main;
9 |
10 | interface MainArgs extends minimist.ParsedArgs {
11 | // output in json format
12 | j?: boolean;
13 | json?: boolean;
14 |
15 | h?: boolean;
16 | help?: boolean;
17 |
18 | pretty?: boolean;
19 |
20 | // ast mode. output parsed ast in JSON with no transform
21 | ast?: boolean;
22 | }
23 |
24 | function help() {
25 | var helpdoc =
26 | `
27 | xmd [--json | --j] [xmdfile]
28 |
29 | Renders extensible markdown to xml (defualt) or json.
30 |
31 | --ast output parsed document in JSON
32 | -j, --json output JSON
33 | -p, --pretty pretty print the output
34 | -h, --help show help
35 | `
36 | console.log(helpdoc);
37 | process.exit(0);
38 | }
39 |
40 | function main() {
41 | var args = minimist(process.argv.slice(2),{
42 | alias: {
43 | "h": ["help"],
44 | "j": ["json"],
45 | "p": ["pretty"]
46 | },
47 | boolean: ["ast","json","pretty"]
48 | });
49 |
50 | if(args.h || args.help) {
51 | help();
52 | }
53 |
54 | var srcInput: NodeJS.ReadableStream;
55 | if(args._.length == 0) {
56 | srcInput = process.stdin;
57 | } else {
58 | var path = args._[0];
59 | srcInput = fs.createReadStream(path,"utf8");
60 | }
61 |
62 | readInput(srcInput,(err,src) => {
63 | var output: string;
64 | var opts: any = {};
65 | if(args.pretty) {
66 | opts.indent = 4;
67 | }
68 |
69 |
70 | if(err) {
71 | console.log(err);
72 | process.exit(1);
73 | } else {
74 | if(args.json || args.ast) {
75 | opts.raw = !!args.ast;
76 | output = xmd.renderJSON(src,opts);
77 | } else {
78 | output = xmd.renderXML(src,opts);
79 | }
80 | }
81 | process.stdout.write(output);
82 | });
83 | }
84 |
85 | function readInput(stream:NodeJS.ReadableStream,cb: (err,src?:string) => void) {
86 | var src = "";
87 | stream.on("readable",() => {
88 | if(src != null) {
89 | src += stream.read();
90 | }
91 | });
92 |
93 | stream.on("error",(err) => {
94 | cb(err);
95 | });
96 |
97 | stream.on("end",() => {
98 | cb(null,src);
99 | });
100 | }
101 |
--------------------------------------------------------------------------------
/src/Node.ts:
--------------------------------------------------------------------------------
1 | export = Node;
2 | type Node = Tag | string;
3 | import Tag = require("./Tag");
--------------------------------------------------------------------------------
/src/Reader.ts:
--------------------------------------------------------------------------------
1 | import TagInfo = require("./TagInfo");
2 |
3 | export = Reader;
4 | class Reader {
5 | src: string;
6 | at: number;
7 | // // line count
8 | // private line: number;
9 | // // column count
10 | // private col: number;
11 |
12 | eof: boolean;
13 | ch: string;
14 |
15 | constructor(src: string) {
16 | this.src = src;
17 | this.at = 0;
18 | this.ch = this.src[this.at];
19 | this.eof = this.ch == null;
20 |
21 | }
22 |
23 | read(): string {
24 | var c = this.ch;
25 | this.at += 1;
26 | this.ch = this.src[this.at];
27 |
28 | if(this.ch == null) {
29 | this.eof = true;
30 | }
31 | return c;
32 | }
33 |
34 | peek(n:number): string {
35 | return this.src[this.at+n];
36 | }
37 |
38 | setAt(at: number) {
39 | this.at = at;
40 | this.ch = this.src[this.at];
41 | if(this.ch == null) {
42 | this.eof = true;
43 | }
44 | }
45 |
46 | want(e: string) {
47 | if(e && e != this.ch) {
48 | throw "Expected '" + e + "' instead of '" + this.ch + "'";
49 | }
50 | this.read();
51 | }
52 |
53 | wantAll(str: string) {
54 | for(var i = 0; i < str.length; i++) {
55 | if(this.ch != str[i]) {
56 | throw `Expected ${str[i]} instead of ${this.ch}`;
57 | }
58 | this.read();
59 | }
60 | }
61 |
62 | /**
63 | * Consume input up to but not including a string pattern.
64 | */
65 | readUpto(str: string): string {
66 | var endPos = this.src.indexOf(str,this.at)
67 | if(endPos == -1) {
68 | return null;
69 | }
70 | var content = this.src.slice(this.at,endPos);
71 | this.setAt(endPos);
72 | return content;
73 | }
74 |
75 | // content string
76 | readDelimited(delimiter:string): string/*null*/ {
77 | this.wantAll(delimiter);
78 | var content = this.readUpto(delimiter);
79 | if(content == null) {
80 | throw `Unable to find closing ${delimiter}`
81 | }
82 | this.wantAll(delimiter);
83 | return content;
84 | }
85 |
86 | readIf(test: (c:string) => boolean): string {
87 | var acc = "";
88 | while(true) {
89 | if(this.eof) {
90 | break;
91 | }
92 |
93 | if(test(this.ch)) {
94 | acc += this.read();
95 | } else {
96 | break;
97 | }
98 | }
99 | return acc;
100 | }
101 |
102 | /*
103 | * Read a symbol at current position.
104 | *
105 | * Symbo is used as tag names, tag arguments, and heredoc tokens.
106 | *
107 | * @grammar
108 | */
109 | readSymbol(): string {
110 | // TODO
111 | if(this.ch === '"') {
112 | throw "Quoted symbol not yet implemented"
113 | } else {
114 | return this.readIf((c) => {
115 | return !(
116 | c == " " ||
117 | c == "\n" ||
118 | c == "[" ||
119 | c == "]" ||
120 | c == "\"" ||
121 | c == "="
122 | );
123 | });
124 | }
125 | }
126 |
127 | /*
128 | * Parses a delimited list of arguments. Arguments list should end with `]`
129 | *
130 | * Grammar:
131 | */
132 | parseArguments(): TagInfo {
133 | // must be on one-line
134 | // stops when it sees "]"
135 | var opts: {[key:string]: string} = {};
136 | var args = [];
137 |
138 | var hasKV = false;
139 |
140 | while(true) {
141 | this.readIf((c) => {return c == " "});
142 |
143 | if(this.eof || this.ch === "\n" || this.ch === "]") {
144 | break;
145 | }
146 |
147 | var arg = this.readSymbol();
148 | if(arg === "") {
149 | break;
150 | }
151 | if(this.ch === "=") {
152 | this.want("=");
153 | var key = arg;
154 | var val = this.readSymbol();
155 | opts[key] = val;
156 | hasKV = true;
157 | } else {
158 | args.push(arg);
159 | }
160 | }
161 |
162 | if(args.length == 0) {
163 | args = null;
164 | }
165 |
166 | if(!hasKV) {
167 | opts = null;
168 | }
169 |
170 | return {opts: opts, args: args};
171 | }
172 |
173 | residue(): string {
174 | return this.src.slice(this.at);
175 | }
176 | }
--------------------------------------------------------------------------------
/src/ReaderTest.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import Reader = require("./Reader");
3 |
4 | describe("Reader",() => {
5 |
6 | describe("#read",() => {
7 | var reader: Reader;
8 |
9 | beforeEach(() => {
10 | reader = new Reader("abc");
11 | });
12 |
13 | function read(expect?: string): string {
14 | var result = reader.read();
15 | if(expect != null) {
16 | assert.equal(result,expect);
17 | }
18 | return reader.residue();
19 | }
20 |
21 | it("returns the input characters in order",() => {
22 | assert.isFalse(reader.eof);
23 | assert.equal(reader.ch,"a");
24 | read("a");
25 | assert.equal(reader.ch,"b");
26 | read("b");
27 | assert.equal(reader.ch,"c");
28 | read("c");
29 | assert.isTrue(reader.eof);
30 | });
31 | });
32 |
33 | describe("#wantAll",() => {
34 | function wantAll(src:string,pat:string): string {
35 | var r = new Reader(src);
36 | r.wantAll(pat);
37 | return r.residue();
38 | }
39 |
40 | it("throws error if cannot read the specified string",() => {
41 | assert.throw(() => {
42 | wantAll("abcdefg","abcdd");
43 | });
44 | });
45 |
46 | it("does nothing if given an empty string",() => {
47 | var residue = wantAll("abc","");
48 | assert.equal(residue,"abc");
49 | });
50 |
51 | it("advances reader position", () => {
52 | var residue;
53 | assert.doesNotThrow(() => {
54 | residue = wantAll("abcde","abc");
55 | });
56 |
57 | assert.equal(residue,"de");
58 | });
59 | });
60 |
61 | describe("#readUpto",() => {
62 | function readUpto(src:string, delimiter: string, expect?: string): string {
63 | var r = new Reader(src);
64 | var result = r.readUpto(delimiter)
65 | if(expect != null) {
66 | assert.equal(result,expect);
67 | }
68 | return r.residue();
69 | }
70 |
71 | it("returns the content upto, but exclude a string",() => {
72 | readUpto("abcd#rest","#","abcd");
73 | readUpto("ab#cd##rest","##","ab#cd");
74 | readUpto("##rest","##","");
75 | });
76 |
77 | it("reads nothing if reading upto the empty string",() => {
78 | var residue = readUpto("abc","","");
79 | assert.equal(residue,"abc");
80 | });
81 |
82 | it("advances reader position",() => {
83 | var residue = readUpto("abc#rest","#","abc");
84 | assert.equal(residue,"#rest");
85 | });
86 | });
87 |
88 | describe("#readDelimited", () => {
89 | function readDelimited(src: string, delimiter: string, expect?: string): string {
90 | var r = new Reader(src);
91 | var result = r.readDelimited(delimiter);
92 | if(expect != null) {
93 | assert.equal(result,expect);
94 | }
95 | return r.residue();
96 | }
97 |
98 | it("returns the content string between delimiters", () => {
99 | readDelimited("*content*","*","content");
100 | readDelimited("**","*","");
101 | readDelimited("**content**","**","content");
102 | });
103 |
104 | it("throws error if end delimiter cannot be found", () => {
105 | assert.throw(() => {
106 | readDelimited("*content","*");
107 | });
108 | });
109 |
110 | it("advances parser's position", () => {
111 | var residue = readDelimited("*content*more stuff","*");
112 | assert.equal(residue,"more stuff");
113 | });
114 | });
115 | });
116 |
117 |
--------------------------------------------------------------------------------
/src/Tag.ts:
--------------------------------------------------------------------------------
1 | export = Tag;
2 |
3 |
4 | import Ast = require("./Ast");
5 | type Tag = Ast.Tag;
6 | var Tag = Ast.Tag;
7 |
--------------------------------------------------------------------------------
/src/TagInfo.ts:
--------------------------------------------------------------------------------
1 | export = TagInfo;
2 |
3 | interface TagInfo {
4 | opts?: {[key:string]: string};
5 | args?: string[];
6 | }
--------------------------------------------------------------------------------
/src/TagTest.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import Tag = require("./Tag");
3 |
4 | import test = require("./test");
5 | var assertJSONEqual = test.assertJSONEqual;
6 |
7 | describe("Tag",() => {
8 | describe("#walk", () => {
9 | it("walks the nodes depth-first", () => {
10 | var tag = new Tag("doc");
11 | var a = new Tag("a");
12 | a.add("a1");
13 | a.add("a2");
14 | var b = new Tag("b");
15 | b.add("b1");
16 | b.add("b2");
17 | tag.add(a);
18 | tag.add(b);
19 | tag.add("c");
20 | var nodes = [];
21 | tag.walk(function (node,recur) {
22 | nodes.push(node);
23 | if(recur) {
24 | recur();
25 | }
26 | });
27 | assert.deepEqual([tag, a, "a1", "a2", b, "b1", "b2", "c"], nodes);
28 | });
29 | });
30 |
31 | describe("#transform",() => {
32 | it("transforms the recursive tag structure", () => {
33 | var tag = new Tag("doc");
34 | var a = new Tag("a");
35 | a.add("a1");
36 | var b = new Tag("b");
37 | b.add("b1");
38 | tag.add(a); tag.add(b);
39 |
40 | var tag2 = tag.transform((node,recur) => {
41 | if(typeof node === 'string') {
42 | return "#" + node;
43 | } else {
44 | return new Tag("<" + node.name,recur());
45 | }
46 | });
47 |
48 | assertJSONEqual(tag2,{
49 | "name": " {
69 | it("generates xml", () => {
70 | var tag = new Tag("doc");
71 | var a = new Tag("a");
72 | a.add("a1"); a.add("a2");
73 | var aa = new Tag("aa");
74 | aa.add("a3");
75 | a.add(aa);
76 |
77 | var b = new Tag("b");
78 | b.add("b1");
79 | tag.add(a); tag.add(b);
80 |
81 | // console.log("xml:\n",tag.xml({indent: 4}));
82 | });
83 | });
84 | });
--------------------------------------------------------------------------------
/src/TextParser.ts:
--------------------------------------------------------------------------------
1 | export = TextParser;
2 |
3 | import Tag = require("./Tag");
4 | import Reader = require("./Reader");
5 | type Node = Tag | string;
6 |
7 | class TextParser extends Reader {
8 | parse(): Array {
9 | return this._parse(false);
10 | }
11 |
12 | _parse(recursive:boolean): Array {
13 | var nodes: Array = [];
14 |
15 | while(true) {
16 | var str = this.readString();
17 | if(str != "") {
18 | nodes.push(str);
19 | }
20 |
21 | if(this.eof) {
22 | break;
23 | }
24 |
25 | var c = this.ch;
26 | switch(c) {
27 | case "*": // `_
28 | case "_":
29 | case "`":
30 | var tag = this.parseDelimitedTag();
31 | nodes.push(tag);
32 | break;
33 | case "[":
34 | nodes.push(this.parseInlineTag());
35 | break;
36 | case "]":
37 | // throws error if parsing is not in recursive mode
38 | // allow the recursive caller to consume "]"
39 | if(!recursive) {
40 | throw "excess ]";
41 | }
42 | return nodes;
43 | default:
44 | throw "not implemented";
45 | }
46 | }
47 |
48 | if(recursive) {
49 | throw "expecting ]";
50 | }
51 |
52 | return nodes;
53 | }
54 |
55 | parseInlineTag(): Tag {
56 | // parse tag
57 | this.want("[");
58 | // parse tag name
59 | var tagName = this.readSymbol();
60 | if(tagName == "") {
61 | throw "Inline tag cannot have empty name";
62 | }
63 |
64 | var info = this.parseArguments();
65 | this.want("]");
66 |
67 | var nodes: Array;
68 | // tag content is optional
69 | if(this.ch == "[") {
70 | this.want("[");
71 | nodes = this._parse(/*recursive=*/true);
72 | this.want("]");
73 | } else {
74 | nodes = [];
75 | }
76 |
77 | var tag = new Tag(tagName,nodes);
78 | tag.setInfo(info);
79 |
80 | return tag;
81 | }
82 |
83 | /**
84 | * Reads string upto (but excluding) a reserved char.
85 | */
86 | readString(): string {
87 | var acc = "";
88 | while(true) {
89 | if(this.eof) {
90 | break;
91 | }
92 |
93 | var c = this.ch;
94 | if(c === "*" || c === "[" || c === "]" || c === "`" || c === "_") {
95 | break;
96 | }
97 |
98 | if(c == "\\") {
99 | this.read();
100 | if(this.eof) {
101 | throw "Axpecting an escaped char, but end of file";
102 | }
103 | c = this.ch;
104 | }
105 |
106 | this.read();
107 | acc += c;
108 |
109 | }
110 |
111 | return acc;
112 | }
113 |
114 | // Read content delimited by asterisks.
115 | parseDelimitedTag(): Tag {
116 | var delimiter = this.ch;
117 | if(this.peek(1) === '`') {
118 | delimiter = '``'
119 | }
120 | var content = this.readDelimited(delimiter);
121 | if(content == null) {
122 | // TODO: error should contain the context of the bold starting.
123 | throw "could not find end of tag delimited by";
124 | }
125 |
126 | if(content == "") {
127 | throw `Content between #{delimiter} cannot be empty`;
128 | }
129 |
130 | // TODO: it should be error if tag content is empty?
131 | return new Tag(delimiter, [content]);
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/TextParserTest.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import TextParser = require("./TextParser");
4 |
5 | function assertParse(a,b) {
6 | assert.deepEqual(JSON.parse(JSON.stringify(a)),b);
7 | }
8 |
9 | describe("TextParser",() => {
10 | describe("#readString",() => {
11 | function readString(src: string): string {
12 | var p = new TextParser(src);
13 | return p.readString();
14 | }
15 |
16 | it("reads all the way to the end",() => {
17 | assert.equal(readString(""),"");
18 | assert.equal(readString(" ")," ");
19 | assert.equal(readString(" abcdefg abcdefg ")," abcdefg abcdefg ");
20 | });
21 |
22 | it("reads up to reserved chars",() => {
23 | assert.equal(readString("abcdefg* "),"abcdefg");
24 | assert.equal(readString("abcdefg_ "),"abcdefg");
25 | assert.equal(readString("abcdefg` "),"abcdefg");
26 | assert.equal(readString("abcdefg[ "),"abcdefg");
27 | });
28 |
29 | it("escapes any char as itself",() => {
30 | assert.equal(readString("\\*"),"*");
31 | assert.equal(readString("\\a"),"a");
32 | assert.equal(readString("\\1"),"1");
33 | });
34 |
35 | it("advances parser's position",() => {
36 | var p = new TextParser("content*more stuff*");
37 | p.readString();
38 | assert.equal(p.residue(),"*more stuff*");
39 | });
40 |
41 | it("is an error if backslash is followed by EOF",() => {
42 | assert.throw(() => {
43 | readString("foo\\");
44 | });
45 | });
46 | });
47 |
48 | describe("#parseDelimitedTag", () => {
49 | function parseDelimitedTag(src: string,expected?): string {
50 | var parser = new TextParser(src);
51 | var result = parser.parseDelimitedTag();
52 | // console.log(JSON.stringify(result,null,2));
53 | if(expected != null) {
54 | assertParse(result,expected);
55 | }
56 | return parser.residue();
57 | }
58 |
59 | it("throws error if delimited content is empty",() => {
60 | assert.throw(() => {
61 | parseDelimitedTag("**");
62 | });
63 | });
64 |
65 | it("parses literals denoted by reserved chars",() => {
66 | parseDelimitedTag("*content*",{
67 | "name": "*",
68 | "children": [
69 | "content"
70 | ]
71 | });
72 |
73 | parseDelimitedTag("_content_",{
74 | "name": "_",
75 | "children": [
76 | "content"
77 | ]
78 | });
79 | });
80 |
81 | it("treats `` and ` two different delimiters",() => {
82 | parseDelimitedTag("`content`",{
83 | "name": "`",
84 | "children": [
85 | "content"
86 | ]
87 | });
88 |
89 | parseDelimitedTag("``content``",{
90 | "name": "``",
91 | "children": [
92 | "content"
93 | ]
94 | });
95 | });
96 |
97 | it("ignores nestings",() => {
98 | parseDelimitedTag("_*content*_",{
99 | "name": "_",
100 | "children": [
101 | "*content*"
102 | ]
103 | });
104 | });
105 | });
106 |
107 | describe("#parseInlineTag",() => {
108 | function parseInlineTag(src,expected?): string {
109 | var parser = new TextParser(src);
110 | var result = parser.parseInlineTag();
111 | // console.log(JSON.stringify(result,null,2));
112 | if(expected != null) {
113 | assertParse(result,expected);
114 | }
115 | return parser.residue();
116 | }
117 |
118 | it("throws error if tag name is empty",() => {
119 | assert.throw(() => {
120 | parseInlineTag("[][hello]");
121 | });
122 | });
123 |
124 | it("parses inline tag that has no content",() => {
125 | parseInlineTag("[foo]",{
126 | "name": "foo",
127 | "children": []
128 | });
129 |
130 | parseInlineTag("[foo][]",{
131 | "name": "foo",
132 | "children": []
133 | });
134 | });
135 |
136 | it("parses inline tag that has content",() => {
137 | parseInlineTag("[foo][ ]",{
138 | "name": "foo",
139 | "children": [" "]
140 | });
141 |
142 | parseInlineTag("[foo][content of foo]",{
143 | "name": "foo",
144 | "children": [
145 | "content of foo"
146 | ]
147 | });
148 | });
149 |
150 | it("parses arguments",() => {
151 | parseInlineTag("[foo a b=b c][content of foo]",{
152 | "name": "foo",
153 | "args": [
154 | "a",
155 | "c"
156 | ],
157 | "opts": {
158 | "b": "b"
159 | },
160 | "children": [
161 | "content of foo"
162 | ]
163 | });
164 | });
165 |
166 | it("parses nesting tags recursively",() => {
167 | var tag =
168 | `
169 | [foo][
170 | leading *foo* content
171 | [bar][content of bar with [qux]]
172 | trailing foo]
173 | `.trim();
174 |
175 | parseInlineTag(tag,{
176 | "name": "foo",
177 | "children": [
178 | "\nleading ",
179 | {
180 | "name": "*",
181 | "children": [
182 | "foo"
183 | ]
184 | },
185 | " content\n",
186 | {
187 | "name": "bar",
188 | "children": [
189 | "content of bar with ",
190 | {
191 | "name": "qux",
192 | "children": []
193 | }
194 | ]
195 | },
196 | "\ntrailing foo"
197 | ]
198 | });
199 | });
200 | });
201 |
202 | describe("#parse",() => {
203 | function parse(src:string,expected?:any,show?:boolean): string {
204 | var parser = new TextParser(src);
205 | var result = parser.parse();
206 | if(show) {
207 | console.log(JSON.stringify(result,null,2))
208 | }
209 |
210 | if(expected != null) {
211 | assertParse(result,expected);
212 | }
213 |
214 | return parser.residue();
215 | }
216 |
217 | it("throws error is inline tag nesting is imbalanced",() => {
218 | assert.throw(() => {
219 | parse("[foo]]")
220 | });
221 |
222 | assert.throw(() => {
223 | parse("[foo][hello [] lala")
224 | });
225 | })
226 |
227 | it("returns array of nodes",() => {
228 | parse("text*bold*text",
229 | [
230 | "text",
231 | {
232 | "name": "*",
233 | "children": [
234 | "bold"
235 | ]
236 | },
237 | "text"
238 | ]
239 | );
240 | });
241 |
242 |
243 | });
244 |
245 | });
--------------------------------------------------------------------------------
/src/Xmd.ts:
--------------------------------------------------------------------------------
1 |
2 | import Parser = require("./BlockParser");
3 | import Tag = require("./Tag");
4 |
5 | export function parse(src: string): Tag {
6 | var parser = new Parser(src);
7 | var doc = parser.parse();
8 | return doc;
9 | }
10 |
11 | export function renderJSON(src: string, opts?: {indent?: number; raw?: boolean}): string {
12 | if(opts == null) {
13 | opts = {};
14 | }
15 | var doc = parse(src);
16 | opts.raw = !!opts.raw;
17 | if(!opts.raw) {
18 | doc = xmd2html(doc);
19 | }
20 | return doc.json(opts);
21 | }
22 |
23 | export function renderXML(src: string, opts?: {indent?: number}): string {
24 | var doc = xmd2html(parse(src));
25 | return doc.xml(opts);
26 | }
27 |
28 | /*
29 | Rewrite markdown tags to html tags.
30 | "" => "h1"
31 | "#" => "h2"
32 | "##" => "h3"
33 | "*" => "bold"
34 | "_" => "italic"
35 | etc.
36 | */
37 | export function xmd2html(doc: Tag): Tag {
38 | var doc2 = doc.transform((node,recur) => {
39 | if(typeof node === 'string') {
40 | return node
41 | } else {
42 | switch(node.name) {
43 | case "": {
44 | // Since a tag starts with `#`, the tag with the empty name is a hack for h1.
45 | return new Tag("h1",recur());
46 | }
47 | case "#": {
48 | return new Tag("h2",recur());
49 | }
50 | case "##": {
51 | return new Tag("h3",recur());
52 | }
53 | case "###": {
54 | return new Tag("h4",recur());
55 | }
56 | case "####": {
57 | return new Tag("h5",recur());
58 | }
59 | case "#####": {
60 | return new Tag("h6",recur());
61 | }
62 | case "*": {
63 | return new Tag("b",recur());
64 | }
65 | case "_": {
66 | return new Tag("i",recur());
67 | }
68 |
69 | case "`": {
70 | return new Tag("code",recur());
71 | }
72 | case "```": {
73 | var code = new Tag("code",recur());
74 | if(node.args != null) {
75 | code.opts = {lang: node.args[0]};
76 | }
77 | return new Tag("pre",[code]);
78 | }
79 | case "``":
80 | case "````": {
81 | return node.children[0];
82 | }
83 | case "list": {
84 | // look at the first list item to decide whether list should be ul or ol
85 | var li = node.children[0];
86 | if(li == null) {
87 | return new Tag("ul");
88 | }
89 |
90 | var listTagName: string;
91 | if (li.name == "+") {
92 | if(li.args && li.args[0] === "1") {
93 | listTagName = "ol";
94 | } else {
95 | listTagName = "ul";
96 | }
97 | } else {
98 | listTagName = "list";
99 | }
100 |
101 | return new Tag(listTagName,recur());
102 | }
103 | case "+": {
104 | return new Tag("li",recur());
105 | }
106 |
107 | // [> http://google.com]
108 | // [> http://google.com][The Google]
109 | case ">": {
110 | var href = node.args[0];
111 | if(href == null) {
112 | href = "";
113 | }
114 | var a = new Tag("a")
115 | a.opts = {href: href};
116 | if(node.children.length == 0) {
117 | a.children = [href];
118 | } else {
119 | a.children = node.children;
120 | }
121 | return a;
122 | }
123 | default: {
124 | var t = new Tag(node.name,recur());
125 | t.opts = node.opts;
126 | return t;
127 | }
128 | }
129 | }
130 | });
131 |
132 | // Type testing to make the TypeScript compiler happy.
133 | if(typeof doc2 === "string") {
134 | // should't happen
135 | throw new Error("Implementation error. XMarkdown transform shouldn't result in string");
136 | } else {
137 | return doc2;
138 | }
139 | }
--------------------------------------------------------------------------------
/src/XmdTest.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import Tag = require("./Tag");
4 | import xmd = require("./Xmd");
5 |
6 | describe("xmd",() => {
7 | describe(".xmd2html",() => {
8 | var xmd2html = xmd.xmd2html;
9 | function tagname(from,to) {
10 | assert.equal(xmd2html(new Tag(from)).name,to);
11 | }
12 | it("transforms headers",() => {
13 | tagname("","h1");
14 | tagname("#","h2");
15 | tagname("##","h3");
16 | tagname("###","h4");
17 | tagname("####","h5");
18 | tagname("#####","h6");
19 | });
20 |
21 | it("transforms quoted literals",() => {
22 | tagname("*","b");
23 | tagname("_","i");
24 | tagname("`","code");
25 | });
26 |
27 | it('transforms ``` code heredoc',() => {
28 | var tag = new Tag("```",["abc"]);
29 | tag.args = ["foolang"];
30 | var pre = xmd2html(tag);
31 | assert.equal(pre.name,"pre");
32 | assert.equal(pre.children.length,1);
33 | var code = pre.children[0];
34 | assert.equal(code.name,"code");
35 | assert.equal(code.children[0],"abc");
36 | assert.equal(code.opts["lang"],"foolang");
37 | });
38 |
39 | it("transforms inline string quote to string",() => {
40 | var foo = new Tag("foo",[new Tag("``",["abcd"])]);
41 | assert.deepEqual(xmd2html(foo).children,["abcd"]);
42 | });
43 |
44 | it("transforms links",() => {
45 | var a: Tag;
46 | a = new Tag(">");
47 | a.setInfo({args: ["http://google.com"]})
48 | var a_ = xmd2html(a);
49 | assert.equal(a_.name,"a");
50 | assert.equal(a_.opts["href"],"http://google.com");
51 | assert.deepEqual(a_.children,["http://google.com"]);
52 |
53 | a.children = ["The Google"];
54 | a_ = xmd2html(a);
55 | assert.deepEqual(a_.children,["The Google"]);
56 |
57 | });
58 | });
59 | });
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
--------------------------------------------------------------------------------
/src/test.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare var assert: chai.Assert;
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare var global: any;
3 | import chai = require("chai");
4 | global.assert = chai.assert;
5 |
6 | export function assertJSONEqual(a,b) {
7 | assert.deepEqual(
8 | JSON.parse(JSON.stringify(a)),
9 | JSON.parse(JSON.stringify(b)));
10 | }
--------------------------------------------------------------------------------
/tsd.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "v4",
3 | "repo": "borisyankov/DefinitelyTyped",
4 | "ref": "master",
5 | "path": "typings",
6 | "bundle": "typings/tsd.d.ts",
7 | "installed": {
8 | "mocha/mocha.d.ts": {
9 | "commit": "95d860879fb90c405aeb13eea7b8564f9c0df2bf"
10 | },
11 | "chai/chai.d.ts": {
12 | "commit": "95d860879fb90c405aeb13eea7b8564f9c0df2bf"
13 | },
14 | "minimist/minimist.d.ts": {
15 | "commit": "95d860879fb90c405aeb13eea7b8564f9c0df2bf"
16 | },
17 | "node/node.d.ts": {
18 | "commit": "95d860879fb90c405aeb13eea7b8564f9c0df2bf"
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------