├── example.html ├── .vscode ├── settings.json └── launch.json ├── md2html.ts ├── mod.ts ├── example.md ├── LICENSE ├── src ├── extend-regexp.ts ├── helpers.ts ├── renderer.ts ├── marked.ts ├── interfaces.ts ├── parser.ts ├── inline-lexer.ts └── block-lexer.ts └── README.md /example.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubersl0th/markdown/HEAD/example.html -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "[typescript]": { 4 | "editor.defaultFormatter": "axetroy.vscode-deno" 5 | }, 6 | "[typescriptreact]": { 7 | "editor.defaultFormatter": "axetroy.vscode-deno" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /md2html.ts: -------------------------------------------------------------------------------- 1 | import { Marked } from "./mod.ts"; 2 | 3 | const decoder = new TextDecoder("utf-8"); 4 | const filename = Deno.args[0]; 5 | const markdown = decoder.decode(await Deno.readFile(filename)); 6 | const markup = Marked.parse(markdown); 7 | console.log(markup.content); 8 | console.log(JSON.stringify(markup.meta)) 9 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./src/block-lexer.ts"; 2 | export * from "./src/helpers.ts"; 3 | export * from "./src/inline-lexer.ts"; 4 | export * from "./src/interfaces.ts"; 5 | export * from "./src/marked.ts"; 6 | export * from "./src/parser.ts"; 7 | export * from "./src/renderer.ts"; 8 | export * from "./src/extend-regexp.ts"; 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Deno", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceFolder}", 9 | "runtimeExecutable": "deno", 10 | "runtimeArgs": ["run", "--inspect", "-A", "app.ts"], 11 | "port": 9229 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /example.md: -------------------------------------------------------------------------------- 1 | --- 2 | title : Hello world! 3 | subtitle : Front-matter is supported! 4 | boolean: true 5 | list-example: 6 | - this 7 | - is 8 | - a: list 9 | --- 10 | # Hello World 11 | 12 | ## This an example for `md2html.ts` 13 | 14 | A small paragraph that will become a `

` tag 15 | 16 | --- 17 | 18 | Code Block (md2html.ts) 19 | 20 | ```typescript 21 | import { Marked } from "./mod.ts"; 22 | 23 | const decoder = new TextDecoder("utf-8"); 24 | const filename = Deno.args[0]; 25 | const markdown = decoder.decode(await Deno.readFile(filename)); 26 | const markup = Marked.parse(markdown); 27 | console.log(markup.content); 28 | console.log(JSON.stringify(markup.meta)); 29 | ``` 30 | 31 | This module is forked from [ts-stack/markdown](https://github.com/ts-stack/markdown/tree/bb47aa8e625e89e6aa84f49a98536a3089dee831) 32 | 33 | Made for Deno 34 | ![deno-logo](https://deno.land/logo.svg) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Eivind Furuberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/extend-regexp.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * 4 | * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) 5 | * https://github.com/chjj/marked 6 | * 7 | * Copyright (c) 2018, Костя Третяк. (MIT Licensed) 8 | * https://github.com/ts-stack/markdown 9 | */ 10 | 11 | export class ExtendRegexp { 12 | private source: string; 13 | private flags: string; 14 | 15 | constructor(regex: RegExp, flags: string = "") { 16 | this.source = regex.source; 17 | this.flags = flags; 18 | } 19 | 20 | /** 21 | * Extend regular expression. 22 | * 23 | * @param groupName Regular expression for search a group name. 24 | * @param groupRegexp Regular expression of named group. 25 | */ 26 | setGroup(groupName: RegExp | string, groupRegexp: RegExp | string): this { 27 | let newRegexp: string = typeof groupRegexp == "string" 28 | ? groupRegexp 29 | : groupRegexp.source; 30 | newRegexp = newRegexp.replace(/(^|[^\[])\^/g, "$1"); 31 | 32 | // Extend regexp. 33 | this.source = this.source.replace(groupName, newRegexp); 34 | return this; 35 | } 36 | 37 | /** 38 | * Returns a result of extending a regular expression. 39 | */ 40 | getRegexp(): RegExp { 41 | return new RegExp(this.source, this.flags); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * 4 | * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) 5 | * https://github.com/chjj/marked 6 | * 7 | * Copyright (c) 2018, Костя Третяк. (MIT Licensed) 8 | * https://github.com/ts-stack/markdown 9 | */ 10 | 11 | import { Replacements } from "./interfaces.ts"; 12 | 13 | const escapeTest = /[&<>"']/; 14 | const escapeReplace = /[&<>"']/g; 15 | const replacements: Replacements = { 16 | "&": "&", 17 | "<": "<", 18 | ">": ">", 19 | '"': """, 20 | // tslint:disable-next-line:quotemark 21 | "'": "'", 22 | }; 23 | 24 | const escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/; 25 | const escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g; 26 | 27 | export function escape(html: string, encode?: boolean) { 28 | if (encode) { 29 | if (escapeTest.test(html)) { 30 | return html.replace(escapeReplace, (ch: string) => replacements[ch]); 31 | } 32 | } else { 33 | if (escapeTestNoEncode.test(html)) { 34 | return html.replace( 35 | escapeReplaceNoEncode, 36 | (ch: string) => replacements[ch], 37 | ); 38 | } 39 | } 40 | 41 | return html; 42 | } 43 | 44 | export function unescape(html: string) { 45 | // Explicitly match decimal, hex, and named HTML entities 46 | return html.replace( 47 | /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi, 48 | function (_, n) { 49 | n = n.toLowerCase(); 50 | 51 | if (n === "colon") { 52 | return ":"; 53 | } 54 | 55 | if (n.charAt(0) === "#") { 56 | return n.charAt(1) === "x" 57 | ? String.fromCharCode(parseInt(n.substring(2), 16)) 58 | : String.fromCharCode(+n.substring(1)); 59 | } 60 | 61 | return ""; 62 | }, 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown 2 | 3 | Deno Markdown module forked from https://github.com/ts-stack/markdown/tree/bb47aa8e625e89e6aa84f49a98536a3089dee831 4 | 5 | ### Example usage 6 | 7 | Simple md2html.ts script: 8 | 9 | ```typescript 10 | import { Marked } from "./mod.ts"; 11 | 12 | const decoder = new TextDecoder("utf-8"); 13 | const filename = Deno.args[0]; 14 | const markdown = decoder.decode(await Deno.readFile(filename)); 15 | const markup = Marked.parse(markdown); 16 | console.log(markup.content); 17 | console.log(JSON.stringify(markup.meta)) 18 | ``` 19 | 20 | Now running: 21 | 22 | ```bash 23 | deno run --allow-read md2html.ts example.md > example.html 24 | ``` 25 | 26 | Will output: 27 | 28 | ```html 29 |

Hello World

30 |

31 | This an example for md2html.ts 32 |

33 |

A small paragraph that will become a <p> tag

34 |
35 |

Code Block (md2html.ts)

36 | 37 |
import { Marked } from "./mod.ts";
38 | 
39 | const decoder = new TextDecoder("utf-8");
40 | const filename = Deno.args[0];
41 | const markdown = decoder.decode(await Deno.readFile(filename));
42 | const markup = Marked.parse(markdown);
43 | console.log(markup.content);
44 | console.log(JSON.stringify(markup.meta))
45 | 
46 |

47 | This module is forked from 48 | ts-stack/markdown 52 |

53 |

Made for Deno deno-logo

