├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml └── contributing.md ├── .gitignore ├── .nojekyll ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── benchmark ├── fixtures │ ├── block-bq-flat.md │ ├── block-bq-nested.md │ ├── block-code.md │ ├── block-fences.md │ ├── block-heading.md │ ├── block-hr.md │ ├── block-html.md │ ├── block-lheading.md │ ├── block-list-flat.md │ ├── block-list-nested.md │ ├── block-ref-flat.md │ ├── block-ref-nested.md │ ├── block-tables-large.md │ ├── block-tables.md │ ├── inline-autolink.md │ ├── inline-backticks.md │ ├── inline-em-flat.md │ ├── inline-em-nested.md │ ├── inline-em-worst.md │ ├── inline-entity.md │ ├── inline-escape.md │ ├── inline-html.md │ ├── inline-links-flat.md │ ├── inline-links-nested.md │ ├── inline-newlines-large.md │ ├── inline-newlines.md │ ├── lorem1.txt │ ├── rawtabs.md │ └── spec.txt ├── implementations │ ├── commonmark-reference │ │ └── index.js │ ├── current-commonmark │ │ └── index.js │ ├── current │ │ └── index.js │ └── marked-0.3.2 │ │ └── index.js ├── index.js └── profile.js ├── bin └── remarkable.js ├── demo ├── .eslintrc ├── assets │ ├── index.css │ ├── index.js │ └── index.styl ├── example.js ├── example.md ├── index.html └── index.jade ├── docs ├── parser.md ├── parsing_block.md ├── parsing_core.md ├── parsing_inline.md ├── plugins.md └── renderer.md ├── lib ├── cli.js ├── common │ ├── entities.browser.js │ ├── entities.js │ ├── html_blocks.js │ ├── html_re.js │ ├── url_schemas.js │ └── utils.js ├── configs │ ├── commonmark.js │ ├── default.js │ └── full.js ├── helpers │ ├── normalize_link.js │ ├── normalize_reference.js │ ├── parse_link_destination.js │ ├── parse_link_label.js │ └── parse_link_title.js ├── index.js ├── linkify.js ├── parser_block.js ├── parser_core.js ├── parser_inline.js ├── renderer.js ├── ruler.js ├── rules.js ├── rules_block │ ├── blockquote.js │ ├── code.js │ ├── deflist.js │ ├── fences.js │ ├── footnote.js │ ├── heading.js │ ├── hr.js │ ├── htmlblock.js │ ├── lheading.js │ ├── list.js │ ├── paragraph.js │ ├── state_block.js │ └── table.js ├── rules_core │ ├── abbr.js │ ├── abbr2.js │ ├── block.js │ ├── footnote_tail.js │ ├── inline.js │ ├── references.js │ ├── replacements.js │ └── smartquotes.js ├── rules_inline │ ├── autolink.js │ ├── backticks.js │ ├── del.js │ ├── emphasis.js │ ├── entity.js │ ├── escape.js │ ├── footnote_inline.js │ ├── footnote_ref.js │ ├── htmltag.js │ ├── ins.js │ ├── links.js │ ├── mark.js │ ├── newline.js │ ├── state_inline.js │ ├── sub.js │ ├── sup.js │ └── text.js └── umd.js ├── linkify └── package.json ├── package.json ├── rollup.config.js ├── support ├── demodata.js ├── entities.js └── specsplit.js ├── test ├── cli.js ├── commonmark.js ├── fixtures │ ├── cli-input.md │ ├── commonmark │ │ ├── bad.txt │ │ ├── good.txt │ │ └── spec.txt │ ├── linkify.txt │ └── remarkable │ │ ├── abbr.txt │ │ ├── commonmark_extras.txt │ │ ├── deflist.txt │ │ ├── del.txt │ │ ├── footnotes.txt │ │ ├── ins.txt │ │ ├── mark.txt │ │ ├── proto.txt │ │ ├── redos.txt │ │ ├── smartquotes.txt │ │ ├── sub.txt │ │ ├── sup.txt │ │ ├── tables.txt │ │ ├── typographer.txt │ │ └── xss.txt ├── linkify.js ├── misc.js ├── remarkable.js ├── ruler.js ├── test-browser.js └── utils.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{**/{actual,fixtures,expected,templates}/**,*.md}] 12 | trim_trailing_whitespace = false 13 | insert_final_newline = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | benchmark/implementations/ 2 | coverage/ 3 | demo/sample.js 4 | dist/ 5 | node_modules/ 6 | demo/ 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module", 4 | "ecmaVersion": "2015" 5 | }, 6 | "extends": ["eslint:recommended"], 7 | "rules": { 8 | "no-extra-semi": "off", 9 | "no-unexpected-multiline": "off", 10 | "no-control-regex": "off", 11 | "no-empty": "off", 12 | "no-useless-escape": "off" 13 | }, 14 | "overrides": [ 15 | { 16 | "files": "lib/**/*.js", 17 | "env": { 18 | "browser": true 19 | }, 20 | "extends": ["plugin:es5/no-es2015", "plugin:es5/no-es2016"], 21 | "rules": { 22 | "es5/no-modules": "off" 23 | } 24 | }, 25 | { 26 | "files": ["benchmark/**/*.js", "support/**/*.js", "bin/**/*.js", "lib/cli.js"], 27 | "env": { 28 | "browser": false, 29 | "node": true 30 | } 31 | }, 32 | { 33 | "files": "test/**/*.js", 34 | "env": { 35 | "node": true, 36 | "mocha": true 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [jonschlinkert] 2 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | > Before submitting a pull request to Remarkable: 4 | 5 | 1. Run `npm test` to be sure that all tests are passing. 6 | 2. Run `node benchmark` to ensure that performance has not degraded. 7 | 3. DO NOT include any auto-generated browser or demo files in your commit. 8 | 4. Commit to the [dev](https://github.com/jonschlinkert/remarkable/tree/dev) branch, never master. 9 | 10 | Thanks! 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # always ignore files 2 | *.DS_Store 3 | *.sublime-* 4 | 5 | # test related, or directories generated by tests 6 | test/actual 7 | actual 8 | coverage 9 | .nyc_output 10 | 11 | # npm 12 | node_modules 13 | npm-debug.log 14 | 15 | # misc 16 | _gh_pages 17 | vendor 18 | temp 19 | tmp 20 | TODO.md 21 | dist 22 | /index.html 23 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonschlinkert/remarkable/7c5e433620c967618eb38cdb57360734274061c4/.nojekyll -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present, Jon Schlinkert 4 | Copyright (c) 2014 Jon Schlinkert, Vitaly Puzrin. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PATH := ./node_modules/.bin:${PATH} 2 | NPM_VERSION := $(shell node -e 'process.stdout.write(require("./package.json").version)') 3 | GITHUB_PROJ := $(shell node -e 'process.stdout.write(require("./package.json").repository)') 4 | 5 | 6 | demo: 7 | ./support/demodata.js > demo/example.json 8 | jade demo/index.jade -P --obj demo/example.json 9 | stylus -u autoprefixer-stylus demo/assets/index.styl 10 | rm -rf demo/example.json 11 | 12 | gh-pages: 13 | if [ "git branch --list gh-pages" ]; then \ 14 | git branch -D gh-pages ; \ 15 | fi 16 | git branch gh-pages 17 | git push origin gh-pages -f 18 | 19 | publish: 20 | @if test 0 -ne `git status --porcelain | wc -l` ; then \ 21 | echo "Unclean working tree. Commit or stash changes first." >&2 ; \ 22 | exit 128 ; \ 23 | fi 24 | @if test 0 -ne `git fetch ; git status | grep '^# Your branch' | wc -l` ; then \ 25 | echo "Local/Remote history differs. Please push/pull changes." >&2 ; \ 26 | exit 128 ; \ 27 | fi 28 | @if test 0 -ne `git tag -l ${NPM_VERSION} | wc -l` ; then \ 29 | echo "Tag ${NPM_VERSION} exists. Update package.json" >&2 ; \ 30 | exit 128 ; \ 31 | fi 32 | git tag ${NPM_VERSION} && git push origin ${NPM_VERSION} 33 | npm publish ${GITHUB_PROJ}/tarball/${NPM_VERSION} 34 | 35 | todo: 36 | grep 'TODO' -n -r ./lib 2>/dev/null || test true 37 | 38 | 39 | .PHONY: publish lint test gh-pages todo demo coverage 40 | .SILENT: help lint test todo 41 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-bq-flat.md: -------------------------------------------------------------------------------- 1 | > the simple example of a blockquote 2 | > the simple example of a blockquote 3 | > the simple example of a blockquote 4 | > the simple example of a blockquote 5 | ... continuation 6 | ... continuation 7 | ... continuation 8 | ... continuation 9 | 10 | empty blockquote: 11 | 12 | > 13 | > 14 | > 15 | > 16 | 17 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-bq-nested.md: -------------------------------------------------------------------------------- 1 | >>>>>> deeply nested blockquote 2 | >>>>> deeply nested blockquote 3 | >>>> deeply nested blockquote 4 | >>> deeply nested blockquote 5 | >> deeply nested blockquote 6 | > deeply nested blockquote 7 | 8 | > deeply nested blockquote 9 | >> deeply nested blockquote 10 | >>> deeply nested blockquote 11 | >>>> deeply nested blockquote 12 | >>>>> deeply nested blockquote 13 | >>>>>> deeply nested blockquote 14 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-code.md: -------------------------------------------------------------------------------- 1 | 2 | an 3 | example 4 | 5 | of 6 | 7 | 8 | 9 | a code 10 | block 11 | 12 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-fences.md: -------------------------------------------------------------------------------- 1 | 2 | ``````````text 3 | an 4 | example 5 | ``` 6 | of 7 | 8 | 9 | a fenced 10 | ``` 11 | code 12 | block 13 | `````````` 14 | 15 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-heading.md: -------------------------------------------------------------------------------- 1 | # heading 2 | ### heading 3 | ##### heading 4 | 5 | # heading # 6 | ### heading ### 7 | ##### heading \#\#\#\#\###### 8 | 9 | ############ not a heading 10 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-hr.md: -------------------------------------------------------------------------------- 1 | 2 | * * * * * 3 | 4 | - - - - - 5 | 6 | ________ 7 | 8 | 9 | ************************* text 10 | 11 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-html.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | blah blah 4 | 5 |
6 | 7 | 8 | 9 | 12 | 13 |
10 | **test** 11 |
14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 |
20 | 21 | test 22 | 23 |
28 | 29 | 32 | 33 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-lheading.md: -------------------------------------------------------------------------------- 1 | heading 2 | --- 3 | 4 | heading 5 | =================================== 6 | 7 | not a heading 8 | ----------------------------------- text 9 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-list-flat.md: -------------------------------------------------------------------------------- 1 | - tidy 2 | - bullet 3 | - list 4 | 5 | 6 | - loose 7 | 8 | - bullet 9 | 10 | - list 11 | 12 | 13 | 0. ordered 14 | 1. list 15 | 2. example 16 | 17 | 18 | - 19 | - 20 | - 21 | - 22 | 23 | 24 | 1. 25 | 2. 26 | 3. 27 | 28 | 29 | - an example 30 | of a list item 31 | with a continuation 32 | 33 | this part is inside the list 34 | 35 | this part is just a paragraph 36 | 37 | 38 | 1. test 39 | - test 40 | 1. test 41 | - test 42 | 43 | 44 | 111111111111111111111111111111111111111111. is this a valid bullet? 45 | 46 | - _________________________ 47 | 48 | - this 49 | - is 50 | 51 | a 52 | 53 | long 54 | - loose 55 | - list 56 | 57 | - with 58 | - some 59 | 60 | tidy 61 | 62 | - list 63 | - items 64 | - in 65 | 66 | - between 67 | - _________________________ 68 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-list-nested.md: -------------------------------------------------------------------------------- 1 | 2 | - this 3 | - is 4 | - a 5 | - deeply 6 | - nested 7 | - bullet 8 | - list 9 | 10 | 11 | 1. this 12 | 2. is 13 | 3. a 14 | 4. deeply 15 | 5. nested 16 | 6. unordered 17 | 7. list 18 | 19 | 20 | - 1 21 | - 2 22 | - 3 23 | - 4 24 | - 5 25 | - 6 26 | - 7 27 | - 6 28 | - 5 29 | - 4 30 | - 3 31 | - 2 32 | - 1 33 | 34 | 35 | - - - - - - - - - deeply-nested one-element item 36 | 37 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-ref-flat.md: -------------------------------------------------------------------------------- 1 | [1] [2] [3] [1] [2] [3] 2 | 3 | [looooooooooooooooooooooooooooooooooooooooooooooooooong label] 4 | 5 | [1]: 6 | [2]: http://something.example.com/foo/bar 'test' 7 | [3]: 8 | http://foo/bar 9 | [ looooooooooooooooooooooooooooooooooooooooooooooooooong label ]: 10 | 111 11 | 'test' 12 | [[[[[[[[[[[[[[[[[[[[ this should not slow down anything ]]]]]]]]]]]]]]]]]]]]: q 13 | (as long as it is not referenced anywhere) 14 | 15 | [[[[[[[[[[[[[[[[[[[[]: this is not a valid reference 16 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-ref-nested.md: -------------------------------------------------------------------------------- 1 | [[[[[[[foo]]]]]]] 2 | 3 | [[[[[[[foo]]]]]]]: bar 4 | [[[[[[foo]]]]]]: bar 5 | [[[[[foo]]]]]: bar 6 | [[[[foo]]]]: bar 7 | [[[foo]]]: bar 8 | [[foo]]: bar 9 | [foo]: bar 10 | 11 | [*[*[*[*[foo]*]*]*]*] 12 | 13 | [*[*[*[*[foo]*]*]*]*]: bar 14 | [*[*[*[foo]*]*]*]: bar 15 | [*[*[foo]*]*]: bar 16 | [*[foo]*]: bar 17 | [foo]: bar 18 | -------------------------------------------------------------------------------- /benchmark/fixtures/block-tables.md: -------------------------------------------------------------------------------- 1 | | Heading 1 | Heading 2 2 | | --------- | --------- 3 | | Cell 1 | Cell 2 4 | | Cell 3 | Cell 4 5 | 6 | | Header 1 | Header 2 | Header 3 | Header 4 | 7 | | :------: | -------: | :------- | -------- | 8 | | Cell 1 | Cell 2 | Cell 3 | Cell 4 | 9 | | Cell 5 | Cell 6 | Cell 7 | Cell 8 | 10 | 11 | Test code 12 | 13 | Header 1 | Header 2 14 | -------- | -------- 15 | Cell 1 | Cell 2 16 | Cell 3 | Cell 4 17 | 18 | Header 1|Header 2|Header 3|Header 4 19 | :-------|:------:|-------:|-------- 20 | Cell 1 |Cell 2 |Cell 3 |Cell 4 21 | *Cell 5*|Cell 6 |Cell 7 |Cell 8 22 | -------------------------------------------------------------------------------- /benchmark/fixtures/inline-autolink.md: -------------------------------------------------------------------------------- 1 | closed (valid) autolinks: 2 | 3 | 4 | 5 | 6 | 7 | 8 | these are not autolinks: 9 | 10 | 15 | -------------------------------------------------------------------------------- /benchmark/fixtures/inline-backticks.md: -------------------------------------------------------------------------------- 1 | `lots`of`backticks` 2 | 3 | ``i``wonder``how``this``will``be``parsed`` 4 | -------------------------------------------------------------------------------- /benchmark/fixtures/inline-em-flat.md: -------------------------------------------------------------------------------- 1 | *this* *is* *your* *basic* *boring* *emphasis* 2 | 3 | _this_ _is_ _your_ _basic_ _boring_ _emphasis_ 4 | 5 | **this** **is** **your** **basic** **boring** **emphasis** 6 | -------------------------------------------------------------------------------- /benchmark/fixtures/inline-em-nested.md: -------------------------------------------------------------------------------- 1 | *this *is *a *bunch* of* nested* emphases* 2 | 3 | __this __is __a __bunch__ of__ nested__ emphases__ 4 | 5 | ***this ***is ***a ***bunch*** of*** nested*** emphases*** 6 | -------------------------------------------------------------------------------- /benchmark/fixtures/inline-em-worst.md: -------------------------------------------------------------------------------- 1 | *this *is *a *worst *case *for *em *backtracking 2 | 3 | __this __is __a __worst __case __for __em __backtracking 4 | 5 | ***this ***is ***a ***worst ***case ***for ***em ***backtracking 6 | -------------------------------------------------------------------------------- /benchmark/fixtures/inline-entity.md: -------------------------------------------------------------------------------- 1 | entities: 2 | 3 |   & © Æ Ď ¾ ℋ ⅆ ∲ 4 | 5 | # Ӓ Ϡ � 6 | 7 | non-entities: 8 | 9 | &18900987654321234567890; &1234567890098765432123456789009876543212345678987654; 10 | 11 | &qwertyuioppoiuytrewqwer; &oiuytrewqwertyuioiuytrewqwertyuioytrewqwertyuiiuytri; 12 | -------------------------------------------------------------------------------- /benchmark/fixtures/inline-escape.md: -------------------------------------------------------------------------------- 1 | 2 | \t\e\s\t\i\n\g \e\s\c\a\p\e \s\e\q\u\e\n\c\e\s 3 | 4 | \!\\\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\? 5 | 6 | \@ \[ \] \^ \_ \` \{ \| \} \~ \- \' 7 | 8 | \ 9 | \\ 10 | \\\ 11 | \\\\ 12 | \\\\\ 13 | 14 | \ \ \ \ 15 | 16 | -------------------------------------------------------------------------------- /benchmark/fixtures/inline-html.md: -------------------------------------------------------------------------------- 1 | Taking commonmark tests from the spec for benchmarking here: 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 12 | 13 | <33> <__> 14 | 15 | 16 | 17 | 28 | 29 | foo 31 | 32 | foo 33 | 34 | foo 35 | 36 | foo 37 | 38 | foo &<]]> 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /benchmark/fixtures/inline-links-flat.md: -------------------------------------------------------------------------------- 1 | Valid links: 2 | 3 | [this is a link]() 4 | [this is a link]() 5 | [this is a link](http://something.example.com/foo/bar 'test') 6 | ![this is an image]() 7 | ![this is an image]() 8 | ![this is an image](http://something.example.com/foo/bar 'test') 9 | 10 | [escape test](<\>\>\>\>\>\>\>\>\>\>\>\>\>\>> '\'\'\'\'\'\'\'\'\'\'\'\'\'\'') 11 | [escape test \]\]\]\]\]\]\]\]\]\]\]\]\]\]\]\]](\)\)\)\)\)\)\)\)\)\)\)\)\)\)) 12 | 13 | Invalid links: 14 | 15 | [this is not a link 16 | 17 | [this is not a link]( 18 | 19 | [this is not a link](http://something.example.com/foo/bar 'test' 20 | 21 | [this is not a link]((((((((((((((((((((((((((((((((((((((((((((((( 22 | 23 | [this is not a link]((((((((((()))))))))) (((((((((())))))))))) 24 | -------------------------------------------------------------------------------- /benchmark/fixtures/inline-links-nested.md: -------------------------------------------------------------------------------- 1 | Valid links: 2 | 3 | [[[[[[[[](test)](test)](test)](test)](test)](test)](test)] 4 | 5 | [ [[[[[[[[[[[[[[[[[[ [](test) ]]]]]]]]]]]]]]]]]] ](test) 6 | 7 | Invalid links: 8 | 9 | [[[[[[[[[ 10 | 11 | [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ 12 | 13 | ![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![ 14 | -------------------------------------------------------------------------------- /benchmark/fixtures/inline-newlines.md: -------------------------------------------------------------------------------- 1 | 2 | this\ 3 | should\ 4 | be\ 5 | separated\ 6 | by\ 7 | newlines 8 | 9 | this 10 | should 11 | be 12 | separated 13 | by 14 | newlines 15 | too 16 | 17 | this 18 | should 19 | not 20 | be 21 | separated 22 | by 23 | newlines 24 | 25 | -------------------------------------------------------------------------------- /benchmark/fixtures/lorem1.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, __consectetur__ adipiscing elit. Cras imperdiet nec erat ac condimentum. Nulla vel rutrum ligula. Sed hendrerit interdum orci a posuere. Vivamus ut velit aliquet, mollis purus eget, iaculis nisl. Proin posuere malesuada ante. Proin auctor orci eros, ac molestie lorem dictum nec. Vestibulum sit amet erat est. Morbi luctus sed elit ac luctus. Proin blandit, enim vitae egestas posuere, neque elit ultricies dui, vel mattis nibh enim ac lorem. Maecenas molestie nisl sit amet velit dictum lobortis. Aliquam erat volutpat. 2 | 3 | Vivamus sagittis, diam in [vehicula](https://github.com/jonschlinkert/remarked) lobortis, sapien arcu mattis erat, vel aliquet sem urna et risus. Ut feugiat sapien vitae mi elementum laoreet. Suspendisse potenti. Aliquam erat nisl, aliquam pretium libero aliquet, sagittis eleifend nunc. In hac habitasse platea dictumst. Integer turpis augue, tincidunt dignissim mauris id, rhoncus dapibus purus. Maecenas et enim odio. Nullam massa metus, varius quis vehicula sed, pharetra mollis erat. In quis viverra velit. Vivamus placerat, est nec hendrerit varius, enim dui hendrerit magna, ut pulvinar nibh lorem vel lacus. Mauris a orci iaculis, hendrerit eros sed, gravida leo. In dictum mauris vel augue varius, ac ullamcorper nisl ornare. In eu posuere velit, ac fermentum arcu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nullam sed malesuada leo, at interdum elit. 4 | 5 | Nullam ut tincidunt nunc. [Pellentesque][1] metus lacus, commodo eget justo ut, rutrum varius nunc. Sed non rhoncus risus. Morbi sodales gravida pulvinar. Duis malesuada, odio volutpat elementum vulputate, massa magna scelerisque ante, et accumsan tellus nunc in sem. Donec mattis arcu et velit aliquet, non sagittis justo vestibulum. Suspendisse volutpat felis lectus, nec consequat ipsum mattis id. Donec dapibus vehicula facilisis. In tincidunt mi nisi, nec faucibus tortor euismod nec. Suspendisse ante ligula, aliquet vitae libero eu, vulputate dapibus libero. Sed bibendum, sapien at posuere interdum, libero est sollicitudin magna, ac gravida tellus purus eu ipsum. Proin ut quam arcu. 6 | 7 | Suspendisse potenti. Donec ante velit, ornare at augue quis, tristique laoreet sem. Etiam in ipsum elit. Nullam cursus dolor sit amet nulla feugiat tristique. Phasellus ac tellus tincidunt, imperdiet purus eget, ullamcorper ipsum. Cras eu tincidunt sem. Nullam sed dapibus magna. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In id venenatis tortor. In consectetur sollicitudin pharetra. Etiam convallis nisi nunc, et aliquam turpis viverra sit amet. Maecenas faucibus sodales tortor. Suspendisse lobortis mi eu leo viverra volutpat. Pellentesque velit ante, vehicula sodales congue ut, elementum a urna. Cras tempor, ipsum eget luctus rhoncus, arcu ligula fermentum urna, vulputate pharetra enim enim non libero. 8 | 9 | Proin diam quam, elementum in eleifend id, elementum et metus. Cras in justo consequat justo semper ultrices. Sed dignissim lectus a ante mollis, nec vulputate ante molestie. Proin in porta nunc. Etiam pulvinar turpis sed velit porttitor, vel adipiscing velit fringilla. Cras ac tellus vitae purus pharetra tincidunt. Sed cursus aliquet aliquet. Cras eleifend commodo malesuada. In turpis turpis, ullamcorper ut tincidunt a, ullamcorper a nunc. Etiam luctus tellus ac dapibus gravida. Ut nec lacus laoreet neque ullamcorper volutpat. 10 | 11 | Nunc et leo erat. Aenean mattis ultrices lorem, eget adipiscing dolor ultricies eu. In hac habitasse platea dictumst. Vivamus cursus feugiat sapien quis aliquam. Mauris quam libero, porta vel volutpat ut, blandit a purus. Vivamus vestibulum dui vel tortor molestie, sit amet feugiat sem commodo. Nulla facilisi. Sed molestie arcu eget tellus vestibulum tristique. 12 | 13 | [1]: https://github.com/jonschlinkert 14 | -------------------------------------------------------------------------------- /benchmark/fixtures/rawtabs.md: -------------------------------------------------------------------------------- 1 | 2 | this is a test for tab expansion, be careful not to replace them with spaces 3 | 4 | 1 4444 5 | 22 333 6 | 333 22 7 | 4444 1 8 | 9 | 10 | tab-indented line 11 | space-indented line 12 | tab-indented line 13 | 14 | 15 | a lot of spaces in between here 16 | 17 | a lot of tabs in between here 18 | 19 | -------------------------------------------------------------------------------- /benchmark/implementations/commonmark-reference/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var commonmark = require('commonmark'); 4 | var parser = new commonmark.DocParser(); 5 | var renderer = new commonmark.HtmlRenderer(); 6 | 7 | exports.run = function(data) { 8 | return renderer.render(parser.parse(data)); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /benchmark/implementations/current-commonmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Remarkable = require('../../../'); 4 | var md = new Remarkable('commonmark'); 5 | 6 | exports.run = function(data) { 7 | return md.render(data); 8 | } 9 | -------------------------------------------------------------------------------- /benchmark/implementations/current/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Remarkable = require('../../../'); 4 | var md = new Remarkable({ 5 | html: true, 6 | typographer: true 7 | }); 8 | 9 | exports.run = function(data) { 10 | return md.render(data); 11 | } 12 | -------------------------------------------------------------------------------- /benchmark/implementations/marked-0.3.2/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var marked = new require('marked'); 4 | 5 | exports.run = function(data) { 6 | return marked(data); 7 | } 8 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /*eslint no-console:0*/ 3 | 4 | 'use strict'; 5 | 6 | var path = require('path'); 7 | var fs = require('fs'); 8 | var util = require('util'); 9 | var Benchmark = require('benchmark'); 10 | var ansi = require('ansi'); 11 | var cursor = ansi(process.stdout); 12 | 13 | var IMPLS_DIRECTORY = path.join(__dirname, 'implementations'); 14 | var IMPLS_PATHS = {}; 15 | var IMPLS = []; 16 | 17 | 18 | fs.readdirSync(IMPLS_DIRECTORY).sort().forEach(function (name) { 19 | var file = path.join(IMPLS_DIRECTORY, name); 20 | var code = require(file); 21 | 22 | IMPLS_PATHS[name] = file; 23 | IMPLS.push({ 24 | name: name, 25 | code: code 26 | }); 27 | }); 28 | 29 | 30 | var FIXTURES_DIRECTORY = path.join(__dirname, 'fixtures'); 31 | var FIXTURES = []; 32 | 33 | fs.readdirSync(FIXTURES_DIRECTORY).sort().forEach(function (sample) { 34 | var filepath = path.join(FIXTURES_DIRECTORY, sample); 35 | var extname = path.extname(filepath); 36 | var basename = path.basename(filepath, extname); 37 | var content = {}; 38 | 39 | content.string = fs.readFileSync(filepath, 'utf8'); 40 | var title = util.format('(%d bytes)', content.string.length); 41 | 42 | function onComplete() { 43 | cursor.write('\n'); 44 | } 45 | 46 | var suite = new Benchmark.Suite(title, { 47 | onStart: function onStart() { 48 | console.log('\nSample: %s %s', sample, title); 49 | }, 50 | onComplete: onComplete 51 | }); 52 | 53 | 54 | IMPLS.forEach(function (impl) { 55 | suite.add(impl.name, { 56 | onCycle: function onCycle(event) { 57 | cursor.horizontalAbsolute(); 58 | cursor.eraseLine(); 59 | cursor.write(' > ' + event.target); 60 | }, 61 | onComplete: onComplete, 62 | fn: function () { 63 | impl.code.run(content.string); 64 | return; 65 | } 66 | }); 67 | }); 68 | 69 | 70 | FIXTURES.push({ 71 | name: basename, 72 | title: title, 73 | content: content, 74 | suite: suite 75 | }); 76 | }); 77 | 78 | 79 | function select(patterns) { 80 | var result = []; 81 | 82 | if (!(patterns instanceof Array)) { 83 | patterns = [ patterns ]; 84 | } 85 | 86 | function checkName(name) { 87 | return patterns.length === 0 || patterns.some(function (regexp) { 88 | return regexp.test(name); 89 | }); 90 | } 91 | 92 | FIXTURES.forEach(function (sample) { 93 | if (checkName(sample.name)) { 94 | result.push(sample); 95 | } 96 | }); 97 | 98 | return result; 99 | } 100 | 101 | 102 | function run(files) { 103 | var selected = select(files); 104 | 105 | if (selected.length > 0) { 106 | console.log('Selected fixtures: (%d of %d)', selected.length, FIXTURES.length); 107 | selected.forEach(function (sample) { 108 | console.log(' > %s', sample.name); 109 | }); 110 | } else { 111 | console.log('There isn\'t any sample matches any of these patterns: %s', util.inspect(files)); 112 | } 113 | 114 | selected.forEach(function (sample) { 115 | sample.suite.run(); 116 | }); 117 | } 118 | 119 | module.exports.IMPLS_DIRECTORY = IMPLS_DIRECTORY; 120 | module.exports.IMPLS_PATHS = IMPLS_PATHS; 121 | module.exports.IMPLS = IMPLS; 122 | module.exports.FIXTURES_DIRECTORY = FIXTURES_DIRECTORY; 123 | module.exports.FIXTURES = FIXTURES; 124 | module.exports.select = select; 125 | module.exports.run = run; 126 | 127 | run(process.argv.slice(2).map(function (source) { 128 | return new RegExp(source, 'i'); 129 | })); 130 | -------------------------------------------------------------------------------- /benchmark/profile.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /*eslint no-console:0*/ 3 | 'use strict'; 4 | 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var Remarkable = require('../'); 8 | 9 | var md = new Remarkable({ 10 | html: true, 11 | typographer: false 12 | }); 13 | 14 | // var data = fs.readFileSync(path.join(__dirname, '/fixtures/lorem1.txt'), 'utf8'); 15 | var data = fs.readFileSync(path.join(__dirname, '../test/fixtures/commonmark/spec.txt'), 'utf8'); 16 | 17 | for (var i = 0; i < 20; i++) { 18 | md.render(data); 19 | } 20 | -------------------------------------------------------------------------------- /bin/remarkable.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/cjs/cli.js'); 4 | -------------------------------------------------------------------------------- /demo/.eslintrc: -------------------------------------------------------------------------------- 1 | env: 2 | node: false 3 | browser: true 4 | 5 | globals: 6 | $: false 7 | _: false 8 | -------------------------------------------------------------------------------- /demo/assets/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | .full-height { 4 | height: 100%; 5 | } 6 | body { 7 | overflow-x: hidden; 8 | padding-bottom: 180px; 9 | background-color: #fbfbfb; 10 | } 11 | .demo-options { 12 | margin-bottom: 10px; 13 | } 14 | .opt__strict .not-strict { 15 | opacity: 0.3; 16 | } 17 | .checkbox { 18 | margin-right: 10px; 19 | } 20 | .source { 21 | width: 100%; 22 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 23 | font-size: 13px; 24 | padding: 2px; 25 | } 26 | .result-html { 27 | padding: 2px 10px; 28 | overflow: auto; 29 | background-color: #fff; 30 | border: 1px solid #ccc; 31 | border-radius: 4px; 32 | } 33 | .result-html img { 34 | max-width: 35%; 35 | } 36 | .result-src, 37 | .result-debug { 38 | display: none; 39 | background-color: #fff; 40 | } 41 | .result-src-content, 42 | .result-debug-content { 43 | white-space: pre; 44 | } 45 | .result-as-html .result-html { 46 | display: block; 47 | } 48 | .result-as-html .result-src, 49 | .result-as-html .result-debug { 50 | display: none; 51 | } 52 | .result-as-src .result-src { 53 | display: block; 54 | } 55 | .result-as-src .result-html, 56 | .result-as-src .result-debug { 57 | display: none; 58 | } 59 | .result-as-debug .result-debug { 60 | display: block; 61 | } 62 | .result-as-debug .result-html, 63 | .result-as-debug .result-src { 64 | display: none; 65 | } 66 | .demo-control { 67 | position: absolute; 68 | right: 40px; 69 | top: 0; 70 | border-radius: 0 0 6px 6px; 71 | font-size: 12px; 72 | background-color: #666; 73 | color: #fff !important; 74 | opacity: 0.6; 75 | -webkit-transition: opacity 0.5s ease-in-out; 76 | transition: opacity 0.5s ease-in-out; 77 | } 78 | .demo-control:hover { 79 | opacity: 1; 80 | } 81 | .demo-control a { 82 | padding: 0 20px; 83 | color: #fff !important; 84 | } 85 | .demo-control a:first-child { 86 | padding-left: 30px; 87 | } 88 | .demo-control a:last-child { 89 | padding-right: 30px; 90 | } 91 | .hljs { 92 | background: none; 93 | padding: 0; 94 | } 95 | .footnotes { 96 | -moz-column-count: 2; 97 | -webkit-column-count: 2; 98 | column-count: 2; 99 | } 100 | .footnotes-list { 101 | padding-left: 2em; 102 | } 103 | .gh-ribbon { 104 | display: block; 105 | position: absolute; 106 | right: -60px; 107 | top: 44px; 108 | -webkit-transform: rotate(45deg); 109 | transform: rotate(45deg); 110 | width: 230px; 111 | z-index: 10000; 112 | white-space: nowrap; 113 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 114 | background-color: #686868; 115 | box-shadow: 0 0 2px rgba(102,102,102,0.4); 116 | padding: 1px 0; 117 | } 118 | .gh-ribbon a { 119 | text-decoration: none !important; 120 | border: 1px solid #ccc; 121 | color: #fff; 122 | display: block; 123 | font-size: 13px; 124 | font-weight: 700; 125 | outline: medium none; 126 | padding: 4px 50px 2px; 127 | text-align: center; 128 | } 129 | .form-inline .radio, 130 | .form-inline .checkbox { 131 | display: inline-block; 132 | margin-bottom: 0; 133 | margin-top: 0; 134 | } 135 | .form-inline .form-group { 136 | display: inline-block; 137 | margin-bottom: 0; 138 | vertical-align: middle; 139 | } 140 | .form-inline .form-control { 141 | display: inline-block; 142 | vertical-align: middle; 143 | width: auto; 144 | } 145 | -------------------------------------------------------------------------------- /demo/assets/index.styl: -------------------------------------------------------------------------------- 1 | html, body, .full-height 2 | height 100% 3 | 4 | 5 | body 6 | overflow-x hidden 7 | padding-bottom 180px 8 | background-color #fbfbfb 9 | 10 | .demo-options 11 | margin-bottom 10px 12 | 13 | .opt__strict .not-strict 14 | opacity 0.3 15 | 16 | .checkbox 17 | margin-right 10px 18 | 19 | .source 20 | width 100% 21 | font-family Menlo, Monaco, Consolas, "Courier New", monospace 22 | font-size 13px 23 | padding 2px 24 | 25 | .result-html 26 | padding 2px 10px 27 | overflow auto 28 | background-color #fff 29 | border 1px solid #ccc 30 | border-radius 4px 31 | img 32 | max-width 35% 33 | 34 | .result-src 35 | .result-debug 36 | display none 37 | background-color #fff 38 | 39 | .result-src-content 40 | .result-debug-content 41 | white-space pre 42 | 43 | .result-as-html 44 | .result-html 45 | display block 46 | .result-src 47 | .result-debug 48 | display none 49 | .result-as-src 50 | .result-src 51 | display block 52 | .result-html 53 | .result-debug 54 | display none 55 | .result-as-debug 56 | .result-debug 57 | display block 58 | .result-html 59 | .result-src 60 | display none 61 | 62 | .demo-control 63 | position absolute 64 | right 40px 65 | top 0 66 | //padding 0 10px 67 | border-radius 0 0 6px 6px 68 | font-size 12px 69 | background-color #666 70 | color #fff !important 71 | opacity .6 72 | transition opacity 0.5s ease-in-out 73 | &:hover 74 | opacity 1 75 | a 76 | padding 0 20px 77 | color #fff !important 78 | a:first-child 79 | padding-left 30px 80 | a:last-child 81 | padding-right 30px 82 | 83 | .hljs 84 | background none 85 | padding 0 86 | 87 | .footnotes 88 | -moz-column-count 2 89 | -webkit-column-count 2 90 | column-count 2 91 | 92 | .footnotes-list 93 | padding-left 2em 94 | 95 | .gh-ribbon 96 | display block 97 | position absolute 98 | right -60px 99 | top 44px 100 | transform rotate(45deg) 101 | width 230px 102 | z-index 10000 103 | white-space nowrap 104 | font-family "Helvetica Neue", Helvetica, Arial, sans-serif 105 | background-color #686868 106 | box-shadow 0 0 2px rgba(102,102,102,0.4) 107 | padding 1px 0 108 | 109 | a 110 | text-decoration none !important 111 | border 1px solid #ccc 112 | color #fff 113 | display block 114 | font-size 13px 115 | font-weight 700 116 | outline medium none 117 | padding 4px 50px 2px 118 | text-align center 119 | 120 | //////////////////////////////////////////////////////////////////////////////// 121 | // Override default responsiveness 122 | // 123 | .form-inline 124 | .radio 125 | .checkbox 126 | display inline-block 127 | margin-bottom 0 128 | margin-top 0 129 | 130 | .form-group 131 | display inline-block 132 | margin-bottom 0 133 | vertical-align: middle 134 | 135 | .form-control 136 | display inline-block 137 | vertical-align middle 138 | width auto 139 | -------------------------------------------------------------------------------- /demo/example.js: -------------------------------------------------------------------------------- 1 | var Remarkable = require('remarkable'); 2 | var hljs = require('highlight.js') // https://highlightjs.org/ 3 | 4 | var md = new Remarkable('full', { 5 | html: false, // Enable HTML tags in source 6 | xhtmlOut: false, // Use '/' to close single tags (
) 7 | breaks: false, // Convert '\n' in paragraphs into
8 | langPrefix: 'language-', // CSS language prefix for fenced blocks 9 | linkify: true, // autoconvert URL-like texts to links 10 | linkTarget: '', // set target to open link in 11 | 12 | // Enable some language-neutral replacements + quotes beautification 13 | typographer: false, 14 | 15 | // Double + single quotes replacement pairs, when typographer enabled, 16 | // and smartquotes on. Set doubles to '«»' for Russian, '„“' for German. 17 | quotes: '“”‘’', 18 | 19 | // Highlighter function. Should return escaped HTML, 20 | // or '' if input not changed 21 | highlight: function (str, lang) { 22 | if (lang && hljs.getLanguage(lang)) { 23 | try { 24 | return hljs.highlight(lang, str).value; 25 | } catch (__) {} 26 | } 27 | 28 | try { 29 | return hljs.highlightAuto(str).value; 30 | } catch (__) {} 31 | 32 | return ''; // use external default escaping 33 | } 34 | }); 35 | 36 | console.log(md.render('# Remarkable rulezz!')); 37 | // =>

