From bf7b4c288b224604ba70a80b8412f09441d34a44 Mon Sep 17 00:00:00 2001 From: d3m1d0v Date: Tue, 5 Nov 2024 22:57:40 +0300 Subject: [PATCH] feat(plugin): support directive syntax --- package-lock.json | 39 +- package.json | 3 + src/plugin/directive.ts | 51 +++ src/plugin/plugin.ts | 1 + src/plugin/transform.ts | 22 +- tests/src/__snapshots__/plugin.test.ts.snap | 396 +++++++++++++++++++- tests/src/plugin.test.ts | 142 +++++++ 7 files changed, 636 insertions(+), 18 deletions(-) create mode 100644 src/plugin/directive.ts diff --git a/package-lock.json b/package-lock.json index 1738739..c9bf4e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@diplodoc/cut-extension", "version": "0.3.2", "license": "MIT", + "dependencies": { + "@diplodoc/directive": "file:../diplodoc-directive/diplodoc-directive-0.0.0.tgz" + }, "devDependencies": { "@diplodoc/lint": "^1.2.0", "@diplodoc/tsconfig": "^1.0.2", @@ -589,6 +592,14 @@ "postcss-selector-parser": "^6.0.13" } }, + "node_modules/@diplodoc/directive": { + "version": "0.0.0", + "resolved": "file:../diplodoc-directive/diplodoc-directive-0.0.0.tgz", + "integrity": "sha512-4KJDS9LuAukmV1gdRUp+njMaeAJEB8YUogBGqUGHrFvQi5gUV1cbXLsqw2Fne964YLfZ4oUrUXTPMeh7fVWODA==", + "dependencies": { + "markdown-it-directive": "2.0.2" + } + }, "node_modules/@diplodoc/lint": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@diplodoc/lint/-/lint-1.2.0.tgz", @@ -1431,14 +1442,12 @@ "node_modules/@types/linkify-it": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", - "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", - "dev": true + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==" }, "node_modules/@types/markdown-it": { "version": "13.0.9", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.9.tgz", "integrity": "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==", - "dev": true, "license": "MIT", "dependencies": { "@types/linkify-it": "^3", @@ -1448,8 +1457,7 @@ "node_modules/@types/mdurl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", - "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", - "dev": true + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==" }, "node_modules/@types/minimist": { "version": "1.2.5", @@ -1822,8 +1830,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { "version": "5.1.3", @@ -5248,7 +5255,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", - "dev": true, "dependencies": { "uc.micro": "^1.0.1" } @@ -5791,7 +5797,6 @@ "version": "13.0.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", - "dev": true, "dependencies": { "argparse": "^2.0.1", "entities": "~3.0.1", @@ -5803,11 +5808,19 @@ "markdown-it": "bin/markdown-it.js" } }, + "node_modules/markdown-it-directive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/markdown-it-directive/-/markdown-it-directive-2.0.2.tgz", + "integrity": "sha512-UceeGZwl9bjnuEJWW/4UnJIFfcJ3NYmEXSTrffq6N/77E5wpbAYyopsuH/H4R8Ccuwge9E7otyuvHP5bULkccg==", + "peerDependencies": { + "@types/markdown-it": "^12.0.0 || ^13.0.0", + "markdown-it": "^12.0.0 || ^13.0.0" + } + }, "node_modules/markdown-it/node_modules/entities": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", - "dev": true, "engines": { "node": ">=0.12" }, @@ -5836,8 +5849,7 @@ "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", - "dev": true + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "node_modules/memorystream": { "version": "0.3.1", @@ -8615,8 +8627,7 @@ "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "node_modules/unbox-primitive": { "version": "1.0.2", diff --git a/package.json b/package.json index 47548c3..c542fad 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,9 @@ "pre-commit": "lint update && lint-staged", "prepare": "husky" }, + "dependencies": { + "@diplodoc/directive": "file:../diplodoc-directive/diplodoc-directive-0.0.0.tgz" + }, "devDependencies": { "@diplodoc/lint": "^1.2.0", "@diplodoc/tsconfig": "^1.0.2", diff --git a/src/plugin/directive.ts b/src/plugin/directive.ts new file mode 100644 index 0000000..55d4542 --- /dev/null +++ b/src/plugin/directive.ts @@ -0,0 +1,51 @@ +import type MarkdownIt from 'markdown-it'; + +import { + createBlockInlineToken, + directive, + registerBlockDirective, + tokenizeBlockContent, +} from '@diplodoc/directive'; + +import {ClassNames, ENV_FLAG_NAME, TokenType} from './const'; + +export const cutDirective: MarkdownIt.PluginSimple = (md) => { + md.use(directive()); + + registerBlockDirective(md, 'cut', (state, params) => { + if (!params.content) { + return false; + } + + state.env ??= {}; + state.env[ENV_FLAG_NAME] = true; + + let token = state.push(TokenType.CutOpen, 'details', 1); + token.block = true; + token.attrSet('class', ClassNames.Cut); + token.map = [params.startLine, params.endLine]; + token.markup = ':::'; + + token = state.push(TokenType.TitleOpen, 'summary', 1); + token.attrSet('class', ClassNames.Title); + + if (params.inlineContent) { + token = createBlockInlineToken(state, params); + } + + token = state.push(TokenType.TitleClose, 'summary', -1); + + token = state.push(TokenType.ContentOpen, 'div', 1); + token.attrSet('class', ClassNames.Content); + token.map = [params.startLine + 1, params.endLine - 1]; + + tokenizeBlockContent(state, params.content, 'yfm-cut'); + + token = state.push(TokenType.ContentClose, 'div', -1); + + token = state.push(TokenType.CutClose, 'details', -1); + token.block = true; + + return true; + }); +}; diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index a5c1980..a18d860 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -34,6 +34,7 @@ export const cutPlugin: MarkdownIt.PluginSimple = (md) => { const newOpenToken = new state.Token(TokenType.CutOpen, 'details', 1); newOpenToken.attrSet('class', ClassNames.Cut); newOpenToken.map = tokens[i].map; + newOpenToken.markup = '{%'; attrsParser.apply(newOpenToken); diff --git a/src/plugin/transform.ts b/src/plugin/transform.ts index 6bf3b68..c9f0125 100644 --- a/src/plugin/transform.ts +++ b/src/plugin/transform.ts @@ -3,6 +3,9 @@ import type MarkdownIt from 'markdown-it'; import {cutPlugin} from './plugin'; import {type Runtime, copyRuntime, dynrequire, hidden} from './utils'; import {ENV_FLAG_NAME} from './const'; +import {cutDirective} from './directive'; + +type Syntax = 'both' | 'curly' | 'directive'; export type TransformOptions = { runtime?: @@ -12,10 +15,13 @@ export type TransformOptions = { style: string; }; bundle?: boolean; + /** @default 'curly' */ + syntax?: Syntax; }; -type NormalizedPluginOptions = Omit & { +type NormalizedPluginOptions = Omit & { runtime: Runtime; + syntax: Syntax; }; const registerTransform = ( @@ -23,12 +29,18 @@ const registerTransform = ( { runtime, bundle, + syntax, output, - }: Pick & { + }: Pick & { output: string; }, ) => { - md.use(cutPlugin); + if (syntax === 'curly' || syntax === 'both') { + md.use(cutPlugin); + } + if (syntax === 'directive' || syntax === 'both') { + md.use(cutDirective); + } md.core.ruler.push('yfm_cut_after', ({env}) => { hidden(env, 'bundled', new Set()); @@ -57,6 +69,8 @@ export function transform(options: Partial = {}) { throw new TypeError('Option `runtime` should be record when `bundle` is enabled.'); } + const syntax: Syntax = options.syntax ?? 'curly'; + const runtime: Runtime = typeof options.runtime === 'string' ? {script: options.runtime, style: options.runtime} @@ -70,6 +84,7 @@ export function transform(options: Partial = {}) { {output = '.'} = {}, ) { registerTransform(md, { + syntax, runtime, bundle, output, @@ -81,6 +96,7 @@ export function transform(options: Partial = {}) { const MdIt = dynrequire('markdown-it'); const md = new MdIt().use((md: MarkdownIt) => { registerTransform(md, { + syntax, runtime, bundle, output: destRoot, diff --git a/tests/src/__snapshots__/plugin.test.ts.snap b/tests/src/__snapshots__/plugin.test.ts.snap index c977f7c..2633f39 100644 --- a/tests/src/__snapshots__/plugin.test.ts.snap +++ b/tests/src/__snapshots__/plugin.test.ts.snap @@ -1,5 +1,68 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Cut extension - plugin Options: syntax should render both cuts 1`] = ` +
+ + Old cut + +
+

+ Old cut content +

+
+
+
+ + Directive cut + +
+

+ Directive cut content +

+
+
+`; + +exports[`Cut extension - plugin Options: syntax should render only new (directive) cut 1`] = ` +

+

+

+ Old cut content +

+

+

+
+ + Directive cut + +
+

+ Directive cut content +

+
+
+`; + +exports[`Cut extension - plugin Options: syntax should render only old (curly) cut 1`] = ` +
+ + Old cut + +
+

+ Old cut content +

+
+
+

+ :::cut[Directive cut] +
+ Directive cut content +
+ ::: +

+`; + exports[`Cut extension - plugin curly syntax should close all tags correctly and insert two p tags 1`] = `
  • @@ -80,7 +143,7 @@ exports[`Cut extension - plugin curly syntax should parse markup with cut to tok 2, 3, ], - "markup": "", + "markup": "{%", "meta": null, "nesting": 1, "tag": "details", @@ -414,3 +477,334 @@ exports[`Cut extension - plugin curly syntax should render title with format 1`] `; + +exports[`Cut extension - plugin directive syntax should not render leaf cut 1`] = ` +

    + ::cut[Title] +

    +`; + +exports[`Cut extension - plugin directive syntax should parse markup with cut to token stream 1`] = ` +[ + Token { + "attrs": [ + [ + "class", + "yfm-cut", + ], + ], + "block": true, + "children": null, + "content": "", + "hidden": false, + "info": "", + "level": 0, + "map": [ + 2, + 5, + ], + "markup": ":::", + "meta": null, + "nesting": 1, + "tag": "details", + "type": "yfm_cut_open", + }, + Token { + "attrs": [ + [ + "class", + "yfm-cut-title", + ], + ], + "block": true, + "children": null, + "content": "", + "hidden": false, + "info": "", + "level": 1, + "map": null, + "markup": "", + "meta": null, + "nesting": 1, + "tag": "summary", + "type": "yfm_cut_title_open", + }, + Token { + "attrs": null, + "block": true, + "children": [ + Token { + "attrs": null, + "block": false, + "children": null, + "content": "Cut ", + "hidden": false, + "info": "", + "level": 0, + "map": null, + "markup": "", + "meta": null, + "nesting": 0, + "tag": "", + "type": "text", + }, + Token { + "attrs": null, + "block": false, + "children": null, + "content": "", + "hidden": false, + "info": "", + "level": 0, + "map": null, + "markup": "_", + "meta": null, + "nesting": 1, + "tag": "em", + "type": "em_open", + }, + Token { + "attrs": null, + "block": false, + "children": null, + "content": "title", + "hidden": false, + "info": "", + "level": 1, + "map": null, + "markup": "", + "meta": null, + "nesting": 0, + "tag": "", + "type": "text", + }, + Token { + "attrs": null, + "block": false, + "children": null, + "content": "", + "hidden": false, + "info": "", + "level": 0, + "map": null, + "markup": "_", + "meta": null, + "nesting": -1, + "tag": "em", + "type": "em_close", + }, + ], + "content": "Cut _title_", + "hidden": false, + "info": "", + "level": 2, + "map": [ + 2, + 3, + ], + "markup": "", + "meta": null, + "nesting": 0, + "tag": "", + "type": "inline", + }, + Token { + "attrs": null, + "block": true, + "children": null, + "content": "", + "hidden": false, + "info": "", + "level": 1, + "map": null, + "markup": "", + "meta": null, + "nesting": -1, + "tag": "summary", + "type": "yfm_cut_title_close", + }, + Token { + "attrs": [ + [ + "class", + "yfm-cut-content", + ], + ], + "block": true, + "children": null, + "content": "", + "hidden": false, + "info": "", + "level": 1, + "map": [ + 3, + 4, + ], + "markup": "", + "meta": null, + "nesting": 1, + "tag": "div", + "type": "yfm_cut_content_open", + }, + Token { + "attrs": null, + "block": true, + "children": null, + "content": "", + "hidden": false, + "info": "", + "level": 2, + "map": [ + 3, + 4, + ], + "markup": "", + "meta": null, + "nesting": 1, + "tag": "p", + "type": "paragraph_open", + }, + Token { + "attrs": null, + "block": true, + "children": [ + Token { + "attrs": null, + "block": false, + "children": null, + "content": "Cut content", + "hidden": false, + "info": "", + "level": 0, + "map": null, + "markup": "", + "meta": null, + "nesting": 0, + "tag": "", + "type": "text", + }, + ], + "content": "Cut content", + "hidden": false, + "info": "", + "level": 3, + "map": [ + 3, + 4, + ], + "markup": "", + "meta": null, + "nesting": 0, + "tag": "", + "type": "inline", + }, + Token { + "attrs": null, + "block": true, + "children": null, + "content": "", + "hidden": false, + "info": "", + "level": 2, + "map": null, + "markup": "", + "meta": null, + "nesting": -1, + "tag": "p", + "type": "paragraph_close", + }, + Token { + "attrs": null, + "block": true, + "children": null, + "content": "", + "hidden": false, + "info": "", + "level": 1, + "map": null, + "markup": "", + "meta": null, + "nesting": -1, + "tag": "div", + "type": "yfm_cut_content_close", + }, + Token { + "attrs": null, + "block": true, + "children": null, + "content": "", + "hidden": false, + "info": "", + "level": 0, + "map": null, + "markup": "", + "meta": null, + "nesting": -1, + "tag": "details", + "type": "yfm_cut_close", + }, +] +`; + +exports[`Cut extension - plugin directive syntax should render cut with empty title 1`] = ` +
    + + +
    +

    + Cut content +

    +
    +
    +`; + +exports[`Cut extension - plugin directive syntax should render cut with inline markup in title 1`] = ` +
    + + + Strong cut title + + +
    +

    + Content we want to hide +

    +
    +
    +`; + +exports[`Cut extension - plugin directive syntax should render directive cut 1`] = ` +
    + + Cut title + +
    +

    + Cut content +

    +
    +
    +`; + +exports[`Cut extension - plugin directive syntax should render nested cut 1`] = ` +
    + + Outer title + +
    +

    + Outer content +

    +
    + + Inner title + +
    +

    + Inner content +

    +
    +
    +
    +
    +`; diff --git a/tests/src/plugin.test.ts b/tests/src/plugin.test.ts index 03fe240..e860f38 100644 --- a/tests/src/plugin.test.ts +++ b/tests/src/plugin.test.ts @@ -209,4 +209,146 @@ describe('Cut extension - plugin', () => { ).toMatchSnapshot(); }); }); + + describe('directive syntax', () => { + it('should render directive cut', () => { + expect( + html( + dd` + :::cut[Cut title] + Cut content + ::: + `, + {syntax: 'directive'}, + ), + ).toMatchSnapshot(); + }); + + it('should render nested cut', () => { + expect( + html( + dd` + :::cut[Outer title] + Outer content + + :::cut[Inner title] + Inner content + ::: + ::: + `, + {syntax: 'directive'}, + ), + ).toMatchSnapshot(); + }); + + it('should render cut with inline markup in title', () => { + expect( + html( + dd` + :::cut[**Strong cut title**] + Content we want to hide + ::: + `, + {syntax: 'directive'}, + ), + ).toMatchSnapshot(); + }); + + it('should dont add assets to meta if no yfm-cut is found', () => { + expect(meta('paragraph', {syntax: 'directive'})).toBeUndefined(); + }); + + it('should add assets to meta', () => { + expect( + meta( + dd` + :::cut[Cut title] + Cut content + ::: + `, + {syntax: 'directive'}, + ), + ).toStrictEqual({ + script: ['_assets/cut-extension.js'], + style: ['_assets/cut-extension.css'], + }); + }); + + it('should parse markup with cut to token stream', () => { + expect( + parse( + dd` + + + :::cut[Cut _title_] + Cut content + ::: + + + `, + {syntax: 'directive'}, + ), + ).toMatchSnapshot(); + }); + + it('should render cut with empty title', () => { + expect( + html( + dd` + :::cut + Cut content + :::`, + {syntax: 'directive'}, + ), + ).toMatchSnapshot(); + }); + + it('should not render leaf cut', () => { + expect( + html( + dd` + ::cut[Title] + `, + {syntax: 'directive'}, + ), + ).toMatchSnapshot(); + }); + + it('should not add assets with leaf cut', () => { + expect( + meta( + dd` + ::cut[Title] + `, + {syntax: 'directive'}, + ), + ).toBeUndefined(); + }); + }); + + describe('Options: syntax', () => { + const markup = dd` + {% cut "Old cut" %} + + Old cut content + + {% endcut %} + + :::cut[Directive cut] + Directive cut content + ::: + `; + + it('should render only old (curly) cut', () => { + expect(html(markup, {syntax: 'curly'})).toMatchSnapshot(); + }); + + it('should render only new (directive) cut', () => { + expect(html(markup, {syntax: 'directive'})).toMatchSnapshot(); + }); + + it('should render both cuts', () => { + expect(html(markup, {syntax: 'both'})).toMatchSnapshot(); + }); + }); });