├── .editorconfig ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode ├── settings.json └── spellright.dict ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── src ├── index.ts └── slimdown.test.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | 6 | [*.ts] 7 | indent_style = space 8 | indent_size = 2 9 | tab_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | end_of_line = lf 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # repo specific 64 | .rpt2_cache 65 | dist 66 | lib 67 | *.test.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .editorconfig 3 | .gitignore 4 | src 5 | node_modules 6 | tsconfig.json 7 | tslint.json 8 | shrinkwrap.yaml 9 | pnpm-lock.yaml 10 | .rpt2_cache 11 | rollup.config.js 12 | dist 13 | *.test.js 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "spellright.language": [ 3 | "en" 4 | ], 5 | "spellright.documentTypes": [ 6 | "markdown", 7 | "latex", 8 | "plaintext" 9 | ] 10 | } -------------------------------------------------------------------------------- /.vscode/spellright.dict: -------------------------------------------------------------------------------- 1 | slimdown-js 2 | gzip 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Erik Vullings 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # slimdown-js 2 | 3 | A basic regex-based Markdown parser based on the gist by [Johnny Broadway](https://gist.github.com/jbroadway/2836900), converted from PHP to TypeScript, extended with several additional elements (images, tables, code blocks, underscores) and published to `npm`. 4 | 5 | Inspired by: 6 | 7 | - [Landmark: the simplest Markdown engine for the browser](https://gist.github.com/plugnburn/f0d12e38b6416a77c098) 8 | - [parse-md-js](https://github.com/Chalarangelo/parse-md-js/blob/master/parsemd.js) 9 | 10 | Supports the following elements (and can be extended via `addRule(regexp: RegExp, replacement: string | Function)`): 11 | 12 | - Headers: `# Header 1`, or `## Header 2` 13 | - Images: `![ALT TEXT](https://my_image_source)` 14 | - Links: `[ALT TEXT](https://my_image_source)` 15 | - Bold: `**bold**` or `__bold__` 16 | - Emphasis: `*italics*` or `_italics_` 17 | - Deletions: `~~bold~~` 18 | - Quotes: `This is a quote: :"my quote":` 19 | - Inline code: `This is \`inline\` code`. 20 | - Code blocks: Use three subsequent backticks \` to open and close a code block. 21 | - Blockquotes: Lines starting with `> `. 22 | - Tables: Use pipes `|` to separate columns, and '-' to separate the table header from its body. 23 | - Underscores (Escape underscores to keep them `\_`) 24 | - Ordered/unordered lists (up to three levels deep, may be nested) 25 | - Superscript and subscript (`z~1~` or `a^2^`) 26 | - Footnotes, e.g. `footnote[^1]` and `[^1]: Footnote reference`. 27 | 28 | ## Size 29 | 30 | The main reason for using this library, which hasn't been extensively tested and is not completely compatible with the spec, is to have something small. Version 0.7.0's size is 2.261 bytes uncompressed, and 1.241 bytes using 7z compression. 31 | 32 | For more advanced scenario's, however, I can recommend [marked](https://github.com/markedjs/marked), albeit at a bigger size: marked.min.js is 23.372 bytes uncompressed, and 7.684 bytes using gzip. 33 | 34 | ## Playground 35 | 36 | Head over to [flems.io](https://flems.io) for a [live example](https://flems.io/#0=N4IgtglgJlA2CmIBcAWArAOgMwBoQGd4EBjAF3imRA1PxD32ICcB7WWZAbQAYcBGNAHYAungBmEBHSSdQAOwCGYREmq16IYiznkdVeAA8ADiyakABKQCeR+OYBK8AOaHHR2AuLwm5gLzmACnwAVwAjfFImCDknJHMIqJiccwxUhSYnfDiFOStOYQBKPwA+eMjopwBuAB05WoB6ACpG2vNG8wBlWAgwKBYAdzlzAFpzAEFzADdvK3NQhXwIYnMmZ0Nh+cIocwBZdIBrPsHzI3TCJgxO4KMTM3xLAAt4VvaxNlgBivMieGUde4COW2xByczshl0UAoUwgCk63V6AzkSCQChgAH0mMEEAECgUkC8XiNzAAJeBo7z4ImjACSYAULipQ3aowAMtF9ky2sSAEJsKDU8wAUTARgeCwgXJZ5gAIj9SBBtFLiQBFYIscjK2lybpyOxaKGCgDCLChcw+xE5gp5Fv2AEd1ZrBQB5JhQ1ZQerBOSmd3Q7oRAHaOwIaawczaWBWAqCkmmCAAL20pAU4axUkFHWulOYECMFiB8TCjCi+cCAAME+i+OjyxGfOWFAA9ABMTfLMeZRNdECc0VT5gUwVID1McQAUiwHnJcuYeaw0f0FLMADwAKynM6sAAEN9PcqEF1Al1YMFowMUiQB1eDhCDkOIPUikIxZer1PsRDB9kdhM8sMB6jXQ8WEXZd6hbAAOLAADYAE5uG4IkaTkfAjAgJgFAVbQCWZYknxfN8P0lUhv3vB4-3Pep3GCJw5FCYImDkeoxG4KA+BbeAsEg0IYJQPgYIUQRBGIbg4MgwUCNfJB3x-CjQn-QCjXFDxMJiIgWGos54GGXphjXfB6lCD5QnqekIm8LSmEIXoMAMyTn2k2SSLI38FKomi6IYpiWLYjiuJ4viBKEkSxIkrs8LGYdRyYOIhSifZzAANWxXVMnMFdvAgfYMEmVKKnwbcnHpSRFMvPCTTkaZrMVIYxFYMBzAABRJJrLBYcwABUbHgDpc3zZIFBuKMvgkAx4HuQtNVoQaYC+MBTHBBA-hmwc5G2IwwgDB4vlIDq5CMMBcO5G87wfcwpKIuTKIA+osv2PL2AK+p8ARI45H05V2S8VD4DiHYaU6l56lqBp6k6PbVnMA07GMlhLXuchRVMdJJFmPaTlWaYdHMekmEOJFMfhibFhicx+nI6JHl+WotFQiwYZteHOTiBIKnyPxzHyGo5DpiJzGiXV4BNKFWfKGIOf8bmwYhxbzDeDUfSdXmlQsBXSCVia4jGJhMKsFdODZpIykSJxhFKKXhB52nVe+RghvgPYjDiRwtDdFcjacZJPYt8xgFacwAHIADJA7iEOlCMSpA5wAPA5XMOg+D2BSGj2OhiD4pE5DpxU5juPqhAbPg4dDU04DwvA8L8Pg4AYiwODy7kABfa2Vfpu3iEcFwDE5vV+gcZwhWMAJy04AASYBnVCNd4DIDB9ngKx8ACCaQVsJ2CjslhogCQPA4KZvhHLZJA6cA+275iw185oIxdN-ETa+XxyvMeIMFWdxPHgVfGG7wxkgBHpKQYgDwigv07g7J2nBgGgMKJfW2pxMK33RPfCoyQhZoJiOA0o-sM5X0sFEMAyhtj+CFjQIhuIeZv1WKQRiQx6hNhXNUao9QAD8ARsQAB82BcO6Fwh4XCjBcOMlwlMxl4BiKYGIqABR6gQBoBNUgARyjEIoJ2N+b82HmHLCwuQk8hbNz0bojOb84i6NqCuIwl5aiT1USQoxljqI2LkOWHmrdQbMXBgQ7E7J+b+ACKgp+xt0RBM9ske8vxOb7xwTLN+5YVzYhcSw0gK5ujFEnpEsAFCei4mbiueo6S9EFKSW4zxPjYB+IsAEgO6JyAGFIFgr2AdohQh0E09Ob90QMXYPARpwTmkZyyR02oOC-YBwIaGIgnM+DmAANS7Cwg8DAYgPimACK0+AOgMAIBiCOcw4MWwFGoSsPp9DAgBzflXSxSTA4f3gLYLCAQpmwCKHMy5Qc9EpLSRALO8yPlZJyWAXE-zTFBwKekwOoLNGfKcbc+5jzlEvI0eYY5tQPF1C8dDW2bAqkoI6QLJG0SD4lDiTolcbBknVFSekzJSMgV5Ihb84p9RKVlMxQQ3FJFb61Pqf08JLT1pbP5eLQZXSekIBFabTphLfgjLkGMvBb9JnwDDDM+ZiyRwrLWUwDZQrtm7Nzg8A55gjknNoecgIHzrlyApbALOCLyRItVUQN51qvnUp+X895YLAWqJBT6mF8dCm-KhYGzRNqCmUruZ-J1zyXWvIDmiluCCO5w0tKXcg+KBnJFCQSrJxLYkZwsba9N9pHTwAycAP1lDD4FLLZmyt7KZZNVYF4fA9x1aa1OWIbwWz20CyGCOOwfKuZNj4MIG2Hcu0angI4XtqxeZzoeR4LwPgAnolgQ8fNUAmmKoDu+cwRpVhYTsHCXUiVoiLDNHCEIthrL9QsCmJw5NyJE3vT2vtS6uSnLoUxcld6BZQF8IXMQchVhiCQJkqARiQDFBXHCB4EGQMgFrmBqD1aYOF2KBPTDR8CkKHgy9a4xR2UYpbW2km8sWCK1neYKEEg5D3lqvcTg47hBxAAGI0Y1nRvlU7+YzvIHKRjzHtBuFXd4HlGdN1YVAfKt+0AFOWAhPK-dGchMTQwJtfADwAicGgMkPlDKCjwIDha-9+9KgHPBo4Ba0xqb0fgKJ7CdUGqOZKkOiE6K26HoAOJbO8Kexzmn7iEDILVATFgXB6kwuQbjtHNQdDnq52+6nFNiECKFnZWyjV+F8P4bgRQLNDCs545VttQsklIGAcM-hQsfIwPSIwVqwVv304ZlTDTCglB0R8n5QGUPoeg7B1+MLzB2IhEYtrGU73wcQ8hwuaHwPOYw9AUbgBKwgI0Rub-WQ2kZlW-FFb9t67xtRfcrv7zkmJXFACAkxoYeA7UNnjmsqRwYDiuJDY27VjbfpPKrNXYDTbflG+1Ti7uTFI+41N-NxEIGzeEi65J3RZBzdRpgwCCV010FKio6XsUd1TL2IY9XTDAMa2hboyjA5cIPo1iQKdvABECeiCJg1dZjIgOYUo3BzDB2DgLDKg5dY5b2ca0YfBjspGayzrQrzSUzfqEgYYcyZJOEUREAI8vpcwu0YHH65AmBVxm2Yg5qv1ea+UTrj5eug5RCcE+E343NFxGV6r+oVvtdsF15o-XCAxCkGdy78w4d85guTRVjupBSGBHl3EOQwQwChG8ATqP-M4T+GJ3RTg8urYfJK4Ocw2i6zZ7kChyeChYN1jDycjF6eLDGv8AkyIVangUmspT9w94950+l01oaLOHiyN68Pkz-fGdG6HyPiBw-+eC7H4akc-fZcBGHxEsZLeHh2KgBsw+beYMFJHKR-vG4zsH3yfUVv7KG8rAGPcfwOPhVd+p3vPR9OwUD5awEVg-Qxm--HwZ0kCnx-wGH-wGHnzv36DFyNUj0J35mWH8F-x-RO1l1t0CF-yLRD3JVb0nl-3QJhQwCpx71pw-2wJO0n2Z1Z3ZxFyYC5x53MD5wFyFxXFoJgP2Ul190INXy8HYA316xbygB3z32birV4NgBM0vxjxPwIM0VOzkD3gvyPyYAOw+VPx3gUJiXNTOX-RLRXHh0rX0NCFNCsDb2bknmICkOMKgFMKPwUAkRcTI1hwZgQByAAFUjA3CmA6tAgL091esL0EVJMAh6hmFWE2FfhigPxT50QLsOVbZ293REdRVkgt0CUm8g4SUIElV4CLAXlOYt12CHhtC-0hgElt9gAXlRDJ5ll-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-oyM-4gYvCzk82sEGquwOM9taOgOSVzmJksgGY3MleEtSeSVxxVxOAhN-wJN0gT8w07EoBUChVX1vSsYYaWYBNxSn9WtlZUwIUTwPTfTVYHub2YsUgLFmS-wCdH4mq3wvpIXR1lg05RFazCAOZVU8ZGbXNkC7ZuQcCn-NYAwDt8IAsSaXIME30gOBiy7Y198l22GENwsZ2iN4l+8qTZ8tMpweN0tzmTMxaFN-4kthdnN+9pAw9wt7MqEV9385NPWsYGAajRLdi8LVLCAbip4KGdIM9WcBrCVj9+d38kzDVGLILeLFRpLFLWqKhS7SDjA2V+V4505-AIoHIt+OdyVpdxhKxYoJumx6xceF6U+GauIiklpTLVeBpGFrVjkMjmdmFKj0tmj0aulsQFDSmsl9IFwUgSl4yHIfYWl8a9jndmV4CyVxihoMZyKYDzl+AAeRSuITqc1ttwweIUcbEKquwJwEyAcQsJWKOuwMANFyQcNhaKEJZ5kAV4wUwYVonGAVihHAJMzgwF2IeYwLFyTFacLnuAZnwIZz2AnetnTPTTgULqL7+FaMzFNcpW2XoTmcsWuLqe8BATxTqDqIEuwZoLoHod6ZoZIEF0IQleWdzTgA6MAYQNfRyIifoPrjADrgyRSLSS0Eql6N6JET6IoBOxoGq-YdoYdImWeforozgPze8EkMILry6GSYiNyYb+6R6NKQyV6OrybgyLeTxdoZ0PUWodoTqfoFgO7rqOl54D6cwbjRiWoUYTje7N72oTqJRXGWYFhcsdEGGdEUHzxWuYrrMUIRIzxG76rxoUgR75oR4VYZH2b+xpgZoV4P7rz2oJH8wUJVHlgUJDH+AOwdEN4RidEeWP7kn9ERYAwCnwgbGEn+AXsJ8dEQnuQPgS4JH2oFsS4B7p7uQLAUX176H4rzqewhHQwSOMrzFIZuXiRe4EPIZnWOwcg8wIZk0DuvX2ob24YU3s383832Bi36303pAE383u3437FcMLAAWDXmFIZh3J8YYMvaEIZmamCRCI3uQIZ+XU1N3l3UP4VPtbYcbIZ-7DiYPoZhMW8ZBNmWwe4IZ2D8wPULCSPmFGa4Pg4ixEv3oGXrqeX8EAwJX-7kPiv9X7ArXqGXX-XmjcMJPm3zvq3zvh3+3s3x3uvsP13yUfPr30gH37oOiP3ibASIPg6wfjulsCPuP6GaPj0fPhPpf+f5P1P29coDPvXkXOwXPiwFfzRQv+f4vqH6oExWoHYOWUdYl0H52gIceb40H+IGvvn0oCYMtYWitWoKUE8CsAO0lgR7p3T1AAlPEZIKGCP1vQ9BeKIHXjOQDYwTpLgV4N9PgAAhno5ormAcKOgUCB4pM94PnvfyhjGYGWmKVARxl2CzAIMbDeAF52bRyBskorZnH0GIBJ5hUGAawlYFSJ7xIcnjbJJECBLKI2BuqXoHiDxCVANAhAEgBoyoCwQ+ASAXOkoKQB8A4IIAI+HgDqpcB5ASgFQOAHIhRAOAeARiBwFUA7d3w3oIwPsA1xURIAI4EwduBF4i8OIZkYwaVEgCLsDIGgawLYCoAlg8wpALQcIGbhAA). 37 | 38 | ## Development 39 | 40 | Use `npm start` to convert the TypeScript code to JavaScript and `npm test` to run the test watcher. It uses `microbundle` to compile the final code to different output formats. 41 | 42 | ## Usage 43 | 44 | Here is the general use case: 45 | 46 | ```ts 47 | import { render } from 'slimdown-js'; 48 | 49 | console.log( 50 | render('# Page title\n\nAnd **now** for something _completely_ different.',), 51 | ); 52 | ``` 53 | 54 | ### Adding rules 55 | 56 | A simple rule to convert `:)` to an image: 57 | 58 | ```ts 59 | import { render, addRule } from 'slimdown-js'; 60 | 61 | addRule ('/(\W)\:\)(\W)/', '$1$2'); 62 | 63 | console.log(render(('Know what I\'m sayin? :)')); 64 | ``` 65 | 66 | In this example, we add GitHub-style internal linking 67 | (e.g., `[[Another Page]]`). 68 | 69 | ```ts 70 | import { render, addRule } from 'slimdown-js'; 71 | 72 | const mywiki_internal_link = (title: string) => 73 | `${title}`; 74 | 75 | addRule('/[[(.*?)]]/e', mywiki_internal_link('$1')); 76 | 77 | console.log(render('Check [[This Page]] out!')); 78 | ``` 79 | 80 | ### A longer example 81 | 82 | ```ts 83 | import { render } from 'slimdown-js'; 84 | 85 | console.log(render(`# A longer example 86 | 87 | And *now* [a link](http://www.google.com) to **follow** and [another](http://yahoo.com/). 88 | 89 | * One 90 | * Two 91 | * Three 92 | 93 | ## Subhead 94 | 95 | One **two** three **four** five. 96 | 97 | One __two__ three _four_ five __six__ seven _eight_. 98 | 99 | 1. One 100 | 2. Two 101 | 3. Three 102 | 103 | More text with `inline($code)` sample. 104 | 105 | > A block quote 106 | > across two lines. 107 | 108 | More text...`)); 109 | ``` 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slimdown-js", 3 | "version": "1.0.0", 4 | "source": "src/index.ts", 5 | "main": "dist/slimdown.cjs", 6 | "module": "dist/slimdown.module.mjs", 7 | "exports": { 8 | "require": "./dist/slimdown.cjs", 9 | "default": "./dist/slimdown.modern.mjs" 10 | }, 11 | "unpkg": "dist/slimdown.umd.js", 12 | "typings": "dist/index.d.ts", 13 | "type": "module", 14 | "description": "A regex-based Markdown parser.", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/erikvullings/slimdown-js.git" 18 | }, 19 | "keywords": [ 20 | "markdown", 21 | "regex", 22 | "parser", 23 | "lightweight", 24 | "slim", 25 | "typescript" 26 | ], 27 | "author": "Erik Vullings (http://www.tno.nl)", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/erikvullings/slimdown-js/issues" 31 | }, 32 | "homepage": "https://github.com/erikvullings/slimdown-js#readme", 33 | "scripts": { 34 | "test": "ava --watch src/slimdown.test.ts", 35 | "clean": "rimraf .rpt2_cache dist lib", 36 | "build": "microbundle src/index.ts", 37 | "start": "tsc -w -p tsconfig.json", 38 | "dry-run": "npm publish --dry-run", 39 | "patch": "npm run clean && npm run build && npm version patch --force -m \"Patch release\" && npm publish && git push --follow-tags", 40 | "minor": "npm run clean && npm run build && npm version minor --force -m \"Minor release\" && npm publish && git push --follow-tags", 41 | "major": "npm run clean && npm run build && npm version major --force -m \"Major release\" && npm publish && git push --follow-tags" 42 | }, 43 | "devDependencies": { 44 | "@ava/typescript": "^5.0.0", 45 | "ava": "^6.2.0", 46 | "microbundle": "^0.15.1", 47 | "rimraf": "^6.0.1", 48 | "tslib": "^2.8.1", 49 | "typescript": "^5.7.3" 50 | }, 51 | "ava": { 52 | "typescript": { 53 | "rewritePaths": { 54 | "src/": "dist/" 55 | }, 56 | "compile": false 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type RegexReplacer = (substring: string, ...args: any[]) => string; 2 | 3 | /** 4 | * Slimdown - A very basic regex-based Markdown parser. Supports the 5 | * following elements (and can be extended via Slimdown::add_rule()): 6 | * 7 | * - Headers 8 | * - Images 9 | * - Links 10 | * - Bold 11 | * - Emphasis 12 | * - Deletions 13 | * - Quotes 14 | * - Inline code 15 | * - Code blocks 16 | * - Blockquotes 17 | * - Ordered/unordered lists (one level only) 18 | * - Horizontal rules 19 | * - Superscript and subscript (`z_1_` or `a^2^`) 20 | * 21 | * Original author: Johnny Broadway 22 | * Website: https://gist.github.com/jbroadway/2836900 23 | * Inspiration: 24 | * - https://gist.github.com/plugnburn/f0d12e38b6416a77c098 25 | * - https://github.com/Chalarangelo/parse-md-js/blob/master/parsemd.js 26 | * - https://gist.github.com/plugnburn/f0d12e38b6416a77c098 27 | * 28 | * Author: Erik Vullings 29 | * Conversion from PHP to TypeScript, applying fixes and tests, adding more elements, and publishing to npm: 30 | * Website: https://github.com/erikvullings/slimdown-js 31 | * License: MIT 32 | */ 33 | 34 | // Store code blocks temporarily to prevent markdown processing within them 35 | const codeBlocks: string[] = []; 36 | const inlineCode: string[] = []; 37 | // Store footnotes 38 | const footnotes: Array<[id: string, text: string]> = []; 39 | 40 | const escapeMap: Record = { 41 | '&': '&', 42 | '<': '<', 43 | '>': '>', 44 | '"': '"', 45 | "'": ''', 46 | }; 47 | 48 | const escRegex = new RegExp(`[${Object.keys(escapeMap).join('')}]`, 'g'); 49 | 50 | const esc = (s: string): string => 51 | s.replace(escRegex, (match) => escapeMap[match]); 52 | 53 | const para = (_: string, line: string) => { 54 | const trimmed = line.trim(); 55 | return /^<\/?(ul|ol|li|h|p|bl|table|tr|td)/i.test(trimmed) 56 | ? `\n${line}\n` 57 | : `\n

