diff --git a/lib/core/__tests__/__fixtures__/getTOC.md b/lib/core/__tests__/__fixtures__/getTOC.md new file mode 100644 index 000000000000..f56735c17147 --- /dev/null +++ b/lib/core/__tests__/__fixtures__/getTOC.md @@ -0,0 +1,20 @@ +## foo +### foo +### foo 1 +## foo 1 +## foo 2 +### foo +#### 4th level headings +All 4th level headings should not be shown by default + +## bar +### bar +#### bar +4th level heading should be ignored by default, but is should be always taken +into account, when generating slugs +### `bar` +#### `bar` +## bar +### bar +#### bar +## bar diff --git a/lib/core/__tests__/__snapshots__/anchors.tests.js.snap b/lib/core/__tests__/__snapshots__/anchors.tests.js.snap new file mode 100644 index 000000000000..70920fd1b15f --- /dev/null +++ b/lib/core/__tests__/__snapshots__/anchors.tests.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Anchors rendering 1`] = `"
bar
",
+ "hashLink": "bar-3",
+ "rawContent": "\`bar\`",
+ },
+ Object {
+ "children": Array [],
+ "content": "bar
",
+ "hashLink": "bar-4",
+ "rawContent": "\`bar\`",
+ },
+ ],
+ "content": "bar",
+ "hashLink": "bar",
+ "rawContent": "bar",
+ },
+ Object {
+ "children": Array [
+ Object {
+ "children": Array [],
+ "content": "bar",
+ "hashLink": "bar-6",
+ "rawContent": "bar",
+ },
+ Object {
+ "children": Array [],
+ "content": "bar",
+ "hashLink": "bar-7",
+ "rawContent": "bar",
+ },
+ ],
+ "content": "bar",
+ "hashLink": "bar-5",
+ "rawContent": "bar",
+ },
+ Object {
+ "children": Array [],
+ "content": "bar",
+ "hashLink": "bar-8",
+ "rawContent": "bar",
+ },
+]
+`;
+
+exports[`with defaults 1`] = `
+Array [
+ Object {
+ "children": Array [
+ Object {
+ "children": Array [],
+ "content": "foo",
+ "hashLink": "foo-1",
+ "rawContent": "foo",
+ },
+ Object {
+ "children": Array [],
+ "content": "foo 1",
+ "hashLink": "foo-1-1",
+ "rawContent": "foo 1",
+ },
+ ],
+ "content": "foo",
+ "hashLink": "foo",
+ "rawContent": "foo",
+ },
+ Object {
+ "children": Array [],
+ "content": "foo 1",
+ "hashLink": "foo-1-2",
+ "rawContent": "foo 1",
+ },
+ Object {
+ "children": Array [
+ Object {
+ "children": Array [],
+ "content": "foo",
+ "hashLink": "foo-3",
+ "rawContent": "foo",
+ },
+ ],
+ "content": "foo 2",
+ "hashLink": "foo-2",
+ "rawContent": "foo 2",
+ },
+ Object {
+ "children": Array [
+ Object {
+ "children": Array [],
+ "content": "bar",
+ "hashLink": "bar-1",
+ "rawContent": "bar",
+ },
+ Object {
+ "children": Array [],
+ "content": "bar
",
+ "hashLink": "bar-3",
+ "rawContent": "\`bar\`",
+ },
+ ],
+ "content": "bar",
+ "hashLink": "bar",
+ "rawContent": "bar",
+ },
+ Object {
+ "children": Array [
+ Object {
+ "children": Array [],
+ "content": "bar",
+ "hashLink": "bar-6",
+ "rawContent": "bar",
+ },
+ ],
+ "content": "bar",
+ "hashLink": "bar-5",
+ "rawContent": "bar",
+ },
+ Object {
+ "children": Array [],
+ "content": "bar",
+ "hashLink": "bar-8",
+ "rawContent": "bar",
+ },
+]
+`;
diff --git a/lib/core/__tests__/anchors.tests.js b/lib/core/__tests__/anchors.tests.js
new file mode 100644
index 000000000000..c9530f265b60
--- /dev/null
+++ b/lib/core/__tests__/anchors.tests.js
@@ -0,0 +1,104 @@
+const anchors = require('../anchors');
+
+const md = {
+ renderer: {
+ rules: {},
+ },
+};
+
+anchors(md);
+
+const render = md.renderer.rules.heading_open;
+
+test('Anchors rendering', () => {
+ expect(
+ render([{hLevel: 1}, {content: 'Hello world'}], 0, {}, {})
+ ).toMatchSnapshot();
+ expect(
+ render([{hLevel: 2}, {content: 'Hello small world'}], 0, {}, {})
+ ).toMatchSnapshot();
+});
+
+test('Each anchor is unique across rendered document', () => {
+ const tokens = [
+ {hLevel: 1},
+ {content: 'Almost unique heading'},
+ {hLevel: 1},
+ {content: 'Almost unique heading'},
+ {hLevel: 1},
+ {content: 'Almost unique heading 1'},
+ {hLevel: 1},
+ {content: 'Almost unique heading 1'},
+ {hLevel: 1},
+ {content: 'Almost unique heading 2'},
+ {hLevel: 1},
+ {content: 'Almost unique heading'},
+ ];
+ const options = {};
+ const env = {};
+
+ expect(render(tokens, 0, options, env)).toContain(
+ 'id="almost-unique-heading"'
+ );
+ expect(render(tokens, 2, options, env)).toContain(
+ 'id="almost-unique-heading-1"'
+ );
+ expect(render(tokens, 4, options, env)).toContain(
+ 'id="almost-unique-heading-1-1"'
+ );
+ expect(render(tokens, 6, options, env)).toContain(
+ 'id="almost-unique-heading-1-2"'
+ );
+ expect(render(tokens, 8, options, env)).toContain(
+ 'id="almost-unique-heading-2"'
+ );
+ expect(render(tokens, 10, options, env)).toContain(
+ 'id="almost-unique-heading-3"'
+ );
+});
+
+test('Each anchor is unique across rendered document. Case 2', () => {
+ const tokens = [
+ {hLevel: 1},
+ {content: 'foo'},
+ {hLevel: 1},
+ {content: 'foo 1'},
+ {hLevel: 1},
+ {content: 'foo'},
+ {hLevel: 1},
+ {content: 'foo 1'},
+ ];
+ const options = {};
+ const env = {};
+
+ expect(render(tokens, 0, options, env)).toContain('id="foo"');
+ expect(render(tokens, 2, options, env)).toContain('id="foo-1"');
+ expect(render(tokens, 4, options, env)).toContain('id="foo-2"');
+ expect(render(tokens, 6, options, env)).toContain('id="foo-1-1"');
+});
+
+test('Anchor index resets on each render', () => {
+ const tokens = [
+ {hLevel: 1},
+ {content: 'Almost unique heading'},
+ {hLevel: 1},
+ {content: 'Almost unique heading'},
+ ];
+ const options = {};
+ const env = {};
+ const env2 = {};
+
+ expect(render(tokens, 0, options, env)).toContain(
+ 'id="almost-unique-heading"'
+ );
+ expect(render(tokens, 2, options, env)).toContain(
+ 'id="almost-unique-heading-1"'
+ );
+
+ expect(render(tokens, 0, options, env2)).toContain(
+ 'id="almost-unique-heading"'
+ );
+ expect(render(tokens, 2, options, env2)).toContain(
+ 'id="almost-unique-heading-1"'
+ );
+});
diff --git a/lib/core/__tests__/getTOC.tests.js b/lib/core/__tests__/getTOC.tests.js
new file mode 100644
index 000000000000..da0e62a005cc
--- /dev/null
+++ b/lib/core/__tests__/getTOC.tests.js
@@ -0,0 +1,26 @@
+const path = require('path');
+const readFileSync = require('fs').readFileSync;
+const getTOC = require('../getTOC');
+
+const mdContents = readFileSync(
+ path.join(__dirname, '__fixtures__', 'getTOC.md'),
+ 'utf-8'
+);
+
+test('with defaults', () => {
+ const headings = getTOC(mdContents);
+ const headingsJson = JSON.stringify(headings);
+
+ expect(headings).toMatchSnapshot();
+ expect(headingsJson).toContain('bar-8'); // maximum unique bar index is 8
+ expect(headingsJson).not.toContain('4th level headings');
+});
+
+test('with custom heading levels', () => {
+ const headings = getTOC(mdContents, 'h2', ['h3', 'h4']);
+ const headingsJson = JSON.stringify(headings);
+
+ expect(headings).toMatchSnapshot();
+ expect(headingsJson).toContain('bar-8'); // maximum unique bar index is 8
+ expect(headingsJson).toContain('4th level headings');
+});
diff --git a/lib/core/__tests__/toSlug.tests.js b/lib/core/__tests__/toSlug.tests.js
index a21b726732ea..9d472c547c71 100644
--- a/lib/core/__tests__/toSlug.tests.js
+++ b/lib/core/__tests__/toSlug.tests.js
@@ -12,3 +12,18 @@ const toSlug = require('../toSlug');
expect(toSlug(input)).toBe(output);
});
});
+
+test('unique slugs if `context` argument passed', () => {
+ [
+ ['foo', 'foo'],
+ ['foo', 'foo-1'],
+ ['foo 1', 'foo-1-1'],
+ ['foo 1', 'foo-1-2'],
+ ['foo 2', 'foo-2'],
+ ['foo', 'foo-3'],
+ ].reduce((context, [input, output]) => {
+ expect(toSlug(input, context)).toBe(output);
+
+ return context;
+ }, {});
+});
diff --git a/lib/core/anchors.js b/lib/core/anchors.js
new file mode 100644
index 000000000000..f13745cc42fe
--- /dev/null
+++ b/lib/core/anchors.js
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2017-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+const toSlug = require('./toSlug.js');
+
+/**
+ * The anchors plugin adds GFM-style anchors to headings.
+ */
+function anchors(md) {
+ md.renderer.rules.heading_open = function(tokens, idx, options, env) {
+ const textToken = tokens[idx + 1];
+ const anchor = toSlug(textToken.content, env);
+
+ return (
+ '