├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── bin ├── build-marky-info.js └── marky-markdown.js ├── example.js ├── index.js ├── lib ├── cleanup.js ├── gfm │ ├── image.js │ ├── indented-headings.js │ ├── link.js │ ├── override-link-destination-parser.js │ └── relaxed-link-reference.js ├── highlights-tokens.js ├── linkify.js ├── plugin │ ├── cdn.js │ ├── code-wrap.js │ ├── github.js │ ├── gravatar.js │ ├── heading-links.js │ ├── html-heading.js │ ├── language-alias.js │ ├── nofollow.js │ ├── packagize.js │ └── youtube.js ├── render.js ├── sanitize.js └── token-util.js ├── marky.json ├── package-lock.json ├── package.json └── test ├── cdn.js ├── emoji.js ├── fixtures.js ├── fixtures ├── basic.md ├── benchmark.md ├── burblewibble.md ├── dirty.md ├── emojiheadings.md ├── enterprise.md ├── github.md ├── gravatar.md ├── htmlheading.md ├── htmlheadingalign.md ├── lazyheading.md ├── link-ref-relaxed.md ├── link-ref.md ├── readmes │ ├── async.md │ ├── benchmark.md │ ├── cicada.md │ ├── express.md │ ├── grunt-angular-templates.md │ ├── johnny-five.md │ ├── maintenance-modules.md │ ├── memoize.md │ ├── mkhere.md │ ├── payform.md │ └── wzrd.md ├── tables.md ├── task-list.md └── wibble.md ├── github.js ├── gravatar.js ├── headings.js ├── index.js ├── markdown.js ├── marky.js ├── nofollow.js ├── packagize.js ├── readmes.js ├── repo-github.js ├── repo-other.js ├── sanitize.js └── youtube.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .travis.yml 3 | example.js 4 | !dist -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | notifications: 4 | email: false 5 | 6 | node_js: 7 | - 8 8 | 9 | before_install: 10 | - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test 11 | - sudo apt-get update -qq 12 | - sudo apt-get install -qq -y g++-4.8 13 | - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.8 50 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # marky-markdown 2 | 3 | [![Build Status](https://travis-ci.org/npm/marky-markdown.svg?branch=master)](https://travis-ci.org/npm/marky-markdown) 4 | [![Code Climate](https://codeclimate.com/github/npm/marky-markdown/badges/gpa.svg)](https://codeclimate.com/github/npm/marky-markdown) 5 | [![Dependency Status](https://david-dm.org/npm/marky-markdown.svg)](https://david-dm.org/npm/marky-markdown) 6 | [![Pull Requests](https://img.shields.io/github/issues-pr/npm/marky-markdown.svg)](https://github.com/npm/marky-markdown/pulls) 7 | [![Issues](https://img.shields.io/github/issues/npm/marky-markdown.svg)](https://github.com/npm/marky-markdown/issues) 8 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) 9 | 10 | `marky-markdown` is a markdown parser, written in NodeJS, that aims for 11 | parity with [GitHub-style markdown]. It is built on top of [`markdown-it`], 12 | a [CommonMark] markdown parser. You can use marky-markdown: 13 | 14 | - [programmatically in NodeJS] 15 | - [in your terminal] 16 | - [in the browser] *does not yet support syntax highlighting 17 | 18 | `marky-markdown` is the thing that parses package READMEs on 19 | http://www.npmjs.com. If you see a markdown parsing bug there, 20 | [file an issue here]! 21 | 22 | [file an issue here]: https://github.com/npm/marky-markdown/issues 23 | [GitHub-style markdown]: https://help.github.com/articles/basic-writing-and-formatting-syntax/ 24 | [CommonMark]: http://spec.commonmark.org/ 25 | [`markdown-it`]: https://github.com/markdown-it/markdown-it 26 | [programmatically in NodeJS]: #programmatic-usage 27 | [in your terminal]: #command-line-usage 28 | [in the browser]: #in-the-browser 29 | 30 | ## Node Version Support 31 | 32 | marky-markdown strives to support all LTS, current, and maintenance 33 | versions of Node.js. When a version of Node.js is EOL, we will EOL 34 | support for that version for marky-markdown. 35 | 36 | For more information on Node.js LTS and support, click [here](https://github.com/nodejs/LTS). 37 | 38 | - marky-markdown < `9.0.0` supports `0.10`, `0.12`, `iojs`, `4`, `5` 39 | - marky-markdown >= `9.0.0` supports `0.12`, `4`, `6` 40 | 41 | ## Installation 42 | 43 | ```sh 44 | npm install marky-markdown --save 45 | ``` 46 | 47 | ## Programmatic Usage 48 | 49 | marky-markdown exports a single function. For basic use, that function 50 | takes a single argument: a string to convert. 51 | 52 | ```js 53 | var marky = require("marky-markdown") 54 | var html = marky("# hello, I'm markdown") 55 | ``` 56 | 57 | ### Options 58 | 59 | The exported function takes an optional options object 60 | as its second argument: 61 | 62 | ```js 63 | marky("some trusted string", {sanitize: false}) 64 | ``` 65 | 66 | The default options are as follows: 67 | 68 | ```js 69 | { 70 | sanitize: true, // remove script tags and stuff 71 | nofollow: true, // add rel=nofollow to all links 72 | linkify: true, // turn orphan URLs into hyperlinks 73 | highlightSyntax: true, // run highlights on fenced code blocks 74 | prefixHeadingIds: true, // prevent DOM id collisions 75 | enableHeadingLinkIcons: true, // render icons inside generated section links 76 | serveImagesWithCDN: false, // use npm's CDN to proxy images over HTTPS 77 | debug: false, // console.log() all the things 78 | package: null, // npm package metadata, 79 | headingAnchorClass: 'anchor', // the classname used for anchors in headings. 80 | headingSvgClass: ['octicon'] // the class used for svg icon in headings. 81 | } 82 | ``` 83 | 84 | ### Low Level Parser Access 85 | 86 | If you need lower level access to the markdown-it parser (to add your own 87 | [markdown-it plugins](https://www.npmjs.com/search?q=markdown-it-plugin), for 88 | example), you can call the `getParser` method: 89 | 90 | ```js 91 | var parser = marky.getParser() 92 | parser.use(someMarkdownItPlugin) 93 | var html = parser.render("# markdown string") 94 | ``` 95 | 96 | `getParser` takes an optional `options` argument, the same format as the main 97 | marky-markdown export function. If you omit it, it uses the same default options 98 | described above. 99 | 100 | When you're done customizing the parser, call `parser.render(markdown)` to 101 | render to HTML. 102 | 103 | ## Command-line Usage 104 | 105 | You can use marky-markdown to parse markdown files in the shell. 106 | The easiest way to do this is to install globally: 107 | 108 | ``` 109 | npm i -g marky-markdown 110 | marky-markdown some.md > some.html 111 | ``` 112 | 113 | ## In the Browser 114 | 115 | This module mostly works in the browser, with the exception of the `highlights` module. 116 | 117 | You can `require('marky-markdown')` in scripts you browserify yourself, 118 | or just use the standalone file in [dist/marky-markdown.js]. 119 | 120 | Here is an example using HTML5 to render text inside `` tags. 121 | 122 | ```html 123 | 124 | 125 | **Here** _is_ some [Markdown](https://github.com/) 126 | 127 | 132 | ``` 133 | 134 | Note: Usage with [webpack](https://webpack.github.io/) requires that your 135 | `webpack.config.js` configure a loader (such as 136 | [json-loader](https://github.com/webpack/json-loader)) for .json files. Also, you need to config `process.browser` in `webpack.config.js` when you target browser: 137 | 138 | ```js 139 | plugins: [ 140 | new webpack.DefinePlugin({ 141 | 'process.browser': true 142 | }) 143 | ], 144 | ``` 145 | 146 | ## Tests 147 | 148 | ```sh 149 | npm install 150 | npm test 151 | ``` 152 | 153 | ## What it does 154 | 155 | - Parses markdown with [markdown-it](https://github.com/markdown-it/markdown-it), a fast and [commonmark-compliant](http://commonmark.org/) parser. 156 | - Removes broken and malicious user input with [sanitize-html](https://www.npmjs.com/package/sanitize-html) 157 | - Applies syntax highlighting to [GitHub-flavored code blocks](https://help.github.com/articles/creating-and-highlighting-code-blocks/) using the [highlights](https://www.npmjs.com/package/highlights) library from [Atom](https://atom.io/). 158 | - Converts `:emoji:`-style [shortcuts](http://www.emoji-cheat-sheet.com/) to unicode emojis. 159 | - Converts headings (h1, h2, etc) into anchored hyperlinks. 160 | - Converts relative GitHub links to their absolute equivalents. 161 | - Converts relative GitHub images sources to their GitHub raw equivalents. 162 | - Converts insecure Gravatar URLs to HTTPS. 163 | - Converts list items with leading `[ ]` and `[x]` into [GitHub-style task lists](https://github.com/blog/1825-task-lists-in-all-markdown-documents) 164 | - Wraps embedded YouTube videos so they can be styled. 165 | - Parses and sanitizes `package.description` as markdown. 166 | - Applies CSS classes to redundant content that closely matches npm package name and description. 167 | 168 | ### npm packages 169 | 170 | Pass in an npm `package` object to do stuff like rewriting relative URLs 171 | to their absolute equivalent on GitHub, normalizing package metadata 172 | with redundant readme content, etc 173 | 174 | ```js 175 | var package = { 176 | name: "foo", 177 | description: "foo is a thing", 178 | repository: { 179 | type: "git", 180 | url: "https://github.com/kung/foo" 181 | } 182 | } 183 | 184 | marky( 185 | "# hello, I am the foo readme", 186 | {package: package} 187 | ) 188 | ``` 189 | 190 | ## Dependencies 191 | 192 | - [github-slugger](https://github.com/Flet/github-slugger): Generate a slug just like GitHub does for markdown headings 193 | - [github-url-to-object](https://github.com/zeke/github-url-to-object): Extract user, repo, and other interesting properties from GitHub URLs 194 | - [highlights](https://github.com/atom/highlights): Syntax highlighter 195 | - [highlights-tokens](https://github.com/zeke/highlights-tokens): A list of the language tokens used by the Atom.app [highlights](https://www.npmjs.com/package/highlights) syntax highlighter 196 | - [innertext](https://github.com/revin/innertext): Extract the `innerText` from a snippet of HTML 197 | - [lodash](https://github.com/lodash/lodash): A utility library delivering consistency, customization, performance, & extras. 198 | - [markdown-it](https://github.com/markdown-it/markdown-it): Markdown-it - modern pluggable markdown parser. 199 | - [markdown-it-emoji](https://github.com/markdown-it/markdown-it-emoji): Markdown-it-emoji extension for Markdown-it that parses markdown emoji syntax to unicode. 200 | - [markdown-it-expand-tabs](https://github.com/revin/markdown-it-expand-tabs): Replace leading tabs with spaces in fenced code blocks 201 | - [markdown-it-lazy-headers](https://github.com/Galadirith/markdown-it-lazy-headers): Lazy ATX headers plugin for markdown-it 202 | - [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists): Render GitHub-style [task lists](https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments) 203 | - [property-ttl](https://github.com/soldair/property-ttl): Save memory by nulling out a property after ttl if it has not been accessed 204 | - [sanitize-html](https://github.com/punkave/sanitize-html): Clean up user-submitted HTML, preserving whitelisted elements and whitelisted attributes on a per-element basis 205 | - [similarity](https://github.com/zeke/similarity): How similar are these two strings? 206 | 207 | Extra syntax highlighting, in addition to what comes with [highlights](https://www.npmjs.com/package/highlights): 208 | 209 | - [atom-language-diff](https://github.com/revin/atom-language-diff): Diff/patch files 210 | - [atom-language-nginx](https://github.com/hnagato/atom-language-nginx): [NGINX](http://nginx.org/) configuration files 211 | - [language-dart](https://github.com/Daegalus/atom-language-dart): [Dart](https://www.dartlang.org/) language 212 | - [language-erlang](https://github.com/jonathanmarvens/atom-language-erlang): [Erlang](http://www.erlang.org/) language 213 | - [language-glsl](https://github.com/hughsk/language-glsl): [OpenGL Shading Language](https://www.opengl.org/documentation/glsl/) files 214 | - [language-haxe](https://github.com/theRemix/language-haxe): [Haxe](http://haxe.org/) language 215 | - [language-ini](https://github.com/jacobbednarz/atom-language-ini): .ini configuration files 216 | - [language-rust](https://github.com/zargony/atom-language-rust): [Rust](http://www.rust-lang.org/) language 217 | - [language-stylus](https://github.com/matthojo/language-stylus): [Stylus](http://stylus-lang.com/) CSS preprocessor 218 | 219 | ## License 220 | 221 | ISC 222 | -------------------------------------------------------------------------------- /bin/build-marky-info.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var markyPackage = require('../package.json') 3 | 4 | var info = { 5 | version: markyPackage.version, 6 | repositoryUrl: markyPackage.repository.url, 7 | issuesUrl: markyPackage.repository.url + '/issues' 8 | } 9 | 10 | var contents = JSON.stringify(info) 11 | 12 | fs.writeFile('marky.json', contents, function (err) { 13 | if (err) throw err 14 | console.log('Built marky.json. ' + contents) 15 | }) 16 | -------------------------------------------------------------------------------- /bin/marky-markdown.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require('fs') 3 | var path = require('path') 4 | var marky = require('..') 5 | 6 | if (process.argv.length < 3) { 7 | console.log('Usage:\n\nmarky-markdown some.md > some.html') 8 | process.exit() 9 | } 10 | 11 | var filePath = path.resolve(process.cwd(), process.argv[2]) 12 | 13 | fs.readFile(filePath, function (err, data) { 14 | if (err) throw err 15 | var html = marky(data.toString()) 16 | process.stdout.write(html) 17 | }) 18 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var marky = require('./') 2 | 3 | // Clean up a regular old markdown string 4 | marky("# hello, I'm markdown").html() 5 | 6 | // Pass in an npm `package` object to do stuff like 7 | // rewriting relative URLs to their absolute equivalent on github, 8 | // normalizing package metadata with redundant readme content, 9 | // etcs 10 | var pkg = { 11 | name: 'foo is a thing', 12 | repository: { 13 | type: 'git', 14 | url: 'https://github.com/kung/foo' 15 | } 16 | } 17 | 18 | marky( 19 | '# hello, I am the foo readme', 20 | {package: pkg} 21 | ).html() 22 | 23 | // Disable syntax highlighting 24 | marky( 25 | "# I'm a file with github flavored markdown", 26 | {highlightSyntax: false} 27 | ).html() 28 | 29 | // Pass in a `debug` for verbose output 30 | marky( 31 | "# hello, I'm an evil document", 32 | {debug: true} 33 | ).html() 34 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var defaults = require('lodash.defaults') 2 | var isPlainObj = require('is-plain-obj') 3 | var render = require('./lib/render') 4 | var sanitize = require('./lib/sanitize') 5 | var markyInfo = require('./marky.json') 6 | 7 | var defaultOptions = { 8 | sanitize: true, 9 | nofollow: true, 10 | linkify: true, 11 | highlightSyntax: true, 12 | prefixHeadingIds: true, 13 | enableHeadingLinkIcons: true, 14 | serveImagesWithCDN: false, 15 | debug: false, 16 | package: null, 17 | headingAnchorClass: 'anchor', 18 | headingSvgClass: ['octicon', 'octicon-link'] 19 | } 20 | 21 | var marky = module.exports = function (markdown, options) { 22 | var html 23 | 24 | if (typeof markdown !== 'string') { 25 | throw Error('first argument must be a string') 26 | } 27 | if (typeof options !== 'undefined' && !isPlainObj(options)) { 28 | throw Error('second argument must be an object') 29 | } 30 | 31 | options = options || {} 32 | defaults(options, defaultOptions) 33 | 34 | var log = function (msg) { 35 | if (options.debug) { 36 | console.log('marky-markdown: ' + msg) 37 | } 38 | } 39 | 40 | log('\n\n' + markdown + '\n\n') 41 | 42 | log('Parse markdown into HTML and add syntax highlighting') 43 | html = render(markdown, options) 44 | 45 | if (options.sanitize) { 46 | log('Sanitize malicious or malformed HTML') 47 | html = sanitize(html, options) 48 | } 49 | 50 | if (options.debug) { 51 | var debugHeader = 52 | '' 58 | 59 | html = debugHeader + '\n' + html 60 | } 61 | 62 | return html 63 | } 64 | 65 | marky.parsePackageDescription = function (description) { 66 | return sanitize(render.renderPackageDescription(description), defaultOptions) 67 | } 68 | 69 | marky.getParser = function (options) { 70 | options = options || {} 71 | 72 | var parser = render.getParser(defaults(options, defaultOptions)) 73 | 74 | if (options.sanitize) { 75 | var originalRender = parser.render 76 | parser.render = function (markdown) { 77 | return sanitize(originalRender.call(parser, markdown), options) 78 | } 79 | } 80 | return parser 81 | } 82 | -------------------------------------------------------------------------------- /lib/cleanup.js: -------------------------------------------------------------------------------- 1 | var clean = require('property-ttl') 2 | 3 | module.exports = function (grammars) { 4 | var managed = 0 5 | 6 | var stops = [] 7 | 8 | scan() 9 | 10 | return function stopAll () { 11 | while (stops.length) stops.shift()() 12 | } 13 | 14 | function scan () { 15 | while (managed < grammars.length - 1) { 16 | stops.push( 17 | clean(grammars[managed], 'repository', 2000, function () { 18 | scan() // start watching new grammars if they are added later. 19 | }), 20 | clean(grammars[managed++], 'initialRule', 2000) 21 | ) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/gfm/image.js: -------------------------------------------------------------------------------- 1 | // CommonMark specifies that images have no whitespace between the alt text and 2 | // src, e.g.: 3 | // 4 | // ![alt text](path/to/the/image) 5 | // 6 | // However, GitHub allows you to put whitespace between the `]` and `(` 7 | // characters, like so: 8 | // 9 | // ![alt text] (path/to/the/image) 10 | // 11 | // To account for the difference, we need to override markdown-it's image rule. 12 | // This file is a copy of the original markdown-it source, plus the necessary 13 | // modifications, wrapped as a markdown-it plugin. 14 | 15 | // Process ![image]( "title") 16 | 17 | 'use strict' 18 | 19 | module.exports = function (md) { 20 | md.inline.ruler.at('image', image) 21 | } 22 | 23 | function image (state, silent) { 24 | var attrs 25 | var code 26 | var content 27 | var label 28 | var labelEnd 29 | var labelStart 30 | var pos 31 | var ref 32 | var res 33 | var title 34 | var token 35 | var tokens 36 | var start 37 | var href = '' 38 | var oldPos = state.pos 39 | var max = state.posMax 40 | var isSpace = state.md.utils.isSpace 41 | var normalizeReference = state.md.utils.normalizeReference 42 | 43 | if (state.src.charCodeAt(state.pos) !== 0x21/* ! */) { return false } 44 | if (state.src.charCodeAt(state.pos + 1) !== 0x5B/* [ */) { return false } 45 | 46 | labelStart = state.pos + 2 47 | labelEnd = state.md.helpers.parseLinkLabel(state, state.pos + 1, false) 48 | 49 | // parser failed to find ']', so it's not a valid link 50 | if (labelEnd < 0) { return false } 51 | 52 | pos = labelEnd + 1 53 | 54 | // BEGIN CUSTOM MODIFICATION 55 | // look ahead to see if the next non-whitespace character is '(', and if 56 | // it is, skip the whitespace 57 | for (; pos < max; pos++) { 58 | code = state.src.charCodeAt(pos) 59 | if (!isSpace(code) && code !== 0x0A) { break } 60 | } 61 | if (pos < max && state.src.charCodeAt(pos) !== 0x28) { pos = labelEnd + 1 } 62 | // END CUSTOM MODIFICATION 63 | 64 | if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) { 65 | // 66 | // Inline link 67 | // 68 | 69 | // [link]( "title" ) 70 | // ^^ skipping these spaces 71 | pos++ 72 | for (; pos < max; pos++) { 73 | code = state.src.charCodeAt(pos) 74 | if (!isSpace(code) && code !== 0x0A) { break } 75 | } 76 | if (pos >= max) { return false } 77 | 78 | // [link]( "title" ) 79 | // ^^^^^^ parsing link destination 80 | start = pos 81 | res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax) 82 | if (res.ok) { 83 | href = state.md.normalizeLink(res.str) 84 | if (state.md.validateLink(href)) { 85 | pos = res.pos 86 | } else { 87 | href = '' 88 | } 89 | } 90 | 91 | // [link]( "title" ) 92 | // ^^ skipping these spaces 93 | start = pos 94 | for (; pos < max; pos++) { 95 | code = state.src.charCodeAt(pos) 96 | if (!isSpace(code) && code !== 0x0A) { break } 97 | } 98 | 99 | // [link]( "title" ) 100 | // ^^^^^^^ parsing link title 101 | res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax) 102 | if (pos < max && start !== pos && res.ok) { 103 | title = res.str 104 | pos = res.pos 105 | 106 | // [link]( "title" ) 107 | // ^^ skipping these spaces 108 | for (; pos < max; pos++) { 109 | code = state.src.charCodeAt(pos) 110 | if (!isSpace(code) && code !== 0x0A) { break } 111 | } 112 | } else { 113 | title = '' 114 | } 115 | 116 | if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) { 117 | state.pos = oldPos 118 | return false 119 | } 120 | pos++ 121 | } else { 122 | // 123 | // Link reference 124 | // 125 | if (typeof state.env.references === 'undefined') { return false } 126 | 127 | if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) { 128 | start = pos + 1 129 | pos = state.md.helpers.parseLinkLabel(state, pos) 130 | if (pos >= 0) { 131 | label = state.src.slice(start, pos++) 132 | } else { 133 | pos = labelEnd + 1 134 | } 135 | } else { 136 | pos = labelEnd + 1 137 | } 138 | 139 | // covers label === '' and label === undefined 140 | // (collapsed reference link and shortcut reference link respectively) 141 | if (!label) { label = state.src.slice(labelStart, labelEnd) } 142 | 143 | ref = state.env.references[normalizeReference(label)] 144 | if (!ref) { 145 | state.pos = oldPos 146 | return false 147 | } 148 | href = ref.href 149 | title = ref.title 150 | } 151 | 152 | // 153 | // We found the end of the link, and know for a fact it's a valid link; 154 | // so all that's left to do is to call tokenizer. 155 | // 156 | if (!silent) { 157 | content = state.src.slice(labelStart, labelEnd) 158 | 159 | state.md.inline.parse( 160 | content, 161 | state.md, 162 | state.env, 163 | tokens = [] 164 | ) 165 | 166 | token = state.push('image', 'img', 0) 167 | token.attrs = attrs = [ [ 'src', href ], [ 'alt', '' ] ] 168 | token.children = tokens 169 | token.content = content 170 | 171 | if (title) { 172 | attrs.push([ 'title', title ]) 173 | } 174 | } 175 | 176 | state.pos = pos 177 | state.posMax = max 178 | return true 179 | } 180 | -------------------------------------------------------------------------------- /lib/gfm/indented-headings.js: -------------------------------------------------------------------------------- 1 | module.exports = function (md, options) { 2 | // Unfortunately, there's no public API for getting access to the existing 3 | // installed parsing rules; rather than import the 'heading' rule directly 4 | // from markdown-it just so we can wrap it with our own processing, we have to 5 | // use internal utility methods to pick it up first, then do the replacement 6 | // via normal means. 7 | // 8 | // This plugin allows us to more closely mimic GitHub's heading parsing. Per 9 | // CommonMark, ATX headings are allowed to have one to three spaces before the 10 | // hash characters. GitHub doesn't allow any leading whitespace whatsoever. 11 | // 12 | // For example (the `·` characters represent spaces): 13 | // 14 | // ·### Renders as H3 in CommonMark 15 | // ··## Renders as H2 in CommonMark 16 | // ···# Renders as H1 in CommonMark 17 | // 18 | // ... whereas on GitHub, those become standard paragraphs. 19 | 20 | var ruler = md.block.ruler 21 | var originalEntry = ruler.__rules__[ruler.__find__('heading')] 22 | var originalRule = originalEntry.fn 23 | 24 | ruler.at('heading', noIndentedHeadings, {alt: originalEntry.alt}) 25 | 26 | function noIndentedHeadings (state, startLine, endLine, silent) { 27 | // conveniently, state.tShift holds a count of the number of leading spaces 28 | // on each line, so all we need to do is return false if it's non-zero 29 | if (state.tShift[startLine] > 0) { 30 | return false 31 | } else { 32 | return originalRule.apply(this, arguments) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/gfm/link.js: -------------------------------------------------------------------------------- 1 | // CommonMark specifies that links have no whitespace between the label and 2 | // destination, e.g.: 3 | // 4 | // [link text](path/to/the/thing) 5 | // 6 | // However, GitHub allows you to put whitespace between the `]` and `(` 7 | // characters, like so: 8 | // 9 | // [link text] (path/to/the/thing) 10 | // 11 | // This is against CommonMark because it's ambiguous. Consider a link reference 12 | // followed by a parenthetical statement: 13 | // 14 | // [link text] (parenthetical statement) 15 | // 16 | // [link text]: path/to/the/thing 17 | // 18 | // CommonMark renders: link text 19 | // GitHub renders: link text 20 | // 21 | // To account for the difference, we need to override markdown-it's link rule. 22 | // This file is a copy of the original markdown-it source, plus the necessary 23 | // modifications, wrapped as a markdown-it plugin. 24 | 25 | // Process [link]( "stuff") 26 | 'use strict' 27 | 28 | module.exports = function (md) { 29 | md.inline.ruler.at('link', link) 30 | } 31 | 32 | function link (state, silent) { 33 | var attrs 34 | var code 35 | var label 36 | var labelEnd 37 | var labelStart 38 | var pos 39 | var res 40 | var ref 41 | var title 42 | var token 43 | var href = '' 44 | var oldPos = state.pos 45 | var max = state.posMax 46 | var start = state.pos 47 | var parseReference = true 48 | var isSpace = state.md.utils.isSpace 49 | var normalizeReference = state.md.utils.normalizeReference 50 | 51 | if (state.src.charCodeAt(state.pos) !== 0x5B/* [ */) { return false } 52 | 53 | labelStart = state.pos + 1 54 | labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, true) 55 | 56 | // parser failed to find ']', so it's not a valid link 57 | if (labelEnd < 0) { return false } 58 | 59 | pos = labelEnd + 1 60 | 61 | // BEGIN CUSTOM MODIFICATION 62 | // look ahead to see if the next non-whitespace character is '(', and if 63 | // it is, skip the whitespace 64 | for (; pos < max; pos++) { 65 | code = state.src.charCodeAt(pos) 66 | if (!isSpace(code) && code !== 0x0A) { break } 67 | } 68 | if (pos < max && state.src.charCodeAt(pos) !== 0x28) { pos = labelEnd + 1 } 69 | // END CUSTOM MODIFICATION 70 | 71 | if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) { 72 | // 73 | // Inline link 74 | // 75 | 76 | // might have found a valid shortcut link, disable reference parsing 77 | parseReference = false 78 | 79 | // [link]( "title" ) 80 | // ^^ skipping these spaces 81 | pos++ 82 | for (; pos < max; pos++) { 83 | code = state.src.charCodeAt(pos) 84 | if (!isSpace(code) && code !== 0x0A) { break } 85 | } 86 | if (pos >= max) { return false } 87 | 88 | // [link]( "title" ) 89 | // ^^^^^^ parsing link destination 90 | start = pos 91 | res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax) 92 | if (res.ok) { 93 | href = state.md.normalizeLink(res.str) 94 | if (state.md.validateLink(href)) { 95 | pos = res.pos 96 | } else { 97 | href = '' 98 | } 99 | } 100 | 101 | // [link]( "title" ) 102 | // ^^ skipping these spaces 103 | start = pos 104 | for (; pos < max; pos++) { 105 | code = state.src.charCodeAt(pos) 106 | if (!isSpace(code) && code !== 0x0A) { break } 107 | } 108 | 109 | // [link]( "title" ) 110 | // ^^^^^^^ parsing link title 111 | res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax) 112 | if (pos < max && start !== pos && res.ok) { 113 | title = res.str 114 | pos = res.pos 115 | 116 | // [link]( "title" ) 117 | // ^^ skipping these spaces 118 | for (; pos < max; pos++) { 119 | code = state.src.charCodeAt(pos) 120 | if (!isSpace(code) && code !== 0x0A) { break } 121 | } 122 | } else { 123 | title = '' 124 | } 125 | 126 | if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) { 127 | // parsing a valid shortcut link failed, fallback to reference 128 | parseReference = true 129 | } 130 | pos++ 131 | } 132 | 133 | if (parseReference) { 134 | // 135 | // Link reference 136 | // 137 | if (typeof state.env.references === 'undefined') { return false } 138 | 139 | if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) { 140 | start = pos + 1 141 | pos = state.md.helpers.parseLinkLabel(state, pos) 142 | if (pos >= 0) { 143 | label = state.src.slice(start, pos++) 144 | } else { 145 | pos = labelEnd + 1 146 | } 147 | } else { 148 | pos = labelEnd + 1 149 | } 150 | 151 | // covers label === '' and label === undefined 152 | // (collapsed reference link and shortcut reference link respectively) 153 | if (!label) { label = state.src.slice(labelStart, labelEnd) } 154 | 155 | ref = state.env.references[normalizeReference(label)] 156 | if (!ref) { 157 | state.pos = oldPos 158 | return false 159 | } 160 | href = ref.href 161 | title = ref.title 162 | } 163 | 164 | // 165 | // We found the end of the link, and know for a fact it's a valid link; 166 | // so all that's left to do is to call tokenizer. 167 | // 168 | if (!silent) { 169 | state.pos = labelStart 170 | state.posMax = labelEnd 171 | 172 | token = state.push('link_open', 'a', 1) 173 | token.attrs = attrs = [ [ 'href', href ] ] 174 | if (title) { 175 | attrs.push([ 'title', title ]) 176 | } 177 | 178 | state.md.inline.tokenize(state) 179 | 180 | token = state.push('link_close', 'a', -1) 181 | } 182 | 183 | state.pos = pos 184 | state.posMax = max 185 | return true 186 | } 187 | -------------------------------------------------------------------------------- /lib/gfm/override-link-destination-parser.js: -------------------------------------------------------------------------------- 1 | // Override the markdown-it helper rule to allow spaces in the src attributes of images and the href attributes of 2 | // anchor elements: 3 | // e.g., ![Gitter](https://badges.gitter.im/Join Chat.svg) 4 | // 5 | // This is a modified version of the stock markdown-it helper for parsing link destinations 6 | 'use strict' 7 | 8 | function parseLinkDestination (md, str, pos, max) { 9 | var code 10 | var level 11 | var lines = 0 12 | var start = pos 13 | var result = { 14 | ok: false, 15 | pos: 0, 16 | lines: 0, 17 | str: '' 18 | } 19 | var isSpace = md.utils.isSpace 20 | var unescapeAll = md.utils.unescapeAll 21 | 22 | if (str.charCodeAt(pos) === 0x3C /* < */) { 23 | pos++ 24 | while (pos < max) { 25 | code = str.charCodeAt(pos) 26 | if (code === 0x0A /* \n */ || isSpace(code)) { return result } 27 | if (code === 0x3E /* > */) { 28 | result.pos = pos + 1 29 | result.str = unescapeAll(str.slice(start + 1, pos)) 30 | result.ok = true 31 | return result 32 | } 33 | if (code === 0x5C /* \ */ && pos + 1 < max) { 34 | pos += 2 35 | continue 36 | } 37 | 38 | pos++ 39 | } 40 | 41 | // no closing '>' 42 | return result 43 | } 44 | 45 | // this should be ... } else { ... branch 46 | 47 | level = 0 48 | while (pos < max) { 49 | code = str.charCodeAt(pos) 50 | 51 | if (code === 0x20) { // space 52 | pos++ // 0x22 => double quote, 0x27 => single quote, 0x28 => '(', 0x29 => ')' 53 | while (pos < max && code !== 0x22 && code !== 0x27 && code !== 0x28 && code !== 0x29) { 54 | code = str.charCodeAt(pos++) 55 | } 56 | pos-- 57 | if (pos === max) { // end of the line 58 | break 59 | } 60 | if (code === 0x22 || code === 0x27 || code === 0x28) { // single/double quotes or opening paren for title attributes 61 | pos-- 62 | break 63 | } 64 | } 65 | 66 | // ascii control characters 67 | if (code < 0x20 || code === 0x7F) { break } 68 | 69 | if (code === 0x5C /* \ */ && pos + 1 < max) { 70 | pos += 2 71 | continue 72 | } 73 | 74 | if (code === 0x28 /* ( */) { 75 | level++ 76 | if (level > 1) { break } 77 | } 78 | 79 | if (code === 0x29 /* ) */) { 80 | level-- 81 | if (level < 0) { break } 82 | } 83 | 84 | pos++ 85 | } 86 | 87 | if (start === pos) { return result } 88 | 89 | result.str = unescapeAll(str.slice(start, pos)) 90 | result.lines = lines 91 | result.pos = pos 92 | result.ok = true 93 | return result 94 | } 95 | 96 | module.exports = function (md) { 97 | md.helpers.parseLinkDestination = parseLinkDestination.bind(this, md) 98 | } 99 | -------------------------------------------------------------------------------- /lib/gfm/relaxed-link-reference.js: -------------------------------------------------------------------------------- 1 | module.exports = function (md, options) { 2 | // Unfortunately, there's no public API for getting access to the existing 3 | // installed parsing rules; rather than import the 'reference' rule directly 4 | // from markdown-it just so we can re-install it into the parser with the 5 | // 'alt' chain set up correctly, here we're just using internal utility 6 | // methods to modify it in place at runtime. 7 | // 8 | // The net result is that we allow what are known in the CommonMark spec as 9 | // "link reference definitions" to interrupt paragraphs, i.e., we relax the 10 | // requirement that there be a blank line between a paragraph and a reference 11 | // definition. 12 | // 13 | // That means that this works: 14 | // 15 | // Some paragraph text here with a [linkref] 16 | // [linkref]: /actual/link/destination/here 17 | // 18 | // ...whereas spec compliance requires a blank line between the two. 19 | 20 | var ruler = md.block.ruler 21 | var idx = ruler.__find__('reference') 22 | ruler.__rules__[idx].alt.push('paragraph') 23 | ruler.__compile__() 24 | } 25 | -------------------------------------------------------------------------------- /lib/highlights-tokens.js: -------------------------------------------------------------------------------- 1 | var stockHighlightsTokens = require('highlights-tokens') 2 | var diffTokens = require('atom-language-diff') 3 | 4 | // combine stock highlights tokens list with any additional ones we've included 5 | 6 | module.exports = [].concat(stockHighlightsTokens, diffTokens) 7 | -------------------------------------------------------------------------------- /lib/linkify.js: -------------------------------------------------------------------------------- 1 | // github-style linkification for linkify-it 2 | // 3 | // Pass in a markdown-it parser instance and this will return a version fixed 4 | // up for the kind of linkification we're after. 5 | // 6 | // As far as I can determine, the basic algorithm is to linkify anything 7 | // starting with 'www.' and ending with a) a whitespace character, or b) a dot 8 | // followed by a whitespace character (in other words, include internal dots, 9 | // but exclude a trailing dot). There are plenty of cases where this results in 10 | // invalid links, but that's how they do it, so that's how we'll do it. 11 | // 12 | 13 | function isInBrackets (text) { 14 | return text.charAt(0) === '[' && text.charAt(text.length - 1) === ']' 15 | } 16 | 17 | function isInParens (text) { 18 | return text.charAt(0) === '(' && text.charAt(text.length - 1) === ')' 19 | } 20 | 21 | module.exports = function (parser) { 22 | if (parser) { 23 | var linkify = parser.linkify 24 | 25 | linkify.set({fuzzyLink: false}) // turn off auto-linking normal hostnames 26 | linkify.add('//', null) // turn off protocol-relative links 27 | 28 | // linkify everything hostnamey starting with 'www.', optionally in [] or () 29 | var scheme = { 30 | validate: function (text, pos, self) { 31 | if (!self.re.githubLinkify) { 32 | self.re.githubLinkify = new RegExp('^' + self.re.src_host_port_strict + self.re.src_path, 'i') 33 | } 34 | 35 | // chop off any trailing ']' or ')', if necessary 36 | var candidate = text.slice(pos) 37 | if (isInBrackets(text) || isInParens(text)) { 38 | candidate = candidate.slice(0, candidate.length - 1) 39 | } 40 | 41 | if (self.re.githubLinkify.test(candidate)) { 42 | return candidate.match(self.re.githubLinkify)[0].length 43 | } else if (candidate.charAt(0) === ' ') { 44 | // if we have a zero-length match, i.e., if the original string was 45 | // literally 'www.' all by itself, we return -1 because linkify-it does 46 | // a boolean test on the result of the validator function (so we need a 47 | // non-zero result here to make it through that test) and adds the 48 | // returned value to an index it's using to point to the end of the 49 | // matched substring. In this case, that means we'll move backwards one 50 | // character and only link 'www' rather than the full 'www.' that was 51 | // originally matched 52 | return -1 53 | } 54 | return 0 55 | }, 56 | normalize: function (match) { 57 | // since we have 3 different "schemes" in place, our matched URLs might 58 | // have a leading bracket or parenthesis; trim it off if necessary 59 | if (match.schema.charAt(0) === 'w') { 60 | match.url = 'http://' + match.url 61 | } else { 62 | match.url = 'http://' + match.url.slice(1) 63 | } 64 | match.schema = '' 65 | } 66 | } 67 | 68 | parser.linkify.add('www.', scheme) 69 | parser.linkify.add('[www.', scheme) 70 | parser.linkify.add('(www.', scheme) 71 | 72 | // linkify-it's default behavior is to create matches against the 73 | // registered schema prefixes when the character immediately preceding a 74 | // potential match is non-alphanumeric; however, we also need to skip 75 | // linkification when said character is punctuation. So here we rebuild the 76 | // internal regular expressions the LinkifyIt instance will use. 77 | var regexes = linkify.re 78 | regexes.schema_test = RegExp(regexes.schema_test.source.replace(regexes.src_ZPCc, regexes.src_ZCc), 'i') 79 | regexes.schema_search = RegExp(regexes.schema_search.source.replace(regexes.src_ZPCc, regexes.src_ZCc), 'ig') 80 | regexes.pretest = RegExp('(' + regexes.schema_test.source + ')|(' + regexes.host_fuzzy_test.source + ')|@', 'i') 81 | } 82 | 83 | return parser 84 | } 85 | -------------------------------------------------------------------------------- /lib/plugin/cdn.js: -------------------------------------------------------------------------------- 1 | // This plugin rewrites relative image URLs to use cdn.npm.im instead, when the 2 | // calling code provides package.json data (at least name and version) 3 | // 4 | var URL = require('url') 5 | var path = require('path') 6 | 7 | // CDN-ize image URLs 8 | 9 | module.exports = function (md, opts) { 10 | if (!opts) return 11 | if (!opts.package) return 12 | if (!opts.package.name) return 13 | if (!opts.package.version) return 14 | 15 | var originalRule = md.renderer.rules.image 16 | md.renderer.rules.image = function (tokens, idx, options, env, self) { 17 | var url = URL.parse(tokens[idx].attrGet('src')) 18 | 19 | // skip fully-qualified and protocol-relative URLs 20 | if (!url.host && !url.path.match(/^\/\//)) { 21 | url.protocol = 'https' 22 | url.host = 'cdn.npm.im' 23 | url.pathname = '/' + opts.package.name + '@' + opts.package.version + path.join('/', url.path) 24 | tokens[idx].attrSet('src', URL.format(url)) 25 | } 26 | 27 | return originalRule.call(this, tokens, idx, options, env, self) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/plugin/code-wrap.js: -------------------------------------------------------------------------------- 1 | var plugin = module.exports = function (md, options) { 2 | // monkey patch the 'fence' parsing rule to restore markdown-it's pre-5.1 behavior 3 | // (see https://github.com/markdown-it/markdown-it/issues/190) 4 | var stockFenceRule = md.renderer.rules.fence 5 | md.renderer.rules.fence = function (tokens, idx, options, env, slf) { 6 | // call the original rule first rather than inside the 'return' statement 7 | // because we need the 'class' attribute processing it does 8 | var output = stockFenceRule(tokens, idx, options, env, slf).trim() 9 | 10 | // markdown-it 6.1 and later stopped overwriting the class attribute in the 11 | // renderer rule, so we construct it ourselves here 12 | var token = tokens[idx] 13 | var index = token.attrIndex('class') 14 | var attributes = token.attrs ? token.attrs.slice() : [] 15 | var langName = token.info.split(/\s+/g)[0] 16 | var classes = options.langPrefix + langName 17 | 18 | if (index < 0) { 19 | attributes.push(['class', classes]) 20 | } else { 21 | attributes[index] += ' ' + classes 22 | } 23 | 24 | var fakeToken = { 25 | attrs: attributes 26 | } 27 | 28 | if (!langName) { 29 | return output 30 | } else { 31 | return '<' + plugin.tag + slf.renderAttrs(fakeToken) + '>' + output + '\n' 32 | } 33 | } 34 | } 35 | 36 | plugin.tag = 'div' 37 | -------------------------------------------------------------------------------- /lib/plugin/github.js: -------------------------------------------------------------------------------- 1 | // markdown-it plugin to rewrite image and link URLs to be static github URLs 2 | // when the calling code provides github repository information 3 | // 4 | var gh = require('github-url-to-object') 5 | var URL = require('url') 6 | var path = require('path') 7 | 8 | var DEFAULT_REF = 'HEAD' 9 | 10 | function buildImageUrl (repository, url) { 11 | var prefix = 'https://raw.githubusercontent.com/' 12 | return prefix + path.join(repository.user, repository.repo, DEFAULT_REF, url.href) 13 | } 14 | 15 | function buildLinkUrl (repository, url) { 16 | return repository.https_url + path.join('/blob/' + DEFAULT_REF + '/', url.href) 17 | } 18 | 19 | // search the provided HTML snippet and rewrite the attribute on the given tag 20 | // using the repository object provided 21 | function replaceTagAttribute (html, tag, attribute, buildUrl, repository) { 22 | // look for the attribute's value 23 | var regex = new RegExp('<\\s*' + tag + '[^>]*\\b' + attribute + '\\s*=\\s*', 'i') 24 | var attr = html.match(regex) 25 | if (attr) { 26 | // mark the location and figure out what kind of quotation mark is delimiting the value 27 | var position = attr.index + attr[0].length 28 | var quote = html.charAt(position) 29 | var substring = html.slice(position + 1) 30 | 31 | // now that we've found the first delimiting quotation mark, match 32 | // everything up to the next instance of the same quotation mark 33 | var src = substring.match(new RegExp('^[^' + quote + ']*')) 34 | if (src) { 35 | var url = URL.parse(src[0]) 36 | 37 | // Skip fully-qualified URLs, #hash fragments, and protocol-relative URLs 38 | if (!url.host && url.path && !url.path.match(/^\/\//)) { 39 | var newUrl = buildUrl(repository, url) 40 | return html.slice(0, attr.index) + attr[0] + quote + newUrl + quote + substring.slice(src[0].length + 1) 41 | } 42 | } 43 | } 44 | return false // indicate that we did no replacement 45 | } 46 | 47 | module.exports = function (md, opts) { 48 | if (!opts) return 49 | if (!opts.package) return 50 | if (!opts.package.repository) return 51 | 52 | var repo = gh(opts.package.repository) 53 | 54 | if (!repo) return 55 | 56 | // rewrite image locations to be fully qualified github URLs 57 | var originalImageRule = md.renderer.rules.image 58 | md.renderer.rules.image = function (tokens, idx, options, env, self) { 59 | var src = tokens[idx].attrGet('src') 60 | 61 | if (src && src.length) { 62 | var url = URL.parse(src) 63 | 64 | // Skip fully-qualified URLs, #hash fragments, and protocol-relative URLs 65 | if (!url.host && url.path && !url.path.match(/^\/\//)) { 66 | tokens[idx].attrSet('src', buildImageUrl(repo, url)) 67 | } 68 | } 69 | return originalImageRule.call(this, tokens, idx, options, env, self) 70 | } 71 | 72 | // rewrite link hrefs to be fully qualified github URLs 73 | var originalLinkRule = md.renderer.rules.link_open 74 | md.renderer.rules.link_open = function (tokens, idx, options, env, self) { 75 | var href = tokens[idx].attrGet('href') 76 | 77 | if (href && href.length) { 78 | var url = URL.parse(href) 79 | 80 | // Skip fully-qualified URLs, #hash fragments, and protocol-relative URLs 81 | if (!url.host && url.path && !url.path.match(/^\/\//)) { 82 | tokens[idx].attrSet('href', buildLinkUrl(repo, url)) 83 | } 84 | } 85 | if (originalLinkRule) { 86 | return originalLinkRule.call(this, tokens, idx, options, env, self) 87 | } else { 88 | return md.renderer.renderToken(tokens, idx, options) 89 | } 90 | } 91 | 92 | function makeHtmlTransformer (originalRule) { 93 | return function (tokens, idx, options, env, self) { 94 | var content = tokens[idx].content 95 | var imgHtml = replaceTagAttribute(content, 'img', 'src', buildImageUrl, repo) 96 | var linkHtml = replaceTagAttribute(content, 'a', 'href', buildLinkUrl, repo) 97 | 98 | tokens[idx].content = imgHtml || linkHtml || content 99 | 100 | return originalRule.call(this, tokens, idx, options, env, self) 101 | } 102 | } 103 | 104 | // rewrite image locations in inline/block HTML as fully qualified github URLs 105 | md.renderer.rules.html_inline = makeHtmlTransformer(md.renderer.rules.html_inline) 106 | md.renderer.rules.html_block = makeHtmlTransformer(md.renderer.rules.html_block) 107 | } 108 | -------------------------------------------------------------------------------- /lib/plugin/gravatar.js: -------------------------------------------------------------------------------- 1 | // markdown-it plugin that ensures all gravatar img src URLs are secure 2 | // 3 | var URL = require('url') 4 | 5 | module.exports = function (md, opts) { 6 | // patch the current rule, don't replace it completely 7 | var originalRule = md.renderer.rules.image 8 | md.renderer.rules.image = function (tokens, idx, options, env, self) { 9 | var url = URL.parse(tokens[idx].attrGet('src')) 10 | if (url.host && url.host.match(/^(\w+\.)?gravatar\.com$/)) { 11 | url.protocol = 'https' 12 | url.host = 'secure.gravatar.com' 13 | tokens[idx].attrSet('src', URL.format(url)) 14 | } 15 | return originalRule.call(this, tokens, idx, options, env, self) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/plugin/heading-links.js: -------------------------------------------------------------------------------- 1 | var GithubSlugger = require('github-slugger') 2 | var innertext = require('innertext') 3 | var tokenUtil = require('../token-util') 4 | 5 | var headings = module.exports = function (md, options) { 6 | var headingAnchorClass = options.headingAnchorClass || 'anchor' 7 | var headingSvgClass = options.headingSvgClass || ['octicon', 'octicon-link'] 8 | var iconClasses = [].concat(headingSvgClass).join(' ') 9 | // shamelessly borrowed from GitHub, thanks y'all 10 | var svgLinkIconText = '' 11 | if (options && !options.prefixHeadingIds) { 12 | headings.prefix = '' 13 | } else { 14 | headings.prefix = headings.prefix || headings.defaultPrefix 15 | } 16 | 17 | var slugger = new GithubSlugger() 18 | var Token 19 | 20 | md.core.ruler.push('headingLinks', function (state) { 21 | // save the Token constructor because we'll be building a few instances at render 22 | // time; that's sort of outside the intended markdown-it parsing sequence, but 23 | // since we have tight control over what we're creating (a link), we're safe 24 | if (!Token) { 25 | Token = state.Token 26 | tokenUtil.set(Token) 27 | } 28 | }) 29 | 30 | md.renderer.rules.heading_open = function (tokens, idx, opts, env, self) { 31 | var children = tokens[idx + 1].children 32 | // make sure heading is not empty 33 | if (children && children.length) { 34 | // Generate an ID based on the heading's innerHTML; first, render without 35 | // converting gemoji strings to unicode emoji characters 36 | var rendered = md.renderer.renderInline(children.map(tokenUtil.unemoji), opts, env) 37 | var postfix = slugger.slug( 38 | innertext(rendered) 39 | .replace(/[<>]/g, '') // In case the heading contains `` 40 | .toLowerCase() // because `slug` doesn't lowercase 41 | ) 42 | 43 | // add 3 new token objects link_open, text, link_close 44 | var linkOpen = new Token('link_open', 'a', 1) 45 | var text = new Token('html_inline', '', 0) 46 | if (options && options.enableHeadingLinkIcons) { 47 | text.content = svgLinkIconText 48 | } 49 | var linkClose = new Token('link_close', 'a', -1) 50 | 51 | // add some link attributes 52 | linkOpen.attrSet('id', headings.prefix + postfix) 53 | linkOpen.attrSet('class', headingAnchorClass) 54 | linkOpen.attrSet('href', '#' + postfix) 55 | linkOpen.attrSet('aria-hidden', 'true') 56 | 57 | // add new token objects as children of heading 58 | children.unshift(linkClose) 59 | children.unshift(text) 60 | children.unshift(linkOpen) 61 | } 62 | 63 | return md.renderer.renderToken(tokens, idx, options, env, self) 64 | } 65 | } 66 | 67 | headings.defaultPrefix = 'user-content-' 68 | -------------------------------------------------------------------------------- /lib/plugin/html-heading.js: -------------------------------------------------------------------------------- 1 | module.exports = function (md, opts) { 2 | var htmlHeader = function (state, startLine, endLine, silent) { 3 | var pos = state.bMarks[startLine] + state.tShift[startLine] 4 | var max = state.eMarks[startLine] 5 | 6 | if (!state.md.options.html) { return false } 7 | 8 | // Check start 9 | if (state.src.charCodeAt(pos) !== 0x3C /* < */ || pos + 2 >= max) { 10 | return false 11 | } 12 | 13 | // Quick fail on second char 14 | var ch = state.src[pos + 1] 15 | if (ch !== 'h') { 16 | return false 17 | } 18 | 19 | var match = state.src.slice(pos).match(//) 20 | if (!match) { return false } 21 | var level = parseInt(match[0][2]) 22 | 23 | var end = state.src.slice(pos).match(new RegExp('')) 24 | if (!end) { return false } 25 | 26 | state.line = startLine + 1 27 | 28 | var token = state.push('heading_open', 'h' + String(level), 1) 29 | token.markup = '########'.slice(0, level) 30 | token.map = [ startLine, state.line ] 31 | 32 | token = state.push('inline', '', 0) 33 | token.content = state.src.slice(pos + match.index + 4, pos + end.index).trim() 34 | token.map = [ startLine, state.line ] 35 | token.children = [] 36 | 37 | token = state.push('heading_close', 'h' + String(level), -1) 38 | token.markup = '########'.slice(0, level) 39 | 40 | return true 41 | } 42 | 43 | md.block.ruler.before('html_block', 'html_header', htmlHeader, ['paragraph', 'reference', 'blockquote']) 44 | } 45 | -------------------------------------------------------------------------------- /lib/plugin/language-alias.js: -------------------------------------------------------------------------------- 1 | // This plugin allows for code fences written in certain languages to be rendered 2 | // as though they'd been written in a different language, e.g., 3 | // 4 | // ```bash 5 | // $ marky-markdown test.md 6 | // ``` 7 | // 8 | // can have the exact same output as 9 | // 10 | // ```sh 11 | // $ marky-markdown test.md 12 | // ``` 13 | // 14 | // (doing the normal thing and setting up a syntax highlighter mapping in our 15 | // render module would almost work, except the wrapping
still has the 16 | // "bash" class applied) 17 | // 18 | 19 | var plugin = module.exports = function (md, opts) { 20 | var previousFenceRule = md.renderer.rules.fence 21 | md.renderer.rules.fence = function (tokens, idx, options, env, slf) { 22 | // code fence language marker is in token.info; do we have an alias? 23 | if (languageAliases.hasOwnProperty(tokens[idx].info)) { 24 | tokens[idx].info = languageAliases[tokens[idx].info] 25 | } 26 | return previousFenceRule.call(this, tokens, idx, options, env, slf) 27 | } 28 | } 29 | 30 | var languageAliases = plugin.languageAliases = { 31 | bash: 'sh', 32 | typescript: 'ts' 33 | } 34 | -------------------------------------------------------------------------------- /lib/plugin/nofollow.js: -------------------------------------------------------------------------------- 1 | // Set rel=nofollow on all links if we have set `nofollow` in the options. 2 | 3 | module.exports = function (md, options) { 4 | var defaultRender = md.renderer.rules.link_open || function (tokens, idx, options, env, self) { 5 | return self.renderToken(tokens, idx, options) 6 | } 7 | 8 | md.renderer.rules.link_open = function handleLink (tokens, idx, options, env, self) { 9 | tokens[idx].attrPush(['rel', 'nofollow']) 10 | return defaultRender(tokens, idx, options, env, self) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/plugin/packagize.js: -------------------------------------------------------------------------------- 1 | // markdown-it plugin to add some helper classes to existing elements that look 2 | // like duplicates of package names and descriptions 3 | // 4 | var similarity = require('similarity') 5 | var tokenUtil = require('../token-util') 6 | 7 | module.exports = function (md, opts) { 8 | if (!opts) return 9 | if (!opts.package) return 10 | if (!opts.package.name) return 11 | 12 | var packageName = opts.package.name 13 | var packageDescription = opts.package.description + '' // invalid description in some cases can cause a crash 14 | 15 | // 16 | // process the first