\n${trimmed}\n

\n`; 58 | }; 59 | 60 | // const ulList = (_: string, __: string, item = '') => 61 | // `
    \n\t
  • ${item.trim()}
  • \n
`; 62 | 63 | const ulList = ( 64 | _text: string, 65 | indent: string, 66 | _bullet: string, 67 | item: string, 68 | ) => { 69 | const level = 1 + Math.floor(indent.length / 2); 70 | return ( 71 | '\n
    '.repeat(level) + 72 | '\n\t
  • ' + 73 | item.trim() + 74 | '
  • ' + 75 | '\n
'.repeat(level) 76 | ); 77 | }; 78 | 79 | // const olList = (_: string, item = '') => 80 | // `
    \n\t
  1. ${item.trim()}
  2. \n
`; 81 | 82 | const olList = ( 83 | _text: string, 84 | indent: string, 85 | _bullet: string, 86 | item: string, 87 | ) => { 88 | const level = 1 + Math.floor(indent.length / 2); 89 | return ( 90 | '\n
    '.repeat(level) + 91 | '\n\t
  1. ' + 92 | item.trim() + 93 | '
  2. ' + 94 | '\n
'.repeat(level) 95 | ); 96 | }; 97 | 98 | const blockquote = (_: string, __: string, item = '') => 99 | `\n
${item.trim()}
`; 100 | 101 | // Process footnote references in the text [^1] 102 | const footnoteReferenceReplacer = (_match: string, id: string) => { 103 | // Create a link inside a superscript tag with proper references 104 | return `[${id}]`; 105 | }; 106 | 107 | // Process footnote definitions [^1]: Footnote text 108 | const footnoteDefinitionReplacer = ( 109 | _match: string, 110 | id: string, 111 | text: string, 112 | ) => { 113 | footnotes.push([id, text.trim()]); 114 | return ''; // Remove the definition from the main text 115 | }; 116 | 117 | // Generate the footnotes section 118 | const generateFootnotesSection = () => { 119 | if (footnotes.length === 0) return ''; 120 | 121 | const footnotesHtml = footnotes 122 | .map( 123 | ([id, text]) => ` 124 |
  • 125 | ${text} 126 | 127 |
  • `, 128 | ) 129 | .join('\n'); 130 | 131 | return ` 132 |
    133 |
    134 |
      135 | ${footnotesHtml} 136 |
    137 |
    `; 138 | }; 139 | 140 | const table = (_: string, headers: string, format: string, content: string) => { 141 | const align = format 142 | .split('|') 143 | .filter((__, i, arr) => i > 0 && i < arr.length - 1) 144 | .map((col) => 145 | /:-+:/g.test(col) 146 | ? 'center' 147 | : /-+:/g.test(col) 148 | ? 'right' 149 | : /:-+/.test(col) 150 | ? 'left' 151 | : '', 152 | ); 153 | const td = (col: number) => { 154 | const a = align[col]; 155 | return a ? ` align="${a}"` : ''; 156 | }; 157 | const h = `${headers 158 | .split('|') 159 | .map((hd) => hd.trim()) 160 | .filter((hd) => hd && hd.length) 161 | .map((hd, i) => `${hd}`) 162 | .join('')}`; 163 | const rows = content 164 | .split('\n') 165 | .map((row) => row.trim()) 166 | .filter((row) => row && row.length); 167 | const c = rows 168 | .map( 169 | (row) => 170 | `${row 171 | .split('|') 172 | .filter((__, i, arr) => i > 0 && i < arr.length - 1) 173 | .map((cell, i) => `${cell.trim()}`) 174 | .join('')}`, 175 | ) 176 | .join(''); 177 | return `\n${h}${c}
    \n`; 178 | }; 179 | 180 | const cleanUpUrl = (link: string) => link.replace(/<\/?em>/g, '_'); 181 | 182 | const header = (_: string, match: string, h = '') => { 183 | const level = match.length; 184 | return `${h.trim()}`; 185 | }; 186 | 187 | // Function to extract and store code blocks 188 | const extractCodeBlocks = (markdown: string): string => { 189 | return markdown.replace( 190 | /\n\s*```\w*\n([^]*?)\n\s*```\s*\n/g, 191 | (_match, code) => { 192 | codeBlocks.push(code); 193 | return `\n
    {{CODEBLOCKPH${codeBlocks.length - 1}}}
    \n`; 194 | }, 195 | ); 196 | }; 197 | 198 | // Function to extract and store inline code 199 | const extractInlineCode = (markdown: string): string => { 200 | return markdown.replace(/`([^`]+)`/g, (_match, code) => { 201 | inlineCode.push(code); 202 | return `{{INLINECODEPH${inlineCode.length - 1}}}`; 203 | }); 204 | }; 205 | 206 | // Function to restore code blocks with proper HTML escaping 207 | const restoreCodeBlocks = (markdown: string): string => { 208 | return markdown.replace( 209 | /
    {{CODEBLOCKPH(\d+)}}<\/pre>/g,
    210 |     (_match, index) => {
    211 |       const code = codeBlocks[parseInt(index)];
    212 |       return `
    ${esc(code)}
    `; 213 | }, 214 | ); 215 | }; 216 | 217 | // Function to restore inline code with proper HTML escaping 218 | const restoreInlineCode = (markdown: string): string => { 219 | return markdown.replace(/{{INLINECODEPH(\d+)}}/g, (_match, index) => { 220 | const code = inlineCode[parseInt(index)]; 221 | return `${esc(code)}`; 222 | }); 223 | }; 224 | 225 | /** Rules consist of tuples: RegExp, replacer function, repeat */ 226 | const rules = [ 227 | [/\r\n/g, '\n'], // Remove \r 228 | [/\n(#+)(.*)/g, header], // headers 229 | [/!\[([^\[]+)\]\((?:javascript:)?([^\)]+)\)/g, '$1'], // images, invoked before links 230 | [/\[([^\[]+)\]\((?:javascript:)?([^\)]+)\)/g, '$1'], // links 231 | [/([^\\])(\*\*|__)(.*?(_|\*)?)\2/g, '$1$3'], // bold 232 | [/([^\\])(\*|_)(.*?)\2/g, '$1$3'], // emphasis 233 | [/\\_/g, '_'], // underscores part 1 234 | [/\~\~(.*?)\~\~/g, '$1'], // del 235 | [/\:\"(.*?)\"\:/g, '$1'], // quote 236 | // [/\n\s*```\n([^]*?)\n\s*```\s*\n/g, '\n
    $1
    '], // codeblock 237 | // [/`(.*?)`/g, (_: string, code: string) => `${esc(code)}`], // inline code 238 | [/\n( *)(\*|-|\+)(.*)/g, ulList], // ul lists using +, - or * to denote an entry 239 | [/\n( *)([0-9]+\.)(.*)/g, olList], // ul lists 240 | // [/\n[0-9]+\.(.*)/g, olList], // ol lists 241 | [/\n(>|\>)(.*)/g, blockquote], // blockquotes 242 | [/(\^)(.*?)\1/g, '$2'], // superscript 243 | [/(\~)(.*?)\1/g, '$2'], // subscript 244 | [/\n-{5,}/g, '\n
    '], // horizontal rule 245 | [ 246 | /( *\|[^\n]+\|\r?\n)((?: *\|:?[ -]+:?)+ *\|)(\n(?: *\|[^\n]+\|\r?\n?)*)?/g, 247 | table, 248 | ], 249 | [/\[\^([^\]]+)\](?!:)/g, footnoteReferenceReplacer], // footnote references 250 | [/\[\^([^\]]+)\]:\s*((?:[^\n]*\n?)*)/g, footnoteDefinitionReplacer], // footnote definitions 251 | [/\n([^\n]+)\n/g, para], // add paragraphs 252 | [/\s?<\/[ou]l>\s?<[ou]l>/g, '', 3], // fix extra ol and ul 253 | [/<\/blockquote>\n
    /g, '
    \n'], // fix extra blockquote 254 | [/https?:\/\/[^"']*/g, cleanUpUrl], // fix em in links 255 | [/_/g, '_'], // underscores part 2 256 | ] as Array<[RegExp, RegexReplacer | string]>; 257 | 258 | /** 259 | * Render Markdown text into HTML. 260 | * 261 | * @param markdown Markdown text 262 | * @param removeParagraphs If true (default false), remove the \...\ around paragraphs 263 | * @param externalLinks If true (default false), replace \ with \ 264 | * to open them in a new page 265 | * @returns 266 | */ 267 | export const render = ( 268 | markdown: string, 269 | removeParagraphs = false, 270 | externalLinks = false, 271 | ) => { 272 | // Reset the storage arrays 273 | codeBlocks.length = 0; 274 | inlineCode.length = 0; 275 | footnotes.length = 0; 276 | 277 | // Extract code blocks and inline code before processing 278 | markdown = extractCodeBlocks(`\n${markdown}\n`); 279 | markdown = extractInlineCode(markdown); 280 | 281 | // Apply markdown rules 282 | rules.forEach(([regex, subst, repeat = 1]) => { 283 | for (let i = 0; i < repeat; i++) { 284 | markdown = markdown.replace(regex, subst as any); 285 | } 286 | }); 287 | 288 | // Restore code blocks and inline code with proper escaping 289 | markdown = restoreCodeBlocks(markdown); 290 | markdown = restoreInlineCode(markdown); 291 | 292 | // Add footnotes section if there are any footnotes 293 | markdown = markdown.trim() + generateFootnotesSection(); 294 | 295 | if (removeParagraphs) { 296 | markdown = markdown.replace(/^

    (.*)<\/p>$/s, '$1'); 297 | } 298 | if (externalLinks) { 299 | markdown = markdown.replace(/[1]. With some additional text after it. 51 |

    52 |
    53 |
    54 |
      55 | 56 |
    1. 57 | My reference. 58 | 59 |
    2. 60 |
    61 |
    `; 62 | const html = render( 63 | `Here is a simple footnote[^1]. With some additional text after it. 64 | 65 | [^1]: My reference.`, 66 | ); 67 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 68 | }); 69 | 70 | test('code', (t) => { 71 | const expected = 72 | '

    This is italics and this is _italics_ too.

    '; 73 | const html = render('This is `italics` and this is `_italics_` too.'); 74 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 75 | }); 76 | 77 | test('multiline codeblock', (t) => { 78 | const expected = `
    ## Table example
     79 | 
     80 | | Tables        | Are           | Cool  |
     81 | |---------------|:-------------:|------:|
     82 | | col 3 is      | right-aligned | $1600 |
     83 | | col 2 is      | centered      |   $12 |
     84 | | zebra stripes | are neat      |    $1 |
    `; 85 | const html = render(` 86 | \`\`\`md 87 | 88 | ## Table example 89 | 90 | | Tables | Are | Cool | 91 | |---------------|:-------------:|------:| 92 | | col 3 is | right-aligned | $1600 | 93 | | col 2 is | centered | $12 | 94 | | zebra stripes | are neat | $1 | 95 | 96 | \`\`\` 97 | `); 98 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 99 | }); 100 | 101 | test('ul', (t) => { 102 | const expected = '
    • Item 1
    • Item 2
    • Item 3
    '; 103 | const html = render(`- Item 1\n- Item 2\n- Item 3`); 104 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 105 | }); 106 | 107 | test('ul using +', (t) => { 108 | const expected = '
    • Item 1
    • Item 2
    • Item 3
    '; 109 | const html = render(`+ Item 1\n+ Item 2\n+ Item 3`); 110 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 111 | }); 112 | 113 | test('ul using *', (t) => { 114 | const expected = '
    • Item 1
    • Item 2
    • Item 3
    '; 115 | const html = render(`* Item 1\n* Item 2\n* Item 3`); 116 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 117 | }); 118 | 119 | test('nested ul + ul', (t) => { 120 | const expected = 121 | '
    • Item 1
      • Item 1.1
      • Item 1.2
      • Item 1.3
    • Item 2
    • Item 3
    '; 122 | const html = render( 123 | `- Item 1\n - Item 1.1\n - Item 1.2\n - Item 1.3\n- Item 2\n- Item 3`, 124 | ); 125 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 126 | }); 127 | 128 | test('nested ul + ol', (t) => { 129 | const expected = 130 | '
    • Item 1
      1. Item 1.1
      2. Item 1.2
      3. Item 1.3
    • Item 2
    • Item 3
    '; 131 | const html = render( 132 | `- Item 1\n 1. Item 1.1\n 2. Item 1.2\n 3. Item 1.3\n- Item 2\n- Item 3`, 133 | ); 134 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 135 | }); 136 | 137 | test('ol', (t) => { 138 | const expected = '
    1. Item 1
    2. Item 2
    3. Item 3
    '; 139 | const html = render(`1. Item 1\n2. Item 2\n3. Item 3`); 140 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 141 | }); 142 | 143 | test('nested ol + ol', (t) => { 144 | const expected = 145 | '
    1. Item 1
      1. Item 1.1
      2. Item 1.2
      3. Item 1.3
    2. Item 2
    3. Item 3
    '; 146 | const html = render( 147 | `1. Item 1\n 1. Item 1.1\n 2. Item 1.2\n 3. Item 1.3\n2. Item 2\n3. Item 3`, 148 | ); 149 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 150 | }); 151 | 152 | test('nested ol + ul', (t) => { 153 | const expected = 154 | '
    1. Item 1
      • Item 1.1
      • Item 1.2
      • Item 1.3
    2. Item 2
    3. Item 3
    '; 155 | const html = render( 156 | `1. Item 1\n - Item 1.1\n - Item 1.2\n - Item 1.3\n2. Item 2\n3. Item 3`, 157 | ); 158 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 159 | }); 160 | 161 | test('table 1', (t) => { 162 | const table = ` 163 | | Threat \\ Context | rainy | sunny | 164 | |------------------|------------|------------| 165 | | terrorist | scenario 1 | scenario 2 | 166 | | criminal | scenario 3 | scenario 4 | 167 | `; 168 | const expected = ` 169 | 170 | 171 | 172 | 173 | 174 |
    Threat \\ Contextrainysunny
    terroristscenario 1scenario 2
    criminalscenario 3scenario 4
    `; 175 | const html = render(table); 176 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 177 | }); 178 | 179 | test('table 2', (t) => { 180 | const table = ` 181 | | Threat \\ Context | rainy | sunny | 182 | | ---------------- | ---------- | ---------- | 183 | | terrorist | scenario 1 | scenario 2 | 184 | | criminal | scenario 3 | scenario 4 | 185 | `; 186 | const expected = ` 187 | 188 | 189 | 190 | 191 | 192 |
    Threat \\ Contextrainysunny
    terroristscenario 1scenario 2
    criminalscenario 3scenario 4
    `; 193 | const html = render(table); 194 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 195 | }); 196 | 197 | test('parsing strong in own paragraph', (t) => { 198 | const md = `An **indie electronica music** bundle. 199 | 200 | **Featuring** songs by ... 201 | 202 | Pay what you want for Music`; 203 | const generated = render(md); 204 | const expected = `

    205 | An indie electronica music bundle. 206 |

    207 | 208 |

    209 | Featuring songs by ... 210 |

    211 | 212 |

    213 | Pay what you want for Music 214 |

    `; 215 | t.is(generated, expected); 216 | }); 217 | 218 | test('parsing longer text', (t) => { 219 | const md = `# Title 220 | 221 | To use **Slimdown**, grap it from [npm](https://www.npmjs.com/package/slimdown-js) or *fork* the project on [GitHub](https://github.com/erikvullings/slimdown-js). 222 | 223 | * One 224 | * Two 225 | * Three 226 | 227 | ## Underscores 228 | 229 | my\\_var\\_is 230 | 231 | ## Subhead 232 | 233 | One **two** three **four** five. 234 | 235 | One __two__ three _four_ five __six__ seven _eight_. 236 | 237 | 1. One 238 | 2. Two 239 | 3. Three 240 | 241 | More text with \`inline($code);\` sample. 242 | 243 | > A block quote 244 | > across two lines. 245 | 246 | More text...`; 247 | const expected = `

    Title

    248 | 249 |

    250 | To use Slimdown, grap it from npm or fork the project on GitHub. 251 |

    252 |
      253 |
    • One
    • 254 |
    • Two
    • 255 |
    • Three
    • 256 |
    257 | 258 |

    Underscores

    259 | 260 |

    my_var_is

    261 | 262 |

    Subhead

    263 | 264 |

    265 | One two three four five. 266 |

    267 | 268 |

    269 | One two three four five six seven eight. 270 |

    271 |
      272 |
    1. One
    2. 273 |
    3. Two
    4. 274 |
    5. Three
    6. 275 |
    276 | 277 |

    278 | More text with inline($code); sample. 279 |

    280 | 281 |
    A block quote
    282 | across two lines.
    283 | 284 |

    285 | More text... 286 |

    `; 287 | const html = render(md); 288 | 289 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 290 | }); 291 | 292 | test('parsing links with underscores', (t) => { 293 | const md = `# Links fail with underscores 294 | 295 | [Test Link](http://www.google.com/?some_param=another_value)`; 296 | const expected = `

    Links fail with underscores

    297 |

    Test Link

    `; 298 | const html = render(md); 299 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 300 | }); 301 | 302 | test('parsing images', (t) => { 303 | const md = `NS logo image: ![ns logo](https://www.ns.nl/static/generic/2.49.1/images/nslogo.svg)`; 304 | const expected = `

    NS logo image: ns logo

    `; 305 | const html = render(md); 306 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 307 | }); 308 | 309 | test('parsing code blocks', (t) => { 310 | const md = `# Code example 311 | \`\`\` 312 | Tab indented 313 | codeblock 314 | \`\`\` 315 | `; 316 | const expected = `

    Code example

    Tab indented codeblock
    `; 317 | const html = render(md); 318 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 319 | }); 320 | 321 | test('parsing inline code', (t) => { 322 | const md = `This is \`inline A & B\` code.`; 323 | const expected = `This is inline A & B code.`; 324 | const html = render(md, true); 325 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 326 | }); 327 | 328 | test('parsing inline HTML code', (t) => { 329 | const md = `This is \`

    An HTML paragrahp

    \` code.`; 330 | const expected = `This is <p>An HTML paragrahp</p> code.`; 331 | const html = render(md, true); 332 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 333 | }); 334 | 335 | test('bypassing HTML code', (t) => { 336 | const md = `An HTML paragrahp`; 337 | const expected = `An HTML paragrahp`; 338 | const html = render(md, true); 339 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 340 | }); 341 | 342 | test('parsing tables', (t) => { 343 | const md = `# Table example 344 | 345 | | Tables | Are | Cool | 346 | |---------------|:-------------:|------:| 347 | | col 3 is | right-aligned | $1600 | 348 | | col 2 is | centered | $12 | 349 | | zebra stripes | are neat | $1 | 350 | `; 351 | const expected = `

    Table example

    352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 |
    TablesAreCool
    col 3 isright-aligned$1600
    col 2 iscentered$12
    zebra stripesare neat$1
    `; 376 | const html = render(md); 377 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 378 | }); 379 | 380 | test('removing paragraphs', (t) => { 381 | const expected = 'Hello world'; 382 | const html = render('Hello world', true); 383 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 384 | }); 385 | 386 | test('do not remove paragraphs for longer text', (t) => { 387 | const expected = `

    Hello world

    How are you?

    `; 388 | const html = render( 389 | `# Hello world 390 | 391 | How are you?`, 392 | true, 393 | ); 394 | t.is(removeWhitespaces(html), removeWhitespaces(expected)); 395 | }); 396 | 397 | test('creating links', (t) => { 398 | const md = 'This is a [link](https://www.google.com).'; 399 | const expected = 'This is a link.'; 400 | const html = render(md, true); 401 | t.is(html.trim(), expected); 402 | }); 403 | 404 | test('creating external links', (t) => { 405 | const md = 'This is a [link](https://www.google.com).'; 406 | const expected = 407 | 'This is a link.'; 408 | const html = render(md, true, true); 409 | t.is(html.trim(), expected); 410 | }); 411 | 412 | test('creating links with underscores', (t) => { 413 | const md = 'This is a [link](https://my_test_page.com).'; 414 | const expected = 'This is a link.'; 415 | const html = render(md, true); 416 | t.is(html.trim(), expected); 417 | }); 418 | 419 | test('creating emphasized text', (t) => { 420 | const md = 'This is _emphasized_ text.'; 421 | const expected = 'This is emphasized text.'; 422 | const html = render(md, true); 423 | t.is(html.trim(), expected); 424 | }); 425 | 426 | test('creating emphasized text 2', (t) => { 427 | const md = 'This is *emphasized* text.'; 428 | const expected = 'This is emphasized text.'; 429 | const html = render(md, true); 430 | t.is(html.trim(), expected); 431 | }); 432 | 433 | test('creating strong text', (t) => { 434 | const md = 'This is **strong** text.'; 435 | const expected = 'This is strong text.'; 436 | const html = render(md, true); 437 | t.is(html.trim(), expected); 438 | }); 439 | 440 | test('creating strong text 2', (t) => { 441 | const md = 'This is __strong__ text.'; 442 | const expected = 'This is strong text.'; 443 | const html = render(md, true); 444 | t.is(html.trim(), expected); 445 | }); 446 | 447 | test('creating strong and empasized text', (t) => { 448 | const md = 'This is ***strong and emphasized*** text.'; 449 | const expected = 450 | 'This is strong and emphasized text.'; 451 | const html = render(md, true); 452 | t.is(html.trim(), expected); 453 | }); 454 | 455 | test('creating strong and empasized text 2', (t) => { 456 | const md = 'This is ___strong and emphasized___ text.'; 457 | const expected = 458 | 'This is strong and emphasized text.'; 459 | const html = render(md, true); 460 | t.is(html.trim(), expected); 461 | }); 462 | 463 | test('creating deleted text', (t) => { 464 | const md = 'This is ~~deleted~~ text.'; 465 | const expected = 'This is deleted text.'; 466 | const html = render(md, true); 467 | t.is(html.trim(), expected); 468 | }); 469 | 470 | test('creating quotes', (t) => { 471 | const md = 'This is a quote: :"quoted": text.'; 472 | const expected = 'This is a quote: quoted text.'; 473 | const html = render(md, true); 474 | t.is(html.trim(), expected); 475 | }); 476 | 477 | test('creating block quotes', (t) => { 478 | const md = '> This is a blockquoted text.'; 479 | const expected = '
    This is a blockquoted text.
    '; 480 | const html = render(md, true); 481 | t.is(html.trim(), expected); 482 | }); 483 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | // "lib": [ 7 | // "dom", 8 | // "es5", 9 | // "es2015.promise", 10 | // "es2017" 11 | // ] /* Specify library files to be included in the compilation. */, 12 | // "allowJs": true, /* Allow javascript files to be compiled. */ 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 15 | "declaration": true /* Generates corresponding '.d.ts' file. */, 16 | "skipLibCheck": true, 17 | "sourceMap": true /* Generates corresponding '.map' file. */, 18 | // "outFile": "./", /* Concatenate and emit output to single file. */ 19 | "outDir": "./dist" /* Redirect output structure to the directory. */, 20 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 21 | "removeComments": false /* Do not emit comments to output. */, 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | "importHelpers": true /* Import emit helpers from 'tslib'. */, 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | /* Strict Type-Checking Options */ 27 | "strict": true /* Enable all strict type-checking options. */, 28 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 29 | "strictNullChecks": true /* Enable strict null checks. */, 30 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 31 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 32 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 33 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 34 | /* Additional Checks */ 35 | "noUnusedLocals": true /* Report errors on unused locals. */, 36 | "noUnusedParameters": true /* Report errors on unused parameters. */, 37 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 38 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 39 | /* Module Resolution Options */ 40 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | /* Source Map Options */ 50 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 51 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 52 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 53 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 54 | /* Experimental Options */ 55 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 56 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 57 | } 58 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": {}, 5 | "rules": { 6 | "no-console": false, 7 | "no-debugger": false, 8 | "quotemark": [true, "single"], 9 | "trailing-comma": [ 10 | true, 11 | { 12 | "multiline": { 13 | "objects": "always", 14 | "arrays": "always", 15 | "functions": "never", 16 | "typeLiterals": "ignore" 17 | }, 18 | "esSpecCompliant": true 19 | } 20 | ], 21 | "object-literal-sort-keys": false, 22 | "ordered-imports": false, 23 | "arrow-parens": [false, "ban-single-arg-parens"] 24 | }, 25 | "rulesDirectory": [] 26 | } 27 | --------------------------------------------------------------------------------