diff --git a/lib/configs/commonmark.js b/lib/configs/commonmark.js index e22ab98..0263d1b 100644 --- a/lib/configs/commonmark.js +++ b/lib/configs/commonmark.js @@ -10,7 +10,23 @@ module.exports = { breaks: false, // Convert '\n' in paragraphs into
langPrefix: 'language-', // CSS language prefix for fenced blocks linkify: false, // autoconvert URL-like texts to links - typographer: false, // Enable smartypants and other sweet transforms + + // Enable some language-neutral replacement + quotes beautification + // + // (c) (C) → © + // (tm) (TM) → ™ + // (r) (R) → ® + // +- → ± + // (p) (P) → § + // ... → … + // ???????? → ???, !!!!! → !!!, `,,` → `,` + // -- → — + // + typographer: false, + + // Double + single quotes replacement pairs, when typographer enabled, + // and smartquotes on. Set doubles to '«»' for Russian, '„“' for German. + quotes: '“”‘’', // Highlighter function. Should return escaped HTML, // or '' if input not changed @@ -21,6 +37,8 @@ module.exports = { components: { + core: {}, + block: { rules: [ 'blockquote', @@ -47,21 +65,6 @@ module.exports = { 'newline', 'text' ] - }, - - typographer: { - options: { - singleQuotes: '‘’', // set empty to disable - doubleQuotes: '“”', // set '«»' for Russian, '„“' for German, empty to disable - copyright: true, // (c) (C) → © - trademark: true, // (tm) (TM) → ™ - registered: true, // (r) (R) → ® - plusminus: true, // +- → ± - paragraph: true, // (p) (P) → § - ellipsis: true, // ... → … - dupes: true, // ???????? → ???, !!!!! → !!!, `,,` → `,` - dashes: true // -- → — - } } } }; diff --git a/lib/configs/default.js b/lib/configs/default.js index 4328640..76a29c4 100644 --- a/lib/configs/default.js +++ b/lib/configs/default.js @@ -10,7 +10,23 @@ module.exports = { breaks: false, // Convert '\n' in paragraphs into
langPrefix: 'language-', // CSS language prefix for fenced blocks linkify: false, // autoconvert URL-like texts to links - typographer: false, // Enable smartypants and other sweet transforms + + // Enable some language-neutral replacement + quotes beautification + // + // (c) (C) → © + // (tm) (TM) → ™ + // (r) (R) → ® + // +- → ± + // (p) (P) → § + // ... → … + // ???????? → ???, !!!!! → !!!, `,,` → `,` + // -- → — + // + typographer: false, + + // Double + single quotes replacement pairs, when typographer enabled, + // and smartquotes on. Set doubles to '«»' for Russian, '„“' for German. + quotes: '“”‘’', // Highlighter function. Should return escaped HTML, // or '' if input not changed @@ -21,6 +37,8 @@ module.exports = { components: { + core: {}, + block: { rules: [ 'blockquote', @@ -49,21 +67,6 @@ module.exports = { 'newline', 'text' ] - }, - - typographer: { - options: { - singleQuotes: '‘’', // set empty to disable - doubleQuotes: '“”', // set '«»' for Russian, '„“' for German, empty to disable - copyright: true, // (c) (C) → © - trademark: true, // (tm) (TM) → ™ - registered: true, // (r) (R) → ® - plusminus: true, // +- → ± - paragraph: true, // (p) (P) → § - ellipsis: true, // ... → … - dupes: true, // ???????? → ???, !!!!! → !!!, `,,` → `,` - dashes: true // -- → — - } } } }; diff --git a/lib/configs/full.js b/lib/configs/full.js index 90522a6..601348e 100644 --- a/lib/configs/full.js +++ b/lib/configs/full.js @@ -10,7 +10,23 @@ module.exports = { breaks: false, // Convert '\n' in paragraphs into
langPrefix: 'language-', // CSS language prefix for fenced blocks linkify: false, // autoconvert URL-like texts to links - typographer: false, // Enable smartypants and other sweet transforms + + // Enable some language-neutral replacement + quotes beautification + // + // (c) (C) → © + // (tm) (TM) → ™ + // (r) (R) → ® + // +- → ± + // (p) (P) → § + // ... → … + // ???????? → ???, !!!!! → !!!, `,,` → `,` + // -- → — + // + typographer: false, + + // Double + single quotes replacement pairs, when typographer enabled, + // and smartquotes on. Set doubles to '«»' for Russian, '„“' for German. + quotes: '“”‘’', // Highlighter function. Should return escaped HTML, // or '' if input not changed @@ -21,23 +37,9 @@ module.exports = { components: { - // Don't restrict block/inline rules + // Don't restrict core/block/inline rules + core: {}, block: {}, - inline: {}, - - typographer: { - options: { - singleQuotes: '‘’', // set empty to disable - doubleQuotes: '“”', // set '«»' for Russian, '„“' for German, empty to disable - copyright: true, // (c) (C) → © - trademark: true, // (tm) (TM) → ™ - registered: true, // (r) (R) → ® - plusminus: true, // +- → ± - paragraph: true, // (p) (P) → § - ellipsis: true, // ... → … - dupes: true, // ???????? → ???, !!!!! → !!!, `,,` → `,` - dashes: true // -- → — - } - } + inline: {} } }; diff --git a/lib/index.js b/lib/index.js index acb8bf7..b3114de 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,9 +6,9 @@ var assign = require('./common/utils').assign; var isString = require('./common/utils').isString; var Renderer = require('./renderer'); +var ParserCore = require('./parser_core'); var ParserBlock = require('./parser_block'); var ParserInline = require('./parser_inline'); -var Typographer = require('./typographer'); var Ruler = require('./ruler'); var config = { @@ -17,14 +17,6 @@ var config = { commonmark: require('./configs/commonmark') }; -var _rules = [ - [ 'block', require('./rules_core/block') ], - [ 'references', require('./rules_core/references') ], - [ 'inline', require('./rules_core/inline') ], - [ 'linkify', require('./rules_core/linkify') ], - [ 'typographer', require('./rules_core/typographer') ] -]; - function StateCore(self, src, env) { this.src = src; @@ -50,17 +42,14 @@ function Remarkable(presetName, options) { this.inline = new ParserInline(); this.block = new ParserBlock(); + this.core = new ParserCore(); this.renderer = new Renderer(); this.ruler = new Ruler(); - this.typographer = new Typographer(); this.options = {}; this.configure(config[presetName]); - if (options) { this.set(options); } - for (var i = 0; i < _rules.length; i++) { - this.ruler.push(_rules[i][0], _rules[i][1]); - } + if (options) { this.set(options); } } @@ -112,7 +101,7 @@ Remarkable.prototype.use = function (plugin, opts) { // Remarkable.prototype.parse = function (src, env) { var i, len, - rules = this.ruler.getRules(''), + rules = this.core.ruler.getRules(''), state = new StateCore(this, src, env); len = rules.length; diff --git a/lib/parser_core.js b/lib/parser_core.js new file mode 100644 index 0000000..7f69b98 --- /dev/null +++ b/lib/parser_core.js @@ -0,0 +1,41 @@ +// Class of top level (`core`) rules +// +'use strict'; + + +var Ruler = require('./ruler'); + + +var _rules = [ + [ 'block', require('./rules_core/block') ], + [ 'references', require('./rules_core/references') ], + [ 'inline', require('./rules_core/inline') ], + [ 'replacements', require('./rules_core/replacements') ], + [ 'smartquotes', require('./rules_core/smartquotes') ], + [ 'linkify', require('./rules_core/linkify') ] +]; + + +function Core() { + this.options = {}; + + this.ruler = new Ruler(); + + for (var i = 0; i < _rules.length; i++) { + this.ruler.push(_rules[i][0], _rules[i][1]); + } +} + + +Core.prototype.process = function (state) { + var i, l, rules; + + rules = this.ruler.getRules(''); + + for (i = 0, l = rules.length; i < l; i++) { + rules[i](state); + } +}; + + +module.exports = Core; diff --git a/lib/rules_core/replacements.js b/lib/rules_core/replacements.js new file mode 100644 index 0000000..e2a6568 --- /dev/null +++ b/lib/rules_core/replacements.js @@ -0,0 +1,52 @@ +// Simple typographyc replacements +// +'use strict'; + +// TODO: +// - fractionals 1/2, 1/4, 3/4 -> ½, ¼, ¾ +// - miltiplication 2 x 4 -> 2 × 4 + +var COPY_RE = /\((c|tm|r|p)\)/i; +var RARE_RE = /\+-|\.\.|\?\?\?\?|!!!!|,,|--/; + +module.exports = function replace(state) { + var i, token, text, inlineTokens, blkIdx; + + if (!state.options.typographer) { return; } + + for (blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) { + + if (state.tokens[blkIdx].type !== 'inline') { continue; } + + inlineTokens = state.tokens[blkIdx].children; + + for (i = inlineTokens.length - 1; i >= 0; i--) { + token = inlineTokens[i]; + if (token.type === 'text') { + text = token.content; + + if (COPY_RE.test(text)) { + text = text.replace(/\(c\)/gi, '©') + .replace(/\(tm\)/gi, '™') + .replace(/\(r\)/gi, '®') + .replace(/\(p\)/gi, '§'); + } + + if (RARE_RE.test(text)) { + text = text.replace(/\+-/g, '±') + // .., ..., ....... -> … + // but ?..... & !..... -> ?.. & !.. + .replace(/\.{2,}/g, '…').replace(/([?!])…/g, '$1..') + .replace(/([?!]){4,}/g, '$1$1$1').replace(/,{2,}/g, ',') + // em-dash + .replace(/(^|[^-])---([^-]|$)/mg, '$1\u2014$2') + // en-dash + .replace(/(^|\s)--(\s|$)/mg, '$1\u2013$2') + .replace(/(^|[^-\s])--([^-\s]|$)/mg, '$1\u2013$2'); + } + + token.content = text; + } + } + } +}; diff --git a/lib/rules_text/smartquotes.js b/lib/rules_core/smartquotes.js similarity index 85% rename from lib/rules_text/smartquotes.js rename to lib/rules_core/smartquotes.js index fe495ec..12a2e93 100644 --- a/lib/rules_text/smartquotes.js +++ b/lib/rules_core/smartquotes.js @@ -24,11 +24,12 @@ function replaceAt(str, index, ch) { module.exports = function smartquotes(state) { /*eslint max-depth:0*/ var i, token, text, t, pos, max, thisLevel, lastSpace, nextSpace, item, - canOpen, canClose, j, isSingle, chars, blkIdx, tokens, - typographer = state.typographer, - options = typographer.options, - stack = []; + canOpen, canClose, j, isSingle, blkIdx, tokens, + stack; + if (!state.options.typographer) { return; } + + stack = []; for (blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) { @@ -83,10 +84,12 @@ module.exports = function smartquotes(state) { if (stack[j].level < thisLevel) { break; } if (item.single === isSingle && stack[j].level === thisLevel) { item = stack[j]; - chars = isSingle ? options.singleQuotes : options.doubleQuotes; - if (chars) { - tokens[item.token].content = replaceAt(tokens[item.token].content, item.pos, chars[0]); - token.content = replaceAt(token.content, t.index, chars[1]); + if (isSingle) { + tokens[item.token].content = replaceAt(tokens[item.token].content, item.pos, state.options.quotes[2]); + token.content = replaceAt(token.content, t.index, state.options.quotes[3]); + } else { + tokens[item.token].content = replaceAt(tokens[item.token].content, item.pos, state.options.quotes[0]); + token.content = replaceAt(token.content, t.index, state.options.quotes[1]); } stack.length = j; continue OUTER; diff --git a/lib/rules_text/replace.js b/lib/rules_text/replace.js deleted file mode 100644 index 9d76b61..0000000 --- a/lib/rules_text/replace.js +++ /dev/null @@ -1,66 +0,0 @@ -// Simple typographyc replacements -// -'use strict'; - - -var COPY_RE = /\((c|tm|r|p)\)/i; -var RARE_RE = /\+-|\.\.|\?\?\?\?|!!!!|,,|--/; - -module.exports = function replace(state) { - var i, token, text, inlineTokens, blkIdx, - typographer = state.typographer, - options = typographer.options; - - for (blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) { - - if (state.tokens[blkIdx].type !== 'inline') { continue; } - - inlineTokens = state.tokens[blkIdx].children; - - for (i = inlineTokens.length - 1; i >= 0; i--) { - token = inlineTokens[i]; - if (token.type === 'text') { - text = token.content; - - if (COPY_RE.test(text)) { - if (options.copyright) { - text = text.replace(/\(c\)/gi, '©'); - } - if (options.trademark) { - text = text.replace(/\(tm\)/gi, '™'); - } - if (options.registered) { - text = text.replace(/\(r\)/gi, '®'); - } - if (options.paragraph) { - text = text.replace(/\(p\)/gi, '§'); - } - } - - if (RARE_RE.test(text)) { - if (options.plusminus) { - text = text.replace(/\+-/g, '±'); - } - if (options.ellipsis) { - // .., ..., ....... -> … - // but ?..... & !..... -> ?.. & !.. - text = text.replace(/\.{2,}/g, '…').replace(/([?!])…/g, '$1..'); - } - if (options.dupes) { - text = text.replace(/([?!]){4,}/g, '$1$1$1').replace(/,{2,}/g, ','); - } - if (options.dashes) { - text = text - // em-dash - .replace(/(^|[^-])---([^-]|$)/mg, '$1\u2014$2') - // en-dash - .replace(/(^|\s)--(\s|$)/mg, '$1\u2013$2') - .replace(/(^|[^-\s])--([^-\s]|$)/mg, '$1\u2013$2'); - } - } - - token.content = text; - } - } - } -};