Skip to content

Commit

Permalink
✨ Add callout parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
leonardfactory committed Mar 11, 2019
1 parent b35043b commit c2cd108
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 10 deletions.
3 changes: 2 additions & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = api => {
}
],
'@babel/preset-typescript'
]
],
plugins: ['add-module-exports']
};
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@
"@types/jest": "^24.0.11",
"@types/remarkable": "^1.7.2",
"babel-jest": "^24.3.1",
"babel-plugin-add-module-exports": "^1.0.0",
"conventional-changelog-cli": "^2.0.12",
"dedent": "^0.7.0",
"husky": "^1.3.1",
"jest": "^24.3.1",
"prettier": "^1.16.4",
"release-it": "^10.3.1",
"remarkable": "^1.7.1",
"typescript": "^3.3.3333"
Expand Down
25 changes: 23 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
export const callout = (md: any) => {
return null;
import Remarkable from 'remarkable';
import { TOKENS } from './tokens';
import { parser } from './parser';
import { calloutOpenRenderer, calloutCloseRenderer } from './renderer';

/**
* Remarkable plugin that recognizes callout syntax in markdown and renders
* it in a dedicated paragraph.
*
* Example syntax:
*
* :::info
* This is an information callout
* :::
*
* @todo Add opts to customize rendering.
*/
const plugin: Remarkable.Plugin = (md, opts) => {
md.block.ruler.before('code', TOKENS.CALLOUT, parser, opts);
md.renderer.rules[TOKENS.CALLOUT_OPEN] = calloutOpenRenderer;
md.renderer.rules[TOKENS.CALLOUT_CLOSE] = calloutCloseRenderer;
};

export default plugin;
96 changes: 92 additions & 4 deletions src/parser.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,110 @@
import Remarkable from 'remarkable';
import { parser } from './parser';
import plugin from '.';
import dedent from 'dedent';

let md!: Remarkable;

describe('remarkable-callout', () => {
describe('parser', () => {
beforeEach(() => {
md = new Remarkable();
});

test('should render a callout', () => {
md.use(parser);
md.use(plugin);
expect(
md.render(dedent`
:::info
Info
:::
`)
).toEqual(`<p class="callout-info">Info</p>`);
).toMatchInlineSnapshot(`
"<p class=\\"callout callout-info\\"><p>Info
:::</p>
</p>"
`);
});

test('should ignore content after callout', () => {
md.use(plugin);
expect(
md.render(dedent`
:::info
Info
:::
Normal paragraph
`)
).toMatchInlineSnapshot(`
"<p class=\\"callout callout-info\\"><p>Info
:::</p>
</p><p>Normal paragraph</p>
"
`);
});

test('should ignore content before the callout', () => {
md.use(plugin);
expect(
md.render(dedent`
Normal paragraph
:::info
Info
:::
`)
).toMatchInlineSnapshot(`
"<p>Normal paragraph</p>
<p class=\\"callout callout-info\\"><p>Info
:::</p>
</p>"
`);
});

test('should allow nested blocks', () => {
md.use(plugin);
expect(
md.render(dedent`
:::caution
*Alert!*
~~~
this is my code block
~~~
:::
`)
).toMatchInlineSnapshot(`
"<p class=\\"callout callout-caution\\"><p><em>Alert!</em></p>
<pre><code>this is my code block
</code></pre>
</p>"
`);
});

test('should close tag on EOF', () => {
md.use(plugin);
expect(
md.render(dedent`
:::caution
*Alert!*
`)
).toMatchInlineSnapshot(`
"<p class=\\"callout callout-caution\\"><p><em>Alert!</em></p>
</p>"
`);
});

test('should ignore text starting with ":" if not closing tag', () => {
md.use(plugin);
expect(
md.render(dedent`
:::caution
Message
:art:
`)
).toMatchInlineSnapshot(`
"<p class=\\"callout callout-caution\\"><p>Message
:art:</p>
</p>"
`);
});
});
59 changes: 57 additions & 2 deletions src/parser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,60 @@
import Remarkable from 'remarkable';
import { TOKENS } from './tokens';

export const parser: Remarkable.Plugin = (md, opts) => {
// hello
/**
* Remarkable block parser that recognizes callouts.
* @todo Add options.
*/
export const parser: Remarkable.BlockParsingRule = (
state,
startLine,
endLine,
silent
) => {
const pos = state.bMarks[startLine] + state.blkIndent;
const max = state.eMarks[startLine];

// Not enough chars or ending line with `:::`.
if (pos + 3 >= max) return false;

const marker = state.src.charCodeAt(pos);

// Wrong marker
if (marker !== 0x3a /* ':' */) return false;

const calloutType = state.src.slice(pos + 3, max).trim();

// Scan for marker ending
let nextLine = startLine;
let hasEnding = false;

while (nextLine < endLine) {
nextLine++;

const nextPos = state.bMarks[nextLine] + state.tShift[nextLine];
const nextMax = state.eMarks[nextLine];

if (state.src.charCodeAt(nextPos) !== marker) continue;

const nextLineText = state.src.slice(nextPos, nextMax).trim();
if (nextLineText === ':::') {
hasEnding = true;
break;
}
}

// Let register token and progress
state.tokens.push({
type: TOKENS.CALLOUT_OPEN,
level: state.level++,
lines: [startLine, nextLine + (hasEnding ? 1 : 0)],
calloutType
} as any);
state.parser.tokenize(state, startLine + 1, nextLine);
state.tokens.push({
type: TOKENS.CALLOUT_CLOSE,
level: state.level--
} as any);
state.line = nextLine + (hasEnding ? 1 : 0);
return true;
};
26 changes: 26 additions & 0 deletions src/renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Remarkable from 'remarkable';

/**
* Remarkable callout renderer.
*/
export const calloutOpenRenderer: Remarkable.Rule = (
tokens,
idx,
options,
env
) => {
const token = tokens[idx] as any;
return `<p class="callout callout-${token.calloutType}">`;
};

/**
* Callout closing tag renderer
*/
export const calloutCloseRenderer: Remarkable.Rule = (
tokens,
idx,
options,
env
) => {
return `</p>`;
};
5 changes: 5 additions & 0 deletions src/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const TOKENS = {
CALLOUT: 'callout',
CALLOUT_OPEN: 'callout_open',
CALLOUT_CLOSE: 'callout_close'
};
14 changes: 13 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,13 @@ babel-jest@^24.3.1:
chalk "^2.4.2"
slash "^2.0.0"

babel-plugin-add-module-exports@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-1.0.0.tgz#72b5424d941a336c6a35357f373d8b8366263031"
integrity sha512-m0sMxPL4FaN2K69GQgaRJa4Ny15qKSdoknIcpN+gz+NaJlAW9pge/povs13tPYsKDboflrEQC+/3kfIsONBTaw==
optionalDependencies:
chokidar "^2.0.4"

babel-plugin-istanbul@^5.1.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.1.tgz#7981590f1956d75d67630ba46f0c22493588c893"
Expand Down Expand Up @@ -1480,7 +1487,7 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==

chokidar@^2.0.3:
chokidar@^2.0.3, chokidar@^2.0.4:
version "2.1.2"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058"
integrity sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==
Expand Down Expand Up @@ -4741,6 +4748,11 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=

prettier@^1.16.4:
version "1.16.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717"
integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==

pretty-format@^24.3.1:
version "24.3.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.3.1.tgz#ae4a98e93d73d86913a8a7dd1a7c3c900f8fda59"
Expand Down

0 comments on commit c2cd108

Please sign in to comment.