Remarkable rulezz!

38 | -------------------------------------------------------------------------------- /demo/example.md: -------------------------------------------------------------------------------- 1 | # Remarkable 2 | 3 | > Experience real-time editing with Remarkable! 4 | 5 | Click the `clear` link to start with a clean slate, or get the `permalink` to share or save your results. 6 | 7 | *** 8 | 9 | # h1 Heading 10 | ## h2 Heading 11 | ### h3 Heading 12 | #### h4 Heading 13 | ##### h5 Heading 14 | ###### h6 Heading 15 | 16 | 17 | ## Horizontal Rules 18 | 19 | ___ 20 | 21 | *** 22 | 23 | *** 24 | 25 | 26 | ## Typographic replacements 27 | 28 | Enable typographer option to see result. 29 | 30 | (c) (C) (r) (R) (tm) (TM) (p) (P) +- 31 | 32 | test.. test... test..... test?..... test!.... 33 | 34 | !!!!!! ???? ,, 35 | 36 | Remarkable -- awesome 37 | 38 | "Smartypants, double quotes" 39 | 40 | 'Smartypants, single quotes' 41 | 42 | 43 | ## Emphasis 44 | 45 | **This is bold text** 46 | 47 | __This is bold text__ 48 | 49 | *This is italic text* 50 | 51 | _This is italic text_ 52 | 53 | ~~Deleted text~~ 54 | 55 | Superscript: 19^th^ 56 | 57 | Subscript: H~2~O 58 | 59 | ++Inserted text++ 60 | 61 | ==Marked text== 62 | 63 | 64 | ## Blockquotes 65 | 66 | > Blockquotes can also be nested... 67 | >> ...by using additional greater-than signs right next to each other... 68 | > > > ...or with spaces between arrows. 69 | 70 | 71 | ## Lists 72 | 73 | Unordered 74 | 75 | + Create a list by starting a line with `+`, `-`, or `*` 76 | + Sub-lists are made by indenting 2 spaces: 77 | - Marker character change forces new list start: 78 | * Ac tristique libero volutpat at 79 | + Facilisis in pretium nisl aliquet 80 | - Nulla volutpat aliquam velit 81 | + Very easy! 82 | 83 | Ordered 84 | 85 | 1. Lorem ipsum dolor sit amet 86 | 2. Consectetur adipiscing elit 87 | 3. Integer molestie lorem at massa 88 | 89 | 90 | 1. You can use sequential numbers... 91 | 1. ...or keep all the numbers as `1.` 92 | 93 | Start numbering with offset: 94 | 95 | 57. foo 96 | 1. bar 97 | 98 | 99 | ## Code 100 | 101 | Inline `code` 102 | 103 | Indented code 104 | 105 | // Some comments 106 | line 1 of code 107 | line 2 of code 108 | line 3 of code 109 | 110 | 111 | Block code "fences" 112 | 113 | ``` 114 | Sample text here... 115 | ``` 116 | 117 | Syntax highlighting 118 | 119 | ``` js 120 | var foo = function (bar) { 121 | return bar++; 122 | }; 123 | 124 | console.log(foo(5)); 125 | ``` 126 | 127 | ## Tables 128 | 129 | | Option | Description | 130 | | ------ | ----------- | 131 | | data | path to data files to supply the data that will be passed into templates. | 132 | | engine | engine to be used for processing templates. Handlebars is the default. | 133 | | ext | extension to be used for dest files. | 134 | 135 | Right aligned columns 136 | 137 | | Option | Description | 138 | | ------:| -----------:| 139 | | data | path to data files to supply the data that will be passed into templates. | 140 | | engine | engine to be used for processing templates. Handlebars is the default. | 141 | | ext | extension to be used for dest files. | 142 | 143 | 144 | ## Links 145 | 146 | [link text](http://dev.nodeca.com) 147 | 148 | [link with title](http://nodeca.github.io/pica/demo/ "title text!") 149 | 150 | Autoconverted link https://github.com/nodeca/pica (enable linkify to see) 151 | 152 | 153 | ## Images 154 | 155 | ![Minion](https://octodex.github.com/images/minion.png) 156 | ![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat") 157 | 158 | Like links, Images also have a footnote style syntax 159 | 160 | ![Alt text][id] 161 | 162 | With a reference later in the document defining the URL location: 163 | 164 | [id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" 165 | 166 | 167 | ## Footnotes 168 | 169 | Footnote 1 link[^first]. 170 | 171 | Footnote 2 link[^second]. 172 | 173 | Inline footnote^[Text of inline footnote] definition. 174 | 175 | Duplicated footnote reference[^second]. 176 | 177 | [^first]: Footnote **can have markup** 178 | 179 | and multiple paragraphs. 180 | 181 | [^second]: Footnote text. 182 | 183 | 184 | ## Definition lists 185 | 186 | Term 1 187 | 188 | : Definition 1 189 | with lazy continuation. 190 | 191 | Term 2 with *inline markup* 192 | 193 | : Definition 2 194 | 195 | { some code, part of Definition 2 } 196 | 197 | Third paragraph of definition 2. 198 | 199 | _Compact style:_ 200 | 201 | Term 1 202 | ~ Definition 1 203 | 204 | Term 2 205 | ~ Definition 2a 206 | ~ Definition 2b 207 | 208 | 209 | ## Abbreviations 210 | 211 | This is HTML abbreviation example. 212 | 213 | It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on. 214 | 215 | *[HTML]: Hyper Text Markup Language 216 | 217 | 218 | *** 219 | 220 | __Advertisement :)__ 221 | 222 | - __[pica](https://nodeca.github.io/pica/demo/)__ - high quality and fast image 223 | resize in browser. 224 | - __[babelfish](https://github.com/nodeca/babelfish/)__ - developer friendly 225 | i18n with plurals support and easy syntax. 226 | 227 | You'll like those projects! :) 228 | -------------------------------------------------------------------------------- /demo/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Remarkable demo 5 | meta(charset='UTF-8') 6 | meta(http-equiv='X-UA-Compatible' content='IE=edge') 7 | meta(name='viewport' content='width=device-width, initial-scale=1') 8 | 9 | script(src='https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.0.5/es5-shim.min.js') 10 | 11 | script(src='https://cdn.jsdelivr.net/jquery/1.11.1/jquery.min.js') 12 | script(src='https://cdn.jsdelivr.net/lodash/2.4.1/lodash.js') 13 | 14 | script(src='https://cdn.jsdelivr.net/bootstrap/3.2.0/js/bootstrap.min.js') 15 | link(rel='stylesheet' href='https://cdn.jsdelivr.net/bootstrap/3.2.0/css/bootstrap.css') 16 | 17 | script(src='https://cdn.jsdelivr.net/highlight.js/8.4.0/highlight.min.js') 18 | link(rel='stylesheet' href='https://cdn.jsdelivr.net/highlight.js/8.4.0/styles/solarized_light.min.css') 19 | 20 | script(src='../dist/remarkable.js') 21 | 22 | link(rel='stylesheet' href='./assets/index.css') 23 | script(src='./assets/index.js') 24 | 25 | // Ancient IE support - load shiv & kill broken highlighter 26 | 30 | 31 | // GA counter 32 | script. 33 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 34 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 35 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 36 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 37 | 38 | ga('create', 'UA-26895916-3', 'auto'); 39 | ga('require', 'displayfeatures'); 40 | ga('require', 'linkid', 'linkid.js'); 41 | ga('send', 'pageview'); 42 | 43 | body 44 | .container 45 | - var s = self.self.demo; 46 | h1 Remarkable demo 47 | 48 | p: a(data-toggle='collapse' href='#code-example') code example 49 | pre.collapse.code-example(id='code-example') 50 | code.js= s.code 51 | 52 | .form-inline.demo-options 53 | .checkbox.not-strict 54 | label._tip(title='enable html tags in source text') 55 | input#html(type='checkbox') 56 | | html 57 | .checkbox.not-strict 58 | label._tip(title='produce xtml output (add / to single tags (
instead of
)') 59 | input#xhtmlOut(type='checkbox') 60 | | xhtmlOut 61 | .checkbox.not-strict 62 | label._tip(title='newlines in paragraphs are rendered as
') 63 | input#breaks(type='checkbox') 64 | | breaks 65 | .checkbox.not-strict 66 | label._tip(title='autoconvert link-like texts to links') 67 | input#linkify(type='checkbox') 68 | | linkify 69 | .form-group.not-strict 70 | input#linkTarget.form-control._tip( 71 | type='input' 72 | placeholder='link target' 73 | title='target to open link in' 74 | ) 75 | .checkbox.not-strict 76 | label._tip(title='do typographyc replacements, (c) -> © and so on') 77 | input#typographer(type='checkbox') 78 | | typographer 79 | .checkbox.not-strict 80 | label._tip(title='enable output highlight for fenced blocks') 81 | input#_highlight(type='checkbox') 82 | | highlight 83 | .form-group.not-strict 84 | input#langPrefix.form-control._tip( 85 | type='input' 86 | placeholder='language prefix' 87 | title='css class language prefix for fenced code blocks' 88 | ) 89 | .checkbox 90 | label._tip(title='force strict CommonMark mode - output will be equal to reference parser') 91 | input#_strict(type='checkbox') 92 | | CommonMark strict 93 | 94 | .container.full-height 95 | .row.full-height 96 | .col-xs-6.full-height 97 | .demo-control 98 | a.source-clear(href='#') clear 99 | a#permalink(href='./' title='Share this snippet as link'): strong permalink 100 | textarea.source.full-height= s.source 101 | section.col-xs-6.full-height 102 | .demo-control 103 | a(href='#' data-result-as='html') html 104 | a(href='#' data-result-as='src') source 105 | a(href='#' data-result-as='debug') debug 106 | .result-html.full-height 107 | pre.result-src.full-height 108 | code.result-src-content.full-height 109 | pre.result-debug.full-height 110 | code.result-debug-content.full-height 111 | 112 | .gh-ribbon 113 | a(href='https://github.com/jonschlinkert/remarkable' target='_blank') Fork me on GitHub 114 | -------------------------------------------------------------------------------- /docs/parser.md: -------------------------------------------------------------------------------- 1 | # Parsing Rules 2 | 3 | Parsing is the first step of converting markdown to HTML. The parsing rules are 4 | responsible for separating markdown syntax from plain text. The parsers scan the 5 | markdown content and use various rules to produce a list of tokens. 6 | 7 | Each token represents either a piece of markdown syntax (for example, the 8 | begining of a fenced code block, a list item, etc.) or plain text that will be 9 | included as-is (escaped) in the final HTML text. The tokens will be later be 10 | used by the [Renderer][renderer] to produce actual HTML. 11 | 12 | There are three kind of parsing rules in Remarkable: 13 | 14 | 1. [Core rules][core] 15 | 2. [Block rules][block] 16 | 3. [Inline rules][inline] 17 | 18 | Each uses different datastructures and signatures. Unless you wish to modify the 19 | internal workflow of Remarkable, you will most probably only deal with Block and 20 | Inline rules. 21 | 22 | ## Parsing tokens 23 | 24 | Tokens comes in three kinds: 25 | 26 | 1. Tag token 27 | 2. Content (`text`) token 28 | 3. Block content (`inline`) tokens 29 | 30 | All tokens have the following properties: 31 | 32 | * `type`: The type of the token 33 | * `level`: The nesting level of the associated markdown structure in the source. 34 | 35 | Tokens generated by [block parsing rules][block] will also include a `lines` 36 | property which is a 2 elements array marking the first and last line of the 37 | `src` used to generate the token. 38 | 39 | Parsing rules will usually generates at least three tokens: 40 | 41 | 1. The start or open token marking the beginning of the markdown structure 42 | 2. The content token (usually with type `inline` for a block rule, or `text` for 43 | an inline rule) 44 | 3. The end or close token makring the end of the markdown structure 45 | 46 | ### Tag token 47 | 48 | Tag tokens are used to represent markdown syntax. Each tag token represents a 49 | special markdown syntax in the original markdown source. They are usually used 50 | for the open and close tokens. For example the "\`\`\`" at the begining of a 51 | fenced block code, the start of an item list or the end of a emphasized part of 52 | a line. 53 | 54 | Tag tokens have a variety of types and each is associated to a 55 | [rendering rule][renderer]. 56 | 57 | ### Content (`text`) token 58 | 59 | Text tokens represent plain text. It is usually used for the content of inline 60 | structures. Most of them will be generated automatically by the inline 61 | parser. They are also sometimes generated explicitly by the 62 | [inline parsing rules][inline]. 63 | 64 | A text token has a `content` property containing the text it represents. 65 | 66 | ### Block content (`inline`) tokens 67 | 68 | Inline tokens represent the content of a block structure. These tokens have two 69 | additional properties: 70 | 71 | * `content`: The content of the block. This might include inline mardown syntax 72 | which may need further processing by the inline rules. 73 | * `children`: This is initialized with an empty array (`[]`) and will be filled 74 | with the inline parser tokens as the inline parsing rules are applied. 75 | 76 | [renderer]: renderer.md 77 | [core]: parsing_core.md 78 | [block]: parsing_block.md 79 | [inline]: parsing_inline.md 80 | -------------------------------------------------------------------------------- /docs/parsing_core.md: -------------------------------------------------------------------------------- 1 | # Core rules 2 | 3 | The core rules are the one managing global parsing or Markdown syntaxes 4 | requiring a global access to the document. For example, management of reference 5 | links or abbreviations. They are also the one triggering the execution of 6 | [block][block] and [inline][inline] rules. 7 | 8 | A core rule is a function expecting a single `StateCore` argument. The code for 9 | the StateCore prototype is in [`index.js`](../lib/index.js) and its data consists 10 | of: 11 | 12 | * `src`: the complete string the parser is currently working on 13 | * `tokens`: the tokens generated by the previous core rules, up to now 14 | * `env`: a namespaced data key-value store to allow core rules to exchange data 15 | * `inlineMode`: a flag mentionning whether the parser is currently working as inline-mode only 16 | or standard mode 17 | 18 | The rest are mostly components of the underlying remarkable instance. 19 | 20 | Some core rules process directly the `src` to produce a new set of tokens, 21 | others process the existing list of tokens in order to produce a new one. 22 | 23 | The most important predefined core rules are `block` and `inline` which are 24 | reponsible for calling the subparsers for the [block][block] and 25 | [inline][inline] rules. 26 | 27 | To better understand how the core rules work, please read the code in 28 | [`parser_core.js`](../lib/parser_core.js) and the predefined rules in 29 | [`rules_core/`](../lib/rules_core/). 30 | 31 | [inline]: parsing_inline.md 32 | [block]: parsing_block.md 33 | -------------------------------------------------------------------------------- /docs/parsing_inline.md: -------------------------------------------------------------------------------- 1 | # Inline rules 2 | 3 | The inline rules are responsible for parsing inline Markdown syntax formatting 4 | only part of a line of text and emitting the [tokens][tokens] required to 5 | represent them. For example, a link, bold, emphasis, super/sub scripts, inline 6 | code, and so on. 7 | 8 | An inline rule is a function expecting two argumnents: 9 | 10 | 1. `state`: an instance of `StateInline` 11 | 4. `checkMode`: a flag indicating whether we should simply check if the current 12 | position marks the begining of the syntax we are trying to parse. 13 | 14 | The `checkMode` is here for optimization, if it's set to `true`, we can return 15 | `true` as soon as we are sure the present position marks the beginning of an 16 | inline syntax we are trying to parse (For example, in the case of emphasized 17 | (italic) text, the check would be "Is the current position a '\*' or a '\_'?"). 18 | 19 | ## State inline 20 | 21 | The definition for `StateInline` prototype is in `rules_inline/state_inline.js` 22 | and its data consists of: 23 | 24 | * `src`: the complete string the parser is currently working on (i.e. the 25 | current line/block content). 26 | * `parser`: The current inline parser (here to make nested calls easier) 27 | * `env`: a namespaced data key-value store to allow core rules to exchange data 28 | * `tokens`: the tokens generated by the parser up to now 29 | * `pos`: the current position in `src` reached by the parser 30 | * `posMax`: the last position available in `src` 31 | * `level`: the nested level for the current inline rule 32 | 33 | The most important methods are: 34 | 35 | * `push(token)`: adds a new token to the (end of the) output 36 | * `cacheSet(key, value)`: adds a new value to the cache key-value store. `key` 37 | MUST be a non-negative integer (`>= 0`) and value MUST a positive integer (`> 38 | 0`) 39 | * `cacheGet(key)`: fetches a value from the cache key-value store. `key` MUST be 40 | a non-negative integer (`>= 0`) 41 | 42 | ## Rule parser behaviour 43 | 44 | If `checkMode` is set to true, simply return a boolean depending on whether the 45 | current position should be considered has the begining of your 46 | syntax. Otherwise, proceed with the complete parsing. 47 | 48 | NB: It is your responsibility to make sure you have reached the maximum nesting 49 | level allowed by comparing `state.level` and `state.options.maxNesting`. 50 | 51 | NB: If for any reason, the syntax you are trying to parse is incorrectly 52 | formated and you are unable to parse it, you must abort and return `false` 53 | without modifying `state` in any way. 54 | 55 | To completely parse a block, you will need to push new [tokens][tokens] by 56 | calling `state.push(token)`. 57 | 58 | Once you are sure the current position marks the beginning of the syntax you are 59 | trying to parse, you should push an [open tag token][tokens] corresponding to 60 | the begining of your inline section. You will also need to find its end. 61 | 62 | Your next decision should be whether you wish to allow other inline syntaxes to 63 | be nested in your content or not. If you do, you will need to invoke 64 | `state.parser.tokenize(state)` with an updated `state` accordingly to allow the 65 | next batch of rules to run smoothly (incl. updating `pos`, `posMax` and 66 | `level`). Don't forget to restore the previous values afterward. 67 | 68 | The last token you will need to emit is the [end tag token][tokens] for your 69 | inline section. 70 | 71 | Finally, you will need to update `state.pos` to the first position after the 72 | part of `src` you processed and return `true`. 73 | 74 | [renderer doc]: renderer.md 75 | [tokens]: parser.md 76 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Plugins are the most easy way to distribute an extension for remarkable. 4 | 5 | Plugins are expected to be loaded using `md.use(plugin[, opts])` (where `md` is 6 | your instance of Remarkable.). 7 | 8 | Plugins are nothing more than a function taking two arguments: 9 | 10 | 1. `md`: the Remarkable instance on which the plugin needs to be activated 11 | 2. `options`: the set of options that has been provided to `md.use` 12 | 13 | Plugins are then expected to add [parsing][parser] and [rendering][renderer] 14 | rules and carry on appropriate modifications on the remarkable instance. 15 | 16 | It's as simple as that. 17 | 18 | ## Extending remarkable and the Markdown syntax 19 | 20 | Remarkable converts markdown to HTML in two steps: 21 | 22 | 1. Parsing markdown raw text to a list of tokens 23 | 2. Rendering the list of tokens to actual HTML code. 24 | 25 | Parsing rules are divided into three differents kind of rules 26 | ([core][core parsing], [block][block parsing] and [inline][inline parsing]). 27 | 28 | To add a parsing rule, you will need to get the relevant parser for your rule 29 | (`core`, `block` or `inline`) and insert your rule at the appropriate position 30 | in the `ruler`. 31 | 32 | For example, to add a new inline for strike-through, you'd need to do: 33 | `md.inline.ruler.push("strike-through", strikeThroughInlineRule, { strokesCount: 34 | 2 });` where `strikeThroughInlineRule` is your [parsing rule][parser] for 35 | strike-through. 36 | 37 | To add a rendering rule, you need to follow exactly the same process, but using 38 | the ruler `md.renderer.rules`. 39 | 40 | ### Rulers 41 | 42 | Rulers provide four main methods to manage their rules: 43 | 44 | * `before(beforeName, ruleName, fn, options)`: inserts a new rule before the 45 | rule `beforeName`. 46 | * `after(afterName, ruleName, fn, options)`: inserts a new rule after the 47 | rule `afterName`. 48 | * `push(ruleName, fn, options)`: inserts a new rule at the end of the rule list. 49 | * `at(ruleName, fn, options)`: replace the rule `ruleName` with a new rule. 50 | 51 | [parser]: parser.md 52 | [renderer]: renderer.md 53 | [core parsing]: parsing_core.md 54 | [block parsing]: parsing_block.md 55 | [inline parsing]: parsing_inline.md 56 | -------------------------------------------------------------------------------- /docs/renderer.md: -------------------------------------------------------------------------------- 1 | # Renderer 2 | 3 | Renderering is the second part of converting markdown to HTML. The renderer 4 | converts the list of [tokens][parser] produced by the [Parsers][parser] to 5 | produce actual HTML code. 6 | 7 | Each rendering rule is a function taking four arguments: 8 | 9 | * `tokens`: the list of tokens currently being processed 10 | * `idx`: the index of the token currently being processed 11 | * `options`: the options given to remarkable 12 | * `env`: the key-value store created by the parsing rules 13 | 14 | Each rule is registered with a name corresponding to a token's `type`. When the 15 | renderer meets a token, it will invoke the rule associated to said token's 16 | `type` and expect the function to return appropriate HTML code. 17 | 18 | NB: Rendering rules are not provided with helpers to recursively invoke the renderer 19 | and should not do so. 20 | 21 | [parser]: parser.md 22 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import argparse from 'argparse'; 3 | import { Remarkable } from './index'; 4 | import { linkify } from './linkify'; 5 | import { version } from '../package.json'; 6 | 7 | //////////////////////////////////////////////////////////////////////////////// 8 | 9 | var cli = new argparse.ArgumentParser({ 10 | prog: 'remarkable', 11 | version: version, 12 | addHelp: true 13 | }); 14 | 15 | cli.addArgument([ 'file' ], { 16 | help: 'File to read', 17 | nargs: '?', 18 | defaultValue: '-' 19 | }); 20 | 21 | var options = cli.parseArgs(); 22 | 23 | 24 | function readFile(filename, encoding, callback) { 25 | if (options.file === '-') { 26 | var chunks = []; 27 | 28 | // read from stdin 29 | process.stdin.on('data', function(chunk) { 30 | chunks.push(chunk); 31 | }); 32 | 33 | process.stdin.on('end', function() { 34 | return callback(null, Buffer.concat(chunks).toString(encoding)); 35 | }); 36 | } else { 37 | fs.readFile(filename, encoding, callback); 38 | } 39 | } 40 | 41 | 42 | //////////////////////////////////////////////////////////////////////////////// 43 | 44 | readFile(options.file, 'utf8', function (err, input) { 45 | var output, md; 46 | 47 | if (err) { 48 | if (err.code === 'ENOENT') { 49 | console.error('File not found: ' + options.file); 50 | process.exit(2); 51 | } 52 | 53 | console.error(err.stack || err.message || String(err)); 54 | process.exit(1); 55 | } 56 | 57 | md = new Remarkable('full', { 58 | html: true, 59 | xhtmlOut: true, 60 | typographer: true, 61 | }).use(linkify); 62 | 63 | try { 64 | output = md.render(input); 65 | } catch (e) { 66 | console.error(e.stack || e.message || String(e)); 67 | process.exit(1); 68 | } 69 | 70 | process.stdout.write(output); 71 | process.exit(0); 72 | }); 73 | -------------------------------------------------------------------------------- /lib/common/entities.browser.js: -------------------------------------------------------------------------------- 1 | var textarea; 2 | 3 | export function decodeEntity(name) { 4 | textarea = textarea || document.createElement('textarea'); 5 | textarea.innerHTML = '&' + name + ';'; 6 | return textarea.value; 7 | } 8 | -------------------------------------------------------------------------------- /lib/common/html_blocks.js: -------------------------------------------------------------------------------- 1 | // List of valid html blocks names, accorting to commonmark spec 2 | // http://jgm.github.io/CommonMark/spec.html#html-blocks 3 | 4 | var html_blocks = {}; 5 | 6 | [ 7 | 'article', 8 | 'aside', 9 | 'button', 10 | 'blockquote', 11 | 'body', 12 | 'canvas', 13 | 'caption', 14 | 'col', 15 | 'colgroup', 16 | 'dd', 17 | 'div', 18 | 'dl', 19 | 'dt', 20 | 'embed', 21 | 'fieldset', 22 | 'figcaption', 23 | 'figure', 24 | 'footer', 25 | 'form', 26 | 'h1', 27 | 'h2', 28 | 'h3', 29 | 'h4', 30 | 'h5', 31 | 'h6', 32 | 'header', 33 | 'hgroup', 34 | 'hr', 35 | 'iframe', 36 | 'li', 37 | 'map', 38 | 'object', 39 | 'ol', 40 | 'output', 41 | 'p', 42 | 'pre', 43 | 'progress', 44 | 'script', 45 | 'section', 46 | 'style', 47 | 'table', 48 | 'tbody', 49 | 'td', 50 | 'textarea', 51 | 'tfoot', 52 | 'th', 53 | 'tr', 54 | 'thead', 55 | 'ul', 56 | 'video' 57 | ].forEach(function (name) { html_blocks[name] = true; }); 58 | 59 | 60 | export default html_blocks; 61 | -------------------------------------------------------------------------------- /lib/common/html_re.js: -------------------------------------------------------------------------------- 1 | // Regexps to match html elements 2 | 3 | function replace(regex, options) { 4 | regex = regex.source; 5 | options = options || ''; 6 | 7 | return function self(name, val) { 8 | if (!name) { 9 | return new RegExp(regex, options); 10 | } 11 | val = val.source || val; 12 | regex = regex.replace(name, val); 13 | return self; 14 | }; 15 | } 16 | 17 | 18 | var attr_name = /[a-zA-Z_:][a-zA-Z0-9:._-]*/; 19 | 20 | var unquoted = /[^"'=<>`\x00-\x20]+/; 21 | var single_quoted = /'[^']*'/; 22 | var double_quoted = /"[^"]*"/; 23 | 24 | /*eslint no-spaced-func:0*/ 25 | var attr_value = replace(/(?:unquoted|single_quoted|double_quoted)/) 26 | ('unquoted', unquoted) 27 | ('single_quoted', single_quoted) 28 | ('double_quoted', double_quoted) 29 | (); 30 | 31 | var attribute = replace(/(?:\s+attr_name(?:\s*=\s*attr_value)?)/) 32 | ('attr_name', attr_name) 33 | ('attr_value', attr_value) 34 | (); 35 | 36 | var open_tag = replace(/<[A-Za-z][A-Za-z0-9]*attribute*\s*\/?>/) 37 | ('attribute', attribute) 38 | (); 39 | 40 | var close_tag = /<\/[A-Za-z][A-Za-z0-9]*\s*>/; 41 | var comment = /|/; 42 | var processing = /<[?].*?[?]>/; 43 | var declaration = /]*>/; 44 | var cdata = //; 45 | 46 | export var HTML_TAG_RE = replace(/^(?:open_tag|close_tag|comment|processing|declaration|cdata)/) 47 | ('open_tag', open_tag) 48 | ('close_tag', close_tag) 49 | ('comment', comment) 50 | ('processing', processing) 51 | ('declaration', declaration) 52 | ('cdata', cdata) 53 | (); 54 | -------------------------------------------------------------------------------- /lib/common/url_schemas.js: -------------------------------------------------------------------------------- 1 | // List of valid url schemas, accorting to commonmark spec 2 | // http://jgm.github.io/CommonMark/spec.html#autolinks 3 | 4 | export default [ 5 | 'coap', 6 | 'doi', 7 | 'javascript', 8 | 'aaa', 9 | 'aaas', 10 | 'about', 11 | 'acap', 12 | 'cap', 13 | 'cid', 14 | 'crid', 15 | 'data', 16 | 'dav', 17 | 'dict', 18 | 'dns', 19 | 'file', 20 | 'ftp', 21 | 'geo', 22 | 'go', 23 | 'gopher', 24 | 'h323', 25 | 'http', 26 | 'https', 27 | 'iax', 28 | 'icap', 29 | 'im', 30 | 'imap', 31 | 'info', 32 | 'ipp', 33 | 'iris', 34 | 'iris.beep', 35 | 'iris.xpc', 36 | 'iris.xpcs', 37 | 'iris.lwz', 38 | 'ldap', 39 | 'mailto', 40 | 'mid', 41 | 'msrp', 42 | 'msrps', 43 | 'mtqp', 44 | 'mupdate', 45 | 'news', 46 | 'nfs', 47 | 'ni', 48 | 'nih', 49 | 'nntp', 50 | 'opaquelocktoken', 51 | 'pop', 52 | 'pres', 53 | 'rtsp', 54 | 'service', 55 | 'session', 56 | 'shttp', 57 | 'sieve', 58 | 'sip', 59 | 'sips', 60 | 'sms', 61 | 'snmp', 62 | 'soap.beep', 63 | 'soap.beeps', 64 | 'tag', 65 | 'tel', 66 | 'telnet', 67 | 'tftp', 68 | 'thismessage', 69 | 'tn3270', 70 | 'tip', 71 | 'tv', 72 | 'urn', 73 | 'vemmi', 74 | 'ws', 75 | 'wss', 76 | 'xcon', 77 | 'xcon-userid', 78 | 'xmlrpc.beep', 79 | 'xmlrpc.beeps', 80 | 'xmpp', 81 | 'z39.50r', 82 | 'z39.50s', 83 | 'adiumxtra', 84 | 'afp', 85 | 'afs', 86 | 'aim', 87 | 'apt', 88 | 'attachment', 89 | 'aw', 90 | 'beshare', 91 | 'bitcoin', 92 | 'bolo', 93 | 'callto', 94 | 'chrome', 95 | 'chrome-extension', 96 | 'com-eventbrite-attendee', 97 | 'content', 98 | 'cvs', 99 | 'dlna-playsingle', 100 | 'dlna-playcontainer', 101 | 'dtn', 102 | 'dvb', 103 | 'ed2k', 104 | 'facetime', 105 | 'feed', 106 | 'finger', 107 | 'fish', 108 | 'gg', 109 | 'git', 110 | 'gizmoproject', 111 | 'gtalk', 112 | 'hcp', 113 | 'icon', 114 | 'ipn', 115 | 'irc', 116 | 'irc6', 117 | 'ircs', 118 | 'itms', 119 | 'jar', 120 | 'jms', 121 | 'keyparc', 122 | 'lastfm', 123 | 'ldaps', 124 | 'magnet', 125 | 'maps', 126 | 'market', 127 | 'message', 128 | 'mms', 129 | 'ms-help', 130 | 'msnim', 131 | 'mumble', 132 | 'mvn', 133 | 'notes', 134 | 'oid', 135 | 'palm', 136 | 'paparazzi', 137 | 'platform', 138 | 'proxy', 139 | 'psyc', 140 | 'query', 141 | 'res', 142 | 'resource', 143 | 'rmi', 144 | 'rsync', 145 | 'rtmp', 146 | 'secondlife', 147 | 'sftp', 148 | 'sgn', 149 | 'skype', 150 | 'smb', 151 | 'soldat', 152 | 'spotify', 153 | 'ssh', 154 | 'steam', 155 | 'svn', 156 | 'teamspeak', 157 | 'things', 158 | 'udp', 159 | 'unreal', 160 | 'ut2004', 161 | 'ventrilo', 162 | 'view-source', 163 | 'webcal', 164 | 'wtai', 165 | 'wyciwyg', 166 | 'xfire', 167 | 'xri', 168 | 'ymsgr' 169 | ]; 170 | -------------------------------------------------------------------------------- /lib/common/utils.js: -------------------------------------------------------------------------------- 1 | import { decodeEntity } from './entities'; 2 | 3 | /** 4 | * Utility functions 5 | */ 6 | 7 | function typeOf(obj) { 8 | return Object.prototype.toString.call(obj); 9 | } 10 | 11 | export function isString(obj) { 12 | return typeOf(obj) === '[object String]'; 13 | } 14 | 15 | var hasOwn = Object.prototype.hasOwnProperty; 16 | 17 | export function has(object, key) { 18 | return object 19 | ? hasOwn.call(object, key) 20 | : false; 21 | } 22 | 23 | // Extend objects 24 | // 25 | export function assign(obj /*from1, from2, from3, ...*/) { 26 | var sources = [].slice.call(arguments, 1); 27 | 28 | sources.forEach(function (source) { 29 | if (!source) { return; } 30 | 31 | if (typeof source !== 'object') { 32 | throw new TypeError(source + 'must be object'); 33 | } 34 | 35 | Object.keys(source).forEach(function (key) { 36 | obj[key] = source[key]; 37 | }); 38 | }); 39 | 40 | return obj; 41 | } 42 | 43 | //////////////////////////////////////////////////////////////////////////////// 44 | 45 | var UNESCAPE_MD_RE = /\\([\\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g; 46 | 47 | export function unescapeMd(str) { 48 | if (str.indexOf('\\') < 0) { return str; } 49 | return str.replace(UNESCAPE_MD_RE, '$1'); 50 | } 51 | 52 | //////////////////////////////////////////////////////////////////////////////// 53 | 54 | export function isValidEntityCode(c) { 55 | /*eslint no-bitwise:0*/ 56 | // broken sequence 57 | if (c >= 0xD800 && c <= 0xDFFF) { return false; } 58 | // never used 59 | if (c >= 0xFDD0 && c <= 0xFDEF) { return false; } 60 | if ((c & 0xFFFF) === 0xFFFF || (c & 0xFFFF) === 0xFFFE) { return false; } 61 | // control codes 62 | if (c >= 0x00 && c <= 0x08) { return false; } 63 | if (c === 0x0B) { return false; } 64 | if (c >= 0x0E && c <= 0x1F) { return false; } 65 | if (c >= 0x7F && c <= 0x9F) { return false; } 66 | // out of range 67 | if (c > 0x10FFFF) { return false; } 68 | return true; 69 | } 70 | 71 | export function fromCodePoint(c) { 72 | /*eslint no-bitwise:0*/ 73 | if (c > 0xffff) { 74 | c -= 0x10000; 75 | var surrogate1 = 0xd800 + (c >> 10), 76 | surrogate2 = 0xdc00 + (c & 0x3ff); 77 | 78 | return String.fromCharCode(surrogate1, surrogate2); 79 | } 80 | return String.fromCharCode(c); 81 | } 82 | 83 | var NAMED_ENTITY_RE = /&([a-z#][a-z0-9]{1,31});/gi; 84 | var DIGITAL_ENTITY_TEST_RE = /^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i; 85 | 86 | function replaceEntityPattern(match, name) { 87 | var code = 0; 88 | var decoded = decodeEntity(name); 89 | 90 | if (name !== decoded) { 91 | return decoded; 92 | } else if (name.charCodeAt(0) === 0x23/* # */ && DIGITAL_ENTITY_TEST_RE.test(name)) { 93 | code = name[1].toLowerCase() === 'x' ? 94 | parseInt(name.slice(2), 16) 95 | : 96 | parseInt(name.slice(1), 10); 97 | if (isValidEntityCode(code)) { 98 | return fromCodePoint(code); 99 | } 100 | } 101 | return match; 102 | } 103 | 104 | export function replaceEntities(str) { 105 | if (str.indexOf('&') < 0) { return str; } 106 | 107 | return str.replace(NAMED_ENTITY_RE, replaceEntityPattern); 108 | } 109 | 110 | //////////////////////////////////////////////////////////////////////////////// 111 | 112 | var HTML_ESCAPE_TEST_RE = /[&<>"]/; 113 | var HTML_ESCAPE_REPLACE_RE = /[&<>"]/g; 114 | var HTML_REPLACEMENTS = { 115 | '&': '&', 116 | '<': '<', 117 | '>': '>', 118 | '"': '"' 119 | }; 120 | 121 | function replaceUnsafeChar(ch) { 122 | return HTML_REPLACEMENTS[ch]; 123 | } 124 | 125 | export function escapeHtml(str) { 126 | if (HTML_ESCAPE_TEST_RE.test(str)) { 127 | return str.replace(HTML_ESCAPE_REPLACE_RE, replaceUnsafeChar); 128 | } 129 | return str; 130 | } 131 | -------------------------------------------------------------------------------- /lib/configs/commonmark.js: -------------------------------------------------------------------------------- 1 | // Commonmark default options 2 | 3 | export default { 4 | options: { 5 | html: true, // Enable HTML tags in source 6 | xhtmlOut: true, // Use '/' to close single tags (
) 7 | breaks: false, // Convert '\n' in paragraphs into
8 | langPrefix: 'language-', // CSS language prefix for fenced blocks 9 | linkTarget: '', // set target to open link in 10 | 11 | // Enable some language-neutral replacements + quotes beautification 12 | typographer: false, 13 | 14 | // Double + single quotes replacement pairs, when typographer enabled, 15 | // and smartquotes on. Set doubles to '«»' for Russian, '„“' for German. 16 | quotes: '“”‘’', 17 | 18 | // Highlighter function. Should return escaped HTML, 19 | // or '' if input not changed 20 | // 21 | // function (/*str, lang*/) { return ''; } 22 | // 23 | highlight: null, 24 | 25 | maxNesting: 20 // Internal protection, recursion limit 26 | }, 27 | 28 | components: { 29 | 30 | core: { 31 | rules: [ 32 | 'block', 33 | 'inline', 34 | 'references', 35 | 'abbr2' 36 | ] 37 | }, 38 | 39 | block: { 40 | rules: [ 41 | 'blockquote', 42 | 'code', 43 | 'fences', 44 | 'heading', 45 | 'hr', 46 | 'htmlblock', 47 | 'lheading', 48 | 'list', 49 | 'paragraph' 50 | ] 51 | }, 52 | 53 | inline: { 54 | rules: [ 55 | 'autolink', 56 | 'backticks', 57 | 'emphasis', 58 | 'entity', 59 | 'escape', 60 | 'htmltag', 61 | 'links', 62 | 'newline', 63 | 'text' 64 | ] 65 | } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /lib/configs/default.js: -------------------------------------------------------------------------------- 1 | // Remarkable default options 2 | 3 | export default { 4 | options: { 5 | html: false, // Enable HTML tags in source 6 | xhtmlOut: false, // Use '/' to close single tags (
) 7 | breaks: false, // Convert '\n' in paragraphs into
8 | langPrefix: 'language-', // CSS language prefix for fenced blocks 9 | linkTarget: '', // set target to open link in 10 | 11 | // Enable some language-neutral replacements + quotes beautification 12 | typographer: false, 13 | 14 | // Double + single quotes replacement pairs, when typographer enabled, 15 | // and smartquotes on. Set doubles to '«»' for Russian, '„“' for German. 16 | quotes: '“”‘’', 17 | 18 | // Highlighter function. Should return escaped HTML, 19 | // or '' if input not changed 20 | // 21 | // function (/*str, lang*/) { return ''; } 22 | // 23 | highlight: null, 24 | 25 | maxNesting: 20 // Internal protection, recursion limit 26 | }, 27 | 28 | components: { 29 | 30 | core: { 31 | rules: [ 32 | 'block', 33 | 'inline', 34 | 'references', 35 | 'replacements', 36 | 'smartquotes', 37 | 'references', 38 | 'abbr2', 39 | 'footnote_tail' 40 | ] 41 | }, 42 | 43 | block: { 44 | rules: [ 45 | 'blockquote', 46 | 'code', 47 | 'fences', 48 | 'footnote', 49 | 'heading', 50 | 'hr', 51 | 'htmlblock', 52 | 'lheading', 53 | 'list', 54 | 'paragraph', 55 | 'table' 56 | ] 57 | }, 58 | 59 | inline: { 60 | rules: [ 61 | 'autolink', 62 | 'backticks', 63 | 'del', 64 | 'emphasis', 65 | 'entity', 66 | 'escape', 67 | 'footnote_ref', 68 | 'htmltag', 69 | 'links', 70 | 'newline', 71 | 'text' 72 | ] 73 | } 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /lib/configs/full.js: -------------------------------------------------------------------------------- 1 | // Remarkable default options 2 | 3 | export default { 4 | options: { 5 | html: false, // Enable HTML tags in source 6 | xhtmlOut: false, // Use '/' to close single tags (
) 7 | breaks: false, // Convert '\n' in paragraphs into
8 | langPrefix: 'language-', // CSS language prefix for fenced blocks 9 | linkTarget: '', // set target to open link in 10 | 11 | // Enable some language-neutral replacements + quotes beautification 12 | typographer: false, 13 | 14 | // Double + single quotes replacement pairs, when typographer enabled, 15 | // and smartquotes on. Set doubles to '«»' for Russian, '„“' for German. 16 | quotes: '“”‘’', 17 | 18 | // Highlighter function. Should return escaped HTML, 19 | // or '' if input not changed 20 | // 21 | // function (/*str, lang*/) { return ''; } 22 | // 23 | highlight: null, 24 | 25 | maxNesting: 20 // Internal protection, recursion limit 26 | }, 27 | 28 | components: { 29 | // Don't restrict core/block/inline rules 30 | core: {}, 31 | block: {}, 32 | inline: {} 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /lib/helpers/normalize_link.js: -------------------------------------------------------------------------------- 1 | import { replaceEntities } from '../common/utils'; 2 | 3 | export default function normalizeLink(url) { 4 | var normalized = replaceEntities(url); 5 | // We shouldn't care about the result of malformed URIs, 6 | // and should not throw an exception. 7 | try { 8 | normalized = decodeURI(normalized); 9 | } catch (err) {} 10 | return encodeURI(normalized); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/helpers/normalize_reference.js: -------------------------------------------------------------------------------- 1 | export default function normalizeReference(str) { 2 | // use .toUpperCase() instead of .toLowerCase() 3 | // here to avoid a conflict with Object.prototype 4 | // members (most notably, `__proto__`) 5 | return str.trim().replace(/\s+/g, ' ').toUpperCase(); 6 | }; 7 | -------------------------------------------------------------------------------- /lib/helpers/parse_link_destination.js: -------------------------------------------------------------------------------- 1 | import normalizeLink from './normalize_link'; 2 | import { unescapeMd } from '../common/utils'; 3 | 4 | /** 5 | * Parse link destination 6 | * 7 | * - on success it returns a string and updates state.pos; 8 | * - on failure it returns null 9 | * 10 | * @param {Object} state 11 | * @param {Number} pos 12 | * @api private 13 | */ 14 | 15 | export default function parseLinkDestination(state, pos) { 16 | var code, level, link, 17 | start = pos, 18 | max = state.posMax; 19 | 20 | if (state.src.charCodeAt(pos) === 0x3C /* < */) { 21 | pos++; 22 | while (pos < max) { 23 | code = state.src.charCodeAt(pos); 24 | if (code === 0x0A /* \n */) { return false; } 25 | if (code === 0x3E /* > */) { 26 | link = normalizeLink(unescapeMd(state.src.slice(start + 1, pos))); 27 | if (!state.parser.validateLink(link)) { return false; } 28 | state.pos = pos + 1; 29 | state.linkContent = link; 30 | return true; 31 | } 32 | if (code === 0x5C /* \ */ && pos + 1 < max) { 33 | pos += 2; 34 | continue; 35 | } 36 | 37 | pos++; 38 | } 39 | 40 | // no closing '>' 41 | return false; 42 | } 43 | 44 | // this should be ... } else { ... branch 45 | 46 | level = 0; 47 | while (pos < max) { 48 | code = state.src.charCodeAt(pos); 49 | 50 | if (code === 0x20) { break; } 51 | 52 | // ascii control chars 53 | if (code < 0x20 || code === 0x7F) { break; } 54 | 55 | if (code === 0x5C /* \ */ && pos + 1 < max) { 56 | pos += 2; 57 | continue; 58 | } 59 | 60 | if (code === 0x28 /* ( */) { 61 | level++; 62 | if (level > 1) { break; } 63 | } 64 | 65 | if (code === 0x29 /* ) */) { 66 | level--; 67 | if (level < 0) { break; } 68 | } 69 | 70 | pos++; 71 | } 72 | 73 | if (start === pos) { return false; } 74 | 75 | link = unescapeMd(state.src.slice(start, pos)); 76 | if (!state.parser.validateLink(link)) { return false; } 77 | 78 | state.linkContent = link; 79 | state.pos = pos; 80 | return true; 81 | }; 82 | -------------------------------------------------------------------------------- /lib/helpers/parse_link_label.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse link labels 3 | * 4 | * This function assumes that first character (`[`) already matches; 5 | * returns the end of the label. 6 | * 7 | * @param {Object} state 8 | * @param {Number} start 9 | * @api private 10 | */ 11 | 12 | export default function parseLinkLabel(state, start) { 13 | var level, found, marker, 14 | labelEnd = -1, 15 | max = state.posMax, 16 | oldPos = state.pos, 17 | oldFlag = state.isInLabel; 18 | 19 | if (state.isInLabel) { return -1; } 20 | 21 | if (state.labelUnmatchedScopes) { 22 | state.labelUnmatchedScopes--; 23 | return -1; 24 | } 25 | 26 | state.pos = start + 1; 27 | state.isInLabel = true; 28 | level = 1; 29 | 30 | while (state.pos < max) { 31 | marker = state.src.charCodeAt(state.pos); 32 | if (marker === 0x5B /* [ */) { 33 | level++; 34 | } else if (marker === 0x5D /* ] */) { 35 | level--; 36 | if (level === 0) { 37 | found = true; 38 | break; 39 | } 40 | } 41 | 42 | state.parser.skipToken(state); 43 | } 44 | 45 | if (found) { 46 | labelEnd = state.pos; 47 | state.labelUnmatchedScopes = 0; 48 | } else { 49 | state.labelUnmatchedScopes = level - 1; 50 | } 51 | 52 | // restore old state 53 | state.pos = oldPos; 54 | state.isInLabel = oldFlag; 55 | 56 | return labelEnd; 57 | }; 58 | -------------------------------------------------------------------------------- /lib/helpers/parse_link_title.js: -------------------------------------------------------------------------------- 1 | import { unescapeMd } from '../common/utils'; 2 | 3 | /** 4 | * Parse link title 5 | * 6 | * - on success it returns a string and updates state.pos; 7 | * - on failure it returns null 8 | * 9 | * @param {Object} state 10 | * @param {Number} pos 11 | * @api private 12 | */ 13 | 14 | export default function parseLinkTitle(state, pos) { 15 | var code, 16 | start = pos, 17 | max = state.posMax, 18 | marker = state.src.charCodeAt(pos); 19 | 20 | if (marker !== 0x22 /* " */ && marker !== 0x27 /* ' */ && marker !== 0x28 /* ( */) { return false; } 21 | 22 | pos++; 23 | 24 | // if opening marker is "(", switch it to closing marker ")" 25 | if (marker === 0x28) { marker = 0x29; } 26 | 27 | while (pos < max) { 28 | code = state.src.charCodeAt(pos); 29 | if (code === marker) { 30 | state.pos = pos + 1; 31 | state.linkContent = unescapeMd(state.src.slice(start + 1, pos)); 32 | return true; 33 | } 34 | if (code === 0x5C /* \ */ && pos + 1 < max) { 35 | pos += 2; 36 | continue; 37 | } 38 | 39 | pos++; 40 | } 41 | 42 | return false; 43 | }; 44 | -------------------------------------------------------------------------------- /lib/linkify.js: -------------------------------------------------------------------------------- 1 | // Autoconvert URL-like texts to links 2 | // 3 | // Currently restricted by `inline.validateLink()` to http/https/ftp 4 | // 5 | 6 | import Autolinker from 'autolinker'; 7 | 8 | 9 | var LINK_SCAN_RE = /www|@|\:\/\//; 10 | 11 | 12 | function isLinkOpen(str) { 13 | return /^\s]/i.test(str); 14 | } 15 | function isLinkClose(str) { 16 | return /^<\/a\s*>/i.test(str); 17 | } 18 | 19 | // Stupid fabric to avoid singletons, for thread safety. 20 | // Required for engines like Nashorn. 21 | // 22 | function createLinkifier() { 23 | var links = []; 24 | var autolinker = new Autolinker({ 25 | stripPrefix: false, 26 | url: true, 27 | email: true, 28 | replaceFn: function (match) { 29 | // Only collect matched strings but don't change anything. 30 | switch (match.getType()) { 31 | /*eslint default-case:0*/ 32 | case 'url': 33 | links.push({ 34 | text: match.matchedText, 35 | url: match.getUrl() 36 | }); 37 | break; 38 | case 'email': 39 | links.push({ 40 | text: match.matchedText, 41 | // normalize email protocol 42 | url: 'mailto:' + match.getEmail().replace(/^mailto:/i, '') 43 | }); 44 | break; 45 | } 46 | return false; 47 | } 48 | }); 49 | 50 | return { 51 | links: links, 52 | autolinker: autolinker 53 | }; 54 | } 55 | 56 | 57 | function parseTokens(state) { 58 | var i, j, l, tokens, token, text, nodes, ln, pos, level, htmlLinkLevel, 59 | blockTokens = state.tokens, 60 | linkifier = null, links, autolinker; 61 | 62 | for (j = 0, l = blockTokens.length; j < l; j++) { 63 | if (blockTokens[j].type !== 'inline') { continue; } 64 | tokens = blockTokens[j].children; 65 | 66 | htmlLinkLevel = 0; 67 | 68 | // We scan from the end, to keep position when new tags added. 69 | // Use reversed logic in links start/end match 70 | for (i = tokens.length - 1; i >= 0; i--) { 71 | token = tokens[i]; 72 | 73 | // Skip content of markdown links 74 | if (token.type === 'link_close') { 75 | i--; 76 | while (tokens[i].level !== token.level && tokens[i].type !== 'link_open') { 77 | i--; 78 | } 79 | continue; 80 | } 81 | 82 | // Skip content of html tag links 83 | if (token.type === 'htmltag') { 84 | if (isLinkOpen(token.content) && htmlLinkLevel > 0) { 85 | htmlLinkLevel--; 86 | } 87 | if (isLinkClose(token.content)) { 88 | htmlLinkLevel++; 89 | } 90 | } 91 | if (htmlLinkLevel > 0) { continue; } 92 | 93 | if (token.type === 'text' && LINK_SCAN_RE.test(token.content)) { 94 | 95 | // Init linkifier in lazy manner, only if required. 96 | if (!linkifier) { 97 | linkifier = createLinkifier(); 98 | links = linkifier.links; 99 | autolinker = linkifier.autolinker; 100 | } 101 | 102 | text = token.content; 103 | links.length = 0; 104 | autolinker.link(text); 105 | 106 | if (!links.length) { continue; } 107 | 108 | // Now split string to nodes 109 | nodes = []; 110 | level = token.level; 111 | 112 | for (ln = 0; ln < links.length; ln++) { 113 | 114 | if (!state.inline.validateLink(links[ln].url)) { continue; } 115 | 116 | pos = text.indexOf(links[ln].text); 117 | 118 | if (pos) { 119 | nodes.push({ 120 | type: 'text', 121 | content: text.slice(0, pos), 122 | level: level 123 | }); 124 | } 125 | nodes.push({ 126 | type: 'link_open', 127 | href: links[ln].url, 128 | title: '', 129 | level: level++ 130 | }); 131 | nodes.push({ 132 | type: 'text', 133 | content: links[ln].text, 134 | level: level 135 | }); 136 | nodes.push({ 137 | type: 'link_close', 138 | level: --level 139 | }); 140 | text = text.slice(pos + links[ln].text.length); 141 | } 142 | if (text.length) { 143 | nodes.push({ 144 | type: 'text', 145 | content: text, 146 | level: level 147 | }); 148 | } 149 | 150 | // replace current node 151 | blockTokens[j].children = tokens = [].concat(tokens.slice(0, i), nodes, tokens.slice(i + 1)); 152 | } 153 | } 154 | } 155 | }; 156 | 157 | export function linkify(md) { 158 | md.core.ruler.push('linkify', parseTokens); 159 | }; 160 | -------------------------------------------------------------------------------- /lib/parser_block.js: -------------------------------------------------------------------------------- 1 | import Ruler from './ruler'; 2 | import StateBlock from './rules_block/state_block'; 3 | 4 | import code from './rules_block/code'; 5 | import fences from './rules_block/fences'; 6 | import blockquote from './rules_block/blockquote'; 7 | import hr from './rules_block/hr'; 8 | import list from './rules_block/list'; 9 | import footnote from './rules_block/footnote'; 10 | import heading from './rules_block/heading'; 11 | import lheading from './rules_block/lheading'; 12 | import htmlblock from './rules_block/htmlblock'; 13 | import table from './rules_block/table'; 14 | import deflist from './rules_block/deflist'; 15 | import paragraph from './rules_block/paragraph'; 16 | 17 | /** 18 | * Parser rules 19 | */ 20 | 21 | var _rules = [ 22 | [ 'code', code ], 23 | [ 'fences', fences, [ 'paragraph', 'blockquote', 'list' ] ], 24 | [ 'blockquote', blockquote, [ 'paragraph', 'blockquote', 'list' ] ], 25 | [ 'hr', hr, [ 'paragraph', 'blockquote', 'list' ] ], 26 | [ 'list', list, [ 'paragraph', 'blockquote' ] ], 27 | [ 'footnote', footnote, [ 'paragraph' ] ], 28 | [ 'heading', heading, [ 'paragraph', 'blockquote' ] ], 29 | [ 'lheading', lheading ], 30 | [ 'htmlblock', htmlblock, [ 'paragraph', 'blockquote' ] ], 31 | [ 'table', table, [ 'paragraph' ] ], 32 | [ 'deflist', deflist, [ 'paragraph' ] ], 33 | [ 'paragraph', paragraph ] 34 | ]; 35 | 36 | /** 37 | * Block Parser class 38 | * 39 | * @api private 40 | */ 41 | 42 | export default function ParserBlock() { 43 | this.ruler = new Ruler(); 44 | for (var i = 0; i < _rules.length; i++) { 45 | this.ruler.push(_rules[i][0], _rules[i][1], { 46 | alt: (_rules[i][2] || []).slice() 47 | }); 48 | } 49 | } 50 | 51 | /** 52 | * Generate tokens for the given input range. 53 | * 54 | * @param {Object} `state` Has properties like `src`, `parser`, `options` etc 55 | * @param {Number} `startLine` 56 | * @param {Number} `endLine` 57 | * @api private 58 | */ 59 | 60 | ParserBlock.prototype.tokenize = function (state, startLine, endLine) { 61 | var rules = this.ruler.getRules(''); 62 | var len = rules.length; 63 | var line = startLine; 64 | var hasEmptyLines = false; 65 | var ok, i; 66 | 67 | while (line < endLine) { 68 | state.line = line = state.skipEmptyLines(line); 69 | if (line >= endLine) { 70 | break; 71 | } 72 | 73 | // Termination condition for nested calls. 74 | // Nested calls currently used for blockquotes & lists 75 | if (state.tShift[line] < state.blkIndent) { 76 | break; 77 | } 78 | 79 | // Try all possible rules. 80 | // On success, rule should: 81 | // 82 | // - update `state.line` 83 | // - update `state.tokens` 84 | // - return true 85 | 86 | for (i = 0; i < len; i++) { 87 | ok = rules[i](state, line, endLine, false); 88 | if (ok) { 89 | break; 90 | } 91 | } 92 | 93 | // set state.tight iff we had an empty line before current tag 94 | // i.e. latest empty line should not count 95 | state.tight = !hasEmptyLines; 96 | 97 | // paragraph might "eat" one newline after it in nested lists 98 | if (state.isEmpty(state.line - 1)) { 99 | hasEmptyLines = true; 100 | } 101 | 102 | line = state.line; 103 | 104 | if (line < endLine && state.isEmpty(line)) { 105 | hasEmptyLines = true; 106 | line++; 107 | 108 | // two empty lines should stop the parser in list mode 109 | if (line < endLine && state.parentType === 'list' && state.isEmpty(line)) { break; } 110 | state.line = line; 111 | } 112 | } 113 | }; 114 | 115 | var TABS_SCAN_RE = /[\n\t]/g; 116 | var NEWLINES_RE = /\r[\n\u0085]|[\u2424\u2028\u0085]/g; 117 | var SPACES_RE = /\u00a0/g; 118 | 119 | /** 120 | * Tokenize the given `str`. 121 | * 122 | * @param {String} `str` Source string 123 | * @param {Object} `options` 124 | * @param {Object} `env` 125 | * @param {Array} `outTokens` 126 | * @api private 127 | */ 128 | 129 | ParserBlock.prototype.parse = function (str, options, env, outTokens) { 130 | var state, lineStart = 0, lastTabPos = 0; 131 | if (!str) { return []; } 132 | 133 | // Normalize spaces 134 | str = str.replace(SPACES_RE, ' '); 135 | 136 | // Normalize newlines 137 | str = str.replace(NEWLINES_RE, '\n'); 138 | 139 | // Replace tabs with proper number of spaces (1..4) 140 | if (str.indexOf('\t') >= 0) { 141 | str = str.replace(TABS_SCAN_RE, function (match, offset) { 142 | var result; 143 | if (str.charCodeAt(offset) === 0x0A) { 144 | lineStart = offset + 1; 145 | lastTabPos = 0; 146 | return match; 147 | } 148 | result = ' '.slice((offset - lineStart - lastTabPos) % 4); 149 | lastTabPos = offset - lineStart + 1; 150 | return result; 151 | }); 152 | } 153 | 154 | state = new StateBlock(str, this, options, env, outTokens); 155 | this.tokenize(state, state.line, state.lineMax); 156 | }; 157 | -------------------------------------------------------------------------------- /lib/parser_core.js: -------------------------------------------------------------------------------- 1 | import Ruler from './ruler'; 2 | 3 | import block from './rules_core/block'; 4 | import abbr from './rules_core/abbr'; 5 | import references from './rules_core/references'; 6 | import inline from './rules_core/inline'; 7 | import footnote_tail from './rules_core/footnote_tail'; 8 | import abbr2 from './rules_core/abbr2'; 9 | import replacements from './rules_core/replacements'; 10 | import smartquotes from './rules_core/smartquotes'; 11 | 12 | /** 13 | * Core parser `rules` 14 | */ 15 | 16 | var _rules = [ 17 | [ 'block', block ], 18 | [ 'abbr', abbr ], 19 | [ 'references', references ], 20 | [ 'inline', inline ], 21 | [ 'footnote_tail', footnote_tail ], 22 | [ 'abbr2', abbr2 ], 23 | [ 'replacements', replacements ], 24 | [ 'smartquotes', smartquotes ], 25 | ]; 26 | 27 | /** 28 | * Class for top level (`core`) parser rules 29 | * 30 | * @api private 31 | */ 32 | 33 | export default function Core() { 34 | this.options = {}; 35 | this.ruler = new Ruler(); 36 | for (var i = 0; i < _rules.length; i++) { 37 | this.ruler.push(_rules[i][0], _rules[i][1]); 38 | } 39 | } 40 | 41 | /** 42 | * Process rules with the given `state` 43 | * 44 | * @param {Object} `state` 45 | * @api private 46 | */ 47 | 48 | Core.prototype.process = function (state) { 49 | var i, l, rules; 50 | rules = this.ruler.getRules(''); 51 | for (i = 0, l = rules.length; i < l; i++) { 52 | rules[i](state); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /lib/parser_inline.js: -------------------------------------------------------------------------------- 1 | import Ruler from './ruler'; 2 | import StateInline from './rules_inline/state_inline'; 3 | import * as utils from './common/utils'; 4 | 5 | import text from './rules_inline/text'; 6 | import newline from './rules_inline/newline'; 7 | import escape from './rules_inline/escape'; 8 | import backticks from './rules_inline/backticks'; 9 | import del from './rules_inline/del'; 10 | import ins from './rules_inline/ins'; 11 | import mark from './rules_inline/mark'; 12 | import emphasis from './rules_inline/emphasis'; 13 | import sub from './rules_inline/sub'; 14 | import sup from './rules_inline/sup'; 15 | import links from './rules_inline/links'; 16 | import footnote_inline from './rules_inline/footnote_inline'; 17 | import footnote_ref from './rules_inline/footnote_ref'; 18 | import autolink from './rules_inline/autolink'; 19 | import htmltag from './rules_inline/htmltag'; 20 | import entity from './rules_inline/entity'; 21 | 22 | /** 23 | * Inline Parser `rules` 24 | */ 25 | 26 | var _rules = [ 27 | [ 'text', text ], 28 | [ 'newline', newline ], 29 | [ 'escape', escape ], 30 | [ 'backticks', backticks ], 31 | [ 'del', del ], 32 | [ 'ins', ins ], 33 | [ 'mark', mark ], 34 | [ 'emphasis', emphasis ], 35 | [ 'sub', sub ], 36 | [ 'sup', sup ], 37 | [ 'links', links ], 38 | [ 'footnote_inline', footnote_inline ], 39 | [ 'footnote_ref', footnote_ref ], 40 | [ 'autolink', autolink ], 41 | [ 'htmltag', htmltag ], 42 | [ 'entity', entity ] 43 | ]; 44 | 45 | /** 46 | * Inline Parser class. Note that link validation is stricter 47 | * in Remarkable than what is specified by CommonMark. If you 48 | * want to change this you can use a custom validator. 49 | * 50 | * @api private 51 | */ 52 | 53 | export default function ParserInline() { 54 | this.ruler = new Ruler(); 55 | for (var i = 0; i < _rules.length; i++) { 56 | this.ruler.push(_rules[i][0], _rules[i][1]); 57 | } 58 | 59 | // Can be overridden with a custom validator 60 | this.validateLink = validateLink; 61 | } 62 | 63 | /** 64 | * Skip a single token by running all rules in validation mode. 65 | * Returns `true` if any rule reports success. 66 | * 67 | * @param {Object} `state` 68 | * @api privage 69 | */ 70 | 71 | ParserInline.prototype.skipToken = function (state) { 72 | var rules = this.ruler.getRules(''); 73 | var len = rules.length; 74 | var pos = state.pos; 75 | var i, cached_pos; 76 | 77 | if ((cached_pos = state.cacheGet(pos)) > 0) { 78 | state.pos = cached_pos; 79 | return; 80 | } 81 | 82 | for (i = 0; i < len; i++) { 83 | if (rules[i](state, true)) { 84 | state.cacheSet(pos, state.pos); 85 | return; 86 | } 87 | } 88 | 89 | state.pos++; 90 | state.cacheSet(pos, state.pos); 91 | }; 92 | 93 | /** 94 | * Generate tokens for the given input range. 95 | * 96 | * @param {Object} `state` 97 | * @api private 98 | */ 99 | 100 | ParserInline.prototype.tokenize = function (state) { 101 | var rules = this.ruler.getRules(''); 102 | var len = rules.length; 103 | var end = state.posMax; 104 | var ok, i; 105 | 106 | while (state.pos < end) { 107 | 108 | // Try all possible rules. 109 | // On success, the rule should: 110 | // 111 | // - update `state.pos` 112 | // - update `state.tokens` 113 | // - return true 114 | for (i = 0; i < len; i++) { 115 | ok = rules[i](state, false); 116 | 117 | if (ok) { 118 | break; 119 | } 120 | } 121 | 122 | if (ok) { 123 | if (state.pos >= end) { break; } 124 | continue; 125 | } 126 | 127 | state.pending += state.src[state.pos++]; 128 | } 129 | 130 | if (state.pending) { 131 | state.pushPending(); 132 | } 133 | }; 134 | 135 | /** 136 | * Parse the given input string. 137 | * 138 | * @param {String} `str` 139 | * @param {Object} `options` 140 | * @param {Object} `env` 141 | * @param {Array} `outTokens` 142 | * @api private 143 | */ 144 | 145 | ParserInline.prototype.parse = function (str, options, env, outTokens) { 146 | var state = new StateInline(str, this, options, env, outTokens); 147 | this.tokenize(state); 148 | }; 149 | 150 | /** 151 | * Validate the given `url` by checking for bad protocols. 152 | * 153 | * @param {String} `url` 154 | * @return {Boolean} 155 | */ 156 | 157 | function validateLink(url) { 158 | var BAD_PROTOCOLS = [ 'vbscript', 'javascript', 'file', 'data' ]; 159 | var str = url.trim().toLowerCase(); 160 | // Care about digital entities "javascript:alert(1)" 161 | str = utils.replaceEntities(str); 162 | if (str.indexOf(':') !== -1 && BAD_PROTOCOLS.indexOf(str.split(':')[0]) !== -1) { 163 | return false; 164 | } 165 | return true; 166 | } 167 | -------------------------------------------------------------------------------- /lib/renderer.js: -------------------------------------------------------------------------------- 1 | import * as utils from './common/utils'; 2 | import rules from './rules'; 3 | 4 | /** 5 | * Renderer class. Renders HTML and exposes `rules` to allow 6 | * local modifications. 7 | */ 8 | 9 | export default function Renderer() { 10 | this.rules = utils.assign({}, rules); 11 | 12 | // exported helper, for custom rules only 13 | this.getBreak = rules.getBreak; 14 | } 15 | 16 | /** 17 | * Render a string of inline HTML with the given `tokens` and 18 | * `options`. 19 | * 20 | * @param {Array} `tokens` 21 | * @param {Object} `options` 22 | * @param {Object} `env` 23 | * @return {String} 24 | * @api public 25 | */ 26 | 27 | Renderer.prototype.renderInline = function (tokens, options, env) { 28 | var _rules = this.rules; 29 | var len = tokens.length, i = 0; 30 | var result = ''; 31 | 32 | while (len--) { 33 | result += _rules[tokens[i].type](tokens, i++, options, env, this); 34 | } 35 | 36 | return result; 37 | }; 38 | 39 | /** 40 | * Render a string of HTML with the given `tokens` and 41 | * `options`. 42 | * 43 | * @param {Array} `tokens` 44 | * @param {Object} `options` 45 | * @param {Object} `env` 46 | * @return {String} 47 | * @api public 48 | */ 49 | 50 | Renderer.prototype.render = function (tokens, options, env) { 51 | var _rules = this.rules; 52 | var len = tokens.length, i = -1; 53 | var result = ''; 54 | 55 | while (++i < len) { 56 | if (tokens[i].type === 'inline') { 57 | result += this.renderInline(tokens[i].children, options, env); 58 | } else { 59 | result += _rules[tokens[i].type](tokens, i, options, env, this); 60 | } 61 | } 62 | return result; 63 | }; 64 | -------------------------------------------------------------------------------- /lib/rules_block/blockquote.js: -------------------------------------------------------------------------------- 1 | // Block quotes 2 | 3 | export default function blockquote(state, startLine, endLine, silent) { 4 | var nextLine, lastLineEmpty, oldTShift, oldBMarks, oldIndent, oldParentType, lines, 5 | terminatorRules, 6 | i, l, terminate, 7 | pos = state.bMarks[startLine] + state.tShift[startLine], 8 | max = state.eMarks[startLine]; 9 | 10 | if (pos > max) { return false; } 11 | 12 | // check the block quote marker 13 | if (state.src.charCodeAt(pos++) !== 0x3E/* > */) { return false; } 14 | 15 | if (state.level >= state.options.maxNesting) { return false; } 16 | 17 | // we know that it's going to be a valid blockquote, 18 | // so no point trying to find the end of it in silent mode 19 | if (silent) { return true; } 20 | 21 | // skip one optional space after '>' 22 | if (state.src.charCodeAt(pos) === 0x20) { pos++; } 23 | 24 | oldIndent = state.blkIndent; 25 | state.blkIndent = 0; 26 | 27 | oldBMarks = [ state.bMarks[startLine] ]; 28 | state.bMarks[startLine] = pos; 29 | 30 | // check if we have an empty blockquote 31 | pos = pos < max ? state.skipSpaces(pos) : pos; 32 | lastLineEmpty = pos >= max; 33 | 34 | oldTShift = [ state.tShift[startLine] ]; 35 | state.tShift[startLine] = pos - state.bMarks[startLine]; 36 | 37 | terminatorRules = state.parser.ruler.getRules('blockquote'); 38 | 39 | // Search the end of the block 40 | // 41 | // Block ends with either: 42 | // 1. an empty line outside: 43 | // ``` 44 | // > test 45 | // 46 | // ``` 47 | // 2. an empty line inside: 48 | // ``` 49 | // > 50 | // test 51 | // ``` 52 | // 3. another tag 53 | // ``` 54 | // > test 55 | // - - - 56 | // ``` 57 | for (nextLine = startLine + 1; nextLine < endLine; nextLine++) { 58 | pos = state.bMarks[nextLine] + state.tShift[nextLine]; 59 | max = state.eMarks[nextLine]; 60 | 61 | if (pos >= max) { 62 | // Case 1: line is not inside the blockquote, and this line is empty. 63 | break; 64 | } 65 | 66 | if (state.src.charCodeAt(pos++) === 0x3E/* > */) { 67 | // This line is inside the blockquote. 68 | 69 | // skip one optional space after '>' 70 | if (state.src.charCodeAt(pos) === 0x20) { pos++; } 71 | 72 | oldBMarks.push(state.bMarks[nextLine]); 73 | state.bMarks[nextLine] = pos; 74 | 75 | pos = pos < max ? state.skipSpaces(pos) : pos; 76 | lastLineEmpty = pos >= max; 77 | 78 | oldTShift.push(state.tShift[nextLine]); 79 | state.tShift[nextLine] = pos - state.bMarks[nextLine]; 80 | continue; 81 | } 82 | 83 | // Case 2: line is not inside the blockquote, and the last line was empty. 84 | if (lastLineEmpty) { break; } 85 | 86 | // Case 3: another tag found. 87 | terminate = false; 88 | for (i = 0, l = terminatorRules.length; i < l; i++) { 89 | if (terminatorRules[i](state, nextLine, endLine, true)) { 90 | terminate = true; 91 | break; 92 | } 93 | } 94 | if (terminate) { break; } 95 | 96 | oldBMarks.push(state.bMarks[nextLine]); 97 | oldTShift.push(state.tShift[nextLine]); 98 | 99 | // A negative number means that this is a paragraph continuation; 100 | // 101 | // Any negative number will do the job here, but it's better for it 102 | // to be large enough to make any bugs obvious. 103 | state.tShift[nextLine] = -1337; 104 | } 105 | 106 | oldParentType = state.parentType; 107 | state.parentType = 'blockquote'; 108 | state.tokens.push({ 109 | type: 'blockquote_open', 110 | lines: lines = [ startLine, 0 ], 111 | level: state.level++ 112 | }); 113 | state.parser.tokenize(state, startLine, nextLine); 114 | state.tokens.push({ 115 | type: 'blockquote_close', 116 | level: --state.level 117 | }); 118 | state.parentType = oldParentType; 119 | lines[1] = state.line; 120 | 121 | // Restore original tShift; this might not be necessary since the parser 122 | // has already been here, but just to make sure we can do that. 123 | for (i = 0; i < oldTShift.length; i++) { 124 | state.bMarks[i + startLine] = oldBMarks[i]; 125 | state.tShift[i + startLine] = oldTShift[i]; 126 | } 127 | state.blkIndent = oldIndent; 128 | 129 | return true; 130 | }; 131 | -------------------------------------------------------------------------------- /lib/rules_block/code.js: -------------------------------------------------------------------------------- 1 | // Code block (4 spaces padded) 2 | 3 | export default function code(state, startLine, endLine/*, silent*/) { 4 | var nextLine, last; 5 | 6 | if (state.tShift[startLine] - state.blkIndent < 4) { return false; } 7 | 8 | last = nextLine = startLine + 1; 9 | 10 | while (nextLine < endLine) { 11 | if (state.isEmpty(nextLine)) { 12 | nextLine++; 13 | continue; 14 | } 15 | if (state.tShift[nextLine] - state.blkIndent >= 4) { 16 | nextLine++; 17 | last = nextLine; 18 | continue; 19 | } 20 | break; 21 | } 22 | 23 | state.line = nextLine; 24 | state.tokens.push({ 25 | type: 'code', 26 | content: state.getLines(startLine, last, 4 + state.blkIndent, true), 27 | block: true, 28 | lines: [ startLine, state.line ], 29 | level: state.level 30 | }); 31 | 32 | return true; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/rules_block/fences.js: -------------------------------------------------------------------------------- 1 | // fences (``` lang, ~~~ lang) 2 | 3 | export default function fences(state, startLine, endLine, silent) { 4 | var marker, len, params, nextLine, mem, 5 | haveEndMarker = false, 6 | pos = state.bMarks[startLine] + state.tShift[startLine], 7 | max = state.eMarks[startLine]; 8 | 9 | if (pos + 3 > max) { return false; } 10 | 11 | marker = state.src.charCodeAt(pos); 12 | 13 | if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) { 14 | return false; 15 | } 16 | 17 | // scan marker length 18 | mem = pos; 19 | pos = state.skipChars(pos, marker); 20 | 21 | len = pos - mem; 22 | 23 | if (len < 3) { return false; } 24 | 25 | params = state.src.slice(pos, max).trim(); 26 | 27 | if (params.indexOf('`') >= 0) { return false; } 28 | 29 | // Since start is found, we can report success here in validation mode 30 | if (silent) { return true; } 31 | 32 | // search end of block 33 | nextLine = startLine; 34 | 35 | for (;;) { 36 | nextLine++; 37 | if (nextLine >= endLine) { 38 | // unclosed block should be autoclosed by end of document. 39 | // also block seems to be autoclosed by end of parent 40 | break; 41 | } 42 | 43 | pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]; 44 | max = state.eMarks[nextLine]; 45 | 46 | if (pos < max && state.tShift[nextLine] < state.blkIndent) { 47 | // non-empty line with negative indent should stop the list: 48 | // - ``` 49 | // test 50 | break; 51 | } 52 | 53 | if (state.src.charCodeAt(pos) !== marker) { continue; } 54 | 55 | if (state.tShift[nextLine] - state.blkIndent >= 4) { 56 | // closing fence should be indented less than 4 spaces 57 | continue; 58 | } 59 | 60 | pos = state.skipChars(pos, marker); 61 | 62 | // closing code fence must be at least as long as the opening one 63 | if (pos - mem < len) { continue; } 64 | 65 | // make sure tail has spaces only 66 | pos = state.skipSpaces(pos); 67 | 68 | if (pos < max) { continue; } 69 | 70 | haveEndMarker = true; 71 | // found! 72 | break; 73 | } 74 | 75 | // If a fence has heading spaces, they should be removed from its inner block 76 | len = state.tShift[startLine]; 77 | 78 | state.line = nextLine + (haveEndMarker ? 1 : 0); 79 | state.tokens.push({ 80 | type: 'fence', 81 | params: params, 82 | content: state.getLines(startLine + 1, nextLine, len, true), 83 | lines: [ startLine, state.line ], 84 | level: state.level 85 | }); 86 | 87 | return true; 88 | }; 89 | -------------------------------------------------------------------------------- /lib/rules_block/footnote.js: -------------------------------------------------------------------------------- 1 | // Process footnote reference list 2 | 3 | export default function footnote(state, startLine, endLine, silent) { 4 | var oldBMark, oldTShift, oldParentType, pos, label, 5 | start = state.bMarks[startLine] + state.tShift[startLine], 6 | max = state.eMarks[startLine]; 7 | 8 | // line should be at least 5 chars - "[^x]:" 9 | if (start + 4 > max) { return false; } 10 | 11 | if (state.src.charCodeAt(start) !== 0x5B/* [ */) { return false; } 12 | if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) { return false; } 13 | if (state.level >= state.options.maxNesting) { return false; } 14 | 15 | for (pos = start + 2; pos < max; pos++) { 16 | if (state.src.charCodeAt(pos) === 0x20) { return false; } 17 | if (state.src.charCodeAt(pos) === 0x5D /* ] */) { 18 | break; 19 | } 20 | } 21 | 22 | if (pos === start + 2) { return false; } // no empty footnote labels 23 | if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 0x3A /* : */) { return false; } 24 | if (silent) { return true; } 25 | pos++; 26 | 27 | if (!state.env.footnotes) { state.env.footnotes = {}; } 28 | if (!state.env.footnotes.refs) { state.env.footnotes.refs = {}; } 29 | label = state.src.slice(start + 2, pos - 2); 30 | state.env.footnotes.refs[':' + label] = -1; 31 | 32 | state.tokens.push({ 33 | type: 'footnote_reference_open', 34 | label: label, 35 | level: state.level++ 36 | }); 37 | 38 | oldBMark = state.bMarks[startLine]; 39 | oldTShift = state.tShift[startLine]; 40 | oldParentType = state.parentType; 41 | state.tShift[startLine] = state.skipSpaces(pos) - pos; 42 | state.bMarks[startLine] = pos; 43 | state.blkIndent += 4; 44 | state.parentType = 'footnote'; 45 | 46 | if (state.tShift[startLine] < state.blkIndent) { 47 | state.tShift[startLine] += state.blkIndent; 48 | state.bMarks[startLine] -= state.blkIndent; 49 | } 50 | 51 | state.parser.tokenize(state, startLine, endLine, true); 52 | 53 | state.parentType = oldParentType; 54 | state.blkIndent -= 4; 55 | state.tShift[startLine] = oldTShift; 56 | state.bMarks[startLine] = oldBMark; 57 | 58 | state.tokens.push({ 59 | type: 'footnote_reference_close', 60 | level: --state.level 61 | }); 62 | 63 | return true; 64 | }; 65 | -------------------------------------------------------------------------------- /lib/rules_block/heading.js: -------------------------------------------------------------------------------- 1 | // heading (#, ##, ...) 2 | 3 | export default function heading(state, startLine, endLine, silent) { 4 | var ch, level, tmp, 5 | pos = state.bMarks[startLine] + state.tShift[startLine], 6 | max = state.eMarks[startLine]; 7 | 8 | if (pos >= max) { return false; } 9 | 10 | ch = state.src.charCodeAt(pos); 11 | 12 | if (ch !== 0x23/* # */ || pos >= max) { return false; } 13 | 14 | // count heading level 15 | level = 1; 16 | ch = state.src.charCodeAt(++pos); 17 | while (ch === 0x23/* # */ && pos < max && level <= 6) { 18 | level++; 19 | ch = state.src.charCodeAt(++pos); 20 | } 21 | 22 | if (level > 6 || (pos < max && ch !== 0x20/* space */)) { return false; } 23 | 24 | if (silent) { return true; } 25 | 26 | // Let's cut tails like ' ### ' from the end of string 27 | 28 | max = state.skipCharsBack(max, 0x20, pos); // space 29 | tmp = state.skipCharsBack(max, 0x23, pos); // # 30 | if (tmp > pos && state.src.charCodeAt(tmp - 1) === 0x20/* space */) { 31 | max = tmp; 32 | } 33 | 34 | state.line = startLine + 1; 35 | 36 | state.tokens.push({ type: 'heading_open', 37 | hLevel: level, 38 | lines: [ startLine, state.line ], 39 | level: state.level 40 | }); 41 | 42 | // only if header is not empty 43 | if (pos < max) { 44 | state.tokens.push({ 45 | type: 'inline', 46 | content: state.src.slice(pos, max).trim(), 47 | level: state.level + 1, 48 | lines: [ startLine, state.line ], 49 | children: [] 50 | }); 51 | } 52 | state.tokens.push({ type: 'heading_close', hLevel: level, level: state.level }); 53 | 54 | return true; 55 | }; 56 | -------------------------------------------------------------------------------- /lib/rules_block/hr.js: -------------------------------------------------------------------------------- 1 | // Horizontal rule 2 | 3 | export default function hr(state, startLine, endLine, silent) { 4 | var marker, cnt, ch, 5 | pos = state.bMarks[startLine], 6 | max = state.eMarks[startLine]; 7 | 8 | pos += state.tShift[startLine]; 9 | 10 | if (pos > max) { return false; } 11 | 12 | marker = state.src.charCodeAt(pos++); 13 | 14 | // Check hr marker 15 | if (marker !== 0x2A/* * */ && 16 | marker !== 0x2D/* - */ && 17 | marker !== 0x5F/* _ */) { 18 | return false; 19 | } 20 | 21 | // markers can be mixed with spaces, but there should be at least 3 one 22 | 23 | cnt = 1; 24 | while (pos < max) { 25 | ch = state.src.charCodeAt(pos++); 26 | if (ch !== marker && ch !== 0x20/* space */) { return false; } 27 | if (ch === marker) { cnt++; } 28 | } 29 | 30 | if (cnt < 3) { return false; } 31 | 32 | if (silent) { return true; } 33 | 34 | state.line = startLine + 1; 35 | state.tokens.push({ 36 | type: 'hr', 37 | lines: [ startLine, state.line ], 38 | level: state.level 39 | }); 40 | 41 | return true; 42 | }; 43 | -------------------------------------------------------------------------------- /lib/rules_block/htmlblock.js: -------------------------------------------------------------------------------- 1 | // HTML block 2 | 3 | import block_names from '../common/html_blocks'; 4 | 5 | 6 | var HTML_TAG_OPEN_RE = /^<([a-zA-Z]{1,15})[\s\/>]/; 7 | var HTML_TAG_CLOSE_RE = /^<\/([a-zA-Z]{1,15})[\s>]/; 8 | 9 | function isLetter(ch) { 10 | /*eslint no-bitwise:0*/ 11 | var lc = ch | 0x20; // to lower case 12 | return (lc >= 0x61/* a */) && (lc <= 0x7a/* z */); 13 | } 14 | 15 | export default function htmlblock(state, startLine, endLine, silent) { 16 | var ch, match, nextLine, 17 | pos = state.bMarks[startLine], 18 | max = state.eMarks[startLine], 19 | shift = state.tShift[startLine]; 20 | 21 | pos += shift; 22 | 23 | if (!state.options.html) { return false; } 24 | 25 | if (shift > 3 || pos + 2 >= max) { return false; } 26 | 27 | if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false; } 28 | 29 | ch = state.src.charCodeAt(pos + 1); 30 | 31 | if (ch === 0x21/* ! */ || ch === 0x3F/* ? */) { 32 | // Directive start / comment start / processing instruction start 33 | if (silent) { return true; } 34 | 35 | } else if (ch === 0x2F/* / */ || isLetter(ch)) { 36 | 37 | // Probably start or end of tag 38 | if (ch === 0x2F/* \ */) { 39 | // closing tag 40 | match = state.src.slice(pos, max).match(HTML_TAG_CLOSE_RE); 41 | if (!match) { return false; } 42 | } else { 43 | // opening tag 44 | match = state.src.slice(pos, max).match(HTML_TAG_OPEN_RE); 45 | if (!match) { return false; } 46 | } 47 | // Make sure tag name is valid 48 | if (block_names[match[1].toLowerCase()] !== true) { return false; } 49 | if (silent) { return true; } 50 | 51 | } else { 52 | return false; 53 | } 54 | 55 | // If we are here - we detected HTML block. 56 | // Let's roll down till empty line (block end). 57 | nextLine = startLine + 1; 58 | while (nextLine < state.lineMax && !state.isEmpty(nextLine)) { 59 | nextLine++; 60 | } 61 | 62 | state.line = nextLine; 63 | state.tokens.push({ 64 | type: 'htmlblock', 65 | level: state.level, 66 | lines: [ startLine, state.line ], 67 | content: state.getLines(startLine, nextLine, 0, true) 68 | }); 69 | 70 | return true; 71 | }; 72 | -------------------------------------------------------------------------------- /lib/rules_block/lheading.js: -------------------------------------------------------------------------------- 1 | // lheading (---, ===) 2 | 3 | export default function lheading(state, startLine, endLine/*, silent*/) { 4 | var marker, pos, max, 5 | next = startLine + 1; 6 | 7 | if (next >= endLine) { return false; } 8 | if (state.tShift[next] < state.blkIndent) { return false; } 9 | 10 | // Scan next line 11 | 12 | if (state.tShift[next] - state.blkIndent > 3) { return false; } 13 | 14 | pos = state.bMarks[next] + state.tShift[next]; 15 | max = state.eMarks[next]; 16 | 17 | if (pos >= max) { return false; } 18 | 19 | marker = state.src.charCodeAt(pos); 20 | 21 | if (marker !== 0x2D/* - */ && marker !== 0x3D/* = */) { return false; } 22 | 23 | pos = state.skipChars(pos, marker); 24 | 25 | pos = state.skipSpaces(pos); 26 | 27 | if (pos < max) { return false; } 28 | 29 | pos = state.bMarks[startLine] + state.tShift[startLine]; 30 | 31 | state.line = next + 1; 32 | state.tokens.push({ 33 | type: 'heading_open', 34 | hLevel: marker === 0x3D/* = */ ? 1 : 2, 35 | lines: [ startLine, state.line ], 36 | level: state.level 37 | }); 38 | state.tokens.push({ 39 | type: 'inline', 40 | content: state.src.slice(pos, state.eMarks[startLine]).trim(), 41 | level: state.level + 1, 42 | lines: [ startLine, state.line - 1 ], 43 | children: [] 44 | }); 45 | state.tokens.push({ 46 | type: 'heading_close', 47 | hLevel: marker === 0x3D/* = */ ? 1 : 2, 48 | level: state.level 49 | }); 50 | 51 | return true; 52 | }; 53 | -------------------------------------------------------------------------------- /lib/rules_block/paragraph.js: -------------------------------------------------------------------------------- 1 | // Paragraph 2 | 3 | export default function paragraph(state, startLine/*, endLine*/) { 4 | var endLine, content, terminate, i, l, 5 | nextLine = startLine + 1, 6 | terminatorRules; 7 | 8 | endLine = state.lineMax; 9 | 10 | // jump line-by-line until empty one or EOF 11 | if (nextLine < endLine && !state.isEmpty(nextLine)) { 12 | terminatorRules = state.parser.ruler.getRules('paragraph'); 13 | 14 | for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { 15 | // this would be a code block normally, but after paragraph 16 | // it's considered a lazy continuation regardless of what's there 17 | if (state.tShift[nextLine] - state.blkIndent > 3) { continue; } 18 | 19 | // Some tags can terminate paragraph without empty line. 20 | terminate = false; 21 | for (i = 0, l = terminatorRules.length; i < l; i++) { 22 | if (terminatorRules[i](state, nextLine, endLine, true)) { 23 | terminate = true; 24 | break; 25 | } 26 | } 27 | if (terminate) { break; } 28 | } 29 | } 30 | 31 | content = state.getLines(startLine, nextLine, state.blkIndent, false).trim(); 32 | 33 | state.line = nextLine; 34 | if (content.length) { 35 | state.tokens.push({ 36 | type: 'paragraph_open', 37 | tight: false, 38 | lines: [ startLine, state.line ], 39 | level: state.level 40 | }); 41 | state.tokens.push({ 42 | type: 'inline', 43 | content: content, 44 | level: state.level + 1, 45 | lines: [ startLine, state.line ], 46 | children: [] 47 | }); 48 | state.tokens.push({ 49 | type: 'paragraph_close', 50 | tight: false, 51 | level: state.level 52 | }); 53 | } 54 | 55 | return true; 56 | }; 57 | -------------------------------------------------------------------------------- /lib/rules_block/state_block.js: -------------------------------------------------------------------------------- 1 | // Parser state class 2 | 3 | export default function StateBlock(src, parser, options, env, tokens) { 4 | var ch, s, start, pos, len, indent, indent_found; 5 | 6 | this.src = src; 7 | 8 | // Shortcuts to simplify nested calls 9 | this.parser = parser; 10 | 11 | this.options = options; 12 | 13 | this.env = env; 14 | 15 | // 16 | // Internal state vartiables 17 | // 18 | 19 | this.tokens = tokens; 20 | 21 | this.bMarks = []; // line begin offsets for fast jumps 22 | this.eMarks = []; // line end offsets for fast jumps 23 | this.tShift = []; // indent for each line 24 | 25 | // block parser variables 26 | this.blkIndent = 0; // required block content indent 27 | // (for example, if we are in list) 28 | this.line = 0; // line index in src 29 | this.lineMax = 0; // lines count 30 | this.tight = false; // loose/tight mode for lists 31 | this.parentType = 'root'; // if `list`, block parser stops on two newlines 32 | this.ddIndent = -1; // indent of the current dd block (-1 if there isn't any) 33 | 34 | this.level = 0; 35 | 36 | // renderer 37 | this.result = ''; 38 | 39 | // Create caches 40 | // Generate markers. 41 | s = this.src; 42 | indent = 0; 43 | indent_found = false; 44 | 45 | for (start = pos = indent = 0, len = s.length; pos < len; pos++) { 46 | ch = s.charCodeAt(pos); 47 | 48 | if (!indent_found) { 49 | if (ch === 0x20/* space */) { 50 | indent++; 51 | continue; 52 | } else { 53 | indent_found = true; 54 | } 55 | } 56 | 57 | if (ch === 0x0A || pos === len - 1) { 58 | if (ch !== 0x0A) { pos++; } 59 | this.bMarks.push(start); 60 | this.eMarks.push(pos); 61 | this.tShift.push(indent); 62 | 63 | indent_found = false; 64 | indent = 0; 65 | start = pos + 1; 66 | } 67 | } 68 | 69 | // Push fake entry to simplify cache bounds checks 70 | this.bMarks.push(s.length); 71 | this.eMarks.push(s.length); 72 | this.tShift.push(0); 73 | 74 | this.lineMax = this.bMarks.length - 1; // don't count last fake line 75 | } 76 | 77 | StateBlock.prototype.isEmpty = function isEmpty(line) { 78 | return this.bMarks[line] + this.tShift[line] >= this.eMarks[line]; 79 | }; 80 | 81 | StateBlock.prototype.skipEmptyLines = function skipEmptyLines(from) { 82 | for (var max = this.lineMax; from < max; from++) { 83 | if (this.bMarks[from] + this.tShift[from] < this.eMarks[from]) { 84 | break; 85 | } 86 | } 87 | return from; 88 | }; 89 | 90 | // Skip spaces from given position. 91 | StateBlock.prototype.skipSpaces = function skipSpaces(pos) { 92 | for (var max = this.src.length; pos < max; pos++) { 93 | if (this.src.charCodeAt(pos) !== 0x20/* space */) { break; } 94 | } 95 | return pos; 96 | }; 97 | 98 | // Skip char codes from given position 99 | StateBlock.prototype.skipChars = function skipChars(pos, code) { 100 | for (var max = this.src.length; pos < max; pos++) { 101 | if (this.src.charCodeAt(pos) !== code) { break; } 102 | } 103 | return pos; 104 | }; 105 | 106 | // Skip char codes reverse from given position - 1 107 | StateBlock.prototype.skipCharsBack = function skipCharsBack(pos, code, min) { 108 | if (pos <= min) { return pos; } 109 | 110 | while (pos > min) { 111 | if (code !== this.src.charCodeAt(--pos)) { return pos + 1; } 112 | } 113 | return pos; 114 | }; 115 | 116 | // cut lines range from source. 117 | StateBlock.prototype.getLines = function getLines(begin, end, indent, keepLastLF) { 118 | var i, first, last, queue, shift, 119 | line = begin; 120 | 121 | if (begin >= end) { 122 | return ''; 123 | } 124 | 125 | // Opt: don't use push queue for single line; 126 | if (line + 1 === end) { 127 | first = this.bMarks[line] + Math.min(this.tShift[line], indent); 128 | last = keepLastLF ? this.eMarks[line] + 1 : this.eMarks[line]; 129 | return this.src.slice(first, last); 130 | } 131 | 132 | queue = new Array(end - begin); 133 | 134 | for (i = 0; line < end; line++, i++) { 135 | shift = this.tShift[line]; 136 | if (shift > indent) { shift = indent; } 137 | if (shift < 0) { shift = 0; } 138 | 139 | first = this.bMarks[line] + shift; 140 | 141 | if (line + 1 < end || keepLastLF) { 142 | // No need for bounds check because we have fake entry on tail. 143 | last = this.eMarks[line] + 1; 144 | } else { 145 | last = this.eMarks[line]; 146 | } 147 | 148 | queue[i] = this.src.slice(first, last); 149 | } 150 | 151 | return queue.join(''); 152 | }; 153 | -------------------------------------------------------------------------------- /lib/rules_block/table.js: -------------------------------------------------------------------------------- 1 | // GFM table, non-standard 2 | 3 | function getLine(state, line) { 4 | var pos = state.bMarks[line] + state.blkIndent, 5 | max = state.eMarks[line]; 6 | 7 | return state.src.substr(pos, max - pos); 8 | } 9 | 10 | export default function table(state, startLine, endLine, silent) { 11 | var ch, lineText, pos, i, nextLine, rows, cell, 12 | aligns, t, tableLines, tbodyLines; 13 | 14 | // should have at least three lines 15 | if (startLine + 2 > endLine) { return false; } 16 | 17 | nextLine = startLine + 1; 18 | 19 | if (state.tShift[nextLine] < state.blkIndent) { return false; } 20 | 21 | // first character of the second line should be '|' or '-' 22 | 23 | pos = state.bMarks[nextLine] + state.tShift[nextLine]; 24 | if (pos >= state.eMarks[nextLine]) { return false; } 25 | 26 | ch = state.src.charCodeAt(pos); 27 | if (ch !== 0x7C/* | */ && ch !== 0x2D/* - */ && ch !== 0x3A/* : */) { return false; } 28 | 29 | lineText = getLine(state, startLine + 1); 30 | if (!/^[-:| ]+$/.test(lineText)) { return false; } 31 | 32 | rows = lineText.split('|'); 33 | if (rows <= 2) { return false; } 34 | aligns = []; 35 | for (i = 0; i < rows.length; i++) { 36 | t = rows[i].trim(); 37 | if (!t) { 38 | // allow empty columns before and after table, but not in between columns; 39 | // e.g. allow ` |---| `, disallow ` ---||--- ` 40 | if (i === 0 || i === rows.length - 1) { 41 | continue; 42 | } else { 43 | return false; 44 | } 45 | } 46 | 47 | if (!/^:?-+:?$/.test(t)) { return false; } 48 | if (t.charCodeAt(t.length - 1) === 0x3A/* : */) { 49 | aligns.push(t.charCodeAt(0) === 0x3A/* : */ ? 'center' : 'right'); 50 | } else if (t.charCodeAt(0) === 0x3A/* : */) { 51 | aligns.push('left'); 52 | } else { 53 | aligns.push(''); 54 | } 55 | } 56 | 57 | lineText = getLine(state, startLine).trim(); 58 | if (lineText.indexOf('|') === -1) { return false; } 59 | rows = lineText.replace(/^\||\|$/g, '').split('|'); 60 | if (aligns.length !== rows.length) { return false; } 61 | if (silent) { return true; } 62 | 63 | state.tokens.push({ 64 | type: 'table_open', 65 | lines: tableLines = [ startLine, 0 ], 66 | level: state.level++ 67 | }); 68 | state.tokens.push({ 69 | type: 'thead_open', 70 | lines: [ startLine, startLine + 1 ], 71 | level: state.level++ 72 | }); 73 | 74 | state.tokens.push({ 75 | type: 'tr_open', 76 | lines: [ startLine, startLine + 1 ], 77 | level: state.level++ 78 | }); 79 | for (i = 0; i < rows.length; i++) { 80 | state.tokens.push({ 81 | type: 'th_open', 82 | align: aligns[i], 83 | lines: [ startLine, startLine + 1 ], 84 | level: state.level++ 85 | }); 86 | state.tokens.push({ 87 | type: 'inline', 88 | content: rows[i].trim(), 89 | lines: [ startLine, startLine + 1 ], 90 | level: state.level, 91 | children: [] 92 | }); 93 | state.tokens.push({ type: 'th_close', level: --state.level }); 94 | } 95 | state.tokens.push({ type: 'tr_close', level: --state.level }); 96 | state.tokens.push({ type: 'thead_close', level: --state.level }); 97 | 98 | state.tokens.push({ 99 | type: 'tbody_open', 100 | lines: tbodyLines = [ startLine + 2, 0 ], 101 | level: state.level++ 102 | }); 103 | 104 | for (nextLine = startLine + 2; nextLine < endLine; nextLine++) { 105 | if (state.tShift[nextLine] < state.blkIndent) { break; } 106 | 107 | lineText = getLine(state, nextLine).trim(); 108 | if (lineText.indexOf('|') === -1) { break; } 109 | rows = lineText.replace(/^\||\|$/g, '').split('|'); 110 | 111 | state.tokens.push({ type: 'tr_open', level: state.level++ }); 112 | for (i = 0; i < rows.length; i++) { 113 | state.tokens.push({ type: 'td_open', align: aligns[i], level: state.level++ }); 114 | // 0x7c === '|' 115 | cell = rows[i].substring( 116 | rows[i].charCodeAt(0) === 0x7c ? 1 : 0, 117 | rows[i].charCodeAt(rows[i].length - 1) === 0x7c ? rows[i].length - 1 : rows[i].length 118 | ).trim(); 119 | state.tokens.push({ 120 | type: 'inline', 121 | content: cell, 122 | level: state.level, 123 | children: [] 124 | }); 125 | state.tokens.push({ type: 'td_close', level: --state.level }); 126 | } 127 | state.tokens.push({ type: 'tr_close', level: --state.level }); 128 | } 129 | state.tokens.push({ type: 'tbody_close', level: --state.level }); 130 | state.tokens.push({ type: 'table_close', level: --state.level }); 131 | 132 | tableLines[1] = tbodyLines[1] = nextLine; 133 | state.line = nextLine; 134 | return true; 135 | }; 136 | -------------------------------------------------------------------------------- /lib/rules_core/abbr.js: -------------------------------------------------------------------------------- 1 | // Parse abbreviation definitions, i.e. `*[abbr]: description` 2 | // 3 | 4 | import StateInline from '../rules_inline/state_inline'; 5 | import parseLinkLabel from '../helpers/parse_link_label'; 6 | 7 | 8 | function parseAbbr(str, parserInline, options, env) { 9 | var state, labelEnd, pos, max, label, title; 10 | 11 | if (str.charCodeAt(0) !== 0x2A/* * */) { return -1; } 12 | if (str.charCodeAt(1) !== 0x5B/* [ */) { return -1; } 13 | 14 | if (str.indexOf(']:') === -1) { return -1; } 15 | 16 | state = new StateInline(str, parserInline, options, env, []); 17 | labelEnd = parseLinkLabel(state, 1); 18 | 19 | if (labelEnd < 0 || str.charCodeAt(labelEnd + 1) !== 0x3A/* : */) { return -1; } 20 | 21 | max = state.posMax; 22 | 23 | // abbr title is always one line, so looking for ending "\n" here 24 | for (pos = labelEnd + 2; pos < max; pos++) { 25 | if (state.src.charCodeAt(pos) === 0x0A) { break; } 26 | } 27 | 28 | label = str.slice(2, labelEnd); 29 | title = str.slice(labelEnd + 2, pos).trim(); 30 | if (title.length === 0) { return -1; } 31 | if (!env.abbreviations) { env.abbreviations = {}; } 32 | // prepend ':' to avoid conflict with Object.prototype members 33 | if (typeof env.abbreviations[':' + label] === 'undefined') { 34 | env.abbreviations[':' + label] = title; 35 | } 36 | 37 | return pos; 38 | } 39 | 40 | export default function abbr(state) { 41 | var tokens = state.tokens, i, l, content, pos; 42 | 43 | if (state.inlineMode) { 44 | return; 45 | } 46 | 47 | // Parse inlines 48 | for (i = 1, l = tokens.length - 1; i < l; i++) { 49 | if (tokens[i - 1].type === 'paragraph_open' && 50 | tokens[i].type === 'inline' && 51 | tokens[i + 1].type === 'paragraph_close') { 52 | 53 | content = tokens[i].content; 54 | while (content.length) { 55 | pos = parseAbbr(content, state.inline, state.options, state.env); 56 | if (pos < 0) { break; } 57 | content = content.slice(pos).trim(); 58 | } 59 | 60 | tokens[i].content = content; 61 | if (!content.length) { 62 | tokens[i - 1].tight = true; 63 | tokens[i + 1].tight = true; 64 | } 65 | } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /lib/rules_core/abbr2.js: -------------------------------------------------------------------------------- 1 | // Enclose abbreviations in tags 2 | // 3 | 4 | var PUNCT_CHARS = ' \n()[]\'".,!?-'; 5 | 6 | 7 | // from Google closure library 8 | // http://closure-library.googlecode.com/git-history/docs/local_closure_goog_string_string.js.source.html#line1021 9 | function regEscape(s) { 10 | return s.replace(/([-()\[\]{}+?*.$\^|,:#= 0; i--) { 37 | token = tokens[i]; 38 | if (token.type !== 'text') { continue; } 39 | 40 | pos = 0; 41 | text = token.content; 42 | reg.lastIndex = 0; 43 | level = token.level; 44 | nodes = []; 45 | 46 | while ((m = reg.exec(text))) { 47 | if (reg.lastIndex > pos) { 48 | nodes.push({ 49 | type: 'text', 50 | content: text.slice(pos, m.index + m[1].length), 51 | level: level 52 | }); 53 | } 54 | 55 | nodes.push({ 56 | type: 'abbr_open', 57 | title: state.env.abbreviations[':' + m[2]], 58 | level: level++ 59 | }); 60 | nodes.push({ 61 | type: 'text', 62 | content: m[2], 63 | level: level 64 | }); 65 | nodes.push({ 66 | type: 'abbr_close', 67 | level: --level 68 | }); 69 | pos = reg.lastIndex - m[3].length; 70 | } 71 | 72 | if (!nodes.length) { continue; } 73 | 74 | if (pos < text.length) { 75 | nodes.push({ 76 | type: 'text', 77 | content: text.slice(pos), 78 | level: level 79 | }); 80 | } 81 | 82 | // replace current node 83 | blockTokens[j].children = tokens = [].concat(tokens.slice(0, i), nodes, tokens.slice(i + 1)); 84 | } 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /lib/rules_core/block.js: -------------------------------------------------------------------------------- 1 | export default function block(state) { 2 | 3 | if (state.inlineMode) { 4 | state.tokens.push({ 5 | type: 'inline', 6 | content: state.src.replace(/\n/g, ' ').trim(), 7 | level: 0, 8 | lines: [ 0, 1 ], 9 | children: [] 10 | }); 11 | 12 | } else { 13 | state.block.parse(state.src, state.options, state.env, state.tokens); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/rules_core/footnote_tail.js: -------------------------------------------------------------------------------- 1 | export default function footnote_block(state) { 2 | var i, l, j, t, lastParagraph, list, tokens, current, currentLabel, 3 | level = 0, 4 | insideRef = false, 5 | refTokens = {}; 6 | 7 | if (!state.env.footnotes) { return; } 8 | 9 | state.tokens = state.tokens.filter(function(tok) { 10 | if (tok.type === 'footnote_reference_open') { 11 | insideRef = true; 12 | current = []; 13 | currentLabel = tok.label; 14 | return false; 15 | } 16 | if (tok.type === 'footnote_reference_close') { 17 | insideRef = false; 18 | // prepend ':' to avoid conflict with Object.prototype members 19 | refTokens[':' + currentLabel] = current; 20 | return false; 21 | } 22 | if (insideRef) { current.push(tok); } 23 | return !insideRef; 24 | }); 25 | 26 | if (!state.env.footnotes.list) { return; } 27 | list = state.env.footnotes.list; 28 | 29 | state.tokens.push({ 30 | type: 'footnote_block_open', 31 | level: level++ 32 | }); 33 | for (i = 0, l = list.length; i < l; i++) { 34 | state.tokens.push({ 35 | type: 'footnote_open', 36 | id: i, 37 | level: level++ 38 | }); 39 | 40 | if (list[i].tokens) { 41 | tokens = []; 42 | tokens.push({ 43 | type: 'paragraph_open', 44 | tight: false, 45 | level: level++ 46 | }); 47 | tokens.push({ 48 | type: 'inline', 49 | content: '', 50 | level: level, 51 | children: list[i].tokens 52 | }); 53 | tokens.push({ 54 | type: 'paragraph_close', 55 | tight: false, 56 | level: --level 57 | }); 58 | } else if (list[i].label) { 59 | tokens = refTokens[':' + list[i].label]; 60 | } 61 | 62 | state.tokens = state.tokens.concat(tokens); 63 | if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') { 64 | lastParagraph = state.tokens.pop(); 65 | } else { 66 | lastParagraph = null; 67 | } 68 | 69 | t = list[i].count > 0 ? list[i].count : 1; 70 | for (j = 0; j < t; j++) { 71 | state.tokens.push({ 72 | type: 'footnote_anchor', 73 | id: i, 74 | subId: j, 75 | level: level 76 | }); 77 | } 78 | 79 | if (lastParagraph) { 80 | state.tokens.push(lastParagraph); 81 | } 82 | 83 | state.tokens.push({ 84 | type: 'footnote_close', 85 | level: --level 86 | }); 87 | } 88 | state.tokens.push({ 89 | type: 'footnote_block_close', 90 | level: --level 91 | }); 92 | }; 93 | -------------------------------------------------------------------------------- /lib/rules_core/inline.js: -------------------------------------------------------------------------------- 1 | export default function inline(state) { 2 | var tokens = state.tokens, tok, i, l; 3 | 4 | // Parse inlines 5 | for (i = 0, l = tokens.length; i < l; i++) { 6 | tok = tokens[i]; 7 | if (tok.type === 'inline') { 8 | state.inline.parse(tok.content, state.options, state.env, tok.children); 9 | } 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /lib/rules_core/references.js: -------------------------------------------------------------------------------- 1 | import StateInline from '../rules_inline/state_inline'; 2 | import parseLinkLabel from '../helpers/parse_link_label'; 3 | import parseLinkDestination from '../helpers/parse_link_destination'; 4 | import parseLinkTitle from '../helpers/parse_link_title'; 5 | import normalizeReference from '../helpers/normalize_reference'; 6 | 7 | 8 | function parseReference(str, parser, options, env) { 9 | var state, labelEnd, pos, max, code, start, href, title, label; 10 | 11 | if (str.charCodeAt(0) !== 0x5B/* [ */) { return -1; } 12 | 13 | if (str.indexOf(']:') === -1) { return -1; } 14 | 15 | state = new StateInline(str, parser, options, env, []); 16 | labelEnd = parseLinkLabel(state, 0); 17 | 18 | if (labelEnd < 0 || str.charCodeAt(labelEnd + 1) !== 0x3A/* : */) { return -1; } 19 | 20 | max = state.posMax; 21 | 22 | // [label]: destination 'title' 23 | // ^^^ skip optional whitespace here 24 | for (pos = labelEnd + 2; pos < max; pos++) { 25 | code = state.src.charCodeAt(pos); 26 | if (code !== 0x20 && code !== 0x0A) { break; } 27 | } 28 | 29 | // [label]: destination 'title' 30 | // ^^^^^^^^^^^ parse this 31 | if (!parseLinkDestination(state, pos)) { return -1; } 32 | href = state.linkContent; 33 | pos = state.pos; 34 | 35 | // [label]: destination 'title' 36 | // ^^^ skipping those spaces 37 | start = pos; 38 | for (pos = pos + 1; pos < max; pos++) { 39 | code = state.src.charCodeAt(pos); 40 | if (code !== 0x20 && code !== 0x0A) { break; } 41 | } 42 | 43 | // [label]: destination 'title' 44 | // ^^^^^^^ parse this 45 | if (pos < max && start !== pos && parseLinkTitle(state, pos)) { 46 | title = state.linkContent; 47 | pos = state.pos; 48 | } else { 49 | title = ''; 50 | pos = start; 51 | } 52 | 53 | // ensure that the end of the line is empty 54 | while (pos < max && state.src.charCodeAt(pos) === 0x20/* space */) { pos++; } 55 | if (pos < max && state.src.charCodeAt(pos) !== 0x0A) { return -1; } 56 | 57 | label = normalizeReference(str.slice(1, labelEnd)); 58 | if (typeof env.references[label] === 'undefined') { 59 | env.references[label] = { title: title, href: href }; 60 | } 61 | 62 | return pos; 63 | } 64 | 65 | 66 | export default function references(state) { 67 | var tokens = state.tokens, i, l, content, pos; 68 | 69 | state.env.references = state.env.references || {}; 70 | 71 | if (state.inlineMode) { 72 | return; 73 | } 74 | 75 | // Scan definitions in paragraph inlines 76 | for (i = 1, l = tokens.length - 1; i < l; i++) { 77 | if (tokens[i].type === 'inline' && 78 | tokens[i - 1].type === 'paragraph_open' && 79 | tokens[i + 1].type === 'paragraph_close') { 80 | 81 | content = tokens[i].content; 82 | while (content.length) { 83 | pos = parseReference(content, state.inline, state.options, state.env); 84 | if (pos < 0) { break; } 85 | content = content.slice(pos).trim(); 86 | } 87 | 88 | tokens[i].content = content; 89 | if (!content.length) { 90 | tokens[i - 1].tight = true; 91 | tokens[i + 1].tight = true; 92 | } 93 | } 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /lib/rules_core/replacements.js: -------------------------------------------------------------------------------- 1 | // Simple typographical replacements 2 | // 3 | // TODO: 4 | // - fractionals 1/2, 1/4, 3/4 -> ½, ¼, ¾ 5 | // - miltiplication 2 x 4 -> 2 × 4 6 | 7 | var RARE_RE = /\+-|\.\.|\?\?\?\?|!!!!|,,|--/; 8 | 9 | var SCOPED_ABBR_RE = /\((c|tm|r|p)\)/ig; 10 | var SCOPED_ABBR = { 11 | 'c': '©', 12 | 'r': '®', 13 | 'p': '§', 14 | 'tm': '™' 15 | }; 16 | 17 | function replaceScopedAbbr(str) { 18 | if (str.indexOf('(') < 0) { return str; } 19 | 20 | return str.replace(SCOPED_ABBR_RE, function(match, name) { 21 | return SCOPED_ABBR[name.toLowerCase()]; 22 | }); 23 | } 24 | 25 | 26 | export default function replace(state) { 27 | var i, token, text, inlineTokens, blkIdx; 28 | 29 | if (!state.options.typographer) { return; } 30 | 31 | for (blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) { 32 | 33 | if (state.tokens[blkIdx].type !== 'inline') { continue; } 34 | 35 | inlineTokens = state.tokens[blkIdx].children; 36 | 37 | for (i = inlineTokens.length - 1; i >= 0; i--) { 38 | token = inlineTokens[i]; 39 | if (token.type === 'text') { 40 | text = token.content; 41 | 42 | text = replaceScopedAbbr(text); 43 | 44 | if (RARE_RE.test(text)) { 45 | text = text 46 | .replace(/\+-/g, '±') 47 | // .., ..., ....... -> … 48 | // but ?..... & !..... -> ?.. & !.. 49 | .replace(/\.{2,}/g, '…').replace(/([?!])…/g, '$1..') 50 | .replace(/([?!]){4,}/g, '$1$1$1').replace(/,{2,}/g, ',') 51 | // em-dash 52 | .replace(/(^|[^-])---([^-]|$)/mg, '$1\u2014$2') 53 | // en-dash 54 | .replace(/(^|\s)--(\s|$)/mg, '$1\u2013$2') 55 | .replace(/(^|[^-\s])--([^-\s]|$)/mg, '$1\u2013$2'); 56 | } 57 | 58 | token.content = text; 59 | } 60 | } 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /lib/rules_core/smartquotes.js: -------------------------------------------------------------------------------- 1 | // Convert straight quotation marks to typographic ones 2 | // 3 | 4 | var QUOTE_TEST_RE = /['"]/; 5 | var QUOTE_RE = /['"]/g; 6 | var PUNCT_RE = /[-\s()\[\]]/; 7 | var APOSTROPHE = '’'; 8 | 9 | // This function returns true if the character at `pos` 10 | // could be inside a word. 11 | function isLetter(str, pos) { 12 | if (pos < 0 || pos >= str.length) { return false; } 13 | return !PUNCT_RE.test(str[pos]); 14 | } 15 | 16 | 17 | function replaceAt(str, index, ch) { 18 | return str.substr(0, index) + ch + str.substr(index + 1); 19 | } 20 | 21 | 22 | export default function smartquotes(state) { 23 | /*eslint max-depth:0*/ 24 | var i, token, text, t, pos, max, thisLevel, lastSpace, nextSpace, item, 25 | canOpen, canClose, j, isSingle, blkIdx, tokens, 26 | stack; 27 | 28 | if (!state.options.typographer) { return; } 29 | 30 | stack = []; 31 | 32 | for (blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) { 33 | 34 | if (state.tokens[blkIdx].type !== 'inline') { continue; } 35 | 36 | tokens = state.tokens[blkIdx].children; 37 | stack.length = 0; 38 | 39 | for (i = 0; i < tokens.length; i++) { 40 | token = tokens[i]; 41 | 42 | if (token.type !== 'text' || QUOTE_TEST_RE.test(token.text)) { continue; } 43 | 44 | thisLevel = tokens[i].level; 45 | 46 | for (j = stack.length - 1; j >= 0; j--) { 47 | if (stack[j].level <= thisLevel) { break; } 48 | } 49 | stack.length = j + 1; 50 | 51 | text = token.content; 52 | pos = 0; 53 | max = text.length; 54 | 55 | /*eslint no-labels:0,block-scoped-var:0*/ 56 | OUTER: 57 | while (pos < max) { 58 | QUOTE_RE.lastIndex = pos; 59 | t = QUOTE_RE.exec(text); 60 | if (!t) { break; } 61 | 62 | lastSpace = !isLetter(text, t.index - 1); 63 | pos = t.index + 1; 64 | isSingle = (t[0] === "'"); 65 | nextSpace = !isLetter(text, pos); 66 | 67 | if (!nextSpace && !lastSpace) { 68 | // middle of word 69 | if (isSingle) { 70 | token.content = replaceAt(token.content, t.index, APOSTROPHE); 71 | } 72 | continue; 73 | } 74 | 75 | canOpen = !nextSpace; 76 | canClose = !lastSpace; 77 | 78 | if (canClose) { 79 | // this could be a closing quote, rewind the stack to get a match 80 | for (j = stack.length - 1; j >= 0; j--) { 81 | item = stack[j]; 82 | if (stack[j].level < thisLevel) { break; } 83 | if (item.single === isSingle && stack[j].level === thisLevel) { 84 | item = stack[j]; 85 | if (isSingle) { 86 | tokens[item.token].content = replaceAt(tokens[item.token].content, item.pos, state.options.quotes[2]); 87 | token.content = replaceAt(token.content, t.index, state.options.quotes[3]); 88 | } else { 89 | tokens[item.token].content = replaceAt(tokens[item.token].content, item.pos, state.options.quotes[0]); 90 | token.content = replaceAt(token.content, t.index, state.options.quotes[1]); 91 | } 92 | stack.length = j; 93 | continue OUTER; 94 | } 95 | } 96 | } 97 | 98 | if (canOpen) { 99 | stack.push({ 100 | token: i, 101 | pos: t.index, 102 | single: isSingle, 103 | level: thisLevel 104 | }); 105 | } else if (canClose && isSingle) { 106 | token.content = replaceAt(token.content, t.index, APOSTROPHE); 107 | } 108 | } 109 | } 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /lib/rules_inline/autolink.js: -------------------------------------------------------------------------------- 1 | // Process autolinks '' 2 | 3 | import url_schemas from '../common/url_schemas'; 4 | import normalizeLink from '../helpers/normalize_link'; 5 | 6 | 7 | /*eslint max-len:0*/ 8 | var EMAIL_RE = /^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/; 9 | var AUTOLINK_RE = /^<([a-zA-Z.\-]{1,25}):([^<>\x00-\x20]*)>/; 10 | 11 | 12 | export default function autolink(state, silent) { 13 | var tail, linkMatch, emailMatch, url, fullUrl, pos = state.pos; 14 | 15 | if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false; } 16 | 17 | tail = state.src.slice(pos); 18 | 19 | if (tail.indexOf('>') < 0) { return false; } 20 | 21 | linkMatch = tail.match(AUTOLINK_RE); 22 | 23 | if (linkMatch) { 24 | if (url_schemas.indexOf(linkMatch[1].toLowerCase()) < 0) { return false; } 25 | 26 | url = linkMatch[0].slice(1, -1); 27 | fullUrl = normalizeLink(url); 28 | if (!state.parser.validateLink(url)) { return false; } 29 | 30 | if (!silent) { 31 | state.push({ 32 | type: 'link_open', 33 | href: fullUrl, 34 | level: state.level 35 | }); 36 | state.push({ 37 | type: 'text', 38 | content: url, 39 | level: state.level + 1 40 | }); 41 | state.push({ type: 'link_close', level: state.level }); 42 | } 43 | 44 | state.pos += linkMatch[0].length; 45 | return true; 46 | } 47 | 48 | emailMatch = tail.match(EMAIL_RE); 49 | 50 | if (emailMatch) { 51 | 52 | url = emailMatch[0].slice(1, -1); 53 | 54 | fullUrl = normalizeLink('mailto:' + url); 55 | if (!state.parser.validateLink(fullUrl)) { return false; } 56 | 57 | if (!silent) { 58 | state.push({ 59 | type: 'link_open', 60 | href: fullUrl, 61 | level: state.level 62 | }); 63 | state.push({ 64 | type: 'text', 65 | content: url, 66 | level: state.level + 1 67 | }); 68 | state.push({ type: 'link_close', level: state.level }); 69 | } 70 | 71 | state.pos += emailMatch[0].length; 72 | return true; 73 | } 74 | 75 | return false; 76 | }; 77 | -------------------------------------------------------------------------------- /lib/rules_inline/backticks.js: -------------------------------------------------------------------------------- 1 | // Parse backticks 2 | 3 | export default function backticks(state, silent) { 4 | var start, max, marker, matchStart, matchEnd, 5 | pos = state.pos, 6 | ch = state.src.charCodeAt(pos); 7 | 8 | if (ch !== 0x60/* ` */) { return false; } 9 | 10 | start = pos; 11 | pos++; 12 | max = state.posMax; 13 | 14 | while (pos < max && state.src.charCodeAt(pos) === 0x60/* ` */) { pos++; } 15 | 16 | marker = state.src.slice(start, pos); 17 | 18 | matchStart = matchEnd = pos; 19 | 20 | while ((matchStart = state.src.indexOf('`', matchEnd)) !== -1) { 21 | matchEnd = matchStart + 1; 22 | 23 | while (matchEnd < max && state.src.charCodeAt(matchEnd) === 0x60/* ` */) { matchEnd++; } 24 | 25 | if (matchEnd - matchStart === marker.length) { 26 | if (!silent) { 27 | state.push({ 28 | type: 'code', 29 | content: state.src.slice(pos, matchStart) 30 | .replace(/[ \n]+/g, ' ') 31 | .trim(), 32 | block: false, 33 | level: state.level 34 | }); 35 | } 36 | state.pos = matchEnd; 37 | return true; 38 | } 39 | } 40 | 41 | if (!silent) { state.pending += marker; } 42 | state.pos += marker.length; 43 | return true; 44 | }; 45 | -------------------------------------------------------------------------------- /lib/rules_inline/del.js: -------------------------------------------------------------------------------- 1 | // Process ~~deleted text~~ 2 | 3 | export default function del(state, silent) { 4 | var found, 5 | pos, 6 | stack, 7 | max = state.posMax, 8 | start = state.pos, 9 | lastChar, 10 | nextChar; 11 | 12 | if (state.src.charCodeAt(start) !== 0x7E/* ~ */) { return false; } 13 | if (silent) { return false; } // don't run any pairs in validation mode 14 | if (start + 4 >= max) { return false; } 15 | if (state.src.charCodeAt(start + 1) !== 0x7E/* ~ */) { return false; } 16 | if (state.level >= state.options.maxNesting) { return false; } 17 | 18 | lastChar = start > 0 ? state.src.charCodeAt(start - 1) : -1; 19 | nextChar = state.src.charCodeAt(start + 2); 20 | 21 | if (lastChar === 0x7E/* ~ */) { return false; } 22 | if (nextChar === 0x7E/* ~ */) { return false; } 23 | if (nextChar === 0x20 || nextChar === 0x0A) { return false; } 24 | 25 | pos = start + 2; 26 | while (pos < max && state.src.charCodeAt(pos) === 0x7E/* ~ */) { pos++; } 27 | if (pos > start + 3) { 28 | // sequence of 4+ markers taking as literal, same as in a emphasis 29 | state.pos += pos - start; 30 | if (!silent) { state.pending += state.src.slice(start, pos); } 31 | return true; 32 | } 33 | 34 | state.pos = start + 2; 35 | stack = 1; 36 | 37 | while (state.pos + 1 < max) { 38 | if (state.src.charCodeAt(state.pos) === 0x7E/* ~ */) { 39 | if (state.src.charCodeAt(state.pos + 1) === 0x7E/* ~ */) { 40 | lastChar = state.src.charCodeAt(state.pos - 1); 41 | nextChar = state.pos + 2 < max ? state.src.charCodeAt(state.pos + 2) : -1; 42 | if (nextChar !== 0x7E/* ~ */ && lastChar !== 0x7E/* ~ */) { 43 | if (lastChar !== 0x20 && lastChar !== 0x0A) { 44 | // closing '~~' 45 | stack--; 46 | } else if (nextChar !== 0x20 && nextChar !== 0x0A) { 47 | // opening '~~' 48 | stack++; 49 | } // else { 50 | // // standalone ' ~~ ' indented with spaces 51 | // } 52 | if (stack <= 0) { 53 | found = true; 54 | break; 55 | } 56 | } 57 | } 58 | } 59 | 60 | state.parser.skipToken(state); 61 | } 62 | 63 | if (!found) { 64 | // parser failed to find ending tag, so it's not valid emphasis 65 | state.pos = start; 66 | return false; 67 | } 68 | 69 | // found! 70 | state.posMax = state.pos; 71 | state.pos = start + 2; 72 | 73 | if (!silent) { 74 | state.push({ type: 'del_open', level: state.level++ }); 75 | state.parser.tokenize(state); 76 | state.push({ type: 'del_close', level: --state.level }); 77 | } 78 | 79 | state.pos = state.posMax + 2; 80 | state.posMax = max; 81 | return true; 82 | }; 83 | -------------------------------------------------------------------------------- /lib/rules_inline/emphasis.js: -------------------------------------------------------------------------------- 1 | // Process *this* and _that_ 2 | 3 | function isAlphaNum(code) { 4 | return (code >= 0x30 /* 0 */ && code <= 0x39 /* 9 */) || 5 | (code >= 0x41 /* A */ && code <= 0x5A /* Z */) || 6 | (code >= 0x61 /* a */ && code <= 0x7A /* z */); 7 | } 8 | 9 | // parse sequence of emphasis markers, 10 | // "start" should point at a valid marker 11 | function scanDelims(state, start) { 12 | var pos = start, lastChar, nextChar, count, 13 | can_open = true, 14 | can_close = true, 15 | max = state.posMax, 16 | marker = state.src.charCodeAt(start); 17 | 18 | lastChar = start > 0 ? state.src.charCodeAt(start - 1) : -1; 19 | 20 | while (pos < max && state.src.charCodeAt(pos) === marker) { pos++; } 21 | if (pos >= max) { can_open = false; } 22 | count = pos - start; 23 | 24 | if (count >= 4) { 25 | // sequence of four or more unescaped markers can't start/end an emphasis 26 | can_open = can_close = false; 27 | } else { 28 | nextChar = pos < max ? state.src.charCodeAt(pos) : -1; 29 | 30 | // check whitespace conditions 31 | if (nextChar === 0x20 || nextChar === 0x0A) { can_open = false; } 32 | if (lastChar === 0x20 || lastChar === 0x0A) { can_close = false; } 33 | 34 | if (marker === 0x5F /* _ */) { 35 | // check if we aren't inside the word 36 | if (isAlphaNum(lastChar)) { can_open = false; } 37 | if (isAlphaNum(nextChar)) { can_close = false; } 38 | } 39 | } 40 | 41 | return { 42 | can_open: can_open, 43 | can_close: can_close, 44 | delims: count 45 | }; 46 | } 47 | 48 | export default function emphasis(state, silent) { 49 | var startCount, 50 | count, 51 | found, 52 | oldCount, 53 | newCount, 54 | stack, 55 | res, 56 | max = state.posMax, 57 | start = state.pos, 58 | marker = state.src.charCodeAt(start); 59 | 60 | if (marker !== 0x5F/* _ */ && marker !== 0x2A /* * */) { return false; } 61 | if (silent) { return false; } // don't run any pairs in validation mode 62 | 63 | res = scanDelims(state, start); 64 | startCount = res.delims; 65 | if (!res.can_open) { 66 | state.pos += startCount; 67 | if (!silent) { state.pending += state.src.slice(start, state.pos); } 68 | return true; 69 | } 70 | 71 | if (state.level >= state.options.maxNesting) { return false; } 72 | 73 | state.pos = start + startCount; 74 | stack = [ startCount ]; 75 | 76 | while (state.pos < max) { 77 | if (state.src.charCodeAt(state.pos) === marker) { 78 | res = scanDelims(state, state.pos); 79 | count = res.delims; 80 | if (res.can_close) { 81 | oldCount = stack.pop(); 82 | newCount = count; 83 | 84 | while (oldCount !== newCount) { 85 | if (newCount < oldCount) { 86 | stack.push(oldCount - newCount); 87 | break; 88 | } 89 | 90 | // assert(newCount > oldCount) 91 | newCount -= oldCount; 92 | 93 | if (stack.length === 0) { break; } 94 | state.pos += oldCount; 95 | oldCount = stack.pop(); 96 | } 97 | 98 | if (stack.length === 0) { 99 | startCount = oldCount; 100 | found = true; 101 | break; 102 | } 103 | state.pos += count; 104 | continue; 105 | } 106 | 107 | if (res.can_open) { stack.push(count); } 108 | state.pos += count; 109 | continue; 110 | } 111 | 112 | state.parser.skipToken(state); 113 | } 114 | 115 | if (!found) { 116 | // parser failed to find ending tag, so it's not valid emphasis 117 | state.pos = start; 118 | return false; 119 | } 120 | 121 | // found! 122 | state.posMax = state.pos; 123 | state.pos = start + startCount; 124 | 125 | if (!silent) { 126 | if (startCount === 2 || startCount === 3) { 127 | state.push({ type: 'strong_open', level: state.level++ }); 128 | } 129 | if (startCount === 1 || startCount === 3) { 130 | state.push({ type: 'em_open', level: state.level++ }); 131 | } 132 | 133 | state.parser.tokenize(state); 134 | 135 | if (startCount === 1 || startCount === 3) { 136 | state.push({ type: 'em_close', level: --state.level }); 137 | } 138 | if (startCount === 2 || startCount === 3) { 139 | state.push({ type: 'strong_close', level: --state.level }); 140 | } 141 | } 142 | 143 | state.pos = state.posMax + startCount; 144 | state.posMax = max; 145 | return true; 146 | }; 147 | -------------------------------------------------------------------------------- /lib/rules_inline/entity.js: -------------------------------------------------------------------------------- 1 | // Process html entity - {, ¯, ", ... 2 | 3 | import { decodeEntity } from '../common/entities'; 4 | import { isValidEntityCode, fromCodePoint } from '../common/utils'; 5 | 6 | 7 | var DIGITAL_RE = /^&#((?:x[a-f0-9]{1,8}|[0-9]{1,8}));/i; 8 | var NAMED_RE = /^&([a-z][a-z0-9]{1,31});/i; 9 | 10 | 11 | export default function entity(state, silent) { 12 | var ch, code, match, pos = state.pos, max = state.posMax; 13 | 14 | if (state.src.charCodeAt(pos) !== 0x26/* & */) { return false; } 15 | 16 | if (pos + 1 < max) { 17 | ch = state.src.charCodeAt(pos + 1); 18 | 19 | if (ch === 0x23 /* # */) { 20 | match = state.src.slice(pos).match(DIGITAL_RE); 21 | if (match) { 22 | if (!silent) { 23 | code = match[1][0].toLowerCase() === 'x' ? parseInt(match[1].slice(1), 16) : parseInt(match[1], 10); 24 | state.pending += isValidEntityCode(code) ? fromCodePoint(code) : fromCodePoint(0xFFFD); 25 | } 26 | state.pos += match[0].length; 27 | return true; 28 | } 29 | } else { 30 | match = state.src.slice(pos).match(NAMED_RE); 31 | if (match) { 32 | var decoded = decodeEntity(match[1]); 33 | if (match[1] !== decoded) { 34 | if (!silent) { state.pending += decoded; } 35 | state.pos += match[0].length; 36 | return true; 37 | } 38 | } 39 | } 40 | } 41 | 42 | if (!silent) { state.pending += '&'; } 43 | state.pos++; 44 | return true; 45 | }; 46 | -------------------------------------------------------------------------------- /lib/rules_inline/escape.js: -------------------------------------------------------------------------------- 1 | // Proceess escaped chars and hardbreaks 2 | 3 | var ESCAPED = []; 4 | 5 | for (var i = 0; i < 256; i++) { ESCAPED.push(0); } 6 | 7 | '\\!"#$%&\'()*+,./:;<=>?@[]^_`{|}~-' 8 | .split('').forEach(function(ch) { ESCAPED[ch.charCodeAt(0)] = 1; }); 9 | 10 | 11 | export default function escape(state, silent) { 12 | var ch, pos = state.pos, max = state.posMax; 13 | 14 | if (state.src.charCodeAt(pos) !== 0x5C/* \ */) { return false; } 15 | 16 | pos++; 17 | 18 | if (pos < max) { 19 | ch = state.src.charCodeAt(pos); 20 | 21 | if (ch < 256 && ESCAPED[ch] !== 0) { 22 | if (!silent) { state.pending += state.src[pos]; } 23 | state.pos += 2; 24 | return true; 25 | } 26 | 27 | if (ch === 0x0A) { 28 | if (!silent) { 29 | state.push({ 30 | type: 'hardbreak', 31 | level: state.level 32 | }); 33 | } 34 | 35 | pos++; 36 | // skip leading whitespaces from next line 37 | while (pos < max && state.src.charCodeAt(pos) === 0x20) { pos++; } 38 | 39 | state.pos = pos; 40 | return true; 41 | } 42 | } 43 | 44 | if (!silent) { state.pending += '\\'; } 45 | state.pos++; 46 | return true; 47 | }; 48 | -------------------------------------------------------------------------------- /lib/rules_inline/footnote_inline.js: -------------------------------------------------------------------------------- 1 | // Process inline footnotes (^[...]) 2 | 3 | import parseLinkLabel from '../helpers/parse_link_label'; 4 | 5 | 6 | export default function footnote_inline(state, silent) { 7 | var labelStart, 8 | labelEnd, 9 | footnoteId, 10 | oldLength, 11 | max = state.posMax, 12 | start = state.pos; 13 | 14 | if (start + 2 >= max) { return false; } 15 | if (state.src.charCodeAt(start) !== 0x5E/* ^ */) { return false; } 16 | if (state.src.charCodeAt(start + 1) !== 0x5B/* [ */) { return false; } 17 | if (state.level >= state.options.maxNesting) { return false; } 18 | 19 | labelStart = start + 2; 20 | labelEnd = parseLinkLabel(state, start + 1); 21 | 22 | // parser failed to find ']', so it's not a valid note 23 | if (labelEnd < 0) { return false; } 24 | 25 | // We found the end of the link, and know for a fact it's a valid link; 26 | // so all that's left to do is to call tokenizer. 27 | // 28 | if (!silent) { 29 | if (!state.env.footnotes) { state.env.footnotes = {}; } 30 | if (!state.env.footnotes.list) { state.env.footnotes.list = []; } 31 | footnoteId = state.env.footnotes.list.length; 32 | 33 | state.pos = labelStart; 34 | state.posMax = labelEnd; 35 | 36 | state.push({ 37 | type: 'footnote_ref', 38 | id: footnoteId, 39 | level: state.level 40 | }); 41 | state.linkLevel++; 42 | oldLength = state.tokens.length; 43 | state.parser.tokenize(state); 44 | state.env.footnotes.list[footnoteId] = { tokens: state.tokens.splice(oldLength) }; 45 | state.linkLevel--; 46 | } 47 | 48 | state.pos = labelEnd + 1; 49 | state.posMax = max; 50 | return true; 51 | }; 52 | -------------------------------------------------------------------------------- /lib/rules_inline/footnote_ref.js: -------------------------------------------------------------------------------- 1 | // Process footnote references ([^...]) 2 | 3 | export default function footnote_ref(state, silent) { 4 | var label, 5 | pos, 6 | footnoteId, 7 | footnoteSubId, 8 | max = state.posMax, 9 | start = state.pos; 10 | 11 | // should be at least 4 chars - "[^x]" 12 | if (start + 3 > max) { return false; } 13 | 14 | if (!state.env.footnotes || !state.env.footnotes.refs) { return false; } 15 | if (state.src.charCodeAt(start) !== 0x5B/* [ */) { return false; } 16 | if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) { return false; } 17 | if (state.level >= state.options.maxNesting) { return false; } 18 | 19 | for (pos = start + 2; pos < max; pos++) { 20 | if (state.src.charCodeAt(pos) === 0x20) { return false; } 21 | if (state.src.charCodeAt(pos) === 0x0A) { return false; } 22 | if (state.src.charCodeAt(pos) === 0x5D /* ] */) { 23 | break; 24 | } 25 | } 26 | 27 | if (pos === start + 2) { return false; } // no empty footnote labels 28 | if (pos >= max) { return false; } 29 | pos++; 30 | 31 | label = state.src.slice(start + 2, pos - 1); 32 | if (typeof state.env.footnotes.refs[':' + label] === 'undefined') { return false; } 33 | 34 | if (!silent) { 35 | if (!state.env.footnotes.list) { state.env.footnotes.list = []; } 36 | 37 | if (state.env.footnotes.refs[':' + label] < 0) { 38 | footnoteId = state.env.footnotes.list.length; 39 | state.env.footnotes.list[footnoteId] = { label: label, count: 0 }; 40 | state.env.footnotes.refs[':' + label] = footnoteId; 41 | } else { 42 | footnoteId = state.env.footnotes.refs[':' + label]; 43 | } 44 | 45 | footnoteSubId = state.env.footnotes.list[footnoteId].count; 46 | state.env.footnotes.list[footnoteId].count++; 47 | 48 | state.push({ 49 | type: 'footnote_ref', 50 | id: footnoteId, 51 | subId: footnoteSubId, 52 | level: state.level 53 | }); 54 | } 55 | 56 | state.pos = pos; 57 | state.posMax = max; 58 | return true; 59 | }; 60 | -------------------------------------------------------------------------------- /lib/rules_inline/htmltag.js: -------------------------------------------------------------------------------- 1 | // Process html tags 2 | 3 | import { HTML_TAG_RE } from '../common/html_re'; 4 | 5 | 6 | function isLetter(ch) { 7 | /*eslint no-bitwise:0*/ 8 | var lc = ch | 0x20; // to lower case 9 | return (lc >= 0x61/* a */) && (lc <= 0x7a/* z */); 10 | } 11 | 12 | 13 | export default function htmltag(state, silent) { 14 | var ch, match, max, pos = state.pos; 15 | 16 | if (!state.options.html) { return false; } 17 | 18 | // Check start 19 | max = state.posMax; 20 | if (state.src.charCodeAt(pos) !== 0x3C/* < */ || 21 | pos + 2 >= max) { 22 | return false; 23 | } 24 | 25 | // Quick fail on second char 26 | ch = state.src.charCodeAt(pos + 1); 27 | if (ch !== 0x21/* ! */ && 28 | ch !== 0x3F/* ? */ && 29 | ch !== 0x2F/* / */ && 30 | !isLetter(ch)) { 31 | return false; 32 | } 33 | 34 | match = state.src.slice(pos).match(HTML_TAG_RE); 35 | if (!match) { return false; } 36 | 37 | if (!silent) { 38 | state.push({ 39 | type: 'htmltag', 40 | content: state.src.slice(pos, pos + match[0].length), 41 | level: state.level 42 | }); 43 | } 44 | state.pos += match[0].length; 45 | return true; 46 | }; 47 | -------------------------------------------------------------------------------- /lib/rules_inline/ins.js: -------------------------------------------------------------------------------- 1 | // Process ++inserted text++ 2 | 3 | export default function ins(state, silent) { 4 | var found, 5 | pos, 6 | stack, 7 | max = state.posMax, 8 | start = state.pos, 9 | lastChar, 10 | nextChar; 11 | 12 | if (state.src.charCodeAt(start) !== 0x2B/* + */) { return false; } 13 | if (silent) { return false; } // don't run any pairs in validation mode 14 | if (start + 4 >= max) { return false; } 15 | if (state.src.charCodeAt(start + 1) !== 0x2B/* + */) { return false; } 16 | if (state.level >= state.options.maxNesting) { return false; } 17 | 18 | lastChar = start > 0 ? state.src.charCodeAt(start - 1) : -1; 19 | nextChar = state.src.charCodeAt(start + 2); 20 | 21 | if (lastChar === 0x2B/* + */) { return false; } 22 | if (nextChar === 0x2B/* + */) { return false; } 23 | if (nextChar === 0x20 || nextChar === 0x0A) { return false; } 24 | 25 | pos = start + 2; 26 | while (pos < max && state.src.charCodeAt(pos) === 0x2B/* + */) { pos++; } 27 | if (pos !== start + 2) { 28 | // sequence of 3+ markers taking as literal, same as in a emphasis 29 | state.pos += pos - start; 30 | if (!silent) { state.pending += state.src.slice(start, pos); } 31 | return true; 32 | } 33 | 34 | state.pos = start + 2; 35 | stack = 1; 36 | 37 | while (state.pos + 1 < max) { 38 | if (state.src.charCodeAt(state.pos) === 0x2B/* + */) { 39 | if (state.src.charCodeAt(state.pos + 1) === 0x2B/* + */) { 40 | lastChar = state.src.charCodeAt(state.pos - 1); 41 | nextChar = state.pos + 2 < max ? state.src.charCodeAt(state.pos + 2) : -1; 42 | if (nextChar !== 0x2B/* + */ && lastChar !== 0x2B/* + */) { 43 | if (lastChar !== 0x20 && lastChar !== 0x0A) { 44 | // closing '++' 45 | stack--; 46 | } else if (nextChar !== 0x20 && nextChar !== 0x0A) { 47 | // opening '++' 48 | stack++; 49 | } // else { 50 | // // standalone ' ++ ' indented with spaces 51 | // } 52 | if (stack <= 0) { 53 | found = true; 54 | break; 55 | } 56 | } 57 | } 58 | } 59 | 60 | state.parser.skipToken(state); 61 | } 62 | 63 | if (!found) { 64 | // parser failed to find ending tag, so it's not valid emphasis 65 | state.pos = start; 66 | return false; 67 | } 68 | 69 | // found! 70 | state.posMax = state.pos; 71 | state.pos = start + 2; 72 | 73 | if (!silent) { 74 | state.push({ type: 'ins_open', level: state.level++ }); 75 | state.parser.tokenize(state); 76 | state.push({ type: 'ins_close', level: --state.level }); 77 | } 78 | 79 | state.pos = state.posMax + 2; 80 | state.posMax = max; 81 | return true; 82 | }; 83 | -------------------------------------------------------------------------------- /lib/rules_inline/mark.js: -------------------------------------------------------------------------------- 1 | // Process ==highlighted text== 2 | 3 | export default function mark(state, silent) { 4 | var found, 5 | pos, 6 | stack, 7 | max = state.posMax, 8 | start = state.pos, 9 | lastChar, 10 | nextChar; 11 | 12 | if (state.src.charCodeAt(start) !== 0x3D/* = */) { return false; } 13 | if (silent) { return false; } // don't run any pairs in validation mode 14 | if (start + 4 >= max) { return false; } 15 | if (state.src.charCodeAt(start + 1) !== 0x3D/* = */) { return false; } 16 | if (state.level >= state.options.maxNesting) { return false; } 17 | 18 | lastChar = start > 0 ? state.src.charCodeAt(start - 1) : -1; 19 | nextChar = state.src.charCodeAt(start + 2); 20 | 21 | if (lastChar === 0x3D/* = */) { return false; } 22 | if (nextChar === 0x3D/* = */) { return false; } 23 | if (nextChar === 0x20 || nextChar === 0x0A) { return false; } 24 | 25 | pos = start + 2; 26 | while (pos < max && state.src.charCodeAt(pos) === 0x3D/* = */) { pos++; } 27 | if (pos !== start + 2) { 28 | // sequence of 3+ markers taking as literal, same as in a emphasis 29 | state.pos += pos - start; 30 | if (!silent) { state.pending += state.src.slice(start, pos); } 31 | return true; 32 | } 33 | 34 | state.pos = start + 2; 35 | stack = 1; 36 | 37 | while (state.pos + 1 < max) { 38 | if (state.src.charCodeAt(state.pos) === 0x3D/* = */) { 39 | if (state.src.charCodeAt(state.pos + 1) === 0x3D/* = */) { 40 | lastChar = state.src.charCodeAt(state.pos - 1); 41 | nextChar = state.pos + 2 < max ? state.src.charCodeAt(state.pos + 2) : -1; 42 | if (nextChar !== 0x3D/* = */ && lastChar !== 0x3D/* = */) { 43 | if (lastChar !== 0x20 && lastChar !== 0x0A) { 44 | // closing '==' 45 | stack--; 46 | } else if (nextChar !== 0x20 && nextChar !== 0x0A) { 47 | // opening '==' 48 | stack++; 49 | } // else { 50 | // // standalone ' == ' indented with spaces 51 | // } 52 | if (stack <= 0) { 53 | found = true; 54 | break; 55 | } 56 | } 57 | } 58 | } 59 | 60 | state.parser.skipToken(state); 61 | } 62 | 63 | if (!found) { 64 | // parser failed to find ending tag, so it's not valid emphasis 65 | state.pos = start; 66 | return false; 67 | } 68 | 69 | // found! 70 | state.posMax = state.pos; 71 | state.pos = start + 2; 72 | 73 | if (!silent) { 74 | state.push({ type: 'mark_open', level: state.level++ }); 75 | state.parser.tokenize(state); 76 | state.push({ type: 'mark_close', level: --state.level }); 77 | } 78 | 79 | state.pos = state.posMax + 2; 80 | state.posMax = max; 81 | return true; 82 | }; 83 | -------------------------------------------------------------------------------- /lib/rules_inline/newline.js: -------------------------------------------------------------------------------- 1 | // Proceess '\n' 2 | 3 | export default function newline(state, silent) { 4 | var pmax, max, pos = state.pos; 5 | 6 | if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false; } 7 | 8 | pmax = state.pending.length - 1; 9 | max = state.posMax; 10 | 11 | // ' \n' -> hardbreak 12 | // Lookup in pending chars is bad practice! Don't copy to other rules! 13 | // Pending string is stored in concat mode, indexed lookups will cause 14 | // convertion to flat mode. 15 | if (!silent) { 16 | if (pmax >= 0 && state.pending.charCodeAt(pmax) === 0x20) { 17 | if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 0x20) { 18 | // Strip out all trailing spaces on this line. 19 | for (var i = pmax - 2; i >= 0; i--) { 20 | if (state.pending.charCodeAt(i) !== 0x20) { 21 | state.pending = state.pending.substring(0, i + 1); 22 | break; 23 | } 24 | } 25 | state.push({ 26 | type: 'hardbreak', 27 | level: state.level 28 | }); 29 | } else { 30 | state.pending = state.pending.slice(0, -1); 31 | state.push({ 32 | type: 'softbreak', 33 | level: state.level 34 | }); 35 | } 36 | 37 | } else { 38 | state.push({ 39 | type: 'softbreak', 40 | level: state.level 41 | }); 42 | } 43 | } 44 | 45 | pos++; 46 | 47 | // skip heading spaces for next line 48 | while (pos < max && state.src.charCodeAt(pos) === 0x20) { pos++; } 49 | 50 | state.pos = pos; 51 | return true; 52 | }; 53 | -------------------------------------------------------------------------------- /lib/rules_inline/state_inline.js: -------------------------------------------------------------------------------- 1 | // Inline parser state 2 | 3 | export default function StateInline(src, parserInline, options, env, outTokens) { 4 | this.src = src; 5 | this.env = env; 6 | this.options = options; 7 | this.parser = parserInline; 8 | this.tokens = outTokens; 9 | this.pos = 0; 10 | this.posMax = this.src.length; 11 | this.level = 0; 12 | this.pending = ''; 13 | this.pendingLevel = 0; 14 | 15 | this.cache = []; // Stores { start: end } pairs. Useful for backtrack 16 | // optimization of pairs parse (emphasis, strikes). 17 | 18 | // Link parser state vars 19 | 20 | this.isInLabel = false; // Set true when seek link label - we should disable 21 | // "paired" rules (emphasis, strikes) to not skip 22 | // tailing `]` 23 | 24 | this.linkLevel = 0; // Increment for each nesting link. Used to prevent 25 | // nesting in definitions 26 | 27 | this.linkContent = ''; // Temporary storage for link url 28 | 29 | this.labelUnmatchedScopes = 0; // Track unpaired `[` for link labels 30 | // (backtrack optimization) 31 | } 32 | 33 | // Flush pending text 34 | // 35 | StateInline.prototype.pushPending = function () { 36 | this.tokens.push({ 37 | type: 'text', 38 | content: this.pending, 39 | level: this.pendingLevel 40 | }); 41 | this.pending = ''; 42 | }; 43 | 44 | // Push new token to "stream". 45 | // If pending text exists - flush it as text token 46 | // 47 | StateInline.prototype.push = function (token) { 48 | if (this.pending) { 49 | this.pushPending(); 50 | } 51 | 52 | this.tokens.push(token); 53 | this.pendingLevel = this.level; 54 | }; 55 | 56 | // Store value to cache. 57 | // !!! Implementation has parser-specific optimizations 58 | // !!! keys MUST be integer, >= 0; values MUST be integer, > 0 59 | // 60 | StateInline.prototype.cacheSet = function (key, val) { 61 | for (var i = this.cache.length; i <= key; i++) { 62 | this.cache.push(0); 63 | } 64 | 65 | this.cache[key] = val; 66 | }; 67 | 68 | // Get cache value 69 | // 70 | StateInline.prototype.cacheGet = function (key) { 71 | return key < this.cache.length ? this.cache[key] : 0; 72 | }; 73 | -------------------------------------------------------------------------------- /lib/rules_inline/sub.js: -------------------------------------------------------------------------------- 1 | // Process ~subscript~ 2 | 3 | // same as UNESCAPE_MD_RE plus a space 4 | var UNESCAPE_RE = /\\([ \\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g; 5 | 6 | export default function sub(state, silent) { 7 | var found, 8 | content, 9 | max = state.posMax, 10 | start = state.pos; 11 | 12 | if (state.src.charCodeAt(start) !== 0x7E/* ~ */) { return false; } 13 | if (silent) { return false; } // don't run any pairs in validation mode 14 | if (start + 2 >= max) { return false; } 15 | if (state.level >= state.options.maxNesting) { return false; } 16 | 17 | state.pos = start + 1; 18 | 19 | while (state.pos < max) { 20 | if (state.src.charCodeAt(state.pos) === 0x7E/* ~ */) { 21 | found = true; 22 | break; 23 | } 24 | 25 | state.parser.skipToken(state); 26 | } 27 | 28 | if (!found || start + 1 === state.pos) { 29 | state.pos = start; 30 | return false; 31 | } 32 | 33 | content = state.src.slice(start + 1, state.pos); 34 | 35 | // don't allow unescaped spaces/newlines inside 36 | if (content.match(/(^|[^\\])(\\\\)*\s/)) { 37 | state.pos = start; 38 | return false; 39 | } 40 | 41 | // found! 42 | state.posMax = state.pos; 43 | state.pos = start + 1; 44 | 45 | if (!silent) { 46 | state.push({ 47 | type: 'sub', 48 | level: state.level, 49 | content: content.replace(UNESCAPE_RE, '$1') 50 | }); 51 | } 52 | 53 | state.pos = state.posMax + 1; 54 | state.posMax = max; 55 | return true; 56 | }; 57 | -------------------------------------------------------------------------------- /lib/rules_inline/sup.js: -------------------------------------------------------------------------------- 1 | // Process ^superscript^ 2 | 3 | // same as UNESCAPE_MD_RE plus a space 4 | var UNESCAPE_RE = /\\([ \\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g; 5 | 6 | export default function sup(state, silent) { 7 | var found, 8 | content, 9 | max = state.posMax, 10 | start = state.pos; 11 | 12 | if (state.src.charCodeAt(start) !== 0x5E/* ^ */) { return false; } 13 | if (silent) { return false; } // don't run any pairs in validation mode 14 | if (start + 2 >= max) { return false; } 15 | if (state.level >= state.options.maxNesting) { return false; } 16 | 17 | state.pos = start + 1; 18 | 19 | while (state.pos < max) { 20 | if (state.src.charCodeAt(state.pos) === 0x5E/* ^ */) { 21 | found = true; 22 | break; 23 | } 24 | 25 | state.parser.skipToken(state); 26 | } 27 | 28 | if (!found || start + 1 === state.pos) { 29 | state.pos = start; 30 | return false; 31 | } 32 | 33 | content = state.src.slice(start + 1, state.pos); 34 | 35 | // don't allow unescaped spaces/newlines inside 36 | if (content.match(/(^|[^\\])(\\\\)*\s/)) { 37 | state.pos = start; 38 | return false; 39 | } 40 | 41 | // found! 42 | state.posMax = state.pos; 43 | state.pos = start + 1; 44 | 45 | if (!silent) { 46 | state.push({ 47 | type: 'sup', 48 | level: state.level, 49 | content: content.replace(UNESCAPE_RE, '$1') 50 | }); 51 | } 52 | 53 | state.pos = state.posMax + 1; 54 | state.posMax = max; 55 | return true; 56 | }; 57 | -------------------------------------------------------------------------------- /lib/rules_inline/text.js: -------------------------------------------------------------------------------- 1 | // Skip text characters for text token, place those to pending buffer 2 | // and increment current pos 3 | 4 | // Rule to skip pure text 5 | // '{}$%@~+=:' reserved for extentions 6 | 7 | function isTerminatorChar(ch) { 8 | switch (ch) { 9 | case 0x0A/* \n */: 10 | case 0x5C/* \ */: 11 | case 0x60/* ` */: 12 | case 0x2A/* * */: 13 | case 0x5F/* _ */: 14 | case 0x5E/* ^ */: 15 | case 0x5B/* [ */: 16 | case 0x5D/* ] */: 17 | case 0x21/* ! */: 18 | case 0x26/* & */: 19 | case 0x3C/* < */: 20 | case 0x3E/* > */: 21 | case 0x7B/* { */: 22 | case 0x7D/* } */: 23 | case 0x24/* $ */: 24 | case 0x25/* % */: 25 | case 0x40/* @ */: 26 | case 0x7E/* ~ */: 27 | case 0x2B/* + */: 28 | case 0x3D/* = */: 29 | case 0x3A/* : */: 30 | return true; 31 | default: 32 | return false; 33 | } 34 | } 35 | 36 | export default function text(state, silent) { 37 | var pos = state.pos; 38 | 39 | while (pos < state.posMax && !isTerminatorChar(state.src.charCodeAt(pos))) { 40 | pos++; 41 | } 42 | 43 | if (pos === state.pos) { return false; } 44 | 45 | if (!silent) { state.pending += state.src.slice(state.pos, pos); } 46 | 47 | state.pos = pos; 48 | 49 | return true; 50 | }; 51 | -------------------------------------------------------------------------------- /lib/umd.js: -------------------------------------------------------------------------------- 1 | export { Remarkable, utils } from './index'; 2 | export { linkify } from './linkify'; 3 | -------------------------------------------------------------------------------- /linkify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remarkable/linkify", 3 | "main": "../dist/cjs/linkify.js", 4 | "module": "../dist/esm/linkify.js" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remarkable", 3 | "description": "Markdown parser, done right. 100% Commonmark support, extensions, syntax plugins, high speed - all in one.", 4 | "version": "2.0.1", 5 | "homepage": "https://github.com/jonschlinkert/remarkable", 6 | "maintainers": [ 7 | "doowb ", 8 | "jonschlinkert ", 9 | "Bogdan Chadkin " 10 | ], 11 | "contributors": [ 12 | "(https://github.com/dohliam)", 13 | "(https://github.com/loveencounterflow)", 14 | "(https://github.com/vyp)", 15 | "Adam Misiorny (http://bitnoi.se)", 16 | "Akuma (https://github.com/akuma)", 17 | "Alex Kocharin (https://github.com/rlidwka)", 18 | "Amila Welihinda (http://amilawelihinda.com)", 19 | "Brenard Cubacub (bren.me)", 20 | "Denis Sokolov (http://sokolov.cc)", 21 | "Eugene Sharygin (https://github.com/eush77)", 22 | "Harry Llewelyn (http://mynameisharry.com)", 23 | "Joey Baker (https://byjoeybaker.com)", 24 | "Jon Schlinkert (http://twitter.com/jonschlinkert)", 25 | "Julian Lam (https://www.nodebb.org)", 26 | "Lucas Parry (https://github.com/lparry)", 27 | "Luke Horvat (http://lukehorvat.com)", 28 | "Mariusz Nowak (http://www.medikoo.com)", 29 | "Mathias Bynens (https://mathiasbynens.be)", 30 | "Mathieu Lemoine (https://github.com/lemoinem)", 31 | "Matthew Mueller (https://standupjack.com)", 32 | "Nik Nyby (http://nikolas.us.to)", 33 | "Per Kristian Næss-Fladset (https://github.com/pkfladset)", 34 | "Peter deHaan (http://about.me/peterdehaan)", 35 | "Rome Li (https://github.com/akaroml)", 36 | "Takezoe,Tomoaki (@sumito3478) (https://twitter.com/sumito3478en)", 37 | "Tom Byrer (https://github.com/tomByrer)", 38 | "Tom MacWright (http://macwright.org)", 39 | "Una Ma (https://github.com/maruilian11)", 40 | "Vitaly Puzrin (http://gravatar.com/puzrin)" 41 | ], 42 | "repository": "https://github.com/jonschlinkert/remarkable", 43 | "bugs": { 44 | "url": "https://github.com/jonschlinkert/remarkable/issues" 45 | }, 46 | "license": "MIT", 47 | "files": [ 48 | "bin", 49 | "linkify", 50 | "dist", 51 | "index.js" 52 | ], 53 | "bin": "./bin/remarkable.js", 54 | "main": "./dist/cjs/index.js", 55 | "module": "./dist/esm/index.js", 56 | "browser": { 57 | "./dist/cjs/index.js": "./dist/cjs/index.browser.js", 58 | "./dist/esm/index.js": "./dist/esm/index.browser.js" 59 | }, 60 | "engines": { 61 | "node": ">= 6.0.0" 62 | }, 63 | "scripts": { 64 | "build": "rm -rf dist && yarn rollup -c", 65 | "lint": "eslint .", 66 | "test:browser": "yarn build && node -r esm ./test/test-browser.js && serve .", 67 | "test:spec": "./support/specsplit.js test/fixtures/commonmark/spec.txt", 68 | "test:mocha": "mocha -r esm -R spec", 69 | "test:ci": "nyc mocha -r esm -R spec --bail", 70 | "test": "yarn test:mocha && yarn test:spec", 71 | "coverage": "yarn add coveralls@2 && nyc report --reporter=text-lcov | coveralls", 72 | "prepublishOnly": "yarn build" 73 | }, 74 | "nyc": { 75 | "exclude": [ 76 | "dist" 77 | ] 78 | }, 79 | "dependencies": { 80 | "argparse": "^1.0.10", 81 | "autolinker": "^3.11.0" 82 | }, 83 | "devDependencies": { 84 | "ansi": "^0.3.0", 85 | "benchmark": "^1.0.0", 86 | "commonmark": "0.12.0", 87 | "eslint": "^6.1.0", 88 | "eslint-plugin-es5": "^1.4.1", 89 | "esm": "^3.2.25", 90 | "gulp-format-md": "^0.1.10", 91 | "highlight.js": "^9.7.0", 92 | "marked": "0.3.2", 93 | "mocha": "^6.1.4", 94 | "nyc": "^14.1.1", 95 | "rollup": "^1.16.7", 96 | "rollup-plugin-json": "^4.0.0", 97 | "rollup-plugin-node-resolve": "^5.2.0", 98 | "rollup-plugin-terser": "^5.1.1", 99 | "serve": "^11.1.0" 100 | }, 101 | "keywords": [ 102 | "commonmark", 103 | "markdown", 104 | "md", 105 | "parse", 106 | "parser", 107 | "process", 108 | "remarkable", 109 | "render", 110 | "renderer", 111 | "text" 112 | ], 113 | "verb": { 114 | "toc": false, 115 | "layout": "nil", 116 | "tasks": [ 117 | "readme" 118 | ], 119 | "plugins": [ 120 | "gulp-format-md" 121 | ], 122 | "lint": { 123 | "reflinks": true 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import nodeResolve from 'rollup-plugin-node-resolve'; 3 | import json from 'rollup-plugin-json'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | 6 | const name = 'remarkable'; 7 | const external = id => !id.startsWith('.') && !path.isAbsolute(id); 8 | 9 | export default [ 10 | { 11 | input: ['./lib/cli.js', './lib/index.js', './lib/linkify.js'], 12 | output: { dir: 'dist/cjs', format: 'cjs' }, 13 | external, 14 | plugins: [json()] 15 | }, 16 | 17 | { 18 | input: './lib/index.js', 19 | output: { file: 'dist/cjs/index.browser.js', format: 'cjs' }, 20 | external, 21 | plugins: [ 22 | nodeResolve({ extensions: ['.browser.js', '.js'] }) 23 | ] 24 | }, 25 | 26 | { 27 | input: ['./lib/index.js', './lib/linkify.js'], 28 | output: { dir: 'dist/esm', format: 'esm' }, 29 | external 30 | }, 31 | 32 | { 33 | input: './lib/index.js', 34 | output: { file: 'dist/esm/index.browser.js', format: 'esm' }, 35 | external, 36 | plugins: [ 37 | nodeResolve({ extensions: ['.browser.js', '.js'] }) 38 | ] 39 | }, 40 | 41 | { 42 | input: './lib/umd.js', 43 | output: { file: 'dist/remarkable.js', format: 'umd', name }, 44 | plugins: [ 45 | nodeResolve(), 46 | ] 47 | }, 48 | 49 | { 50 | input: './lib/umd.js', 51 | output: { file: 'dist/remarkable.min.js', format: 'umd', name }, 52 | plugins: [ 53 | nodeResolve(), 54 | terser({ 55 | output: { 56 | comments(node, comment) { 57 | // multiline comment 58 | if (comment.type == "comment2") { 59 | return /@preserve|@license|@cc_on/i.test(comment.value); 60 | } 61 | } 62 | } 63 | }) 64 | ] 65 | } 66 | ]; 67 | -------------------------------------------------------------------------------- /support/demodata.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -r esm 2 | 3 | // Build demo data for embedding into html 4 | 5 | /*eslint no-console:0*/ 6 | 7 | import fs from 'fs'; 8 | import path from 'path'; 9 | 10 | console.log(JSON.stringify({ 11 | self: { 12 | demo: { 13 | code: fs.readFileSync(path.join(__dirname, '../demo/example.js'), 'utf8'), 14 | source: fs.readFileSync(path.join(__dirname, '../demo/example.md'), 'utf8') 15 | } 16 | } 17 | }, null, 2)); 18 | -------------------------------------------------------------------------------- /support/entities.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -r esm 2 | 3 | // 4 | // Markdown entities generator (from html5 entities) 5 | // 6 | 7 | /*eslint no-console:0*/ 8 | import https from 'https'; 9 | 10 | function codeToUni(code) { 11 | var result = code.toString(16).toUpperCase(); 12 | while (result.length < 4) { result = '0' + result; } 13 | return '\\u' + result; 14 | } 15 | 16 | function strToUni(str) { 17 | var result = codeToUni(str.charCodeAt(0)); 18 | if (str.length > 1) { 19 | result += codeToUni(str.charCodeAt(1)); 20 | } 21 | return result; 22 | } 23 | 24 | https.get('https://html.spec.whatwg.org/entities.json', function (res) { 25 | var body = ''; 26 | res.on('data', function(chunk) { 27 | body += chunk; 28 | }); 29 | res.on('end', function() { 30 | var entities = JSON.parse(body); 31 | var out = {}; 32 | 33 | Object.keys(entities).forEach(function (entity) { 34 | // Skip legacy - not allosed in markdown 35 | if (entity[entity.length - 1] !== ';') { return; } 36 | 37 | out[entity.slice(1, -1)] = strToUni(entities[entity].characters); 38 | }); 39 | 40 | var result = []; 41 | 42 | Object.keys(out).forEach(function (key) { 43 | result.push(' "' + key + '": "' + out[key] + '"'); 44 | }); 45 | 46 | console.log('{\n' + result.join(',\n') + '\n}'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /support/specsplit.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node -r esm 2 | /*eslint no-console:0*/ 3 | 4 | // Fixtures generator from commonmark specs. Split spec to working / not working 5 | // examples, or show total stat. 6 | 7 | import fs from 'fs'; 8 | import util from 'util'; 9 | import argparse from 'argparse'; 10 | import { Remarkable } from '../lib/index.js'; 11 | import pkg from '../package.json'; 12 | 13 | var cli = new argparse.ArgumentParser({ 14 | prog: 'specsplit', 15 | version: pkg.version, 16 | addHelp: true 17 | }); 18 | 19 | cli.addArgument([ 'type' ], { 20 | help: 'type of examples to filter', 21 | nargs: '?', 22 | choices: [ 'good', 'bad' ] 23 | }); 24 | 25 | cli.addArgument([ 'spec' ], { 26 | help: 'spec file to read' 27 | }); 28 | 29 | var options = cli.parseArgs(); 30 | 31 | //////////////////////////////////////////////////////////////////////////////// 32 | 33 | function readFile(filename, encoding, callback) { 34 | if (options.file === '-') { 35 | // read from stdin 36 | 37 | var chunks = []; 38 | 39 | process.stdin.on('data', function(chunk) { 40 | chunks.push(chunk); 41 | }); 42 | 43 | process.stdin.on('end', function() { 44 | return callback(null, Buffer.concat(chunks).toString(encoding)); 45 | }); 46 | } else { 47 | fs.readFile(filename, encoding, callback); 48 | } 49 | } 50 | 51 | 52 | //////////////////////////////////////////////////////////////////////////////// 53 | 54 | readFile(options.spec, 'utf8', function (error, input) { 55 | var good = [], bad = [], 56 | markdown = new Remarkable('commonmark'); 57 | 58 | if (error) { 59 | if (error.code === 'ENOENT') { 60 | process.stderr.write('File not found: ' + options.spec); 61 | process.exit(2); 62 | } 63 | 64 | process.stderr.write(error.stack || error.message || String(error)); 65 | process.exit(1); 66 | } 67 | 68 | input = input.replace(/→/g, '\t'); 69 | 70 | input.replace(/^\.\n([\s\S]*?)^\.\n([\s\S]*?)^\.$/gm, function(__, md, html, offset, orig) { 71 | 72 | var result = { 73 | md: md, 74 | html: html, 75 | line: orig.slice(0, offset).split(/\r?\n/g).length, 76 | err: '' 77 | }; 78 | 79 | try { 80 | if (markdown.render(md) === html) { 81 | good.push(result); 82 | } else { 83 | result.err = markdown.render(md); 84 | bad.push(result); 85 | } 86 | } catch (___) { 87 | bad.push(result); 88 | } 89 | }); 90 | 91 | if (!options.type) { 92 | console.log(util.format('passed samples - %s, failed samples - %s', good.length, bad.length)); 93 | } else { 94 | var data = options.type === 'good' ? good : bad; 95 | 96 | data.forEach(function (sample) { 97 | console.log(util.format( 98 | '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n' + 99 | 'src line: %s\n\n.\n%s.\n%s.\n', 100 | sample.line, sample.md, sample.html)); 101 | if (sample.err) { 102 | console.log(util.format('error:\n\n%s\n', sample.err)); 103 | } 104 | }); 105 | } 106 | 107 | process.exit(0); 108 | }); 109 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | import ChildProcess from "child_process"; 2 | import assert from 'assert'; 3 | 4 | // placed here to make multiline indentation easier, feel free to move if you're 5 | // adding more tests here 6 | const desiredOutput = `

This is a simple Markdown document

7 |
8 |

It’s really just here to make sure the Remarkable CLI can take a Markdown 9 | document as an input

10 |
11 |

It’s not comprehensive or anything

12 |

Hopefully the CLI can handle it though

13 | `; 14 | 15 | describe("Remarkable CLI", function() { 16 | this.timeout(10000); 17 | it("simple Markdown file as input", function(done) { 18 | const command = "node -r esm ../lib/cli.js ./fixtures/cli-input.md" 19 | ChildProcess.exec(command, { cwd: __dirname }, (error, stdout) => { 20 | assert.strictEqual(stdout.toString(), desiredOutput); 21 | done(); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/commonmark.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { addTests } from './utils'; 3 | import { Remarkable } from '../lib/index'; 4 | 5 | describe('CommonMark', function () { 6 | var md = new Remarkable('commonmark'); 7 | addTests(path.join(__dirname, 'fixtures/commonmark/good.txt'), md); 8 | }); 9 | -------------------------------------------------------------------------------- /test/fixtures/cli-input.md: -------------------------------------------------------------------------------- 1 | ### This is a simple Markdown document 2 | 3 | > It's really just here to make sure the Remarkable CLI can take a Markdown 4 | > document as an input 5 | 6 | **It's not comprehensive or anything** 7 | 8 | Hopefully the CLI can handle it though 9 | -------------------------------------------------------------------------------- /test/fixtures/commonmark/bad.txt: -------------------------------------------------------------------------------- 1 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | src line: 3124 3 | 4 | . 5 | - # Foo 6 | - Bar 7 | --- 8 | baz 9 | . 10 |
    11 |
  • Foo

  • 12 |
  • Bar

    13 |

    baz

  • 14 |
15 | . 16 | 17 | error: 18 | 19 |
    20 |
  • Foo

    21 |
  • 22 |
  • Bar

    23 | baz
  • 24 |
25 | 26 | 27 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 28 | src line: 5603 29 | 30 | . 31 | ![foo *bar*] 32 | 33 | [foo *bar*]: train.jpg "train & tracks" 34 | . 35 |

foo <em>bar</em>

36 | . 37 | 38 | error: 39 | 40 |

foo *bar*

41 | 42 | 43 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 44 | src line: 5611 45 | 46 | . 47 | ![foo *bar*][] 48 | 49 | [foo *bar*]: train.jpg "train & tracks" 50 | . 51 |

foo <em>bar</em>

52 | . 53 | 54 | error: 55 | 56 |

foo *bar*

57 | 58 | 59 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 60 | src line: 5619 61 | 62 | . 63 | ![foo *bar*][foobar] 64 | 65 | [FOOBAR]: train.jpg "train & tracks" 66 | . 67 |

foo <em>bar</em>

68 | . 69 | 70 | error: 71 | 72 |

foo *bar*

73 | 74 | 75 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 76 | src line: 5679 77 | 78 | . 79 | ![*foo* bar][] 80 | 81 | [*foo* bar]: /url "title" 82 | . 83 |

<em>foo</em> bar

84 | . 85 | 86 | error: 87 | 88 |

*foo* bar

89 | 90 | 91 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 92 | src line: 5719 93 | 94 | . 95 | ![*foo* bar] 96 | 97 | [*foo* bar]: /url "title" 98 | . 99 |

<em>foo</em> bar

100 | . 101 | 102 | error: 103 | 104 |

*foo* bar

105 | 106 | 107 | -------------------------------------------------------------------------------- /test/fixtures/linkify.txt: -------------------------------------------------------------------------------- 1 | linkify 2 | . 3 | url http://www.youtube.com/watch?v=5Jt5GEr4AYg. 4 | . 5 |

url http://www.youtube.com/watch?v=5Jt5GEr4AYg.

6 | . 7 | 8 | 9 | don't touch text in links 10 | . 11 | [https://example.com](https://example.com) 12 | . 13 |

https://example.com

14 | . 15 | 16 | 17 | don't touch text in autolinks 18 | . 19 | 20 | . 21 |

https://example.com

22 | . 23 | 24 | 25 | don't touch text in html tags 26 | . 27 | https://example.com 28 | . 29 |

https://example.com

30 | . 31 | 32 | 33 | match links without protocol 34 | . 35 | www.example.org 36 | . 37 |

www.example.org

38 | . 39 | 40 | 41 | properly cut domain end 42 | . 43 | www.example.org版权所有 44 | . 45 |

www.example.org版权所有

46 | . 47 | 48 | 49 | unicode 50 | . 51 | www.example.org#版权所有 52 | . 53 |

www.example.org#版权所有

54 | . 55 | 56 | emails 57 | . 58 | test@example.com 59 | 60 | mailto:test@example.com 61 | . 62 |

test@example.com

63 |

mailto:test@example.com

64 | . 65 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/abbr.txt: -------------------------------------------------------------------------------- 1 | 2 | An example from php markdown readme: 3 | 4 | . 5 | *[HTML]: Hyper Text Markup Language 6 | *[W3C]: World Wide Web Consortium 7 | The HTML specification 8 | is maintained by the W3C. 9 | . 10 |

The HTML specification 11 | is maintained by the W3C.

12 | . 13 | 14 | They can be multiline (see pandoc implementation). Not sure about newlines, but we should at least skip those definitions: 15 | 16 | . 17 | *[ 18 | foo 19 | bar 20 | ]: desc 21 | foo 22 | . 23 |

foo

24 | . 25 | 26 | They can contain arbitrary markup (see pandoc implementation): 27 | 28 | . 29 | *[`]:`]: foo 30 | \`]:\` 31 | . 32 |

`]:`

33 | . 34 | 35 | Can contain matched brackets: 36 | 37 | . 38 | *[[abbr]]: foo 39 | [abbr] 40 | . 41 |

[abbr]

42 | . 43 | 44 | No empty abbreviations: 45 | 46 | . 47 | *[foo]: 48 | foo 49 | . 50 |

*[foo]: 51 | foo

52 | . 53 | 54 | Intersecting abbreviations (first should match): 55 | 56 | . 57 | *[Bar Foo]: 123 58 | *[Foo Bar]: 456 59 | 60 | Foo Bar Foo 61 | 62 | Bar Foo Bar 63 | . 64 |

Foo Bar Foo

65 |

Bar Foo Bar

66 | . 67 | 68 | Don't bother with nested abbreviations (yet?): 69 | 70 | . 71 | *[JS]: javascript 72 | *[HTTP]: hyper text blah blah 73 | *[JS HTTP]: is awesome 74 | JS HTTP is a collection of low-level javascript HTTP-related modules 75 | . 76 |

JS HTTP is a collection of low-level javascript HTTP-related modules

77 | . 78 | 79 | Don't match the middle of the string: 80 | 81 | . 82 | *[foo]: blah 83 | *[bar]: blah 84 | foobar 85 | . 86 |

foobar

87 | . 88 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/commonmark_extras.txt: -------------------------------------------------------------------------------- 1 | Regression tests for link backtracking optimizations: 2 | 3 | . 4 | [[some unrelated text [link] 5 | 6 | [link]: foo 7 | . 8 |

[[some unrelated text link

9 | . 10 | 11 | . 12 | [[some unrelated text [[link]] 13 | 14 | [[link]]: foo 15 | . 16 |

[[some unrelated text [link]

17 | . 18 | 19 | 20 | 21 | This is not a valid emphasis, because \n considered a whitespace: 22 | 23 | . 24 | **test 25 | ** 26 | 27 | ** 28 | test** 29 | 30 | ** 31 | test 32 | ** 33 | . 34 |

**test 35 | **

36 |

** 37 | test**

38 |

** 39 | test 40 | **

41 | . 42 | 43 | 44 | Link label has priority over emphasis (not covered by commonmark tests): 45 | 46 | . 47 | [**link]()** 48 | 49 | **[link**]() 50 | . 51 |

**link**

52 |

**link**

53 | . 54 | 55 | 56 | Issue #55: 57 | 58 | . 59 | ![test] 60 | 61 | ![test](foo bar) 62 | . 63 |

![test]

64 |

![test](foo bar)

65 | . 66 | 67 | 68 | Should unescape only needed things in link destinations/titles: 69 | 70 | . 71 | [test](<\f\o\o\>\\>) 72 | . 73 |

test

74 | . 75 | 76 | . 77 | [test](foo "\\\"\b\a\r") 78 | . 79 |

test

80 | . 81 | 82 | 83 | Not a closing tag 84 | 85 | . 86 | 87 | . 88 |

</ 123>

89 | . 90 | 91 | 92 | Not a list item 93 | 94 | . 95 | 1.list 96 | . 97 |

1.list

98 | . 99 | 100 | 101 | Coverage. Direcctive can terminate paragraph. 102 | 103 | . 104 | a 105 | a

108 | 115 | . 116 |

http://example.com/α%CE%B2γ%CE%B4

117 | . 118 | 119 | Autolinks do not allow escaping: 120 | 121 | . 122 | 123 | . 124 |

http://example.com/\[\

125 | . 126 | 127 | 128 | Should not throw exception on mailformed URI 129 | 130 | . 131 | [foo](<%test>) 132 | . 133 |

foo

134 | . 135 | 136 | 137 | Issue #160: 138 | 139 | . 140 | Hello [google] where are you? 141 | 142 | Hello [google][] where are you? 143 | 144 | Hello [google] [] where are you? 145 | 146 | [google]: https://google.com 147 | . 148 |

Hello google where are you?

149 |

Hello google where are you?

150 |

Hello google where are you?

151 | . 152 | 153 | 154 | Image alt should be unescaped 155 | 156 | . 157 | [Foo\\bar\]]: /url 158 | 159 | ![Foo\\bar\]] 160 | . 161 |

Foo\bar]

162 | . 163 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/deflist.txt: -------------------------------------------------------------------------------- 1 | 2 | Pandoc (with slightly changed indents): 3 | 4 | . 5 | Term 1 6 | 7 | : Definition 1 8 | 9 | Term 2 with *inline markup* 10 | 11 | : Definition 2 12 | 13 | { some code, part of Definition 2 } 14 | 15 | Third paragraph of definition 2. 16 | . 17 |
18 |
Term 1
19 |

Definition 1

20 |
21 |
Term 2 with inline markup
22 |

Definition 2

23 |
{ some code, part of Definition 2 }
 24 | 
25 |

Third paragraph of definition 2.

26 |
27 |
28 | . 29 | 30 | Pandoc again: 31 | 32 | . 33 | Term 1 34 | 35 | : Definition 36 | with lazy continuation. 37 | 38 | Second paragraph of the definition. 39 | . 40 |
41 |
Term 1
42 |

Definition 43 | with lazy continuation.

44 |

Second paragraph of the definition.

45 |
46 |
47 | . 48 | 49 | Well, I might just copy-paste the third one while I'm at it: 50 | 51 | . 52 | Term 1 53 | ~ Definition 1 54 | 55 | Term 2 56 | ~ Definition 2a 57 | ~ Definition 2b 58 | . 59 |
60 |
Term 1
61 |
Definition 1 62 |
63 |
Term 2
64 |
Definition 2a 65 |
66 |
Definition 2b 67 |
68 |
69 | . 70 | 71 | Now, with our custom ones. Spaces after a colon: 72 | 73 | . 74 | Term 1 75 | : paragraph 76 | 77 | Term 2 78 | : code block 79 | . 80 |
81 |
Term 1
82 |
paragraph 83 |
84 |
Term 2
85 |
code block
 86 | 
87 |
88 |
89 | . 90 | 91 | There should be something after a colon by the way: 92 | 93 | . 94 | Non-term 1 95 | : 96 | 97 | Non-term 2 98 | : 99 | . 100 |

Non-term 1 101 | :

102 |

Non-term 2 103 | :

104 | . 105 | 106 | Definition lists should be second last in the queue. Exemplī grātiā, this isn't a valid one: 107 | 108 | . 109 | # test 110 | : just a paragraph with a colon 111 | . 112 |

test

113 |

: just a paragraph with a colon

114 | . 115 | 116 | Nested definition lists: 117 | 118 | . 119 | test 120 | : foo 121 | : bar 122 | : baz 123 | : bar 124 | : foo 125 | . 126 |
127 |
test
128 |
129 |
foo
130 |
131 |
bar
132 |
baz 133 |
134 |
135 |
136 |
bar 137 |
138 |
139 |
140 |
foo 141 |
142 |
143 | . 144 | 145 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/del.txt: -------------------------------------------------------------------------------- 1 | . 2 | ~~Strikeout~~ 3 | . 4 |

Strikeout

5 | . 6 | 7 | Strikeouts have the same priority as emphases: 8 | 9 | . 10 | **~~test**~~ 11 | 12 | ~~**test~~** 13 | . 14 |

~~test~~

15 |

**test**

16 | . 17 | 18 | Strikeouts have the same priority as emphases with respect to links: 19 | . 20 | [~~link]()~~ 21 | 22 | ~~[link~~]() 23 | . 24 |

~~link~~

25 |

~~link~~

26 | . 27 | 28 | Strikeouts have the same priority as emphases with respect to backticks: 29 | . 30 | ~~`code~~` 31 | 32 | `~~code`~~ 33 | . 34 |

~~code~~

35 |

~~code~~

36 | . 37 | 38 | Nested strikeouts: 39 | . 40 | ~~foo ~~bar~~ baz~~ 41 | . 42 |

foo bar baz

43 | . 44 | 45 | . 46 | ~~f **o ~~o b~~ a** r~~ 47 | . 48 |

f o o b a r

49 | . 50 | 51 | Should not have a whitespace between text and "~~": 52 | . 53 | foo ~~ bar ~~ baz 54 | . 55 |

foo ~~ bar ~~ baz

56 | . 57 | 58 | 59 | Newline should be considered a whitespace: 60 | 61 | . 62 | ~~test 63 | ~~ 64 | 65 | ~~ 66 | test~~ 67 | 68 | ~~ 69 | test 70 | ~~ 71 | . 72 |

~~test 73 | ~~

74 |

~~ 75 | test~~

76 |

~~ 77 | test 78 | ~~

79 | . 80 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/ins.txt: -------------------------------------------------------------------------------- 1 | . 2 | ++Insert++ 3 | . 4 |

Insert

5 | . 6 | 7 | 8 | These are not inserts, you have to use exactly two "++": 9 | . 10 | x +++foo+++ 11 | 12 | x ++foo+++ 13 | 14 | x +++foo++ 15 | . 16 |

x +++foo+++

17 |

x ++foo+++

18 |

x +++foo++

19 | . 20 | 21 | Inserts have the same priority as emphases: 22 | 23 | . 24 | **++test**++ 25 | 26 | ++**test++** 27 | . 28 |

++test++

29 |

**test**

30 | . 31 | 32 | Inserts have the same priority as emphases with respect to links: 33 | . 34 | [++link]()++ 35 | 36 | ++[link++]() 37 | . 38 |

++link++

39 |

++link++

40 | . 41 | 42 | Inserts have the same priority as emphases with respect to backticks: 43 | . 44 | ++`code++` 45 | 46 | `++code`++ 47 | . 48 |

++code++

49 |

++code++

50 | . 51 | 52 | Nested inserts: 53 | . 54 | ++foo ++bar++ baz++ 55 | . 56 |

foo bar baz

57 | . 58 | 59 | . 60 | ++f **o ++o b++ a** r++ 61 | . 62 |

f o o b a r

63 | . 64 | 65 | Should not have a whitespace between text and "++": 66 | . 67 | foo ++ bar ++ baz 68 | . 69 |

foo ++ bar ++ baz

70 | . 71 | 72 | 73 | Newline should be considered a whitespace: 74 | 75 | . 76 | ++test 77 | ++ 78 | 79 | ++ 80 | test++ 81 | 82 | ++ 83 | test 84 | ++ 85 | . 86 |

++test 87 | ++

88 |

++ 89 | test++

90 |

++ 91 | test 92 | ++

93 | . 94 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/mark.txt: -------------------------------------------------------------------------------- 1 | . 2 | ==Mark== 3 | . 4 |

Mark

5 | . 6 | 7 | 8 | These are not marks, you have to use exactly two "==": 9 | . 10 | x ===foo=== 11 | 12 | x ==foo=== 13 | 14 | x ===foo== 15 | . 16 |

x ===foo===

17 |

x ==foo===

18 |

x ===foo==

19 | . 20 | 21 | Marks have the same priority as emphases: 22 | 23 | . 24 | **==test**== 25 | 26 | ==**test==** 27 | . 28 |

==test==

29 |

**test**

30 | . 31 | 32 | Marks have the same priority as emphases with respect to links: 33 | . 34 | [==link]()== 35 | 36 | ==[link==]() 37 | . 38 |

==link==

39 |

==link==

40 | . 41 | 42 | Marks have the same priority as emphases with respect to backticks: 43 | . 44 | ==`code==` 45 | 46 | `==code`== 47 | . 48 |

==code==

49 |

==code==

50 | . 51 | 52 | Nested marks: 53 | . 54 | ==foo ==bar== baz== 55 | . 56 |

foo bar baz

57 | . 58 | 59 | . 60 | ==f **o ==o b== a** r== 61 | . 62 |

f o o b a r

63 | . 64 | 65 | Should not have a whitespace between text and "==": 66 | . 67 | foo == bar == baz 68 | . 69 |

foo == bar == baz

70 | . 71 | 72 | 73 | Newline should be considered a whitespace: 74 | 75 | . 76 | ==test 77 | == 78 | 79 | == 80 | test== 81 | 82 | == 83 | test 84 | == 85 | . 86 |

==test

87 |

== 88 | test==

89 |

== 90 | test 91 | ==

92 | . 93 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/proto.txt: -------------------------------------------------------------------------------- 1 | 2 | . 3 | [__proto__] 4 | 5 | [__proto__]: blah 6 | . 7 |

proto

8 | . 9 | 10 | . 11 | [^__proto__] 12 | 13 | [^__proto__]: blah 14 | . 15 |

[1]

16 |
17 |
18 |
    19 |
  1. blah

    20 |
  2. 21 |
22 |
23 | . 24 | 25 | . 26 | *[__proto__]: blah 27 | 28 | __proto__ \_\_proto\_\_ 29 | . 30 |

proto __proto__

31 | . 32 | 33 | . 34 | [hasOwnProperty] 35 | 36 | [hasOwnProperty]: blah 37 | . 38 |

hasOwnProperty

39 | . 40 | 41 | . 42 | [^hasOwnProperty] 43 | 44 | [^hasOwnProperty]: blah 45 | . 46 |

[1]

47 |
48 |
49 |
    50 |
  1. blah

    51 |
  2. 52 |
53 |
54 | . 55 | 56 | . 57 | *[hasOwnProperty]: blah 58 | 59 | hasOwnProperty 60 | . 61 |

hasOwnProperty

62 | . 63 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/redos.txt: -------------------------------------------------------------------------------- 1 | . 2 | ReDoS 3 | . 4 |

ReDoS<![CDATA[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]] >

5 | . 6 | 7 | . 8 | z 9 | . 10 |

z<!–aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa—>

11 | . 12 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/smartquotes.txt: -------------------------------------------------------------------------------- 1 | Should parse nested quotes: 2 | 3 | . 4 | "foo 'bar' baz" 5 | . 6 |

“foo ‘bar’ baz”

7 | . 8 | 9 | . 10 | 'foo 'bar' baz' 11 | . 12 |

‘foo ‘bar’ baz’

13 | . 14 | 15 | 16 | Should not overlap quotes: 17 | 18 | . 19 | 'foo "bar' baz" 20 | . 21 |

‘foo "bar’ baz"

22 | . 23 | 24 | 25 | Should match quotes on the same level: 26 | 27 | . 28 | "foo *bar* baz" 29 | . 30 |

“foo bar baz”

31 | . 32 | 33 | 34 | Should not match quotes on different levels: 35 | 36 | . 37 | *"foo* bar" 38 | 39 | "foo *bar"* 40 | . 41 |

"foo bar"

42 |

"foo bar"

43 | . 44 | 45 | . 46 | *"foo* bar *baz"* 47 | . 48 |

"foo bar baz"

49 | . 50 | 51 | 52 | Should try and find matching quote in this case: 53 | 54 | . 55 | "foo "bar 'baz" 56 | . 57 |

"foo “bar 'baz”

58 | . 59 | 60 | 61 | Should not touch 'inches' in quotes: 62 | 63 | . 64 | "Monitor 21"" 65 | . 66 |

“Monitor 21"”

67 | . 68 | 69 | 70 | Should render an apostrophe as a rsquo: 71 | 72 | . 73 | This isn't and can't be the best approach to implement this... 74 | . 75 |

This isn’t and can’t be the best approach to implement this…

76 | . 77 | 78 | 79 | Apostrophe could end the word, that's why original smartypants replaces all of them as rsquo: 80 | 81 | . 82 | users' stuff 83 | . 84 |

users’ stuff

85 | . 86 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/sub.txt: -------------------------------------------------------------------------------- 1 | 2 | . 3 | ~foo\~ 4 | . 5 |

~foo~

6 | . 7 | 8 | . 9 | ~foo bar~ 10 | . 11 |

~foo bar~

12 | . 13 | 14 | . 15 | ~foo\ bar\ baz~ 16 | . 17 |

foo bar baz

18 | . 19 | 20 | . 21 | ~\ foo\ ~ 22 | . 23 |

foo

24 | . 25 | 26 | . 27 | ~foo\\\\\\\ bar~ 28 | . 29 |

foo\\\ bar

30 | . 31 | 32 | . 33 | ~foo\\\\\\ bar~ 34 | . 35 |

~foo\\\ bar~

36 | . 37 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/sup.txt: -------------------------------------------------------------------------------- 1 | 2 | . 3 | ^test^ 4 | . 5 |

test

6 | . 7 | 8 | . 9 | ^foo\^ 10 | . 11 |

^foo^

12 | . 13 | 14 | . 15 | 2^4 + 3^5 16 | . 17 |

2^4 + 3^5

18 | . 19 | 20 | . 21 | ^foo~bar^baz^bar~foo^ 22 | . 23 |

foo~barbazbar~foo

24 | . 25 | 26 | . 27 | ^\ foo\ ^ 28 | . 29 |

foo

30 | . 31 | 32 | . 33 | ^foo\\\\\\\ bar^ 34 | . 35 |

foo\\\ bar

36 | . 37 | 38 | . 39 | ^foo\\\\\\ bar^ 40 | . 41 |

^foo\\\ bar^

42 | . 43 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/tables.txt: -------------------------------------------------------------------------------- 1 | Tests from marked: 2 | 3 | . 4 | | Heading 1 | Heading 2 5 | | --------- | --------- 6 | | Cell 1 | Cell 2 7 | | Cell 3 | Cell 4 8 | . 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Heading 1Heading 2
Cell 1Cell 2
Cell 3Cell 4
18 | . 19 | 20 | . 21 | | Header 1 | Header 2 | Header 3 | Header 4 | 22 | | :------: | -------: | :------- | -------- | 23 | | Cell 1 | Cell 2 | Cell 3 | Cell 4 | 24 | | Cell 5 | Cell 6 | Cell 7 | Cell 8 | 25 | . 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
Header 1Header 2Header 3Header 4
Cell 1Cell 2Cell 3Cell 4
Cell 5Cell 6Cell 7Cell 8
35 | . 36 | 37 | . 38 | Header 1 | Header 2 39 | -------- | -------- 40 | Cell 1 | Cell 2 41 | Cell 3 | Cell 4 42 | . 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
Header 1Header 2
Cell 1Cell 2
Cell 3Cell 4
52 | . 53 | 54 | . 55 | Header 1|Header 2|Header 3|Header 4 56 | :-------|:------:|-------:|-------- 57 | Cell 1 |Cell 2 |Cell 3 |Cell 4 58 | *Cell 5*|Cell 6 |Cell 7 |Cell 8 59 | . 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
Header 1Header 2Header 3Header 4
Cell 1Cell 2Cell 3Cell 4
Cell 5Cell 6Cell 7Cell 8
69 | . 70 | 71 | Nested tables inside blockquotes: 72 | 73 | . 74 | > foo|foo 75 | > ---|--- 76 | > bar|bar 77 | baz|baz 78 | . 79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
foofoo
barbar
88 |
89 |

baz|baz

90 | . 91 | 92 | Nested tables inside lists: 93 | 94 | . 95 | - foo|foo 96 | ---|--- 97 | bar|bar 98 | baz|baz 99 | . 100 |
    101 |
  • 102 | 103 | 104 | 105 | 106 | 107 | 108 |
    foofoo
    barbar
    109 |
  • 110 |
111 |

baz|baz

112 | . 113 | 114 | Minimal one-column table test: 115 | 116 | . 117 | | foo 118 | |---- 119 | | test2 120 | . 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
foo
test2
129 | . 130 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/typographer.txt: -------------------------------------------------------------------------------- 1 | . 2 | (bad) 3 | . 4 |

(bad)

5 | . 6 | 7 | 8 | copyright 9 | . 10 | (c) (C) 11 | . 12 |

© ©

13 | . 14 | 15 | 16 | reserved 17 | . 18 | (r) (R) 19 | . 20 |

® ®

21 | . 22 | 23 | 24 | trademark 25 | . 26 | (tm) (TM) 27 | . 28 |

™ ™

29 | . 30 | 31 | 32 | paragraph 33 | . 34 | (p) (P) 35 | . 36 |

§ §

37 | . 38 | 39 | 40 | plus-minus 41 | . 42 | +-5 43 | . 44 |

±5

45 | . 46 | 47 | 48 | ellipsis 49 | . 50 | test.. test... test..... test?..... test!.... 51 | . 52 |

test… test… test… test?.. test!..

53 | . 54 | 55 | 56 | dupes 57 | . 58 | !!!!!! ???? ,, 59 | . 60 |

!!! ??? ,

61 | . 62 | 63 | 64 | dashes 65 | . 66 | ---remarkable --- super--- 67 | 68 | remarkable---awesome 69 | 70 | abc ---- 71 | . 72 |

—remarkable — super—

73 |

remarkable—awesome

74 |

abc ----

75 | . 76 | . 77 | --remarkable -- super-- 78 | 79 | remarkable--awesome 80 | . 81 |

–remarkable – super–

82 |

remarkable–awesome

83 | . 84 | 85 | -------------------------------------------------------------------------------- /test/fixtures/remarkable/xss.txt: -------------------------------------------------------------------------------- 1 | . 2 | [normal link](javascript) 3 | . 4 |

normal link

5 | . 6 | 7 | 8 | 9 | Should not allow some protocols in links and images 10 | 11 | . 12 | [xss link](javascript:alert(1)) 13 | 14 | [xss link](JAVASCRIPT:alert(1)) 15 | 16 | [xss link](vbscript:alert(1)) 17 | 18 | [xss link](VBSCRIPT:alert(1)) 19 | 20 | [xss link](file:///123) 21 | . 22 |

[xss link](javascript:alert(1))

23 |

[xss link](JAVASCRIPT:alert(1))

24 |

[xss link](vbscript:alert(1))

25 |

[xss link](VBSCRIPT:alert(1))

26 |

[xss link](file:///123)

27 | . 28 | 29 | 30 | . 31 | [xss link]("><script>alert("xss")</script>) 32 | . 33 |

xss link

34 | . 35 | 36 | . 37 | [xss link]() 38 | . 39 |

[xss link](<javascript:alert(1)>)

40 | . 41 | 42 | . 43 | [xss link](javascript:alert(1)) 44 | . 45 |

[xss link](javascript:alert(1))

46 | . 47 | 48 | 49 | Image parser use the same code base. 50 | 51 | . 52 | ![xss link](javascript:alert(1)) 53 | . 54 |

![xss link](javascript:alert(1))

55 | . 56 | 57 | 58 | Autolinks 59 | 60 | . 61 | 62 | 63 | 64 | . 65 |

<javascript:alert(1)>

66 |

<javascript:alert(1)>

67 | . 68 | 69 | 70 | Linkifier 71 | 72 | . 73 | javascript:alert(1) 74 | 75 | javascript:alert(1) 76 | . 77 |

javascript:alert(1)

78 |

javascript:alert(1)

79 | . 80 | 81 | 82 | . 83 | [ASCII control characters XSS](javascript:alert(1)) 84 | . 85 |

[ASCII control characters XSS](javascript:alert(1))

86 | . 87 | -------------------------------------------------------------------------------- /test/linkify.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import assert from 'assert'; 3 | import { addTests } from './utils'; 4 | import { Remarkable } from '../lib/index'; 5 | import { linkify } from '../lib/linkify'; 6 | 7 | describe('linkify plugin', function () { 8 | var md = new Remarkable({ html: true }).use(linkify); 9 | addTests(path.join(__dirname, 'fixtures/linkify.txt'), md); 10 | }); 11 | 12 | describe('linkify option', function () { 13 | it('should warn about using linkify option instead of plugin', () => { 14 | const messages = [] 15 | const oldWarn = console.warn; 16 | console.warn = message => messages.push(message); 17 | new Remarkable({ html: true, linkify: true }); 18 | console.warn = oldWarn; 19 | assert.deepEqual(messages, [ 20 | `linkify option is removed. Use linkify plugin instead:\n\n` + 21 | `import Remarkable from 'remarkable';\n` + 22 | `import linkify from 'remarkable/linkify';\n` + 23 | `new Remarkable().use(linkify)\n` 24 | ]) 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/remarkable.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { addTests } from './utils'; 3 | import { Remarkable } from '../lib/index'; 4 | 5 | describe('remarkable', function () { 6 | var md = new Remarkable('full', { 7 | html: true, 8 | langPrefix: '', 9 | typographer: true, 10 | }); 11 | 12 | addTests(path.join(__dirname, 'fixtures/remarkable'), md); 13 | }); 14 | -------------------------------------------------------------------------------- /test/ruler.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Ruler from '../lib/ruler'; 3 | 4 | describe('Ruler', function () { 5 | 6 | it('should replace rule (.at)', function () { 7 | var ruler = new Ruler(); 8 | var res = 0; 9 | 10 | ruler.push('test', function foo() { res = 1; }); 11 | ruler.at('test', function bar() { res = 2; }); 12 | 13 | var rules = ruler.getRules(''); 14 | 15 | assert.strictEqual(rules.length, 1); 16 | rules[0](); 17 | assert.strictEqual(res, 2); 18 | }); 19 | 20 | 21 | it('should inject before/after rule', function () { 22 | var ruler = new Ruler(); 23 | var res = 0; 24 | 25 | ruler.push('test', function foo() { res = 1; }); 26 | ruler.before('test', 'before_test', function fooBefore() { res = -10; }); 27 | ruler.after('test', 'after_test', function fooAfter() { res = 10; }); 28 | 29 | var rules = ruler.getRules(''); 30 | 31 | assert.strictEqual(rules.length, 3); 32 | rules[0](); 33 | assert.strictEqual(res, -10); 34 | rules[1](); 35 | assert.strictEqual(res, 1); 36 | rules[2](); 37 | assert.strictEqual(res, 10); 38 | }); 39 | 40 | 41 | it('should enable/disable rule', function () { 42 | var rules, ruler = new Ruler(); 43 | 44 | ruler.push('test', function foo() {}); 45 | ruler.push('test2', function bar() {}); 46 | 47 | rules = ruler.getRules(''); 48 | assert.strictEqual(rules.length, 2); 49 | 50 | ruler.disable('test'); 51 | rules = ruler.getRules(''); 52 | assert.strictEqual(rules.length, 1); 53 | ruler.disable('test2'); 54 | rules = ruler.getRules(''); 55 | assert.strictEqual(rules.length, 0); 56 | 57 | ruler.enable('test'); 58 | rules = ruler.getRules(''); 59 | assert.strictEqual(rules.length, 1); 60 | ruler.enable('test2'); 61 | rules = ruler.getRules(''); 62 | assert.strictEqual(rules.length, 2); 63 | }); 64 | 65 | 66 | it('should enable/disable multiple rule', function () { 67 | var rules, ruler = new Ruler(); 68 | 69 | ruler.push('test', function foo() {}); 70 | ruler.push('test2', function bar() {}); 71 | 72 | ruler.disable([ 'test', 'test2' ]); 73 | rules = ruler.getRules(''); 74 | assert.strictEqual(rules.length, 0); 75 | ruler.enable([ 'test', 'test2' ]); 76 | rules = ruler.getRules(''); 77 | assert.strictEqual(rules.length, 2); 78 | }); 79 | 80 | 81 | it('should enable rules by whitelist', function () { 82 | var rules, ruler = new Ruler(); 83 | 84 | ruler.push('test', function foo() {}); 85 | ruler.push('test2', function bar() {}); 86 | 87 | ruler.enable('test', true); 88 | rules = ruler.getRules(''); 89 | assert.strictEqual(rules.length, 1); 90 | }); 91 | 92 | 93 | it('should support multiple chains', function () { 94 | var rules, ruler = new Ruler(); 95 | 96 | ruler.push('test', function foo() {}); 97 | ruler.push('test2', function bar() {}, { alt: [ 'alt1' ] }); 98 | ruler.push('test2', function bar() {}, { alt: [ 'alt1', 'alt2' ] }); 99 | 100 | rules = ruler.getRules(''); 101 | assert.strictEqual(rules.length, 3); 102 | rules = ruler.getRules('alt1'); 103 | assert.strictEqual(rules.length, 2); 104 | rules = ruler.getRules('alt2'); 105 | assert.strictEqual(rules.length, 1); 106 | }); 107 | 108 | 109 | it('should fail on invalid rule name', function () { 110 | var ruler = new Ruler(); 111 | 112 | ruler.push('test', function foo() {}); 113 | 114 | assert.throws(function () { 115 | ruler.at('invalid name', function bar() {}); 116 | }); 117 | assert.throws(function () { 118 | ruler.before('invalid name', function bar() {}); 119 | }); 120 | assert.throws(function () { 121 | ruler.after('invalid name', function bar() {}); 122 | }); 123 | assert.throws(function () { 124 | ruler.enable('invalid name'); 125 | }); 126 | assert.throws(function () { 127 | ruler.disable('invalid name'); 128 | }); 129 | }); 130 | 131 | it('should always return an array, even when no rules are defined for the rule name', function () { 132 | var rules, ruler = new Ruler(); 133 | 134 | rules = ruler.getRules('list'); 135 | assert.strictEqual(rules.constructor, Array); 136 | }); 137 | 138 | }); 139 | -------------------------------------------------------------------------------- /test/test-browser.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | const readFixture = file => 5 | JSON.stringify(fs.readFileSync(file, 'utf-8')); 6 | 7 | const readFixtureDir = dir => 8 | fs.readdirSync(dir).map(file => [JSON.stringify(file), readFixture(path.join(dir, file))]); 9 | 10 | const content = ` 11 | 38 | ` 39 | 40 | fs.writeFileSync('index.html', content) 41 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import assert from 'assert'; 4 | 5 | export function addTests(fPath, markdown, skip) { 6 | var input, 7 | stat = fs.statSync(fPath); 8 | 9 | if (stat.isFile()) { 10 | input = fs.readFileSync(fPath, 'utf8'); 11 | 12 | input = input.replace(/→/g, '\t'); 13 | 14 | describe(fPath, function () { 15 | input.replace(/^\.\n([\s\S]*?)^\.\n([\s\S]*?)^\.$/gm, function(__, md, html, offset, orig) { 16 | var line = orig.slice(0, offset).split(/\r?\n/g).length; 17 | 18 | // Also skip tests if file name starts with "_" 19 | if (!skip && path.basename(fPath)[0] !== '_') { 20 | it('line ' + line, function () { 21 | assert.strictEqual(html, markdown.render(md)); 22 | }); 23 | } else { 24 | it.skip('line ' + line, function () { 25 | assert.strictEqual(html, markdown.render(md)); 26 | }); 27 | } 28 | }); 29 | }); 30 | 31 | return; 32 | } 33 | 34 | if (stat.isDirectory()) { 35 | fs.readdirSync(fPath).forEach(function (name) { 36 | addTests(path.join(fPath, name), markdown, skip); 37 | }); 38 | } 39 | } 40 | --------------------------------------------------------------------------------