54 | 55 | {"title":"Hello world!","subtitle":"Front-matter is supported!","boolean":true,"list-example":["this","is",{"a":"list"}]} 56 | ``` 57 | 58 | --- 59 | 60 | ### Notes 61 | 62 | I had to do some changes to the source code to make the compiler happy, mostly fixes for things that were uninitialized and possibly null or undefined 63 | -------------------------------------------------------------------------------- /src/renderer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * 4 | * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) 5 | * https://github.com/chjj/marked 6 | * 7 | * Copyright (c) 2018, Костя Третяк. (MIT Licensed) 8 | * https://github.com/ts-stack/markdown 9 | */ 10 | 11 | import { Align, MarkedOptions } from "./interfaces.ts"; 12 | import { Marked } from "./marked.ts"; 13 | 14 | export class Renderer { 15 | protected options: MarkedOptions; 16 | 17 | constructor(options?: MarkedOptions) { 18 | this.options = options || Marked.options; 19 | } 20 | 21 | code(code: string, lang?: string, escaped?: boolean): string { 22 | if (this.options.highlight) { 23 | const out = this.options.highlight(code, lang); 24 | 25 | if (out != null && out !== code) { 26 | escaped = true; 27 | code = out; 28 | } 29 | } 30 | 31 | if (!this.options.escape) throw ReferenceError; 32 | 33 | if (!lang) { 34 | return "\n
" + (escaped
 35 |         ? code
 36 |         : this.options.escape(code, true)) +
 37 |         "\n
\n"; 38 | } 39 | 40 | return ( 41 | '\n
' +
 45 |       (escaped ? code : this.options.escape(code, true)) +
 46 |       "\n
\n" 47 | ); 48 | } 49 | 50 | blockquote(quote: string): string { 51 | return "
\n" + quote + "
\n"; 52 | } 53 | 54 | html(html: string): string { 55 | return html; 56 | } 57 | 58 | heading(text: string, level: number, raw: string): string { 59 | const id: string = this.options.headerPrefix + 60 | raw.toLowerCase().replace(/[^\w]+/g, "-"); 61 | 62 | return `${text}\n`; 63 | } 64 | 65 | hr(): string { 66 | return this.options.xhtml ? "
\n" : "
\n"; 67 | } 68 | 69 | list(body: string, ordered?: boolean): string { 70 | const type = ordered ? "ol" : "ul"; 71 | 72 | return `\n<${type}>\n${body}\n`; 73 | } 74 | 75 | listitem(text: string): string { 76 | return "
  • " + text + "
  • \n"; 77 | } 78 | 79 | paragraph(text: string): string { 80 | return "

    " + text + "

    \n"; 81 | } 82 | 83 | table(header: string, body: string): string { 84 | return ` 85 | 86 | 87 | ${header} 88 | 89 | ${body} 90 |
    91 | `; 92 | } 93 | 94 | tablerow(content: string): string { 95 | return "\n" + content + "\n"; 96 | } 97 | 98 | tablecell( 99 | content: string, 100 | flags: { header?: boolean; align?: Align }, 101 | ): string { 102 | const type = flags.header ? "th" : "td"; 103 | const tag = flags.align 104 | ? "<" + type + ' style="text-align:' + flags.align + '">' 105 | : "<" + type + ">"; 106 | return tag + content + "\n"; 107 | } 108 | 109 | // *** Inline level renderer methods. *** 110 | 111 | strong(text: string): string { 112 | return "" + text + ""; 113 | } 114 | 115 | em(text: string): string { 116 | return "" + text + ""; 117 | } 118 | 119 | codespan(text: string): string { 120 | return "" + text + ""; 121 | } 122 | 123 | br(): string { 124 | return this.options.xhtml ? "
    " : "
    "; 125 | } 126 | 127 | del(text: string): string { 128 | return "" + text + ""; 129 | } 130 | 131 | link(href: string, title: string, text: string): string { 132 | if (this.options.sanitize) { 133 | let prot: string; 134 | if (!this.options.unescape) throw ReferenceError; 135 | try { 136 | prot = decodeURIComponent(this.options.unescape(href)) 137 | .replace(/[^\w:]/g, "") 138 | .toLowerCase(); 139 | } catch (e) { 140 | return text; 141 | } 142 | 143 | if ( 144 | prot.indexOf("javascript:") === 0 || 145 | prot.indexOf("vbscript:") === 0 || prot.indexOf("data:") === 0 146 | ) { 147 | return text; 148 | } 149 | } 150 | 151 | let out = '"; 158 | 159 | return out; 160 | } 161 | 162 | image(href: string, title: string, text: string): string { 163 | let out = '' + text + '" : ">"; 170 | 171 | return out; 172 | } 173 | 174 | text(text: string): string { 175 | return text; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/marked.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * 4 | * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) 5 | * https://github.com/chjj/marked 6 | * 7 | * Copyright (c) 2018, Костя Третяк. (MIT Licensed) 8 | * https://github.com/ts-stack/markdown 9 | */ 10 | 11 | import { BlockLexer } from "./block-lexer.ts"; 12 | import { 13 | DebugReturns, 14 | LexerReturns, 15 | Links, 16 | MarkedOptions, 17 | SimpleRenderer, 18 | Token, 19 | TokenType, 20 | Parsed 21 | } from "./interfaces.ts"; 22 | import { Parser } from "./parser.ts"; 23 | 24 | export class Marked { 25 | static options = new MarkedOptions(); 26 | protected static simpleRenderers: SimpleRenderer[] = []; 27 | protected static parsed: Parsed = { 28 | content: "", 29 | meta: {}, 30 | }; 31 | 32 | /** 33 | * Merges the default options with options that will be set. 34 | * 35 | * @param options Hash of options. 36 | */ 37 | static setOptions(options: MarkedOptions) { 38 | Object.assign(this.options, options); 39 | return this; 40 | } 41 | 42 | /** 43 | * Setting simple block rule. 44 | */ 45 | static setBlockRule(regexp: RegExp, renderer: SimpleRenderer = () => "") { 46 | BlockLexer.simpleRules.push(regexp); 47 | this.simpleRenderers.push(renderer); 48 | 49 | return this; 50 | } 51 | 52 | /** 53 | * Accepts Markdown text and returns an object containing HTML and metadata. 54 | * 55 | * @param src String of markdown source to be compiled. 56 | * @param options Hash of options. They replace, but do not merge with the default options. 57 | * If you want the merging, you can to do this via `Marked.setOptions()`. 58 | */ 59 | static parse(src: string, options: MarkedOptions = this.options): Parsed { 60 | try { 61 | const { tokens, links, meta } = this.callBlockLexer(src, options); 62 | this.parsed.content = this.callParser(tokens, links, options); 63 | this.parsed.meta = meta; 64 | return this.parsed; 65 | } catch (e) { 66 | this.parsed.content = this.callMe(e); 67 | return this.parsed; 68 | } 69 | } 70 | 71 | /** 72 | * Accepts Markdown text and returns object with text in HTML format, 73 | * tokens and links from `BlockLexer.parser()`. 74 | * 75 | * @param src String of markdown source to be compiled. 76 | * @param options Hash of options. They replace, but do not merge with the default options. 77 | * If you want the merging, you can to do this via `Marked.setOptions()`. 78 | */ 79 | static debug( 80 | src: string, 81 | options: MarkedOptions = this.options, 82 | ): DebugReturns { 83 | const { tokens, links, meta } = this.callBlockLexer(src, options); 84 | let origin = tokens.slice(); 85 | const parser = new Parser(options); 86 | parser.simpleRenderers = this.simpleRenderers; 87 | const result = parser.debug(links, tokens); 88 | 89 | /** 90 | * Translates a token type into a readable form, 91 | * and moves `line` field to a first place in a token object. 92 | */ 93 | origin = origin.map((token) => { 94 | token.type = (TokenType as any)[token.type] || token.type; 95 | 96 | const line = token.line; 97 | delete token.line; 98 | if (line) { 99 | return { ...{ line }, ...token }; 100 | } else { 101 | return token; 102 | } 103 | }); 104 | 105 | return { tokens: origin, links, meta, result}; 106 | } 107 | 108 | protected static callBlockLexer( 109 | src: string = "", 110 | options?: MarkedOptions, 111 | ): LexerReturns { 112 | if (typeof src != "string") { 113 | throw new Error( 114 | `Expected that the 'src' parameter would have a 'string' type, got '${typeof src}'`, 115 | ); 116 | } 117 | 118 | // Preprocessing. 119 | src = src 120 | .replace(/\r\n|\r/g, "\n") 121 | .replace(/\t/g, " ") 122 | .replace(/^ +$/gm, ""); 123 | 124 | return BlockLexer.lex(src, options, true); 125 | } 126 | 127 | protected static callParser( 128 | tokens: Token[], 129 | links: Links, 130 | options?: MarkedOptions, 131 | ): string { 132 | if (this.simpleRenderers.length) { 133 | const parser = new Parser(options); 134 | parser.simpleRenderers = this.simpleRenderers; 135 | return parser.parse(links, tokens); 136 | } else { 137 | return Parser.parse(tokens, links, options); 138 | } 139 | } 140 | 141 | protected static callMe(err: Error) { 142 | err.message += 143 | "\nPlease report this to https://github.com/ts-stack/markdown"; 144 | 145 | if (this.options.silent && this.options.escape) { 146 | return "

    An error occured:

    " +
    147 |         this.options.escape(err.message + "", true) + "
    "; 148 | } 149 | 150 | throw err; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * 4 | * Copyright (c) 2018, Костя Третяк. (MIT Licensed) 5 | * https://github.com/ts-stack/markdown 6 | */ 7 | 8 | import { escape, unescape } from "./helpers.ts"; 9 | import { Renderer } from "./renderer.ts"; 10 | 11 | export interface Obj { 12 | [key: string]: any; 13 | } 14 | 15 | export interface RulesBlockBase { 16 | newline: RegExp; 17 | code: RegExp; 18 | hr: RegExp; 19 | heading: RegExp; 20 | lheading: RegExp; 21 | blockquote: RegExp; 22 | list: RegExp; 23 | html: RegExp; 24 | def: RegExp; 25 | paragraph: RegExp; 26 | text: RegExp; 27 | bullet: RegExp; 28 | /** 29 | * List item (
  • ). 30 | */ 31 | item: RegExp; 32 | } 33 | 34 | export interface RulesBlockGfm extends RulesBlockBase { 35 | fences: RegExp; 36 | } 37 | 38 | export interface RulesBlockTables extends RulesBlockGfm { 39 | nptable: RegExp; 40 | table: RegExp; 41 | } 42 | 43 | export interface Link { 44 | href: string; 45 | title: string; 46 | } 47 | 48 | export interface Links { 49 | [key: string]: Link; 50 | } 51 | 52 | export enum TokenType { 53 | space = 1, 54 | text, 55 | paragraph, 56 | heading, 57 | listStart, 58 | listEnd, 59 | looseItemStart, 60 | looseItemEnd, 61 | listItemStart, 62 | listItemEnd, 63 | blockquoteStart, 64 | blockquoteEnd, 65 | code, 66 | table, 67 | html, 68 | hr, 69 | } 70 | 71 | export type Align = "center" | "left" | "right" | ""; 72 | 73 | export interface Token { 74 | type: number | string; 75 | text?: string; 76 | lang?: string; 77 | depth?: number; 78 | header?: string[]; 79 | align?: Align[]; 80 | cells?: string[][]; 81 | ordered?: boolean; 82 | pre?: boolean; 83 | escaped?: boolean; 84 | execArr?: RegExpExecArray; 85 | /** 86 | * Used for debugging. Identifies the line number in the resulting HTML file. 87 | */ 88 | line?: number; 89 | } 90 | 91 | export interface RulesInlineBase { 92 | escape: RegExp; 93 | autolink: RegExp; 94 | tag: RegExp; 95 | link: RegExp; 96 | reflink: RegExp; 97 | nolink: RegExp; 98 | strong: RegExp; 99 | em: RegExp; 100 | code: RegExp; 101 | br: RegExp; 102 | text: RegExp; 103 | _inside: RegExp; 104 | _href: RegExp; 105 | } 106 | 107 | export interface RulesInlinePedantic extends RulesInlineBase {} 108 | 109 | /** 110 | * GFM Inline Grammar 111 | */ 112 | export interface RulesInlineGfm extends RulesInlineBase { 113 | url: RegExp; 114 | del: RegExp; 115 | } 116 | 117 | export interface RulesInlineBreaks extends RulesInlineGfm {} 118 | 119 | export class MarkedOptions { 120 | gfm?: boolean = true; 121 | tables?: boolean = true; 122 | breaks?: boolean = false; 123 | pedantic?: boolean = false; 124 | sanitize?: boolean = false; 125 | sanitizer?: (text: string) => string; 126 | mangle?: boolean = false; 127 | smartLists?: boolean = false; 128 | silent?: boolean = false; 129 | /** 130 | * @param code The section of code to pass to the highlighter. 131 | * @param lang The programming language specified in the code block. 132 | */ 133 | highlight?: (code: string, lang?: string) => string; 134 | langPrefix?: string = "lang-"; 135 | smartypants?: boolean = false; 136 | headerPrefix?: string = ""; 137 | /** 138 | * An object containing functions to render tokens to HTML. Default: `new Renderer()` 139 | */ 140 | renderer?: Renderer; 141 | /** 142 | * Self-close the tags for void elements (<br/>, <img/>, etc.) 143 | * with a "/" as required by XHTML. 144 | */ 145 | xhtml?: boolean = false; 146 | /** 147 | * The function that will be using to escape HTML entities. 148 | * By default using inner helper. 149 | */ 150 | escape?: (html: string, encode?: boolean) => string = escape; 151 | /** 152 | * The function that will be using to unescape HTML entities. 153 | * By default using inner helper. 154 | */ 155 | unescape?: (html: string) => string = unescape; 156 | /** 157 | * If set to `true`, an inline text will not be taken in paragraph. 158 | * 159 | * ```ts 160 | * // isNoP == false 161 | * Marked.parse('some text'); // returns '

    some text

    ' 162 | * 163 | * Marked.setOptions({isNoP: true}); 164 | * 165 | * Marked.parse('some text'); // returns 'some text' 166 | * ``` 167 | */ 168 | isNoP?: boolean; 169 | } 170 | 171 | export interface LexerReturns { 172 | tokens: Token[]; 173 | links: Links; 174 | meta: Obj; 175 | } 176 | 177 | export interface Parsed { 178 | content: string; 179 | meta: Obj; 180 | } 181 | 182 | export interface DebugReturns extends LexerReturns { 183 | result: string; 184 | } 185 | 186 | export interface Replacements { 187 | [key: string]: string; 188 | } 189 | 190 | export interface RulesInlineCallback { 191 | regexp?: RegExp; 192 | condition(): RegExp; 193 | tokenize(execArr: RegExpExecArray): void; 194 | } 195 | 196 | export type SimpleRenderer = (execArr?: RegExpExecArray) => string; 197 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * 4 | * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) 5 | * https://github.com/chjj/marked 6 | * 7 | * Copyright (c) 2018, Костя Третяк. (MIT Licensed) 8 | * https://github.com/ts-stack/markdown 9 | */ 10 | 11 | import { InlineLexer } from "./inline-lexer.ts"; 12 | import { 13 | Links, 14 | MarkedOptions, 15 | SimpleRenderer, 16 | Token, 17 | TokenType, 18 | } from "./interfaces.ts"; 19 | import { Marked } from "./marked.ts"; 20 | import { Renderer } from "./renderer.ts"; 21 | 22 | /** 23 | * Parsing & Compiling. 24 | */ 25 | export class Parser { 26 | simpleRenderers: SimpleRenderer[] = []; 27 | protected tokens: Token[]; 28 | protected token: Token | undefined; 29 | protected inlineLexer!: InlineLexer; 30 | protected options: MarkedOptions; 31 | protected renderer: Renderer; 32 | protected line: number = 0; 33 | 34 | constructor(options?: MarkedOptions) { 35 | this.tokens = []; 36 | this.token = undefined; 37 | this.options = options || Marked.options; 38 | this.renderer = this.options.renderer || new Renderer(this.options); 39 | } 40 | 41 | static parse(tokens: Token[], links: Links, options?: MarkedOptions): string { 42 | const parser = new this(options); 43 | return parser.parse(links, tokens); 44 | } 45 | 46 | parse(links: Links, tokens: Token[]) { 47 | this.inlineLexer = new InlineLexer( 48 | InlineLexer, 49 | links, 50 | this.options, 51 | this.renderer, 52 | ); 53 | this.tokens = tokens.reverse(); 54 | 55 | let out = ""; 56 | 57 | while (this.next()) { 58 | out += this.tok(); 59 | } 60 | 61 | return out; 62 | } 63 | 64 | debug(links: Links, tokens: Token[]) { 65 | this.inlineLexer = new InlineLexer( 66 | InlineLexer, 67 | links, 68 | this.options, 69 | this.renderer, 70 | ); 71 | this.tokens = tokens.reverse(); 72 | 73 | let out = ""; 74 | 75 | while (this.next()) { 76 | const outToken: string = this.tok() || ""; 77 | if (!this.token) throw ReferenceError; 78 | this.token.line = this.line += outToken.split("\n").length - 1; 79 | out += outToken; 80 | } 81 | 82 | return out; 83 | } 84 | 85 | protected next() { 86 | return (this.token = this.tokens.pop()); 87 | } 88 | 89 | protected getNextElement() { 90 | return this.tokens[this.tokens.length - 1]; 91 | } 92 | 93 | protected parseText() { 94 | if (!this.token) throw ReferenceError; 95 | let body = this.token.text; 96 | let nextElement: Token; 97 | 98 | while ( 99 | (nextElement = this.getNextElement()) && 100 | nextElement.type == TokenType.text 101 | ) { 102 | body += "\n" + this.next()?.text; 103 | } 104 | 105 | return this.inlineLexer.output(body || ""); 106 | } 107 | 108 | protected tok() { 109 | if (!this.token) throw ReferenceError; 110 | switch (this.token.type) { 111 | case TokenType.space: { 112 | return ""; 113 | } 114 | case TokenType.paragraph: { 115 | return this.renderer.paragraph( 116 | this.inlineLexer.output(this.token.text || ""), 117 | ); 118 | } 119 | case TokenType.text: { 120 | if (this.options.isNoP) { 121 | return this.parseText(); 122 | } else { 123 | return this.renderer.paragraph(this.parseText()); 124 | } 125 | } 126 | case TokenType.heading: { 127 | return this.renderer.heading( 128 | this.inlineLexer.output(this.token.text || ""), 129 | this.token.depth || 0, 130 | this.token.text || "", 131 | ); 132 | } 133 | case TokenType.listStart: { 134 | let body = ""; 135 | const ordered = this.token.ordered; 136 | 137 | while (this.next()?.type != TokenType.listEnd) { 138 | body += this.tok(); 139 | } 140 | 141 | return this.renderer.list(body, ordered); 142 | } 143 | case TokenType.listItemStart: { 144 | let body = ""; 145 | 146 | while (this.next()?.type != TokenType.listItemEnd) { 147 | body += this.token.type == (TokenType.text as any) 148 | ? this.parseText() 149 | : this.tok(); 150 | } 151 | 152 | return this.renderer.listitem(body); 153 | } 154 | case TokenType.looseItemStart: { 155 | let body = ""; 156 | 157 | while (this.next()?.type != TokenType.listItemEnd) { 158 | body += this.tok(); 159 | } 160 | 161 | return this.renderer.listitem(body); 162 | } 163 | case TokenType.code: { 164 | return this.renderer.code( 165 | this.token.text || "", 166 | this.token.lang, 167 | this.token.escaped, 168 | ); 169 | } 170 | case TokenType.table: { 171 | let header = ""; 172 | let body = ""; 173 | let cell; 174 | 175 | if ( 176 | !this.token || !this.token.header || !this.token.align || 177 | !this.token.cells 178 | ) { 179 | throw ReferenceError; 180 | } 181 | // header 182 | cell = ""; 183 | for (let i = 0; i < this.token.header.length; i++) { 184 | const flags = { header: true, align: this.token.align[i] }; 185 | const out = this.inlineLexer.output(this.token.header[i]); 186 | 187 | cell += this.renderer.tablecell(out, flags); 188 | } 189 | 190 | header += this.renderer.tablerow(cell); 191 | 192 | for (const row of this.token.cells) { 193 | cell = ""; 194 | 195 | for (let j = 0; j < row.length; j++) { 196 | cell += this.renderer.tablecell(this.inlineLexer.output(row[j]), { 197 | header: false, 198 | align: this.token.align[j], 199 | }); 200 | } 201 | 202 | body += this.renderer.tablerow(cell); 203 | } 204 | 205 | return this.renderer.table(header, body); 206 | } 207 | case TokenType.blockquoteStart: { 208 | let body = ""; 209 | 210 | while (this.next()?.type != TokenType.blockquoteEnd) { 211 | body += this.tok(); 212 | } 213 | 214 | return this.renderer.blockquote(body); 215 | } 216 | case TokenType.hr: { 217 | return this.renderer.hr(); 218 | } 219 | case TokenType.html: { 220 | const html = !this.token.pre && !this.options.pedantic 221 | ? this.inlineLexer.output(this.token.text || "") 222 | : this.token.text; 223 | return this.renderer.html(html || ""); 224 | } 225 | default: { 226 | if (this.simpleRenderers.length) { 227 | for (let i = 0; i < this.simpleRenderers.length; i++) { 228 | if (this.token.type == "simpleRule" + (i + 1)) { 229 | return this.simpleRenderers[i].call( 230 | this.renderer, 231 | this.token.execArr, 232 | ); 233 | } 234 | } 235 | } 236 | 237 | const errMsg = `Token with "${this.token.type}" type was not found.`; 238 | 239 | if (this.options.silent) { 240 | console.log(errMsg); 241 | } else { 242 | throw new Error(errMsg); 243 | } 244 | } 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/inline-lexer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * 4 | * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) 5 | * https://github.com/chjj/marked 6 | * 7 | * Copyright (c) 2018, Костя Третяк. (MIT Licensed) 8 | * https://github.com/ts-stack/markdown 9 | */ 10 | 11 | import { ExtendRegexp } from "./extend-regexp.ts"; 12 | import { 13 | Link, 14 | Links, 15 | MarkedOptions, 16 | RulesInlineBase, 17 | RulesInlineBreaks, 18 | RulesInlineCallback, 19 | RulesInlineGfm, 20 | RulesInlinePedantic, 21 | } from "./interfaces.ts"; 22 | import { Marked } from "./marked.ts"; 23 | import { Renderer } from "./renderer.ts"; 24 | 25 | /** 26 | * Inline Lexer & Compiler. 27 | */ 28 | export class InlineLexer { 29 | protected static rulesBase: RulesInlineBase; 30 | /** 31 | * Pedantic Inline Grammar. 32 | */ 33 | protected static rulesPedantic: RulesInlinePedantic; 34 | /** 35 | * GFM Inline Grammar 36 | */ 37 | protected static rulesGfm: RulesInlineGfm; 38 | /** 39 | * GFM + Line Breaks Inline Grammar. 40 | */ 41 | protected static rulesBreaks: RulesInlineBreaks; 42 | protected rules!: 43 | | RulesInlineBase 44 | | RulesInlinePedantic 45 | | RulesInlineGfm 46 | | RulesInlineBreaks; 47 | protected renderer: Renderer; 48 | protected inLink!: boolean; 49 | protected hasRulesGfm!: boolean; 50 | protected ruleCallbacks!: RulesInlineCallback[]; 51 | 52 | constructor( 53 | protected staticThis: typeof InlineLexer, 54 | protected links: Links, 55 | protected options: MarkedOptions = Marked.options, 56 | renderer?: Renderer, 57 | ) { 58 | this.renderer = renderer || this.options.renderer || 59 | new Renderer(this.options); 60 | 61 | if (!this.links) { 62 | throw new Error(`InlineLexer requires 'links' parameter.`); 63 | } 64 | 65 | this.setRules(); 66 | } 67 | 68 | /** 69 | * Static Lexing/Compiling Method. 70 | */ 71 | static output(src: string, links: Links, options: MarkedOptions): string { 72 | const inlineLexer = new this(this, links, options); 73 | return inlineLexer.output(src); 74 | } 75 | 76 | protected static getRulesBase(): RulesInlineBase { 77 | if (this.rulesBase) { 78 | return this.rulesBase; 79 | } 80 | 81 | /** 82 | * Inline-Level Grammar. 83 | */ 84 | const base: RulesInlineBase = { 85 | escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, 86 | autolink: /^<([^ <>]+(@|:\/)[^ <>]+)>/, 87 | tag: /^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^<'">])*?>/, 88 | link: /^!?\[(inside)\]\(href\)/, 89 | reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, 90 | nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, 91 | strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, 92 | em: /^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, 93 | code: /^(`+)([\s\S]*?[^`])\1(?!`)/, 94 | br: /^ {2,}\n(?!\s*$)/, 95 | text: /^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/, 98 | }; 99 | 100 | base.link = new ExtendRegexp(base.link) 101 | .setGroup("inside", base._inside) 102 | .setGroup("href", base._href) 103 | .getRegexp(); 104 | 105 | base.reflink = new ExtendRegexp(base.reflink).setGroup( 106 | "inside", 107 | base._inside, 108 | ).getRegexp(); 109 | 110 | return (this.rulesBase = base); 111 | } 112 | 113 | protected static getRulesPedantic(): RulesInlinePedantic { 114 | if (this.rulesPedantic) { 115 | return this.rulesPedantic; 116 | } 117 | 118 | return (this.rulesPedantic = { 119 | ...this.getRulesBase(), 120 | ...{ 121 | strong: 122 | /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, 123 | em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/, 124 | }, 125 | }); 126 | } 127 | 128 | protected static getRulesGfm(): RulesInlineGfm { 129 | if (this.rulesGfm) { 130 | return this.rulesGfm; 131 | } 132 | 133 | const base = this.getRulesBase(); 134 | 135 | const escape = new ExtendRegexp(base.escape).setGroup("])", "~|])") 136 | .getRegexp(); 137 | 138 | const text = new ExtendRegexp(base.text) 139 | .setGroup("]|", "~]|") 140 | .setGroup("|", "|https?://|") 141 | .getRegexp(); 142 | 143 | return (this.rulesGfm = { 144 | ...base, 145 | ...{ 146 | escape, 147 | url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/, 148 | del: /^~~(?=\S)([\s\S]*?\S)~~/, 149 | text, 150 | }, 151 | }); 152 | } 153 | 154 | protected static getRulesBreaks(): RulesInlineBreaks { 155 | if (this.rulesBreaks) { 156 | return this.rulesBreaks; 157 | } 158 | 159 | const inline = this.getRulesGfm(); 160 | const gfm = this.getRulesGfm(); 161 | 162 | return (this.rulesBreaks = { 163 | ...gfm, 164 | ...{ 165 | br: new ExtendRegexp(inline.br).setGroup("{2,}", "*").getRegexp(), 166 | text: new ExtendRegexp(gfm.text).setGroup("{2,}", "*").getRegexp(), 167 | }, 168 | }); 169 | } 170 | 171 | protected setRules() { 172 | if (this.options.gfm) { 173 | if (this.options.breaks) { 174 | this.rules = this.staticThis.getRulesBreaks(); 175 | } else { 176 | this.rules = this.staticThis.getRulesGfm(); 177 | } 178 | } else if (this.options.pedantic) { 179 | this.rules = this.staticThis.getRulesPedantic(); 180 | } else { 181 | this.rules = this.staticThis.getRulesBase(); 182 | } 183 | 184 | this.hasRulesGfm = (this.rules as RulesInlineGfm).url !== undefined; 185 | } 186 | 187 | /** 188 | * Lexing/Compiling. 189 | */ 190 | output(nextPart: string): string { 191 | nextPart = nextPart; 192 | let execArr: RegExpExecArray | null; 193 | let out = ""; 194 | 195 | while (nextPart) { 196 | // escape 197 | if ((execArr = this.rules.escape.exec(nextPart))) { 198 | nextPart = nextPart.substring(execArr[0].length); 199 | out += execArr[1]; 200 | continue; 201 | } 202 | 203 | // autolink 204 | if ((execArr = this.rules.autolink.exec(nextPart))) { 205 | let text: string; 206 | let href: string; 207 | nextPart = nextPart.substring(execArr[0].length); 208 | 209 | if (!this.options.escape) throw ReferenceError; 210 | 211 | if (execArr[2] === "@") { 212 | text = this.options.escape( 213 | execArr[1].charAt(6) === ":" 214 | ? this.mangle(execArr[1].substring(7)) 215 | : this.mangle(execArr[1]), 216 | ); 217 | href = this.mangle("mailto:") + text; 218 | } else { 219 | text = this.options.escape(execArr[1]); 220 | href = text; 221 | } 222 | 223 | out += this.renderer.link(href, "", text); 224 | continue; 225 | } 226 | 227 | // url (gfm) 228 | if ( 229 | !this.inLink && this.hasRulesGfm && 230 | (execArr = (this.rules as RulesInlineGfm).url.exec(nextPart)) 231 | ) { 232 | if (!this.options.escape) throw ReferenceError; 233 | let text: string; 234 | let href: string; 235 | nextPart = nextPart.substring(execArr[0].length); 236 | text = this.options.escape(execArr[1]); 237 | href = text; 238 | out += this.renderer.link(href, "", text); 239 | continue; 240 | } 241 | 242 | // tag 243 | if ((execArr = this.rules.tag.exec(nextPart))) { 244 | if (!this.inLink && /^
    /i.test(execArr[0])) { 247 | this.inLink = false; 248 | } 249 | 250 | nextPart = nextPart.substring(execArr[0].length); 251 | 252 | if (!this.options.escape) throw ReferenceError; 253 | 254 | out += this.options.sanitize 255 | ? this.options.sanitizer 256 | ? this.options.sanitizer(execArr[0]) 257 | : this.options.escape(execArr[0]) 258 | : execArr[0]; 259 | continue; 260 | } 261 | 262 | // link 263 | if ((execArr = this.rules.link.exec(nextPart))) { 264 | nextPart = nextPart.substring(execArr[0].length); 265 | this.inLink = true; 266 | 267 | out += this.outputLink(execArr, { 268 | href: execArr[2], 269 | title: execArr[3], 270 | }); 271 | 272 | this.inLink = false; 273 | continue; 274 | } 275 | 276 | // reflink, nolink 277 | if ( 278 | (execArr = this.rules.reflink.exec(nextPart)) || 279 | (execArr = this.rules.nolink.exec(nextPart)) 280 | ) { 281 | nextPart = nextPart.substring(execArr[0].length); 282 | const keyLink = (execArr[2] || execArr[1]).replace(/\s+/g, " "); 283 | const link = this.links[keyLink.toLowerCase()]; 284 | 285 | if (!link || !link.href) { 286 | out += execArr[0].charAt(0); 287 | nextPart = execArr[0].substring(1) + nextPart; 288 | continue; 289 | } 290 | 291 | this.inLink = true; 292 | out += this.outputLink(execArr, link); 293 | this.inLink = false; 294 | continue; 295 | } 296 | 297 | // strong 298 | if ((execArr = this.rules.strong.exec(nextPart))) { 299 | nextPart = nextPart.substring(execArr[0].length); 300 | out += this.renderer.strong(this.output(execArr[2] || execArr[1])); 301 | continue; 302 | } 303 | 304 | // em 305 | if ((execArr = this.rules.em.exec(nextPart))) { 306 | nextPart = nextPart.substring(execArr[0].length); 307 | out += this.renderer.em(this.output(execArr[2] || execArr[1])); 308 | continue; 309 | } 310 | 311 | // code 312 | if ((execArr = this.rules.code.exec(nextPart))) { 313 | if (!this.options.escape) throw ReferenceError; 314 | nextPart = nextPart.substring(execArr[0].length); 315 | out += this.renderer.codespan( 316 | this.options.escape(execArr[2].trim(), true), 317 | ); 318 | continue; 319 | } 320 | 321 | // br 322 | if ((execArr = this.rules.br.exec(nextPart))) { 323 | nextPart = nextPart.substring(execArr[0].length); 324 | out += this.renderer.br(); 325 | continue; 326 | } 327 | 328 | // del (gfm) 329 | if ( 330 | this.hasRulesGfm && 331 | (execArr = (this.rules as RulesInlineGfm).del.exec(nextPart)) 332 | ) { 333 | nextPart = nextPart.substring(execArr[0].length); 334 | out += this.renderer.del(this.output(execArr[1])); 335 | continue; 336 | } 337 | 338 | // text 339 | if ((execArr = this.rules.text.exec(nextPart))) { 340 | if (!this.options.escape) throw ReferenceError; 341 | nextPart = nextPart.substring(execArr[0].length); 342 | out += this.renderer.text( 343 | this.options.escape(this.smartypants(execArr[0])), 344 | ); 345 | continue; 346 | } 347 | 348 | if (nextPart) { 349 | throw new Error("Infinite loop on byte: " + nextPart.charCodeAt(0)); 350 | } 351 | } 352 | 353 | return out; 354 | } 355 | 356 | /** 357 | * Compile Link. 358 | */ 359 | protected outputLink(execArr: RegExpExecArray, link: Link) { 360 | if (!this.options.escape) throw ReferenceError; 361 | const href = this.options.escape(link.href); 362 | const title = link.title ? this.options.escape(link.title) : null; 363 | 364 | return execArr[0].charAt(0) !== "!" 365 | ? this.renderer.link(href, title || "", this.output(execArr[1])) 366 | : this.renderer.image(href, title || "", this.options.escape(execArr[1])); 367 | } 368 | 369 | /** 370 | * Smartypants Transformations. 371 | */ 372 | protected smartypants(text: string) { 373 | if (!this.options.smartypants) { 374 | return text; 375 | } 376 | 377 | return ( 378 | text 379 | // em-dashes 380 | .replace(/---/g, "\u2014") 381 | // en-dashes 382 | .replace(/--/g, "\u2013") 383 | // opening singles 384 | .replace(/(^|[-\u2014/(\[{"\s])'/g, "$1\u2018") 385 | // closing singles & apostrophes 386 | .replace(/'/g, "\u2019") 387 | // opening doubles 388 | .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, "$1\u201c") 389 | // closing doubles 390 | .replace(/"/g, "\u201d") 391 | // ellipses 392 | .replace(/\.{3}/g, "\u2026") 393 | ); 394 | } 395 | 396 | /** 397 | * Mangle Links. 398 | */ 399 | protected mangle(text: string) { 400 | if (!this.options.mangle) { 401 | return text; 402 | } 403 | 404 | let out = ""; 405 | const length = text.length; 406 | 407 | for (let i = 0; i < length; i++) { 408 | let str: string = ""; 409 | 410 | if (Math.random() > 0.5) { 411 | str = "x" + text.charCodeAt(i).toString(16); 412 | } 413 | 414 | out += "&#" + str + ";"; 415 | } 416 | 417 | return out; 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /src/block-lexer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * 4 | * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) 5 | * https://github.com/chjj/marked 6 | * 7 | * Copyright (c) 2018, Костя Третяк. (MIT Licensed) 8 | * https://github.com/ts-stack/markdown 9 | */ 10 | 11 | import { ExtendRegexp } from "./extend-regexp.ts"; 12 | import { 13 | Align, 14 | LexerReturns, 15 | Links, 16 | MarkedOptions, 17 | RulesBlockBase, 18 | RulesBlockGfm, 19 | RulesBlockTables, 20 | Token, 21 | TokenType, 22 | Obj 23 | } from "./interfaces.ts"; 24 | import { Marked } from "./marked.ts"; 25 | import { load } from "https://deno.land/std/yaml/_loader/loader.ts"; 26 | 27 | export class BlockLexer { 28 | static simpleRules: RegExp[] = []; 29 | protected static rulesBase: RulesBlockBase; 30 | /** 31 | * GFM Block Grammar. 32 | */ 33 | protected static rulesGfm: RulesBlockGfm; 34 | /** 35 | * GFM + Tables Block Grammar. 36 | */ 37 | protected static rulesTables: RulesBlockTables; 38 | protected rules!: RulesBlockBase | RulesBlockGfm | RulesBlockTables; 39 | protected options: MarkedOptions; 40 | protected links: Links = {}; 41 | protected tokens: Token[] = []; 42 | protected frontmatter: Obj = {}; 43 | protected hasRulesGfm!: boolean; 44 | protected hasRulesTables!: boolean; 45 | 46 | constructor(protected staticThis: typeof BlockLexer, options?: object) { 47 | this.options = options || Marked.options; 48 | this.setRules(); 49 | } 50 | 51 | /** 52 | * Accepts Markdown text and returns object with tokens and links. 53 | * 54 | * @param src String of markdown source to be compiled. 55 | * @param options Hash of options. 56 | */ 57 | static lex( 58 | src: string, 59 | options?: MarkedOptions, 60 | top?: boolean, 61 | isBlockQuote?: boolean, 62 | ): LexerReturns { 63 | const lexer = new this(this, options); 64 | return lexer.getTokens(src, top, isBlockQuote); 65 | } 66 | 67 | protected static getRulesBase(): RulesBlockBase { 68 | if (this.rulesBase) { 69 | return this.rulesBase; 70 | } 71 | 72 | const base: RulesBlockBase = { 73 | newline: /^\n+/, 74 | code: /^( {4}[^\n]+\n*)+/, 75 | hr: /^( *[-*_]){3,} *(?:\n+|$)/, 76 | heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, 77 | lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, 78 | blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/, 79 | list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, 80 | html: 81 | /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/, 82 | def: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, 83 | paragraph: 84 | /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/, 85 | text: /^[^\n]+/, 86 | bullet: /(?:[*+-]|\d+\.)/, 87 | item: /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/, 88 | }; 89 | 90 | base.item = new ExtendRegexp(base.item, "gm").setGroup(/bull/g, base.bullet) 91 | .getRegexp(); 92 | 93 | base.list = new ExtendRegexp(base.list) 94 | .setGroup(/bull/g, base.bullet) 95 | .setGroup("hr", "\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))") 96 | .setGroup("def", "\\n+(?=" + base.def.source + ")") 97 | .getRegexp(); 98 | 99 | const tag = "(?!(?:" + 100 | "a|em|strong|small|s|cite|q|dfn|abbr|data|time|code" + 101 | "|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo" + 102 | "|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b"; 103 | 104 | base.html = new ExtendRegexp(base.html) 105 | .setGroup("comment", //) 106 | .setGroup("closed", /<(tag)[\s\S]+?<\/\1>/) 107 | .setGroup("closing", /])*?>/) 108 | .setGroup(/tag/g, tag) 109 | .getRegexp(); 110 | 111 | base.paragraph = new ExtendRegexp(base.paragraph) 112 | .setGroup("hr", base.hr) 113 | .setGroup("heading", base.heading) 114 | .setGroup("lheading", base.lheading) 115 | .setGroup("blockquote", base.blockquote) 116 | .setGroup("tag", "<" + tag) 117 | .setGroup("def", base.def) 118 | .getRegexp(); 119 | 120 | return (this.rulesBase = base); 121 | } 122 | 123 | protected static getRulesGfm(): RulesBlockGfm { 124 | if (this.rulesGfm) { 125 | return this.rulesGfm; 126 | } 127 | 128 | const base = this.getRulesBase(); 129 | 130 | const gfm: RulesBlockGfm = { 131 | ...base, 132 | ...{ 133 | fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/, 134 | paragraph: /^/, 135 | heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/, 136 | }, 137 | }; 138 | 139 | const group1 = gfm.fences.source.replace("\\1", "\\2"); 140 | const group2 = base.list.source.replace("\\1", "\\3"); 141 | 142 | gfm.paragraph = new ExtendRegexp(base.paragraph).setGroup( 143 | "(?!", 144 | `(?!${group1}|${group2}|`, 145 | ).getRegexp(); 146 | 147 | return (this.rulesGfm = gfm); 148 | } 149 | 150 | protected static getRulesTable(): RulesBlockTables { 151 | if (this.rulesTables) { 152 | return this.rulesTables; 153 | } 154 | 155 | return (this.rulesTables = { 156 | ...this.getRulesGfm(), 157 | ...{ 158 | nptable: 159 | /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, 160 | table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/, 161 | }, 162 | }); 163 | } 164 | 165 | protected setRules() { 166 | if (this.options.gfm) { 167 | if (this.options.tables) { 168 | this.rules = this.staticThis.getRulesTable(); 169 | } else { 170 | this.rules = this.staticThis.getRulesGfm(); 171 | } 172 | } else { 173 | this.rules = this.staticThis.getRulesBase(); 174 | } 175 | 176 | this.hasRulesGfm = (this.rules as RulesBlockGfm).fences !== undefined; 177 | this.hasRulesTables = (this.rules as RulesBlockTables).table !== undefined; 178 | } 179 | 180 | /** 181 | * Lexing. 182 | */ 183 | protected getTokens( 184 | src: string, 185 | top?: boolean, 186 | isBlockQuote?: boolean, 187 | ): LexerReturns { 188 | let nextPart = src; 189 | let execArr, fmArr: RegExpExecArray | null; 190 | 191 | mainLoop: 192 | while (nextPart) { 193 | // newline 194 | if ((execArr = this.rules.newline.exec(nextPart))) { 195 | nextPart = nextPart.substring(execArr[0].length); 196 | 197 | if (execArr[0].length > 1) { 198 | this.tokens.push({ type: TokenType.space }); 199 | } 200 | } 201 | 202 | // code 203 | if ((execArr = this.rules.code.exec(nextPart))) { 204 | nextPart = nextPart.substring(execArr[0].length); 205 | const code = execArr[0].replace(/^ {4}/gm, ""); 206 | 207 | this.tokens.push({ 208 | type: TokenType.code, 209 | text: !this.options.pedantic ? code.replace(/\n+$/, "") : code, 210 | }); 211 | continue; 212 | } 213 | 214 | // fences code (gfm) 215 | if ( 216 | this.hasRulesGfm && 217 | (execArr = (this.rules as RulesBlockGfm).fences.exec(nextPart)) 218 | ) { 219 | nextPart = nextPart.substring(execArr[0].length); 220 | 221 | this.tokens.push({ 222 | type: TokenType.code, 223 | lang: execArr[2], 224 | text: execArr[3] || "", 225 | }); 226 | continue; 227 | } 228 | 229 | // heading 230 | if ((execArr = this.rules.heading.exec(nextPart))) { 231 | nextPart = nextPart.substring(execArr[0].length); 232 | this.tokens.push({ 233 | type: TokenType.heading, 234 | depth: execArr[1].length, 235 | text: execArr[2], 236 | }); 237 | continue; 238 | } 239 | 240 | // table no leading pipe (gfm) 241 | if ( 242 | top && this.hasRulesTables && 243 | (execArr = (this.rules as RulesBlockTables).nptable.exec(nextPart)) 244 | ) { 245 | nextPart = nextPart.substring(execArr[0].length); 246 | 247 | const item: Token = { 248 | type: TokenType.table, 249 | header: execArr[1].replace(/^ *| *\| *$/g, "").split(/ *\| */), 250 | align: execArr[2].replace(/^ *|\| *$/g, "").split( 251 | / *\| */, 252 | ) as Align[], 253 | cells: [], 254 | }; 255 | 256 | if (!item.align) throw ReferenceError; 257 | 258 | for (let i = 0; i < item.align.length; i++) { 259 | if (/^ *-+: *$/.test(item.align[i])) { 260 | item.align[i] = "right"; 261 | } else if (/^ *:-+: *$/.test(item.align[i])) { 262 | item.align[i] = "center"; 263 | } else if (/^ *:-+ *$/.test(item.align[i])) { 264 | item.align[i] = "left"; 265 | } else { 266 | item.align[i] = ""; 267 | } 268 | } 269 | 270 | const td: string[] = execArr[3].replace(/\n$/, "").split("\n"); 271 | 272 | if (!item.cells) throw ReferenceError; 273 | 274 | for (let i = 0; i < td.length; i++) { 275 | item.cells[i] = td[i].split(/ *\| */); 276 | } 277 | 278 | this.tokens.push(item); 279 | continue; 280 | } 281 | 282 | // lheading 283 | if ((execArr = this.rules.lheading.exec(nextPart))) { 284 | nextPart = nextPart.substring(execArr[0].length); 285 | 286 | this.tokens.push({ 287 | type: TokenType.heading, 288 | depth: execArr[2] === "=" ? 1 : 2, 289 | text: execArr[1], 290 | }); 291 | continue; 292 | } 293 | 294 | // hr 295 | if ((execArr = this.rules.hr.exec(nextPart))) { 296 | 297 | // Checks if the previous string contains a content. 298 | if ((this.tokens.length == 0) || (this.tokens.every(object => object.type == TokenType.space))) { 299 | 300 | // Grabs front-matter data and parse it into Javascript object. 301 | if (fmArr = /^(?:\-\-\-)(.*?)(?:\-\-\-|\.\.\.)/s.exec(nextPart)) { 302 | nextPart = nextPart.substring(fmArr[0].length); 303 | this.frontmatter = load(fmArr[1]); 304 | } 305 | continue; 306 | 307 | } else { 308 | nextPart = nextPart.substring(execArr[0].length); 309 | this.tokens.push({ type: TokenType.hr }); 310 | continue; 311 | } 312 | } 313 | 314 | // blockquote 315 | if ((execArr = this.rules.blockquote.exec(nextPart))) { 316 | nextPart = nextPart.substring(execArr[0].length); 317 | this.tokens.push({ type: TokenType.blockquoteStart }); 318 | const str = execArr[0].replace(/^ *> ?/gm, ""); 319 | 320 | // Pass `top` to keep the current 321 | // "toplevel" state. This is exactly 322 | // how markdown.pl works. 323 | this.getTokens(str); 324 | this.tokens.push({ type: TokenType.blockquoteEnd }); 325 | continue; 326 | } 327 | 328 | // list 329 | if ((execArr = this.rules.list.exec(nextPart))) { 330 | nextPart = nextPart.substring(execArr[0].length); 331 | const bull: string = execArr[2]; 332 | 333 | this.tokens.push( 334 | { type: TokenType.listStart, ordered: bull.length > 1 }, 335 | ); 336 | 337 | // Get each top-level item. 338 | const str = execArr[0].match(this.rules.item) || ""; 339 | const length = str.length; 340 | 341 | let next = false; 342 | let space: number; 343 | let blockBullet: string; 344 | let loose: boolean; 345 | 346 | for (let i = 0; i < length; i++) { 347 | let item = str[i]; 348 | 349 | // Remove the list item's bullet so it is seen as the next token. 350 | space = item.length; 351 | item = item.replace(/^ *([*+-]|\d+\.) +/, ""); 352 | 353 | // Outdent whatever the list item contains. Hacky. 354 | if (item.indexOf("\n ") !== -1) { 355 | space -= item.length; 356 | item = !this.options.pedantic 357 | ? item.replace(new RegExp("^ {1," + space + "}", "gm"), "") 358 | : item.replace(/^ {1,4}/gm, ""); 359 | } 360 | 361 | // Determine whether the next list item belongs here. 362 | // Backpedal if it does not belong in this list. 363 | if (this.options.smartLists && i !== length - 1) { 364 | const bb = this.staticThis.getRulesBase().bullet.exec(str[i + 1]); 365 | blockBullet = bb ? bb[0] : ""; 366 | 367 | if ( 368 | bull !== blockBullet && 369 | !(bull.length > 1 && blockBullet.length > 1) 370 | ) { 371 | nextPart = (str.slice(i + 1) as string[]).join("\n") + nextPart; 372 | i = length - 1; 373 | } 374 | } 375 | 376 | // Determine whether item is loose or not. 377 | // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ 378 | // for discount behavior. 379 | loose = next || /\n\n(?!\s*$)/.test(item); 380 | 381 | if (i !== length - 1) { 382 | next = item.charAt(item.length - 1) === "\n"; 383 | 384 | if (!loose) { 385 | loose = next; 386 | } 387 | } 388 | 389 | this.tokens.push( 390 | { 391 | type: loose ? TokenType.looseItemStart : TokenType.listItemStart, 392 | }, 393 | ); 394 | 395 | // Recurse. 396 | this.getTokens(item, false, isBlockQuote); 397 | this.tokens.push({ type: TokenType.listItemEnd }); 398 | } 399 | 400 | this.tokens.push({ type: TokenType.listEnd }); 401 | continue; 402 | } 403 | 404 | // html 405 | if ((execArr = this.rules.html.exec(nextPart))) { 406 | nextPart = nextPart.substring(execArr[0].length); 407 | const attr = execArr[1]; 408 | const isPre = attr === "pre" || attr === "script" || attr === "style"; 409 | 410 | this.tokens.push({ 411 | type: this.options.sanitize ? TokenType.paragraph : TokenType.html, 412 | pre: !this.options.sanitizer && isPre, 413 | text: execArr[0], 414 | }); 415 | continue; 416 | } 417 | 418 | // def 419 | if (top && (execArr = this.rules.def.exec(nextPart))) { 420 | nextPart = nextPart.substring(execArr[0].length); 421 | 422 | this.links[execArr[1].toLowerCase()] = { 423 | href: execArr[2], 424 | title: execArr[3], 425 | }; 426 | continue; 427 | } 428 | 429 | // table (gfm) 430 | if ( 431 | top && this.hasRulesTables && 432 | (execArr = (this.rules as RulesBlockTables).table.exec(nextPart)) 433 | ) { 434 | nextPart = nextPart.substring(execArr[0].length); 435 | 436 | const item: Token = { 437 | type: TokenType.table, 438 | header: execArr[1].replace(/^ *| *\| *$/g, "").split(/ *\| */), 439 | align: execArr[2].replace(/^ *|\| *$/g, "").split( 440 | / *\| */, 441 | ) as Align[], 442 | cells: [], 443 | }; 444 | 445 | if (!item.align) throw ReferenceError; 446 | 447 | for (let i = 0; i < item.align.length; i++) { 448 | if (/^ *-+: *$/.test(item.align[i])) { 449 | item.align[i] = "right"; 450 | } else if (/^ *:-+: *$/.test(item.align[i])) { 451 | item.align[i] = "center"; 452 | } else if (/^ *:-+ *$/.test(item.align[i])) { 453 | item.align[i] = "left"; 454 | } else { 455 | item.align[i] = ""; 456 | } 457 | } 458 | 459 | const td = execArr[3].replace(/(?: *\| *)?\n$/, "").split("\n"); 460 | 461 | if (!item.cells) throw ReferenceError; 462 | 463 | for (let i = 0; i < td.length; i++) { 464 | item.cells[i] = td[i].replace(/^ *\| *| *\| *$/g, "").split(/ *\| */); 465 | } 466 | 467 | this.tokens.push(item); 468 | continue; 469 | } 470 | 471 | // simple rules 472 | if (this.staticThis.simpleRules.length) { 473 | const simpleRules = this.staticThis.simpleRules; 474 | for (let i = 0; i < simpleRules.length; i++) { 475 | if ((execArr = simpleRules[i].exec(nextPart))) { 476 | nextPart = nextPart.substring(execArr[0].length); 477 | const type = "simpleRule" + (i + 1); 478 | this.tokens.push({ type, execArr }); 479 | continue mainLoop; 480 | } 481 | } 482 | } 483 | 484 | // top-level paragraph 485 | if (top && (execArr = this.rules.paragraph.exec(nextPart))) { 486 | nextPart = nextPart.substring(execArr[0].length); 487 | 488 | if (execArr[1].slice(-1) === "\n") { 489 | this.tokens.push({ 490 | type: TokenType.paragraph, 491 | text: execArr[1].slice(0, -1), 492 | }); 493 | } else { 494 | this.tokens.push({ 495 | type: this.tokens.length > 0 ? TokenType.paragraph : TokenType.text, 496 | text: execArr[1], 497 | }); 498 | } 499 | continue; 500 | } 501 | 502 | // text 503 | // Top-level should never reach here. 504 | if ((execArr = this.rules.text.exec(nextPart))) { 505 | nextPart = nextPart.substring(execArr[0].length); 506 | this.tokens.push({ type: TokenType.text, text: execArr[0] }); 507 | continue; 508 | } 509 | 510 | if (nextPart) { 511 | throw new Error( 512 | "Infinite loop on byte: " + nextPart.charCodeAt(0) + 513 | `, near text '${nextPart.slice(0, 30)}...'`, 514 | ); 515 | } 516 | } 517 | 518 | return { tokens: this.tokens, links: this.links, meta: this.frontmatter }; 519 | } 520 | } 521 | --------------------------------------------------------------------------------