element 17 | // 18 | var originalHeadingRule = md.renderer.rules.heading_open 19 | md.renderer.rules.heading_open = function (tokens, idx, options, env, self) { 20 | if (tokens[idx].tag === 'h1' && !env.packagizeHeadingDone) { 21 | // extract the text from the heading's 'inline' token 22 | var text = tokenUtil.getText(tokens[idx + 1]) 23 | 24 | // check to see if the heading text matches the package name 25 | if ( 26 | similarity(packageName, text) > 0.6 || 27 | similarity(packageName.replace(/^@[^/]+\//, ''), text) > 0.6 || // filter out scope name 28 | ~text.toLowerCase().indexOf(packageName.toLowerCase()) 29 | ) { 30 | tokens[idx].attrJoin('class', 'package-name-redundant') 31 | } 32 | 33 | // check to see if the heading text matches the package description 34 | if (packageDescription && ( 35 | similarity(packageDescription, text) > 0.6 || 36 | ~text.toLowerCase().indexOf(packageDescription.toLowerCase()) 37 | )) { 38 | tokens[idx].attrJoin('class', 'package-description-redundant') 39 | } 40 | 41 | // only inspect the first h1; skip the rest 42 | env.packagizeHeadingDone = true 43 | } 44 | return originalHeadingRule.call(this, tokens, idx, options, env, self) 45 | } 46 | 47 | // 48 | // process the first paragraph that contains text 49 | // 50 | var originalParagraphRule = md.renderer.rules.paragraph_open 51 | md.renderer.rules.paragraph_open = function (tokens, idx, options, env, self) { 52 | if (!env.packagizeParagraphDone) { 53 | // extract the text from the paragraph's 'inline' token 54 | var text = tokenUtil.getText(tokens[idx + 1]) 55 | 56 | // check to see if the paragraph text matches the description 57 | if (similarity(packageDescription, text) > 0.6) { 58 | tokens[idx].attrJoin('class', 'package-description-redundant') 59 | } 60 | 61 | // inspect the first paragraph that contains text; skip the rest 62 | if (text) { 63 | env.packagizeParagraphDone = true 64 | } 65 | } 66 | if (originalParagraphRule) { 67 | return originalParagraphRule.call(this, tokens, idx, options, env, self) 68 | } else { 69 | return md.renderer.renderToken(tokens, idx, options) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/plugin/youtube.js: -------------------------------------------------------------------------------- 1 | // markdown-it plugin to wrap youtube iframes in a
for styling purposes 2 | // 3 | var URL = require('url') 4 | 5 | var WRAPPER_START = "
" 6 | var WRAPPER_END = '
' 7 | 8 | // determine whether the given HTML string contains an iframe pointing to a youtube.com URL 9 | function isYoutubeIframe (content) { 10 | // look for the src attribute's value 11 | var attr = content.match(/<\s*iframe[^>]*\bsrc\s*=\s*/) 12 | if (attr) { 13 | // mark the location and figure out what kind of quotation mark is delimiting the value 14 | var position = attr.index + attr[0].length 15 | var quote = content.charAt(position) 16 | var substring = content.slice(position + 1) 17 | 18 | // now that we've found the first delimiting quotation mark, match 19 | // everything up to the next instance of the same quotation mark 20 | var src = substring.match(new RegExp('^[^' + quote + ']*')) 21 | if (src) { 22 | var value = src[0] 23 | // for protocol-relative src, prepend a protocol for URL parsing purposes 24 | if (value.indexOf('//') === 0) { 25 | value = 'https:' + value 26 | } 27 | 28 | var url = URL.parse(value) 29 | return (url.host && url.host.match(/^(\w+\.)?youtube\.com$/)) 30 | } 31 | } 32 | return false 33 | } 34 | 35 | module.exports = function (md, opts) { 36 | // Wrap iframes that appear in HTML blocks 37 | // 38 | // In html_block tokens, the entire contents of an HTML block appear as the 39 | // `.content` property of a single token object. For a standalone `` tag, so we can find the end of it 60 | var closingMatch = content.substring(start).match(/>[^>]*<\s*\/\s*iframe\s*>/) 61 | if (closingMatch) { 62 | var endOffset = content.substring(start + closingMatch.index + 1).indexOf('>') + 1 63 | end = start + closingMatch.index + endOffset + 1 64 | } 65 | 66 | // slice up the content according to the positions we've computed 67 | var prefix = content.substring(0, start) 68 | var iframe = content.substring(start, end) 69 | var postfix = content.substring(end) 70 | 71 | // inject the wrapper element 72 | tokens[idx].content = prefix + WRAPPER_START + iframe + WRAPPER_END + postfix 73 | } 74 | 75 | return originalBlockRule.call(this, tokens, idx, options, env, self) 76 | } 77 | 78 | // Wrap iframes that appear inside inline HTML strings 79 | // 80 | // In runs of html_inline tokens, the opening `` tag, so what we have to do if we 82 | // find a match is walk through the list to find out where to put the closing 83 | // `
` part of our wrapper 84 | // 85 | var originalInlineRule = md.renderer.rules.html_inline 86 | md.renderer.rules.html_inline = function (tokens, idx, options, env, self) { 87 | if (isYoutubeIframe(tokens[idx].content)) { 88 | // prepend the opening part of the wrapper 89 | tokens[idx].content = WRAPPER_START + tokens[idx].content 90 | 91 | // find the closing tag and append the closing part of the wrapper 92 | for (var position = idx; position < tokens.length && tokens[position].type === 'html_inline'; position++) { 93 | if (tokens[position].content === '') { 94 | tokens[position].content += WRAPPER_END 95 | } 96 | } 97 | } 98 | 99 | return originalInlineRule.call(this, tokens, idx, options, env, self) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/render.js: -------------------------------------------------------------------------------- 1 | var pickBy = require('lodash.pickby') 2 | var MD = require('markdown-it') 3 | var lazyHeaders = require('markdown-it-lazy-headers') 4 | var emoji = require('markdown-it-emoji') 5 | var expandTabs = require('markdown-it-expand-tabs') 6 | var githubTaskList = require('markdown-it-task-lists') 7 | 8 | var cleanup = require('./cleanup') 9 | var githubLinkify = require('./linkify') 10 | 11 | var codeWrap = require('./plugin/code-wrap') 12 | var headingLinks = require('./plugin/heading-links') 13 | var gravatar = require('./plugin/gravatar') 14 | var github = require('./plugin/github') 15 | var youtube = require('./plugin/youtube') 16 | var cdnImages = require('./plugin/cdn') 17 | var packagize = require('./plugin/packagize') 18 | var htmlHeading = require('./plugin/html-heading') 19 | var fenceLanguageAliasing = require('./plugin/language-alias') 20 | var relaxedLinkRefs = require('./gfm/relaxed-link-reference') 21 | var githubHeadings = require('./gfm/indented-headings') 22 | var overrideLinkDestinationParser = require('./gfm/override-link-destination-parser') 23 | var looseLinkParsing = require('./gfm/link') 24 | var looseImageParsing = require('./gfm/image') 25 | var relNoFollow = require('./plugin/nofollow') 26 | 27 | if (typeof process.browser === 'undefined') { 28 | var Highlights = require('highlights') 29 | var highlighter = new Highlights() 30 | 31 | var languages = [ 32 | 'atom-language-nginx', 33 | 'atom-language-diff', 34 | 'language-dart', 35 | 'language-rust', 36 | 'language-erlang', 37 | 'language-glsl', 38 | 'language-haxe', 39 | 'language-ini', 40 | 'language-stylus' 41 | ] 42 | 43 | languages.forEach(function (language) { 44 | highlighter.requireGrammarsSync({ 45 | modulePath: require.resolve(language + '/package.json') 46 | }) 47 | }) 48 | 49 | // cleanup generated rules in the highlighter registry if they are idle for 2000ms 50 | // they take a tremendous amount of memory if you process many languages in a server type environment. 51 | cleanup(highlighter.registry.grammars) 52 | } 53 | 54 | var render = module.exports = function (markdown, options) { 55 | return render.getParser(options).render(markdown) 56 | } 57 | 58 | render.getParser = function (options) { 59 | var mdOptions = { 60 | html: true, 61 | langPrefix: 'highlight ', 62 | linkify: options.linkify 63 | } 64 | 65 | if (options.highlightSyntax) { 66 | mdOptions.highlight = function (code, lang) { 67 | if (!lang) { return '' } 68 | return highlighter.highlightSync({ 69 | fileContents: code, 70 | scopeName: scopeNameFromLang(highlighter, lang) 71 | }) 72 | } 73 | } 74 | 75 | var parser = MD(mdOptions) 76 | .use(lazyHeaders) 77 | .use(emoji, {shortcuts: {}}) 78 | .use(expandTabs, {tabWidth: 4}) 79 | .use(githubTaskList) 80 | .use(headingLinks, options) 81 | .use(githubHeadings) 82 | .use(relaxedLinkRefs) 83 | .use(gravatar) 84 | .use(github, {package: options.package}) 85 | .use(youtube) 86 | .use(packagize, {package: options.package}) 87 | .use(htmlHeading) 88 | .use(overrideLinkDestinationParser) 89 | .use(looseLinkParsing) 90 | .use(looseImageParsing) 91 | 92 | if (options.nofollow) { 93 | parser.use(relNoFollow) 94 | } 95 | 96 | if (options.highlightSyntax) { 97 | parser.use(codeWrap) 98 | .use(fenceLanguageAliasing) 99 | } 100 | if (options.serveImagesWithCDN) parser.use(cdnImages, {package: options.package}) 101 | 102 | return githubLinkify(parser) 103 | } 104 | 105 | render.renderPackageDescription = function (description) { 106 | return MD({html: true}).renderInline(description) 107 | } 108 | 109 | var mappings = { 110 | jsx: 'source.js.jsx', 111 | sh: 'source.shell', 112 | markdown: 'source.gfm', 113 | erb: 'text.html.erb' 114 | } 115 | 116 | // attempt to look up by the long language name, e.g. Ruby, JavaScript. 117 | // fallback to assuming that lang is the extension of the code snippet. 118 | function scopeNameFromLang (highlighter, lang) { 119 | // alias language names. 120 | 121 | if (mappings[lang]) return mappings[lang] 122 | 123 | var grammar = pickBy(highlighter.registry.grammarsByScopeName, function (val, key) { 124 | return val.name.toLowerCase() === lang 125 | }) 126 | 127 | if (Object.keys(grammar).length) { 128 | return Object.keys(grammar)[0] 129 | } 130 | 131 | var name = 'source.' + lang 132 | // mappings[lang] = name 133 | 134 | return name 135 | } 136 | -------------------------------------------------------------------------------- /lib/sanitize.js: -------------------------------------------------------------------------------- 1 | var sanitizeHtml = require('sanitize-html') 2 | const url = require('url') 3 | 4 | module.exports = function (html, options) { 5 | var config 6 | if (options && options.prefixHeadingIds) { 7 | config = Object.assign({}, getSanitizerConfig(options), { 8 | transformTags: { 9 | '*': prefixHTMLids, 10 | 'td': sanitizeCellStyle, 11 | 'th': sanitizeCellStyle, 12 | 'a': getNofollowSanitize(options) 13 | } 14 | }) 15 | } else { 16 | config = getSanitizerConfig(options) 17 | } 18 | return sanitizeHtml(html, config) 19 | } 20 | 21 | function getSanitizerConfig (options) { 22 | options = options || { 23 | headingAnchorClass: 'anchor', 24 | headingSvgClass: ['octicon', 'octicon-link'] 25 | } 26 | return { 27 | allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 28 | 'dd', 'del', 'details', 'div', 'dl', 'dt', 'h1', 'h2', 'iframe', 'img', 'input', 'ins', 'meta', 'path', 'pre', 's', 'span', 'sub', 'summary', 'sup', 'svg' 29 | ]), 30 | allowedClasses: { 31 | a: [].concat(options.headingAnchorClass), 32 | div: [ 33 | 'highlight', 34 | 'hljs', 35 | 'bash', 36 | 'css', 37 | 'coffee', 38 | 'coffeescript', 39 | 'diff', 40 | 'glsl', 41 | 'http', 42 | 'js', 43 | 'javascript', 44 | 'json', 45 | 'jsx', 46 | 'lang-html', 47 | 'line', 48 | 'sh', 49 | 'shell', 50 | 'typescript', 51 | 'ts', 52 | 'xml', 53 | 'youtube-video' 54 | ], 55 | h1: ['package-name-redundant', 'package-description-redundant'], 56 | input: ['task-list-item-checkbox'], 57 | li: ['task-list-item'], 58 | ol: ['contains-task-list'], 59 | p: ['package-description-redundant'], 60 | pre: ['editor', 'editor-colors'], 61 | span: require('./highlights-tokens'), 62 | svg: [].concat(options.headingSvgClass), 63 | ul: ['contains-task-list'] 64 | }, 65 | allowedAttributes: { 66 | h1: ['id', 'align'], 67 | h2: ['id', 'align'], 68 | h3: ['id', 'align'], 69 | h4: ['id', 'align'], 70 | h5: ['id', 'align'], 71 | h6: ['id', 'align'], 72 | a: ['href', 'id', 'name', 'target', 'title', 'aria-hidden', 'rel'], 73 | img: ['alt', 'id', 'src', 'width', 'height', 'align', 'valign', 'title'], 74 | p: ['align'], 75 | meta: ['name', 'content'], 76 | iframe: ['src', 'frameborder', 'allowfullscreen'], 77 | input: ['checked', 'class', 'disabled', 'type'], 78 | div: ['id'], 79 | span: [], 80 | pre: [], 81 | td: ['colspan', 'rowspan', 'style'], 82 | th: ['colspan', 'rowspan', 'style'], 83 | del: ['cite', 'datetime'], 84 | ins: ['cite', 'datetime'], 85 | path: ['d'], 86 | svg: ['aria-hidden', 'height', 'version', 'viewbox', 'width'] 87 | }, 88 | exclusiveFilter: function (frame) { 89 | // Allow Task List items 90 | if (frame.tag === 'input') { 91 | var isTaskItem = (frame.attribs.class && frame.attribs.class.indexOf('task-list-item-checkbox') > -1) 92 | var isCheckbox = (frame.attribs.type && frame.attribs.type === 'checkbox') 93 | var isDisabled = frame.attribs.hasOwnProperty('disabled') 94 | return !(isTaskItem && isCheckbox && isDisabled) 95 | } 96 | 97 | // Allow YouTube iframes 98 | if (frame.tag !== 'iframe') return false 99 | 100 | const parsed = url.parse(frame.attribs.src || '') 101 | return !['www.youtube.com', 'youtube.com', 'youtu.be'].includes(parsed.hostname) 102 | }, 103 | transformTags: { 104 | 'td': sanitizeCellStyle, 105 | 'th': sanitizeCellStyle, 106 | 'a': getNofollowSanitize(options) 107 | } 108 | } 109 | } 110 | 111 | function getNofollowSanitize (options) { 112 | return options.nofollow ? sanitizeAnchorNofollow : sanitizeIdentity 113 | } 114 | 115 | function sanitizeIdentity (tagName, attribs) { 116 | return { 117 | tagName: tagName, 118 | attribs: attribs 119 | } 120 | } 121 | 122 | function sanitizeAnchorNofollow (tagName, attribs) { 123 | if (attribs.href) { 124 | attribs.rel = 'nofollow' 125 | } 126 | return sanitizeIdentity(tagName, attribs) 127 | } 128 | 129 | // Allow table cell alignment 130 | function sanitizeCellStyle (tagName, attribs) { 131 | // if we don't add the 'style' to the allowedAttributes above, it will be 132 | // stripped out by the time we get here, so we have to filter out 133 | // everything but `text-align` in case something else tries to sneak in 134 | function cell (alignment) { 135 | var attributes = attribs 136 | if (alignment) { 137 | attributes.style = 'text-align:' + alignment 138 | } else { 139 | delete attributes.style 140 | } 141 | return { 142 | tagName: tagName, 143 | attribs: attributes 144 | } 145 | } 146 | 147 | // look for CSS `text-align` directives 148 | var alignmentRegEx = /text-align\s*:\s*(left|center|right)[\s;$]*/igm 149 | var result = alignmentRegEx.exec(attribs.style || '') 150 | return result ? cell(result[1]) : cell() 151 | } 152 | 153 | function prefixHTMLids (tagName, attribs) { 154 | if (attribs.id && !isAlreadyPrefixed(attribs.id, 'user-content-')) { 155 | attribs.id = 'user-content-' + attribs.id 156 | } 157 | return { 158 | tagName: tagName, 159 | attribs: attribs 160 | } 161 | } 162 | 163 | function isAlreadyPrefixed (id, prefix) { 164 | return id.includes(prefix) && id.length > prefix.length 165 | } 166 | -------------------------------------------------------------------------------- /lib/token-util.js: -------------------------------------------------------------------------------- 1 | var assign = require('lodash.assign') 2 | 3 | var Token // Token constructor from markdown-it 4 | 5 | var tokenUtil = module.exports = {} 6 | 7 | tokenUtil.set = function (TokenConstructor) { 8 | Token = TokenConstructor 9 | } 10 | 11 | // Look for tokens with type === 'emoji', which are :shortcode: style emoji 12 | // characters that have been replaced by the markdown-it-emoji plugin. Return the 13 | // original tokens, unless they were converted gemoji strings; then return a copy so 14 | // we haven't clobbered the original when it comes time to render HTML 15 | tokenUtil.unemoji = function (token) { 16 | if (token.type === 'emoji') { 17 | return assign(new Token(), token, {content: token.markup}) 18 | } 19 | return token 20 | } 21 | 22 | // return true if the token is type 'text' or contains any such tokens (except 23 | // for images; they can contain 'text' children because that's how `alt` 24 | // attributes get represented; those don't count toward the text representation) 25 | tokenUtil.isText = function isText (token) { 26 | var containsTextTokens = false 27 | if (token.children && token.children.length) { 28 | containsTextTokens = token.type !== 'image' && token.children.some(isText) 29 | } 30 | return containsTextTokens || token.type === 'text' 31 | } 32 | 33 | // extract the text from the given 'inline' token 34 | tokenUtil.getText = function getText (token) { 35 | var text = token.type === 'text' ? token.content : '' 36 | if (token.children && token.children.length) { 37 | text += token.children.reduce(function (previous, current) { 38 | return previous + (token.type !== 'image' ? getText(current) : '') 39 | }, '') 40 | } 41 | return text 42 | } 43 | -------------------------------------------------------------------------------- /marky.json: -------------------------------------------------------------------------------- 1 | {"version":"12.0.3","repositoryUrl":"https://github.com/npm/marky-markdown","issuesUrl":"https://github.com/npm/marky-markdown/issues"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@npmcorp/marky-markdown", 3 | "version": "12.0.3", 4 | "description": "npm's markdown parser", 5 | "main": "index.js", 6 | "scripts": { 7 | "prebuild": "node bin/build-marky-info.js", 8 | "build": "rm -rf dist && mkdir dist && touch dist/marky-markdown.js && browserify index.js -i highlights -s markyMarkdown > dist/marky-markdown.js", 9 | "test": "standard --fix && mocha --timeout 8000", 10 | "pretest": "npm run build", 11 | "prepublish": "npm run build", 12 | "release": "standard-version --commit-all" 13 | }, 14 | "standard-version": { 15 | "scripts": { 16 | "postbump": "npm run build", 17 | "precommit": "git add marky.json" 18 | } 19 | }, 20 | "browser": { 21 | "highlights": false 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/npm/marky-markdown" 26 | }, 27 | "keywords": [ 28 | "readme", 29 | "gfm", 30 | "github", 31 | "formatted", 32 | "markdown", 33 | "md", 34 | "documentation", 35 | "syntax highlighting", 36 | "html", 37 | "npm" 38 | ], 39 | "author": "Ashley Williams (http://ashleygwilliams.github.io/)", 40 | "license": "ISC", 41 | "bugs": { 42 | "url": "https://github.com/npm/marky-markdown/issues" 43 | }, 44 | "homepage": "https://github.com/npm/marky-markdown", 45 | "dependencies": { 46 | "atom-language-diff": "^1.0.0", 47 | "atom-language-nginx": "^0.6.1", 48 | "github-slugger": "^1.2.0", 49 | "github-url-to-object": "^4.0.0", 50 | "highlights": "^3.2.0-candidate.1", 51 | "highlights-tokens": "^1.0.1", 52 | "innertext": "^1.0.2", 53 | "is-plain-obj": "^1.1.0", 54 | "language-dart": "^0.1.1", 55 | "language-erlang": "^2.0.0", 56 | "language-glsl": "^2.0.1", 57 | "language-haxe": "^0.2.1", 58 | "language-ini": "^1.7.0", 59 | "language-rust": "^0.4.7", 60 | "language-stylus": "^0.5.2", 61 | "lodash.assign": "^4.0.2", 62 | "lodash.defaults": "^4.0.1", 63 | "lodash.pickby": "^4.2.1", 64 | "markdown-it": "^8.4.0", 65 | "markdown-it-emoji": "^1.3.0", 66 | "markdown-it-expand-tabs": "^1.0.12", 67 | "markdown-it-lazy-headers": "^0.1.3", 68 | "markdown-it-task-lists": "^2.0.1", 69 | "property-ttl": "^1.0.0", 70 | "sanitize-html": "^1.17.0", 71 | "similarity": "^1.0.1" 72 | }, 73 | "devDependencies": { 74 | "browserify": "^14.3.0", 75 | "cheerio": "^0.22.0", 76 | "glob": "^7.1.1", 77 | "intercept-stdout": "^0.1.2", 78 | "mocha": "^3.5.3", 79 | "oniguruma": "^7.0.0", 80 | "standard": "^10.0.0", 81 | "standard-version": "^4.1.0" 82 | }, 83 | "bin": "./bin/marky-markdown.js", 84 | "standard": { 85 | "ignore": "dist" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/cdn.js: -------------------------------------------------------------------------------- 1 | /* var assert = require('assert') 2 | var marky = require('..') 3 | var fixtures = require('./fixtures') 4 | var cheerio = require('cheerio') 5 | 6 | describe('cdn', function () { 7 | /* describe('handles missing or empty package data', function () { 8 | var controlHtml 9 | 10 | before(function () { 11 | // generate a processed version with no CDN remapping, to use as a control 12 | controlHtml = marky(fixtures.basic) 13 | }) 14 | 15 | it('skips CDN remap when lacking package data', function () { 16 | var options = { 17 | // leave package undefined 18 | serveImagesWithCDN: true 19 | } 20 | var html = marky(fixtures.basic, options) 21 | assert.equal(html, controlHtml) 22 | }) 23 | 24 | it('skips CDN remap when the package lacks a name', function () { 25 | var options = { 26 | package: {version: '1.0.0'}, 27 | serveImagesWithCDN: true 28 | } 29 | var html = marky(fixtures.basic, options) 30 | assert.equal(html, controlHtml) 31 | }) 32 | 33 | it('skips CDN remap when the package lacks a version', function () { 34 | var options = { 35 | package: {name: 'foo'}, 36 | serveImagesWithCDN: true 37 | } 38 | var html = marky(fixtures.basic, options) 39 | assert.equal(html, controlHtml) 40 | }) 41 | }) 42 | 43 | describe('when serveImagesWithCDN is true', function () { 44 | var $ 45 | var options = { 46 | package: {name: 'foo', version: '1.0.0'}, 47 | serveImagesWithCDN: true 48 | } 49 | 50 | before(function () { 51 | $ = cheerio.load(marky(fixtures.basic, options)) 52 | }) 53 | 54 | it('replaces relative img URLs with npm CDN URLs', function () { 55 | assert(~fixtures.basic.indexOf('![](relative.png)')) 56 | assert($("img[src='https://cdn.npm.im/foo@1.0.0/relative.png']").length) 57 | }) 58 | 59 | it('replaces slashy relative img URLs with npm CDN URLs', function () { 60 | assert(~fixtures.basic.indexOf('![](/slashy/deep.png)')) 61 | assert($("img[src='https://cdn.npm.im/foo@1.0.0/slashy/deep.png']").length) 62 | }) 63 | 64 | it('leaves protocol relative URLs alone', function () { 65 | assert(~fixtures.basic.indexOf('![](//protocollie.com/woof.png)')) 66 | assert($("img[src='//protocollie.com/woof.png']").length) 67 | }) 68 | 69 | it('leaves HTTPS URLs alone', function () { 70 | assert(~fixtures.basic.indexOf('![](https://secure.com/good.png)')) 71 | assert($("img[src='https://secure.com/good.png']").length) 72 | }) 73 | }) 74 | 75 | describe('when serveImagesWithCDN is false (default)', function () { 76 | var $ 77 | var options = { 78 | package: { 79 | name: 'foo', 80 | version: '1.0.0' 81 | } 82 | } 83 | 84 | before(function () { 85 | $ = cheerio.load(marky(fixtures.basic, options)) 86 | }) 87 | 88 | it('leaves relative img alone', function () { 89 | assert(~fixtures.basic.indexOf('![](relative.png)')) 90 | assert($("img[src='relative.png']").length) 91 | }) 92 | 93 | it('leaves slashy relative img URLs alone', function () { 94 | assert(~fixtures.basic.indexOf('![](/slashy/deep.png)')) 95 | assert($("img[src='/slashy/deep.png']").length) 96 | }) 97 | 98 | it('leaves protocol relative URLs alone', function () { 99 | assert(~fixtures.basic.indexOf('![](//protocollie.com/woof.png)')) 100 | assert($("img[src='//protocollie.com/woof.png']").length) 101 | }) 102 | 103 | it('leaves HTTPS URLs alone', function () { 104 | assert(~fixtures.basic.indexOf('![](https://secure.com/good.png)')) 105 | assert($("img[src='https://secure.com/good.png']").length) 106 | }) 107 | }) 108 | }) 109 | */ 110 | -------------------------------------------------------------------------------- /test/emoji.js: -------------------------------------------------------------------------------- 1 | /* globals describe, it */ 2 | 3 | var assert = require('assert') 4 | var marky = require('..') 5 | var fixtures = require('./fixtures') 6 | var cheerio = require('cheerio') 7 | 8 | describe('emoji', function () { 9 | it('replaces markdown syntax for emoji with unicode for the emoji', function () { 10 | assert(~fixtures.github.indexOf(':sparkles:')) 11 | var $ = cheerio.load(marky(fixtures.github)) 12 | assert($.html().indexOf('✨')) 13 | }) 14 | 15 | describe('in headings', function () { 16 | it('parsed the emoji-in-headings test', function () { 17 | assert(~fixtures.emojiheadings.indexOf('# Hello world!')) 18 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 19 | assert.equal($('h1 a#hello-world').length, 1) 20 | }) 21 | 22 | it('single gemoji (no underscore)', function () { 23 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 24 | assert.equal($('h2 a#ok-no-underscore').length, 1) 25 | }) 26 | 27 | it('single gemoji (with underscore)', function () { 28 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 29 | assert.equal($('h2 a#ok_hand-single').length, 1) 30 | }) 31 | 32 | it('two sequential gemojis', function () { 33 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 34 | assert.equal($('h2 a#ok_handhatched_chick-two-in-a-row-with-no-spaces').length, 1) 35 | }) 36 | 37 | it('two space-delimited gemojis', function () { 38 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 39 | assert.equal($('h2 a#ok_hand-hatched_chick-two-in-a-row').length, 1) 40 | }) 41 | 42 | it('single unicode emoji', function () { 43 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 44 | assert.equal($('h2 a#-unicode-emoji').length, 1) 45 | }) 46 | 47 | it('two hyphen-separated unicode emoji', function () { 48 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 49 | assert.equal($('h2 a#--unicode-hyphen-unicode').length, 1) 50 | }) 51 | 52 | it('two underscore-separated unicode emoji', function () { 53 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 54 | assert.equal($('h2 a#_-unicode-underscore-unicode').length, 1) 55 | }) 56 | 57 | it('single unicode emoji plus spaces and a hyphen', function () { 58 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 59 | assert.equal($('h2 a#---an-emoji').length, 1) 60 | }) 61 | 62 | it('single gemoji plus spaces and a hyphen', function () { 63 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 64 | assert.equal($('h2 a#smile---a-gemoji').length, 1) 65 | }) 66 | 67 | it('single emoji plus markdown', function () { 68 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 69 | assert.equal($('h2 a#---an-emoji-and-other-markdown').length, 1) 70 | }) 71 | 72 | it('single gemoji plus markdown', function () { 73 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 74 | assert.equal($('h2 a#---an-emoji-and-other-markdown').length, 1) 75 | }) 76 | 77 | it('single emoji plus HTML', function () { 78 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 79 | assert.equal($('h2 a#---an-emoji-and-html').length, 1) 80 | }) 81 | 82 | it('single gemoji plus HTML', function () { 83 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 84 | assert.equal($('h2 a#smile---a-gemoji-and-html').length, 1) 85 | }) 86 | 87 | it('invalid gemoji', function () { 88 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 89 | assert.equal($('h2 a#invalid_emoji_name-an-invalid-emoji-name').length, 1) 90 | }) 91 | 92 | it('emoji plus colon', function () { 93 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 94 | assert.equal($('h2 a#emoji-with-a-colon-').length, 1) 95 | }) 96 | 97 | it('gemoji plus colon', function () { 98 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 99 | assert.equal($('h2 a#gemoji-with-a-colon-ok_hand').length, 1) 100 | }) 101 | 102 | it('both unicode emoji and gemoji', function () { 103 | var $ = cheerio.load(marky(fixtures.emojiheadings, {prefixHeadingIds: false})) 104 | assert.equal($('h2 a#both--emoji-and-ok_hand-gemoji').length, 1) 105 | }) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /test/fixtures.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var glob = require('glob') 4 | var pkg = require('../package.json') 5 | 6 | var fixtures = { 7 | dependencies: [], // our dependencies and devDependencies 8 | examples: [] // examples manually saved as fixtures 9 | } 10 | 11 | // Read in all the hand-written fixture files 12 | fs.readdirSync(path.join(__dirname, 'fixtures')).forEach(function (file) { 13 | var key = path.basename(file).replace('.md', '') 14 | if (key !== path.basename(file)) { // skip anything lacking a .md extension 15 | fixtures[key] = fs.readFileSync(path.join(__dirname, 'fixtures', file)).toString() 16 | } 17 | }) 18 | 19 | // Read in dependencies' and devDependencies' readmes 20 | fixtures.dependencies = Object.keys(pkg.devDependencies) 21 | .concat(Object.keys(pkg.dependencies)) 22 | .sort() 23 | fixtures.dependencies.forEach(function (name) { 24 | var modulePath = path.resolve('node_modules', name) 25 | 26 | // Find README.md, readme.md, README, readme.markdown, etc 27 | var readmeFilename = glob.sync('readme*', { 28 | nocase: true, 29 | cwd: modulePath 30 | })[0] 31 | 32 | if (readmeFilename) { 33 | var readme = fs.readFileSync(path.resolve(modulePath, readmeFilename), 'utf-8') 34 | fixtures[name] = readme 35 | } 36 | }) 37 | 38 | // filter out any packages that didn't have a readme 39 | fixtures.dependencies = fixtures.dependencies.filter(function (name) { 40 | return !!fixtures[name] 41 | }) 42 | 43 | // Read in all the sample readmes saved as fixtures 44 | fs.readdirSync(path.join(__dirname, 'fixtures', 'readmes')).forEach(function (file) { 45 | var key = path.basename(file).replace('.md', '') 46 | if (key !== path.basename(file)) { // skip anything lacking a .md extension 47 | fixtures[key] = fs.readFileSync(path.join(__dirname, 'fixtures', 'readmes', file)).toString() 48 | fixtures.examples.push(key) 49 | } 50 | }) 51 | fixtures.examples.sort() 52 | 53 | module.exports = fixtures 54 | -------------------------------------------------------------------------------- /test/fixtures/basic.md: -------------------------------------------------------------------------------- 1 | # hello world 2 | 3 | paragraph 4 | 5 | ## example 6 | 7 | ```js 8 | var express = require('express') 9 | var app = express() 10 | 11 | app.get('/', function (req, res) { 12 | res.send('Hello World') 13 | }) 14 | 15 | app.get('/indenting', function (req, res) { 16 | res.send('Hello World') 17 | res.send('Hello Someone Else') 18 | if (foo) { 19 | // doubly indented 20 | } 21 | }) 22 | 23 | app.listen(3000) 24 | ``` 25 | 26 | ```sh 27 | echo hi 28 | ``` 29 | 30 | ```bash 31 | echo hi 32 | ``` 33 | 34 | ```coffeescript 35 | alert "hi" 36 | ``` 37 | 38 | ```diff 39 | -removed 40 | +added 41 | ``` 42 | 43 | ```jsx 44 | class Thinger extends React.Component { 45 | constructor(props) { 46 | super(props) 47 | } 48 | render() { 49 | return ( 50 |
{this.whatever}
51 | ) 52 | } 53 | } 54 | ``` 55 | 56 | ```ts 57 | interface Person { 58 | firstName: string; 59 | lastName: string; 60 | } 61 | 62 | function greeter(person: Person) { 63 | return "Hello, " + person.firstName + " " + person.lastName; 64 | } 65 | ``` 66 | 67 | ```typescript 68 | class Student { 69 | fullName: string; 70 | constructor(public firstName, public middleInitial, public lastName) { 71 | this.fullName = firstName + " " + middleInitial + " " + lastName; 72 | } 73 | } 74 | ``` 75 | 76 | Mustache {{template}} variable {{do.not.replace}} 77 | 78 | ```js 79 | // shouldn't replace these 80 | console.log(faker.fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}')); 81 | ``` 82 | 83 | ### images 84 | 85 | ![](relative.png) 86 | ![](/slashy/deep.png) 87 | ![](//protocollie.com/woof.png) 88 | ![](http://insecure.com/bad.png) 89 | ![](https://secure.com/good.png) 90 | 91 | 92 | 93 | #### h4, anyone 94 | 95 | [youtube video link](https://www.youtube.com/watch?v=dQw4w9WgXcQ) 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |