Skip to content
This repository was archived by the owner on Apr 3, 2024. It is now read-only.
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,14 @@ The default options are as follows:

```js
{
sanitize: true, // remove script tags and stuff
linkify: true, // turn orphan URLs into hyperlinks
highlightSyntax: true, // run highlights on fenced code blocks
prefixHeadingIds: true, // prevent DOM id collisions
serveImagesWithCDN: false, // use npm's CDN to proxy images over HTTPS
debug: false, // console.log() all the things
package: null // npm package metadata
sanitize: true, // remove script tags and stuff
linkify: true, // turn orphan URLs into hyperlinks
highlightSyntax: true, // run highlights on fenced code blocks
prefixHeadingIds: true, // prevent DOM id collisions
enableHeadingLinkIcons: true, // render icons inside generated section links
serveImagesWithCDN: false, // use npm's CDN to proxy images over HTTPS
debug: false, // console.log() all the things
package: null // npm package metadata
}
```

Expand Down
160 changes: 87 additions & 73 deletions dist/marky-markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var marky = module.exports = function (markdown, options) {
linkify: true,
highlightSyntax: true,
prefixHeadingIds: true,
enableHeadingLinkIcons: true,
serveImagesWithCDN: false,
debug: false,
package: null
Expand Down Expand Up @@ -317,13 +318,8 @@ var cheerio = require('cheerio')
var GithubSlugger = require('github-slugger')
var tokenUtil = require('./token-util')

function isMarkdownLink (token) {
return token.type === 'link_open'
}

function isHtmlLink (token) {
return token.type === 'html_inline' && token.content.substring(0, 3).toLowerCase() === '<a '
}
// shamelessly borrowed from GitHub, thanks y'all
var svgLinkIconText = '<svg aria-hidden="true" class="deep-link-icon" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'

var headings = module.exports = function (md, options) {
if (options && !options.prefixHeadingIds) {
Expand All @@ -332,42 +328,49 @@ var headings = module.exports = function (md, options) {
headings.prefix = headings.prefix || headings.defaultPrefix
}

var savedTokenConstructor = false
var slugger = new GithubSlugger()
var Token

md.core.ruler.push('githubHeadings', function (state) {
// save the Token constructor because we'll be building a few instances at render
// time; that's sort of outside the intended markdown-it parsing sequence, but
// since we have tight control over what we're creating (a link), we're safe
if (!savedTokenConstructor) {
tokenUtil.set(state.Token)
savedTokenConstructor = true
if (!Token) {
Token = state.Token
tokenUtil.set(Token)
}
})

md.renderer.rules.heading_open = function (tokens, idx, opts, env, self) {
// Bail if heading already contains a hyperlink
var children = tokens[idx + 1].children
// make sure heading is not empty
if (children && children.length) {
if (children.filter(isMarkdownLink).length === 0) {
// Generate an ID based on the heading's innerHTML; first, render without
// converting gemoji strings to unicode emoji characters
var rendered = md.renderer.renderInline(children.map(tokenUtil.unemoji), opts, env)
var postfix = slugger.slug(
cheerio.load(rendered).root().text()
.replace(/[<>]/g, '') // In case the heading contains `<stuff>`
.toLowerCase() // because `slug` doesn't lowercase
)

// add some heading attributes
tokens[idx].attrSet('id', headings.prefix + postfix)
tokens[idx].attrJoin('class', headings.className)
// Generate an ID based on the heading's innerHTML; first, render without
// converting gemoji strings to unicode emoji characters
var rendered = md.renderer.renderInline(children.map(tokenUtil.unemoji), opts, env)
var postfix = slugger.slug(
cheerio.load(rendered).root().text()
.replace(/[<>]/g, '') // In case the heading contains `<stuff>`
.toLowerCase() // because `slug` doesn't lowercase
)

// if the heading doesn't already contain a link, wrap the contents in one
if (children.filter(isHtmlLink).length === 0) {
tokenUtil.wrap(children, postfix)
}
// add 3 new token objects link_open, text, link_close
var linkOpen = new Token('link_open', 'a', 1)
var text = new Token('html_inline', '', 0)
if (options && options.enableHeadingLinkIcons) {
text.content = svgLinkIconText
}
var linkClose = new Token('link_close', 'a', -1)

// add some link attributes
linkOpen.attrSet('id', headings.prefix + postfix)
linkOpen.attrSet('class', headings.className)
linkOpen.attrSet('href', '#' + postfix)

// add new token objects as children of heading
children.unshift(linkClose)
children.unshift(text)
children.unshift(linkOpen)
}

return md.renderer.renderToken(tokens, idx, options, env, self)
Expand Down Expand Up @@ -533,7 +536,7 @@ var emoji = require('markdown-it-emoji')
var codeWrap = require('./code-wrap')
var expandTabs = require('markdown-it-expand-tabs')
var githubTaskList = require('markdown-it-task-lists')
var githubHeadings = require('./headings')
var headingLinks = require('./heading-links')
var githubLinkify = require('./linkify')
var relaxedLinkRefs = require('./gfm/relaxed-link-reference')

Expand Down Expand Up @@ -585,7 +588,7 @@ module.exports = function (html, options) {
.use(emoji, {shortcuts: {}})
.use(expandTabs, {tabWidth: 4})
.use(githubTaskList)
.use(githubHeadings, options)
.use(headingLinks, options)
.use(relaxedLinkRefs)

if (options.highlightSyntax) parser.use(codeWrap)
Expand Down Expand Up @@ -621,17 +624,18 @@ function scopeNameFromLang (highlighter, lang) {
}

}).call(this,require('_process'))
},{"./cleanup":4,"./code-wrap":5,"./gfm/relaxed-link-reference":6,"./headings":9,"./linkify":11,"_process":175,"highlights":21,"lodash.pickby":99,"markdown-it":113,"markdown-it-emoji":104,"markdown-it-expand-tabs":110,"markdown-it-lazy-headers":111,"markdown-it-task-lists":112}],14:[function(require,module,exports){
},{"./cleanup":4,"./code-wrap":5,"./gfm/relaxed-link-reference":6,"./heading-links":9,"./linkify":11,"_process":175,"highlights":21,"lodash.pickby":99,"markdown-it":113,"markdown-it-emoji":104,"markdown-it-expand-tabs":110,"markdown-it-lazy-headers":111,"markdown-it-task-lists":112}],14:[function(require,module,exports){
var sanitizeHtml = require('sanitize-html')
var sanitizer = module.exports = function (html) {
return sanitizeHtml(html, sanitizer.config)
}

sanitizer.config = {
allowedTags: sanitizeHtml.defaults.allowedTags.concat([
'dd', 'del', 'div', 'dl', 'dt', 'h1', 'h2', 'iframe', 'img', 'input', 'ins', 'meta', 'pre', 's', 'span', 'sub', 'sup'
'dd', 'del', 'div', 'dl', 'dt', 'h1', 'h2', 'iframe', 'img', 'input', 'ins', 'meta', 'path', 'pre', 's', 'span', 'sub', 'sup', 'svg'
]),
allowedClasses: {
a: ['deep-link'],
div: [
'highlight',
'hljs',
Expand Down Expand Up @@ -663,6 +667,7 @@ sanitizer.config = {
ol: ['task-list'],
pre: ['editor', 'editor-colors'],
span: require('./highlights-tokens'),
svg: ['deep-link-icon'],
ul: ['task-list']
},
allowedAttributes: {
Expand All @@ -680,10 +685,12 @@ sanitizer.config = {
div: ['id'],
span: [],
pre: [],
td: ['colspan', 'rowspan'],
th: ['colspan', 'rowspan'],
td: ['colspan', 'rowspan', 'style'],
th: ['colspan', 'rowspan', 'style'],
del: ['cite', 'datetime'],
ins: ['cite', 'datetime']
ins: ['cite', 'datetime'],
path: ['d'],
svg: ['aria-hidden', 'height', 'version', 'viewBox', 'width']
},
exclusiveFilter: function (frame) {
// Allow Task List items
Expand All @@ -697,9 +704,37 @@ sanitizer.config = {
// Allow YouTube iframes
if (frame.tag !== 'iframe') return false
return !String(frame.attribs.src).match(/^(https?:)?\/\/(www\.)?youtube\.com/)
},
transformTags: {
'td': sanitizeCellStyle,
'th': sanitizeCellStyle
}
}

// Allow table cell alignment
function sanitizeCellStyle (tagName, attribs) {
// if we don't add the 'style' to the allowedAttributes above, it will be
// stripped out by the time we get here, so we have to filter out
// everything but `text-align` in case something else tries to sneak in
function cell (alignment) {
var attributes = attribs
if (alignment) {
attributes.style = 'text-align:' + alignment
} else {
delete attributes.style
}
return {
tagName: tagName,
attribs: attributes
}
}

// look for CSS `text-align` directives
var alignmentRegEx = /text-align\s*:\s*(left|center|right)[\s;$]*/igm
var result = alignmentRegEx.exec(attribs.style || '')
return result ? cell(result[1]) : cell()
}

},{"./highlights-tokens":10,"sanitize-html":182}],15:[function(require,module,exports){
var assign = require('lodash.assign')

Expand All @@ -722,18 +757,6 @@ tokenUtil.unemoji = function (token) {
return token
}

// wrap the the array of tokens in a link with the given slug as its destination
// the intent is to operate on the .children array of a token
tokenUtil.wrap = function (tokens, slug) {
var linkOpen = new Token('link_open', 'a', 1)
var linkClose = new Token('link_close', 'a', -1)

linkOpen.attrs = [['href', '#' + slug]]

tokens.unshift(linkOpen)
tokens.push(linkClose)
}

},{"lodash.assign":94}],16:[function(require,module,exports){
module.exports = function ($) {
// Wrap YouTube videos in an element so they're easier to style
Expand Down Expand Up @@ -4754,16 +4777,8 @@ exports.isHtml = function(str) {
module.exports={
"_args": [
[
{
"raw": "cheerio@^0.20.0",
"scope": null,
"escapedName": "cheerio",
"name": "cheerio",
"rawSpec": "^0.20.0",
"spec": ">=0.20.0 <0.21.0",
"type": "range"
},
"/Users/zeke/zeke/marky-markdown"
"cheerio@^0.20.0",
"/Users/revinguillen/Projects/marky-markdown"
]
],
"_from": "cheerio@>=0.20.0 <0.21.0",
Expand All @@ -4773,17 +4788,16 @@ module.exports={
"_location": "/cheerio",
"_nodeVersion": "5.5.0",
"_npmUser": {
"name": "feedic",
"email": "me@feedic.com"
"email": "me@feedic.com",
"name": "feedic"
},
"_npmVersion": "3.6.0",
"_phantomChildren": {},
"_requested": {
"raw": "cheerio@^0.20.0",
"scope": null,
"escapedName": "cheerio",
"name": "cheerio",
"raw": "cheerio@^0.20.0",
"rawSpec": "^0.20.0",
"scope": null,
"spec": ">=0.20.0 <0.21.0",
"type": "range"
},
Expand All @@ -4794,10 +4808,10 @@ module.exports={
"_shasum": "5c710f2bab95653272842ba01c6ea61b3545ec35",
"_shrinkwrap": null,
"_spec": "cheerio@^0.20.0",
"_where": "/Users/zeke/zeke/marky-markdown",
"_where": "/Users/revinguillen/Projects/marky-markdown",
"author": {
"name": "Matt Mueller",
"email": "mattmuelle@gmail.com",
"name": "Matt Mueller",
"url": "mat.io"
},
"bugs": {
Expand Down Expand Up @@ -4847,20 +4861,20 @@ module.exports={
"main": "./index.js",
"maintainers": [
{
"name": "mattmueller",
"email": "mattmuelle@gmail.com"
"email": "mattmuelle@gmail.com",
"name": "mattmueller"
},
{
"name": "davidchambers",
"email": "dc@davidchambers.me"
"email": "dc@davidchambers.me",
"name": "davidchambers"
},
{
"name": "jugglinmike",
"email": "mike@mikepennisi.com"
"email": "mike@mikepennisi.com",
"name": "jugglinmike"
},
{
"name": "feedic",
"email": "me@feedic.com"
"email": "me@feedic.com",
"name": "feedic"
}
],
"name": "cheerio",
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var marky = module.exports = function (markdown, options) {
linkify: true,
highlightSyntax: true,
prefixHeadingIds: true,
enableHeadingLinkIcons: true,
serveImagesWithCDN: false,
debug: false,
package: null
Expand Down
65 changes: 65 additions & 0 deletions lib/heading-links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
var cheerio = require('cheerio')
var GithubSlugger = require('github-slugger')
var tokenUtil = require('./token-util')

// shamelessly borrowed from GitHub, thanks y'all
var svgLinkIconText = '<svg aria-hidden="true" class="deep-link-icon" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>'

var headings = module.exports = function (md, options) {
if (options && !options.prefixHeadingIds) {
headings.prefix = ''
} else {
headings.prefix = headings.prefix || headings.defaultPrefix
}

var slugger = new GithubSlugger()
var Token

md.core.ruler.push('headingLinks', function (state) {
// save the Token constructor because we'll be building a few instances at render
// time; that's sort of outside the intended markdown-it parsing sequence, but
// since we have tight control over what we're creating (a link), we're safe
if (!Token) {
Token = state.Token
tokenUtil.set(Token)
}
})

md.renderer.rules.heading_open = function (tokens, idx, opts, env, self) {
var children = tokens[idx + 1].children
// make sure heading is not empty
if (children && children.length) {
// Generate an ID based on the heading's innerHTML; first, render without
// converting gemoji strings to unicode emoji characters
var rendered = md.renderer.renderInline(children.map(tokenUtil.unemoji), opts, env)
var postfix = slugger.slug(
cheerio.load(rendered).root().text()
.replace(/[<>]/g, '') // In case the heading contains `<stuff>`
.toLowerCase() // because `slug` doesn't lowercase
)

// add 3 new token objects link_open, text, link_close
var linkOpen = new Token('link_open', 'a', 1)
var text = new Token('html_inline', '', 0)
if (options && options.enableHeadingLinkIcons) {
text.content = svgLinkIconText
}
var linkClose = new Token('link_close', 'a', -1)

// add some link attributes
linkOpen.attrSet('id', headings.prefix + postfix)
linkOpen.attrSet('class', headings.className)
linkOpen.attrSet('href', '#' + postfix)

// add new token objects as children of heading
children.unshift(linkClose)
children.unshift(text)
children.unshift(linkOpen)
}

return md.renderer.renderToken(tokens, idx, options, env, self)
}
}

headings.defaultPrefix = 'user-content-'
headings.className = 'deep-link'
Loading