Skip to content

Commit

Permalink
Merge pull request #161 from curbengh/quotes
Browse files Browse the repository at this point in the history
feat: 'quotes' option to override smartypants
  • Loading branch information
curbengh authored Sep 2, 2020
2 parents 3b85cf0 + 1230d1d commit 2711b20
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 11 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ marked:
breaks: true
smartLists: true
smartypants: true
quotes: '“”‘’'
modifyAnchors: 0
autolink: true
sanitizeUrl: false
Expand All @@ -45,6 +46,11 @@ marked:
- **breaks** - Enable GFM [line breaks](https://help.github.com/articles/github-flavored-markdown#newlines). This option requires the `gfm` option to be true.
- **smartLists** - Use smarter list behavior than the original markdown.
- **smartypants** - Use "smart" typograhic punctuation for things like quotes and dashes.
- **quotes** - Defines the double and single quotes used for substituting regular quotes if **smartypants** is enabled.
* Example: '«»“”'
* "double" will be turned into «single»
* 'single' will be turned into “single”
* Both double and single quotes substitution must be specified, otherwise it will be silently ignored.
- **modifyAnchors** - Transform the anchorIds into lower case (`1`) or upper case (`2`).
- **autolink** - Enable autolink for URLs. E.g. `https://hexo.io` will become `<a href="https://hexo.io">https://hexo.io</a>`.
- **sanitizeUrl** - Remove URLs that start with `javascript:`, `vbscript:` and `data:`.
Expand All @@ -59,7 +65,7 @@ marked:
- **postAsset** - Resolve post asset's image path to relative path and prepend root value when [`post_asset_folder`](https://hexo.io/docs/asset-folders) is enabled.
* "image.jpg" is located at "/2020/01/02/foo/image.jpg", which is a post asset of "/2020/01/02/foo/".
* `![](image.jpg)` becomes `<img src="/2020/01/02/foo/image.jpg">`
* Requires `prependRoot:` to be enabled.
* Requires **prependRoot** to be enabled.
- **external_link**
* **enable** - Open external links in a new tab.
* **exclude** - Exclude hostname. Specify subdomain when applicable, including `www`.
Expand Down
45 changes: 45 additions & 0 deletions lib/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,29 @@ marked.setOptions({
langPrefix: ''
});

// https://github.com/markedjs/marked/blob/b6773fca412c339e0cedd56b63f9fa1583cfd372/src/Lexer.js#L8-L24
const smartypants = (str, quotes) => {
const [openDbl, closeDbl, openSgl, closeSgl] = typeof quotes === 'string' && quotes.length === 4
? quotes
: ['\u201c', '\u201d', '\u2018', '\u2019'];

return str
// em-dashes
.replace(/---/g, '\u2014')
// en-dashes
.replace(/--/g, '\u2013')
// opening singles
.replace(/(^|[-\u2014/([{"\s])'/g, '$1' + openSgl)
// closing singles & apostrophes
.replace(/'/g, closeSgl)
// opening doubles
.replace(/(^|[-\u2014/([{\u2018\s])"/g, '$1' + openDbl)
// closing doubles
.replace(/"/g, closeDbl)
// ellipses
.replace(/\.{3}/g, '\u2026');
};

class Tokenizer extends MarkedTokenizer {
// Support AutoLink option
// https://github.com/markedjs/marked/blob/b6773fca412c339e0cedd56b63f9fa1583cfd372/src/Tokenizer.js#L606-L641
Expand Down Expand Up @@ -169,6 +192,28 @@ class Tokenizer extends MarkedTokenizer {
};
}
}

// Override smartypants
inlineText(src, inRawBlock) {
const { options, rules } = this;
const { quotes, smartypants: isSmarty } = options;

// https://github.com/markedjs/marked/blob/b6773fca412c339e0cedd56b63f9fa1583cfd372/src/Tokenizer.js#L643-L658
const cap = rules.inline.text.exec(src);
if (cap) {
let text;
if (inRawBlock) {
text = cap[0];
} else {
text = escape(isSmarty ? smartypants(cap[0], quotes) : cap[0]);
}
return {
type: 'text',
raw: cap[0],
text
};
}
}
}

module.exports = function(data, options) {
Expand Down
117 changes: 107 additions & 10 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,17 +160,114 @@ describe('Marked renderer', () => {
result.should.eql(`<p><a href="${url}">foo</a></p>\n`);
});

describe('autolink option tests', () => {
const hexo = new Hexo(__dirname, {silent: true});
const ctx = Object.assign(hexo, {
config: {
marked: {
autolink: true
}
}
describe('quotes', () => {
beforeEach(() => {
hexo.config.marked.smartypants = true;
});

const r = require('../lib/renderer').bind(ctx);
it('default', () => {
const body = '"foo" \'bar\'';
const quotes = '«»“”';
hexo.config.marked.quotes = quotes;

const result = r({text: body});

result.should.eql('<p>«foo» “bar”</p>\n');
});

it('invalid option', () => {
const body = '"foo" \'bar\'';
const quotes = '«»';
hexo.config.marked.quotes = quotes;

const result = r({text: body});

result.should.eql('<p>“foo” ‘bar’</p>\n');
});

it('smartypants disabled', () => {
const body = '"foo" \'bar\'';
const quotes = '«»“”';
hexo.config.marked = { quotes, smartypants: false };

const result = r({text: body});

result.should.eql(`<p>${escapeHTML(body)}</p>\n`);
});

it('should render other markdown syntax - quotes disabled', () => {
const body = '"[\'foo\'](bar)"\n["foo"](bar)\n ## "foo"\n!["joe"](bar)\n"foo---bar"';
const result = r({text: body});

const expected = [
'<p>',
'“<a href="bar">‘foo’</a>“\n',
'<a href="bar">“foo”</a></p>\n',
'<h2 id="“foo”"><a href="#“foo”" class="headerlink" title="“foo”"></a>“foo”</h2>',
'<p>',
'<img src="bar" alt="&quot;joe&quot;">\n',
'“foo—bar”',
'</p>\n'
].join('');

result.should.eql(expected);
});

it('should render other markdown syntax', () => {
const body = '"[\'foo\'](bar)"\n["foo"](bar)\n ## "foo"\n!["joe"](bar)\n"foo---bar"';
const quotes = '«»“”';
hexo.config.marked.quotes = quotes;
const result = r({text: body});

const expected = [
'<p>',
'«<a href="bar">“foo”</a>«\n',
'<a href="bar">«foo»</a></p>\n',
'<h2 id="«foo»"><a href="#«foo»" class="headerlink" title="«foo»"></a>«foo»</h2>',
'<p>',
'<img src="bar" alt="&quot;joe&quot;">\n',
'«foo—bar»',
'</p>\n'
].join('');

result.should.eql(expected);
});

it('inRawBlock - quotes disabled', () => {
const body = '<kbd class="foo">ctrl</kbd>"bar"';
const result = r({text: body});

result.should.eql('<p><kbd class="foo">ctrl</kbd>“bar”</p>\n');
});

it('inRawBlock - quotes disabled + wrapped', () => {
const body = '"<kbd class="foo">ctrl</kbd>"';
const result = r({text: body});

result.should.eql('<p>“<kbd class="foo">ctrl</kbd>“</p>\n');
});

it('inRawBlock ', () => {
const quotes = '«»“”';
hexo.config.marked.quotes = quotes;
const body = '<kbd class="foo">ctrl</kbd>"bar"';
const result = r({text: body});

result.should.eql('<p><kbd class="foo">ctrl</kbd>«bar»</p>\n');
});

it('inRawBlock - wrapped', () => {
const quotes = '«»“”';
hexo.config.marked.quotes = quotes;
const body = '"<kbd class="foo">ctrl</kbd>"';
const result = r({text: body});

result.should.eql('<p>«<kbd class="foo">ctrl</kbd>«</p>\n');
});
});

describe('autolink option tests', () => {
beforeEach(() => { hexo.config.marked.autolink = true; });

const body = [
'Great website http://hexo.io',
Expand All @@ -197,7 +294,7 @@ describe('Marked renderer', () => {
});

it('autolink disabled', () => {
ctx.config.marked.autolink = false;
hexo.config.marked.autolink = false;
const result = r({text: body});

result.should.eql([
Expand Down

0 comments on commit 2711b20

Please # to comment.