chore: import upstream snapshot with attribution
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`getSection > should match the hints snapshot 1`] = `
|
||||
[
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 11,
|
||||
"line": 19,
|
||||
"offset": 142,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 19,
|
||||
"offset": 132,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "First hint",
|
||||
},
|
||||
],
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 11,
|
||||
"line": 19,
|
||||
"offset": 142,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 19,
|
||||
"offset": 132,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"lang": "js",
|
||||
"meta": null,
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 4,
|
||||
"line": 23,
|
||||
"offset": 166,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 21,
|
||||
"offset": 144,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "// test code",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 18,
|
||||
"line": 25,
|
||||
"offset": 185,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 25,
|
||||
"offset": 168,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Second hint with ",
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 24,
|
||||
"line": 25,
|
||||
"offset": 191,
|
||||
},
|
||||
"start": {
|
||||
"column": 18,
|
||||
"line": 25,
|
||||
"offset": 185,
|
||||
},
|
||||
},
|
||||
"type": "html",
|
||||
"value": "<code>",
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 28,
|
||||
"line": 25,
|
||||
"offset": 195,
|
||||
},
|
||||
"start": {
|
||||
"column": 24,
|
||||
"line": 25,
|
||||
"offset": 191,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "code",
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 35,
|
||||
"line": 25,
|
||||
"offset": 202,
|
||||
},
|
||||
"start": {
|
||||
"column": 28,
|
||||
"line": 25,
|
||||
"offset": 195,
|
||||
},
|
||||
},
|
||||
"type": "html",
|
||||
"value": "</code>",
|
||||
},
|
||||
],
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 35,
|
||||
"line": 25,
|
||||
"offset": 202,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 25,
|
||||
"offset": 168,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"lang": "js",
|
||||
"meta": null,
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 4,
|
||||
"line": 29,
|
||||
"offset": 231,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 27,
|
||||
"offset": 204,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "// more test code",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 7,
|
||||
"line": 31,
|
||||
"offset": 239,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 31,
|
||||
"offset": 233,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Third ",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 12,
|
||||
"line": 31,
|
||||
"offset": 244,
|
||||
},
|
||||
"start": {
|
||||
"column": 8,
|
||||
"line": 31,
|
||||
"offset": 240,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "hint",
|
||||
},
|
||||
],
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 13,
|
||||
"line": 31,
|
||||
"offset": 245,
|
||||
},
|
||||
"start": {
|
||||
"column": 7,
|
||||
"line": 31,
|
||||
"offset": 239,
|
||||
},
|
||||
},
|
||||
"type": "emphasis",
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 19,
|
||||
"line": 31,
|
||||
"offset": 251,
|
||||
},
|
||||
"start": {
|
||||
"column": 13,
|
||||
"line": 31,
|
||||
"offset": 245,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": " with ",
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 25,
|
||||
"line": 31,
|
||||
"offset": 257,
|
||||
},
|
||||
"start": {
|
||||
"column": 19,
|
||||
"line": 31,
|
||||
"offset": 251,
|
||||
},
|
||||
},
|
||||
"type": "html",
|
||||
"value": "<code>",
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 29,
|
||||
"line": 31,
|
||||
"offset": 261,
|
||||
},
|
||||
"start": {
|
||||
"column": 25,
|
||||
"line": 31,
|
||||
"offset": 257,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "code",
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 36,
|
||||
"line": 31,
|
||||
"offset": 268,
|
||||
},
|
||||
"start": {
|
||||
"column": 29,
|
||||
"line": 31,
|
||||
"offset": 261,
|
||||
},
|
||||
},
|
||||
"type": "html",
|
||||
"value": "</code>",
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 41,
|
||||
"line": 31,
|
||||
"offset": 273,
|
||||
},
|
||||
"start": {
|
||||
"column": 36,
|
||||
"line": 31,
|
||||
"offset": 268,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": " and ",
|
||||
},
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 54,
|
||||
"line": 31,
|
||||
"offset": 286,
|
||||
},
|
||||
"start": {
|
||||
"column": 41,
|
||||
"line": 31,
|
||||
"offset": 273,
|
||||
},
|
||||
},
|
||||
"type": "inlineCode",
|
||||
"value": "inline code",
|
||||
},
|
||||
],
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 54,
|
||||
"line": 31,
|
||||
"offset": 286,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 31,
|
||||
"offset": 233,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"lang": "js",
|
||||
"meta": null,
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 4,
|
||||
"line": 38,
|
||||
"offset": 353,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 33,
|
||||
"offset": 288,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "// more test code
|
||||
if(let x of xs) {
|
||||
console.log(x);
|
||||
}",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getSection > should match the instructions snapshot 1`] = `
|
||||
[
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 12,
|
||||
"line": 11,
|
||||
"offset": 89,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 11,
|
||||
"offset": 78,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Paragraph 0",
|
||||
},
|
||||
],
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 12,
|
||||
"line": 11,
|
||||
"offset": 89,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 11,
|
||||
"offset": 78,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"lang": "html",
|
||||
"meta": null,
|
||||
"position": {
|
||||
"end": {
|
||||
"column": 4,
|
||||
"line": 15,
|
||||
"offset": 117,
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 13,
|
||||
"offset": 91,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "code example 0",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,21 @@
|
||||
const find = require('unist-util-find');
|
||||
const findAllBefore = require('unist-util-find-all-before');
|
||||
|
||||
function getAllBefore(tree, marker) {
|
||||
const start = find(tree, {
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: marker
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!start) return [];
|
||||
|
||||
// reverse because it goes up the tree backwards and adds nodes in that order
|
||||
return findAllBefore(tree, start).reverse();
|
||||
}
|
||||
|
||||
module.exports = getAllBefore;
|
||||
@@ -0,0 +1,32 @@
|
||||
import isArray from 'lodash/isArray';
|
||||
import { describe, beforeAll, it, expect } from 'vitest';
|
||||
|
||||
import parseFixture from '../../__fixtures__/parse-fixture';
|
||||
import getAllBefore from './before-heading';
|
||||
|
||||
describe('before-headings', () => {
|
||||
let simpleAst;
|
||||
|
||||
beforeAll(async () => {
|
||||
simpleAst = await parseFixture('simple.md');
|
||||
});
|
||||
|
||||
it('should return an array', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getAllBefore(simpleAst, '--hints--');
|
||||
expect(isArray(actual)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return an empty array if the marker is not present', () => {
|
||||
expect.assertions(2);
|
||||
const actual = getAllBefore(simpleAst, '--not-a-marker--');
|
||||
expect(isArray(actual)).toBe(true);
|
||||
expect(actual.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should include the whole AST before the marker', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getAllBefore(simpleAst, '--hints--');
|
||||
expect(actual).toHaveLength(6);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
const visit = require('unist-util-visit');
|
||||
const _ = require('lodash');
|
||||
|
||||
/**
|
||||
* Finds all nodes in a tree that match a given condition. This is a trivial
|
||||
* extension of `unist-util-find` that returns all matching nodes.
|
||||
*
|
||||
* @param {Object} tree - The unist tree to search through.
|
||||
* @param {Function|Object} condition - The condition to match nodes
|
||||
* against. This can be a function that accepts a single node argument or an object to match.
|
||||
* @returns {Array} An array of nodes that match the condition.
|
||||
*/
|
||||
function findAll(tree, condition) {
|
||||
const predicate = _.iteratee(condition);
|
||||
const results = [];
|
||||
visit(tree, node => {
|
||||
if (predicate(node)) {
|
||||
results.push(node);
|
||||
}
|
||||
|
||||
return visit.CONTINUE;
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
module.exports.findAll = findAll;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { findAll } from './find-all';
|
||||
|
||||
const testTree = {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'heading',
|
||||
depth: 1,
|
||||
children: [{ type: 'text', value: 'test', testId: 1 }]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ type: 'text', value: 'different', testId: 2 }]
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
depth: 2,
|
||||
children: [{ type: 'text', value: 'test', testId: 3 }]
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
depth: 1,
|
||||
children: [{ type: 'text', value: 'test', testId: 4 }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return an array', () => {
|
||||
expect(findAll(testTree, () => false)).toEqual([]);
|
||||
});
|
||||
it('should return an array of nodes that match the test', () => {
|
||||
expect(findAll(testTree, { type: 'text', value: 'test' })).toEqual([
|
||||
{ type: 'text', value: 'test', testId: 1 },
|
||||
{ type: 'text', value: 'test', testId: 3 },
|
||||
{ type: 'text', value: 'test', testId: 4 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
const { isEmpty } = require('lodash');
|
||||
const is = require('unist-util-is');
|
||||
const position = require('unist-util-position');
|
||||
|
||||
const getId = require('./get-id');
|
||||
|
||||
const supportedLanguages = [
|
||||
'js',
|
||||
'css',
|
||||
'html',
|
||||
'jsx',
|
||||
'py',
|
||||
'ts',
|
||||
'tsx',
|
||||
'json'
|
||||
];
|
||||
const longToShortLanguages = {
|
||||
javascript: 'js',
|
||||
typescript: 'ts',
|
||||
python: 'py'
|
||||
};
|
||||
|
||||
function defaultFile(lang, id) {
|
||||
return {
|
||||
ext: lang,
|
||||
name: getFilenames(lang),
|
||||
contents: '',
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
function getFilenames(lang) {
|
||||
const langToFilename = {
|
||||
js: 'script',
|
||||
css: 'styles',
|
||||
py: 'main',
|
||||
json: 'tsconfig'
|
||||
};
|
||||
return langToFilename[lang] ?? 'index';
|
||||
}
|
||||
|
||||
function getFileVisitor(seeds, seedKey, validate) {
|
||||
return (node, index, parent) => {
|
||||
if (is(node, 'root')) return;
|
||||
if (is(node, 'code')) {
|
||||
codeToData(node, seeds, seedKey, validate);
|
||||
return;
|
||||
}
|
||||
idToData(node, index, parent, seeds);
|
||||
};
|
||||
}
|
||||
|
||||
function codeToData(node, seeds, seedKey, validate) {
|
||||
if (validate) validate(node);
|
||||
const lang = node.lang;
|
||||
const shortLang = longToShortLanguages[lang] ?? lang;
|
||||
if (!supportedLanguages.includes(shortLang))
|
||||
throw Error(`On line ${
|
||||
position.start(node).line
|
||||
} '${shortLang}' is not a supported language.
|
||||
Please use one of js, css, html, jsx, ts, tsx or py
|
||||
`);
|
||||
|
||||
const fileId = `index${shortLang}`;
|
||||
const id = seeds[fileId] ? seeds[fileId].id : '';
|
||||
// the contents will be missing if there is an id preceding this code
|
||||
// block.
|
||||
if (!seeds[fileId]) {
|
||||
seeds[fileId] = defaultFile(shortLang, id);
|
||||
}
|
||||
seeds[fileId][seedKey] = isEmpty(seeds[fileId][seedKey])
|
||||
? node.value
|
||||
: seeds[fileId][seedKey] + '\n' + node.value;
|
||||
}
|
||||
|
||||
function idToData(node, index, parent, seeds) {
|
||||
const id = getId(node);
|
||||
|
||||
// If this is reached, the node type is neither root nor code. If it is not
|
||||
// an id, there must be a syntax error.
|
||||
if (!id) {
|
||||
throw Error(
|
||||
'Unexpected syntax in seed/solution. Must be ::id{#id} or a code ' +
|
||||
'block (```) \n'
|
||||
);
|
||||
}
|
||||
const codeNode = parent.children[index + 1];
|
||||
if (codeNode && is(codeNode, 'code')) {
|
||||
const shortLang = longToShortLanguages[codeNode.lang] ?? codeNode.lang;
|
||||
const fileKey = `index${shortLang}`;
|
||||
if (seeds[fileKey]) throw Error('::id{#id}s must come before code blocks');
|
||||
seeds[fileKey] = defaultFile(shortLang, id);
|
||||
} else {
|
||||
throw Error('::id{#id}s must come before code blocks');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.getFileVisitor = getFileVisitor;
|
||||
module.exports.getFilenames = getFilenames;
|
||||
@@ -0,0 +1,8 @@
|
||||
// getId expects the image reference node to be the sole node in a paragraph
|
||||
function getId(node) {
|
||||
const { type, name, attributes } = node;
|
||||
if (type !== 'leafDirective' || name !== 'id' || !attributes) return null;
|
||||
return attributes.id;
|
||||
}
|
||||
|
||||
module.exports = getId;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, beforeAll, it, expect } from 'vitest';
|
||||
|
||||
import parseFixture from '../../__fixtures__/parse-fixture';
|
||||
import getId from './get-id';
|
||||
|
||||
describe('get-id', () => {
|
||||
let idNode, imageNode, multipleChildrenNode;
|
||||
beforeAll(async () => {
|
||||
const withIdNode = await parseFixture('with-id-node.md');
|
||||
idNode = withIdNode.children[0];
|
||||
imageNode = withIdNode.children[1];
|
||||
multipleChildrenNode = withIdNode.children[2];
|
||||
});
|
||||
it('should return a string', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getId(idNode);
|
||||
expect(typeof actual).toBe('string');
|
||||
});
|
||||
|
||||
it('should get the expected identifier', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getId(idNode);
|
||||
expect(actual).toBe('html-key');
|
||||
});
|
||||
|
||||
it('should return null if the node does contain an id', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getId(imageNode);
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
// TODO: bin this (and the json!) after development (it'll be a silly test
|
||||
// once we're using directives)
|
||||
it('should ignore image nodes', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getId(imageNode);
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
// TODO: bin this (and the json!) after development (it'll be a silly test
|
||||
// once we're using directives)
|
||||
|
||||
// TODO: do we want to fail silently? Might it be better to output warnings
|
||||
// or perhaps even stop the parser? Probably warnings if anything.
|
||||
it('should ignore paragraphs that contain more than the id element', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getId(multipleChildrenNode);
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
const mdastToHtml = require('./mdast-to-html');
|
||||
|
||||
function getParagraphContent(node) {
|
||||
return node.type === 'paragraph' ? mdastToHtml(node.children) : null;
|
||||
}
|
||||
|
||||
module.exports.getParagraphContent = getParagraphContent;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, beforeAll, it, expect } from 'vitest';
|
||||
|
||||
import parseFixture from '../../__fixtures__/parse-fixture';
|
||||
import { getParagraphContent } from './get-paragraph-content';
|
||||
|
||||
describe('getParagraphContent', () => {
|
||||
let simpleAST;
|
||||
|
||||
beforeAll(async () => {
|
||||
simpleAST = await parseFixture('simple.md');
|
||||
});
|
||||
|
||||
it('should return the content of a paragraph node', () => {
|
||||
const paragraphNode = simpleAST.children[1];
|
||||
expect(paragraphNode.type).toBe('paragraph');
|
||||
expect(getParagraphContent(paragraphNode)).toEqual('Paragraph 1');
|
||||
});
|
||||
|
||||
it('should return null if the node is not a paragraph', () => {
|
||||
const headingNode = simpleAST.children[0];
|
||||
expect(headingNode.type).toBe('heading');
|
||||
expect(getParagraphContent(headingNode)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
const find = require('unist-util-find');
|
||||
const findAfter = require('unist-util-find-after');
|
||||
const findAllAfter = require('unist-util-find-all-after');
|
||||
const between = require('unist-util-find-all-between');
|
||||
const { findAll } = require('./find-all');
|
||||
|
||||
const isMarker = node => {
|
||||
if (node.children && node.children[0]) {
|
||||
const child = node.children[0];
|
||||
return (
|
||||
child.type === 'text' &&
|
||||
child.value.startsWith('--') &&
|
||||
child.value.endsWith('--') &&
|
||||
node.type === 'heading'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
function _getSection(tree) {
|
||||
return start => {
|
||||
if (!start) return [];
|
||||
|
||||
const isEnd = node => {
|
||||
return node.depth <= start.depth && isMarker(node);
|
||||
};
|
||||
|
||||
const end = findAfter(tree, start, isEnd);
|
||||
|
||||
const targetNodes = end
|
||||
? between(tree, start, end)
|
||||
: findAllAfter(tree, start);
|
||||
return targetNodes;
|
||||
};
|
||||
}
|
||||
|
||||
const startNode = (marker, depth) => ({
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: marker
|
||||
}
|
||||
],
|
||||
...((typeof depth === 'number' && { depth }) || {})
|
||||
});
|
||||
|
||||
function getSection(tree, marker, depth) {
|
||||
const start = find(tree, startNode(marker, depth));
|
||||
return _getSection(tree)(start);
|
||||
}
|
||||
|
||||
function getAllSections(tree, marker) {
|
||||
const starts = findAll(tree, startNode(marker));
|
||||
return starts.map(_getSection(tree));
|
||||
}
|
||||
|
||||
module.exports = { getSection, getAllSections, isMarker };
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, beforeAll, it, expect } from 'vitest';
|
||||
import isArray from 'lodash/isArray';
|
||||
import { root } from 'mdast-builder';
|
||||
import find from 'unist-util-find';
|
||||
|
||||
import parseFixture from '../../__fixtures__/parse-fixture';
|
||||
import { getSection } from './get-section';
|
||||
|
||||
describe('getSection', () => {
|
||||
let simpleAst, extraHeadingAst;
|
||||
|
||||
beforeAll(async () => {
|
||||
extraHeadingAst = await parseFixture('with-extra-heading.md');
|
||||
simpleAst = await parseFixture('simple.md');
|
||||
});
|
||||
|
||||
it('should return an array', () => {
|
||||
const actual = getSection(simpleAst, '--hints--');
|
||||
expect(isArray(actual)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return an empty array if the marker is not present', () => {
|
||||
const actual = getSection(simpleAst, '--not-a-marker--');
|
||||
expect(isArray(actual)).toBe(true);
|
||||
expect(actual.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should include any headings without markers', () => {
|
||||
const actual = getSection(extraHeadingAst, '--description--');
|
||||
expect(
|
||||
find(root(actual), {
|
||||
value: 'this should still be inside --description--'
|
||||
})
|
||||
).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should include the rest of the AST if there is no end marker', () => {
|
||||
const actual = getSection(extraHeadingAst, '--solutions--');
|
||||
expect(actual.length > 0).toBe(true);
|
||||
expect(
|
||||
find(root(actual), { value: 'body {\n background: white;\n}' })
|
||||
).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should ignore a marker if the depth is not correct', () => {
|
||||
const actual = getSection(extraHeadingAst, '--instructions--', 2);
|
||||
expect(actual).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should match the hints snapshot', () => {
|
||||
const actual = getSection(simpleAst, '--hints--');
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
it('should match the instructions snapshot', () => {
|
||||
const actual = getSection(simpleAst, '--instructions--');
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
const mdastToHTML = require('./mdast-to-html');
|
||||
|
||||
// Captures hanzi (pinyin) pairs (hanzi, optional whitespace, then pinyin parentheses)
|
||||
const HANZI_PINYIN_PAIR = '([\u4e00-\u9fff]+)\\s*\\(([^)]+)\\)';
|
||||
|
||||
// Matches the BLANK placeholder
|
||||
const BLANK_TOKEN = 'BLANK';
|
||||
|
||||
// Matches Chinese and English punctuation
|
||||
const PUNCTUATION = '[,。?!!?,;:;:、]+';
|
||||
|
||||
// Matches Latin text with spaces
|
||||
const OTHER_TEXT = '([a-zA-Z\\s]+)';
|
||||
|
||||
const HANZI_PINYIN_REGEX = new RegExp(
|
||||
`${HANZI_PINYIN_PAIR}|${BLANK_TOKEN}|${PUNCTUATION}|${OTHER_TEXT}`,
|
||||
'g'
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses all hanzi-pinyin pairs from text
|
||||
* @param {string} text - Text potentially containing multiple hanzi (pinyin) patterns
|
||||
* @returns {Array<{hanzi: string, pinyin: string}>} Array of parsed pairs
|
||||
*/
|
||||
function parseHanziPinyinPairs(text) {
|
||||
const pairs = [];
|
||||
const regex = new RegExp(HANZI_PINYIN_REGEX);
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
if (match[1] && match[2]) {
|
||||
pairs.push({
|
||||
hanzi: match[1].trim(),
|
||||
pinyin: match[2].trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return pairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom handler for Chinese inline code to render as ruby elements
|
||||
* Matches hanzi-pinyin pairs, BLANK, and punctuation as separate elements
|
||||
* @param {object} state - The state object from mdast-util-to-hast
|
||||
* @param {object} node - The inlineCode node
|
||||
* @returns {object|Array<object>} Hast element node or array of nodes
|
||||
*/
|
||||
function chineseInlineCodeHandler(state, node) {
|
||||
const rubyPairs = parseHanziPinyinPairs(node.value);
|
||||
|
||||
if (rubyPairs.length > 0) {
|
||||
const matches = [...node.value.matchAll(HANZI_PINYIN_REGEX)];
|
||||
const nodes = matches.map(fullMatch => {
|
||||
if (fullMatch[1] && fullMatch[2]) {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'ruby',
|
||||
properties: {},
|
||||
children: [
|
||||
{ type: 'text', value: fullMatch[1].trim() },
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'rp',
|
||||
properties: {},
|
||||
children: [{ type: 'text', value: '(' }]
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'rt',
|
||||
properties: {},
|
||||
children: [{ type: 'text', value: fullMatch[2].trim() }]
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'rp',
|
||||
properties: {},
|
||||
children: [{ type: 'text', value: ')' }]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Other captures (BLANK, punctuation, other text including spaces) should preserve exactly
|
||||
return { type: 'text', value: fullMatch[0] };
|
||||
});
|
||||
|
||||
return nodes.length === 1 ? nodes[0] : nodes;
|
||||
}
|
||||
|
||||
// If static text, return code
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: { className: 'highlighted-text' },
|
||||
children: [{ type: 'text', value: node.value }]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom handler for inline code to render as span elements
|
||||
* @param {object} state - The state object from mdast-util-to-hast
|
||||
* @param {object} node - The inlineCode node
|
||||
* @returns {object} Hast element node
|
||||
*/
|
||||
function spanInlineCodeHandler(state, node) {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: { className: 'highlighted-text' },
|
||||
children: [{ type: 'text', value: node.value }]
|
||||
};
|
||||
}
|
||||
|
||||
const spanOrRubyOptions = {
|
||||
handlers: {
|
||||
inlineCode: chineseInlineCodeHandler
|
||||
}
|
||||
};
|
||||
|
||||
const spanOptions = {
|
||||
handlers: {
|
||||
inlineCode: spanInlineCodeHandler
|
||||
}
|
||||
};
|
||||
|
||||
const createMdastToHtml = lang => {
|
||||
if (lang === 'zh-CN') {
|
||||
return x => mdastToHTML(x, spanOrRubyOptions);
|
||||
} else if (lang === 'en-US' || lang === 'es') {
|
||||
return x => mdastToHTML(x, spanOptions);
|
||||
} else {
|
||||
return mdastToHTML;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
parseHanziPinyinPairs,
|
||||
createMdastToHtml
|
||||
};
|
||||
@@ -0,0 +1,292 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createMdastToHtml, parseHanziPinyinPairs } from './i18n-stringify';
|
||||
|
||||
describe('parseHanziPinyinPairs', () => {
|
||||
it('should parse single hanzi-pinyin pair', () => {
|
||||
const withSpaceSeparator = parseHanziPinyinPairs('你好 (nǐ hǎo)');
|
||||
|
||||
expect(withSpaceSeparator).toHaveLength(1);
|
||||
expect(withSpaceSeparator[0]).toMatchObject({
|
||||
hanzi: '你好',
|
||||
pinyin: 'nǐ hǎo'
|
||||
});
|
||||
|
||||
const withoutSpaceSeparator = parseHanziPinyinPairs('你好(nǐ hǎo)');
|
||||
|
||||
expect(withoutSpaceSeparator).toHaveLength(1);
|
||||
expect(withoutSpaceSeparator[0]).toMatchObject({
|
||||
hanzi: '你好',
|
||||
pinyin: 'nǐ hǎo'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse multiple hanzi-pinyin pairs', () => {
|
||||
const withSpaceSeparator = parseHanziPinyinPairs(
|
||||
'你好 (nǐ hǎo),我是王华 (wǒ shì Wang Hua)'
|
||||
);
|
||||
expect(withSpaceSeparator).toHaveLength(2);
|
||||
expect(withSpaceSeparator[0]).toMatchObject({
|
||||
hanzi: '你好',
|
||||
pinyin: 'nǐ hǎo'
|
||||
});
|
||||
expect(withSpaceSeparator[1]).toMatchObject({
|
||||
hanzi: '我是王华',
|
||||
pinyin: 'wǒ shì Wang Hua'
|
||||
});
|
||||
|
||||
const withoutSpaceSeparator = parseHanziPinyinPairs(
|
||||
'你好(nǐ hǎo),我是王华(wǒ shì Wang Hua)'
|
||||
);
|
||||
expect(withoutSpaceSeparator).toHaveLength(2);
|
||||
expect(withoutSpaceSeparator[0]).toMatchObject({
|
||||
hanzi: '你好',
|
||||
pinyin: 'nǐ hǎo'
|
||||
});
|
||||
expect(withoutSpaceSeparator[1]).toMatchObject({
|
||||
hanzi: '我是王华',
|
||||
pinyin: 'wǒ shì Wang Hua'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array for text without pairs', () => {
|
||||
const result = parseHanziPinyinPairs('你好');
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMdastToHtml', () => {
|
||||
it('should render Chinese inline code as ruby when lang is zh-CN', () => {
|
||||
const toHtml = createMdastToHtml('zh-CN');
|
||||
const nodes = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ type: 'text', value: 'This is ' },
|
||||
{ type: 'inlineCode', value: '请问 (qǐng wèn)' },
|
||||
{ type: 'text', value: '.' }
|
||||
]
|
||||
}
|
||||
];
|
||||
const actual = toHtml(nodes);
|
||||
expect(actual).toBe(
|
||||
'<p>This is <ruby>请问<rp>(</rp><rt>qǐng wèn</rt><rp>)</rp></ruby>.</p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Chinese inline code as ruby with or without space before parenthesis', () => {
|
||||
const toHtml = createMdastToHtml('zh-CN');
|
||||
const nodesWithSpace = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ type: 'inlineCode', value: '你好 (nǐ hǎo)' }]
|
||||
}
|
||||
];
|
||||
const nodesWithoutSpace = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ type: 'inlineCode', value: '你好(nǐ hǎo)' }]
|
||||
}
|
||||
];
|
||||
const expected =
|
||||
'<p><ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby></p>';
|
||||
expect(toHtml(nodesWithSpace)).toBe(expected);
|
||||
expect(toHtml(nodesWithoutSpace)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle multiple Chinese inline codes in one paragraph', () => {
|
||||
const toHtml = createMdastToHtml('zh-CN');
|
||||
const nodes = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ type: 'inlineCode', value: '你好 (nǐ hǎo)' },
|
||||
{ type: 'text', value: ' and ' },
|
||||
{ type: 'inlineCode', value: '再见 (zài jiàn)' }
|
||||
]
|
||||
}
|
||||
];
|
||||
const actual = toHtml(nodes);
|
||||
expect(actual).toBe(
|
||||
'<p><ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby> and <ruby>再见<rp>(</rp><rt>zài jiàn</rt><rp>)</rp></ruby></p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render BLANK tokens and punctuation marks as plain text', () => {
|
||||
const toHtml = createMdastToHtml('zh-CN');
|
||||
const withoutSpacesAroundBlanks = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'inlineCode',
|
||||
value:
|
||||
'你好 (nǐ hǎo),BLANK是王华 (shì Wang Hua),请问你 (qǐng wèn nǐ)BLANK什么名字 (shén me míng zi)?'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
expect(toHtml(withoutSpacesAroundBlanks)).toBe(
|
||||
'<p><ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby>,BLANK<ruby>是王华<rp>(</rp><rt>shì Wang Hua</rt><rp>)</rp></ruby>,<ruby>请问你<rp>(</rp><rt>qǐng wèn nǐ</rt><rp>)</rp></ruby>BLANK<ruby>什么名字<rp>(</rp><rt>shén me míng zi</rt><rp>)</rp></ruby>?</p>'
|
||||
);
|
||||
|
||||
const withSpacesAroundBlanks = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'inlineCode',
|
||||
value:
|
||||
'你好 (nǐ hǎo), BLANK 是王华 (shì Wang Hua),请问你 (qǐng wèn nǐ) BLANK 什么名字 (shén me míng zi)?'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
expect(toHtml(withSpacesAroundBlanks)).toBe(
|
||||
'<p><ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby>, BLANK <ruby>是王华<rp>(</rp><rt>shì Wang Hua</rt><rp>)</rp></ruby>,<ruby>请问你<rp>(</rp><rt>qǐng wèn nǐ</rt><rp>)</rp></ruby> BLANK <ruby>什么名字<rp>(</rp><rt>shén me míng zi</rt><rp>)</rp></ruby>?</p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Latin words as plain text while applying ruby to hanzi-pinyin pairs', () => {
|
||||
const toHtml = createMdastToHtml('zh-CN');
|
||||
const nodes = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'inlineCode',
|
||||
value: '我是 (wǒ shì) UI 设计师 (shè jì shī)'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
const actual = toHtml(nodes);
|
||||
expect(actual).toBe(
|
||||
'<p><ruby>我是<rp>(</rp><rt>wǒ shì</rt><rp>)</rp></ruby> UI <ruby>设计师<rp>(</rp><rt>shè jì shī</rt><rp>)</rp></ruby></p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle BLANK token and Latin word mix', () => {
|
||||
const toHtml = createMdastToHtml('zh-CN');
|
||||
const nodes = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'inlineCode',
|
||||
value: '我 (wǒ) BLANK UI 设计师 (shè jì shī)'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
const actual = toHtml(nodes);
|
||||
expect(actual).toBe(
|
||||
'<p><ruby>我<rp>(</rp><rt>wǒ</rt><rp>)</rp></ruby> BLANK UI <ruby>设计师<rp>(</rp><rt>shè jì shī</rt><rp>)</rp></ruby></p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render multiple adjacent BLANK tokens in Chinese sentence', () => {
|
||||
const toHtml = createMdastToHtml('zh-CN');
|
||||
const nodes = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ type: 'inlineCode', value: 'BLANK BLANK,你好 (nǐ hǎo)' }]
|
||||
}
|
||||
];
|
||||
const actual = toHtml(nodes);
|
||||
expect(actual).toBe(
|
||||
'<p>BLANK BLANK,<ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby></p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to span element if pattern does not match', () => {
|
||||
const toHtml = createMdastToHtml('zh-CN');
|
||||
const nodes = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ type: 'inlineCode', value: '你好' },
|
||||
{ type: 'text', value: ' and ' },
|
||||
{ type: 'inlineCode', value: 'nǐ hǎo' }
|
||||
]
|
||||
}
|
||||
];
|
||||
const actual = toHtml(nodes);
|
||||
expect(actual).toBe(
|
||||
'<p><span class="highlighted-text">你好</span> and <span class="highlighted-text">nǐ hǎo</span></p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render inline code as span when lang is en-US', () => {
|
||||
const toHtml = createMdastToHtml('en-US');
|
||||
const nodes = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ type: 'text', value: 'This is ' },
|
||||
{ type: 'inlineCode', value: 'highlighted text' },
|
||||
{ type: 'text', value: '.' }
|
||||
]
|
||||
}
|
||||
];
|
||||
const actual = toHtml(nodes);
|
||||
expect(actual).toBe(
|
||||
'<p>This is <span class="highlighted-text">highlighted text</span>.</p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render inline code as span when lang is es', () => {
|
||||
const toHtml = createMdastToHtml('es');
|
||||
const nodes = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ type: 'text', value: 'Esto texto ' },
|
||||
{ type: 'inlineCode', value: 'está resaltado' },
|
||||
{ type: 'text', value: '.' }
|
||||
]
|
||||
}
|
||||
];
|
||||
const actual = toHtml(nodes);
|
||||
expect(actual).toBe(
|
||||
'<p>Esto texto <span class="highlighted-text">está resaltado</span>.</p>'
|
||||
);
|
||||
});
|
||||
|
||||
// NOTE: The mobile app looks for the 'highlighted-text' class to apply highlighted styles.
|
||||
// Notify the mobile team if the class name is changed or removed.
|
||||
it("should include the 'highlighted-text' class when rendering as span", () => {
|
||||
const toHtml = createMdastToHtml('en-US');
|
||||
const nodes = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ type: 'inlineCode', value: 'highlighted' }]
|
||||
}
|
||||
];
|
||||
const actual = toHtml(nodes);
|
||||
expect(actual).toMatch(/class="highlighted-text"/);
|
||||
});
|
||||
|
||||
it('should render as regular code when lang is not zh-CN, en-US, or es', () => {
|
||||
const toHtml = createMdastToHtml('zh');
|
||||
const nodes = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ type: 'inlineCode', value: '请问 (qǐng wèn)' }]
|
||||
}
|
||||
];
|
||||
const actual = toHtml(nodes);
|
||||
expect(actual).toBe('<p><code>请问 (qǐng wèn)</code></p>');
|
||||
});
|
||||
|
||||
it('should render as regular code when lang is not defined', () => {
|
||||
const toHtml = createMdastToHtml();
|
||||
const nodes = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ type: 'inlineCode', value: '请问 (qǐng wèn)' }]
|
||||
}
|
||||
];
|
||||
const actual = toHtml(nodes);
|
||||
expect(actual).toBe('<p><code>请问 (qǐng wèn)</code></p>');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
const hastToHTML = require('hast-util-to-html');
|
||||
const { root } = require('mdast-builder');
|
||||
const mdastToHast = require('mdast-util-to-hast');
|
||||
|
||||
function mdastToHTML(nodes, hastOptions = {}) {
|
||||
if (!Array.isArray(nodes))
|
||||
throw Error('mdastToHTML expects an array argument');
|
||||
// - the 'nodes' are children, so first need embedding in a parent
|
||||
|
||||
return hastToHTML(
|
||||
mdastToHast(root(nodes), { allowDangerousHtml: true, ...hastOptions }),
|
||||
{
|
||||
allowDangerousHtml: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = mdastToHTML;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, beforeAll, it, expect } from 'vitest';
|
||||
import parseFixture from '../../__fixtures__/parse-fixture';
|
||||
import mdastToHTML from './mdast-to-html';
|
||||
|
||||
describe('mdast-to-html', () => {
|
||||
let mdastMixedNodes, singleNode, inlineHTMLNodes;
|
||||
|
||||
beforeAll(async () => {
|
||||
const mdastMixedAST = await parseFixture('mixed-nodes.md');
|
||||
const mdastInlineHTMLAST = await parseFixture('inline-html.md');
|
||||
mdastMixedNodes = mdastMixedAST.children;
|
||||
singleNode = mdastMixedAST.children[0];
|
||||
inlineHTMLNodes = mdastInlineHTMLAST.children;
|
||||
});
|
||||
|
||||
it('should return a string', () => {
|
||||
expect.assertions(1);
|
||||
const actual = mdastToHTML(mdastMixedNodes);
|
||||
expect(typeof actual).toBe('string');
|
||||
});
|
||||
|
||||
it('throws if it is not passed an array', () => {
|
||||
expect.assertions(1);
|
||||
expect(() => mdastToHTML(singleNode)).toThrow(
|
||||
'mdastToHTML expects an array argument'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not escape html', () => {
|
||||
const actual = mdastToHTML(mdastMixedNodes);
|
||||
expect(actual).toBe(`<p>Paragraph 1</p>
|
||||
<p>Third <em>hint</em> with <code>code</code> and <code>inline code</code></p>`);
|
||||
});
|
||||
|
||||
it('should put inline html inside the enclosing paragraph', () => {
|
||||
const actual = mdastToHTML(inlineHTMLNodes);
|
||||
expect(actual).toBe(
|
||||
'<p><code> code in </code> code tags <em>emphasis</em> followed' +
|
||||
' by <div><span>some nested html </span></div></p>'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
const is = require('unist-util-is');
|
||||
|
||||
// TODO: specific tests for this would be nice, even though it is somewhat
|
||||
// covered by the plugins that use it.
|
||||
function splitOnThematicBreak(nodes) {
|
||||
return nodes.reduce(
|
||||
(prev, curr) => {
|
||||
if (is(curr, 'thematicBreak')) {
|
||||
return [...prev, []];
|
||||
} else {
|
||||
const first = prev.slice(0, -1);
|
||||
const last = prev.slice(-1)[0];
|
||||
return [...first, [...last, curr]];
|
||||
}
|
||||
},
|
||||
[[]]
|
||||
);
|
||||
}
|
||||
|
||||
exports.splitOnThematicBreak = splitOnThematicBreak;
|
||||
Reference in New Issue
Block a user