@diplodoc/transform 4.73.2 → 4.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/css/_yfm-only.css.map +1 -1
  2. package/dist/css/_yfm-only.min.css.map +1 -1
  3. package/dist/css/base.css.map +1 -1
  4. package/dist/css/base.min.css.map +1 -1
  5. package/dist/css/print.css.map +1 -1
  6. package/dist/css/yfm.css +14 -15
  7. package/dist/css/yfm.css.map +3 -3
  8. package/dist/css/yfm.min.css +1 -1
  9. package/dist/css/yfm.min.css.map +3 -3
  10. package/dist/js/yfm.js +111 -99
  11. package/dist/js/yfm.js.map +3 -3
  12. package/dist/js/yfm.min.js +1 -1
  13. package/dist/js/yfm.min.js.map +3 -3
  14. package/lib/md.js +3 -1
  15. package/lib/md.js.map +1 -1
  16. package/lib/plugins/anchors/index.js +4 -1
  17. package/lib/plugins/anchors/index.js.map +1 -1
  18. package/lib/plugins/code.js +9 -3
  19. package/lib/plugins/code.js.map +1 -1
  20. package/lib/plugins/images/index.d.ts +2 -1
  21. package/lib/plugins/images/index.js +4 -3
  22. package/lib/plugins/images/index.js.map +1 -1
  23. package/lib/plugins/inline-code/index.js +4 -2
  24. package/lib/plugins/inline-code/index.js.map +1 -1
  25. package/lib/plugins/tabs.d.ts +1 -0
  26. package/lib/plugins/term/index.js +55 -31
  27. package/lib/plugins/term/index.js.map +1 -1
  28. package/lib/plugins/utils.d.ts +10 -1
  29. package/lib/plugins/utils.js +16 -3
  30. package/lib/plugins/utils.js.map +1 -1
  31. package/lib/typings.d.ts +11 -0
  32. package/lib/utils.js +1 -1
  33. package/lib/utils.js.map +1 -1
  34. package/package.json +3 -3
  35. package/src/transform/md.ts +3 -0
  36. package/src/transform/plugins/anchors/index.ts +4 -1
  37. package/src/transform/plugins/code.ts +15 -4
  38. package/src/transform/plugins/images/index.ts +12 -3
  39. package/src/transform/plugins/inline-code/index.ts +4 -3
  40. package/src/transform/plugins/term/index.ts +56 -32
  41. package/src/transform/plugins/utils.ts +15 -2
  42. package/src/transform/typings.ts +11 -0
  43. package/src/transform/utils.ts +1 -1
@@ -13,6 +13,7 @@ import getHeadings from './headings';
13
13
  import sanitizeHtml, {defaultOptions, sanitizeStyles} from './sanitize';
14
14
  import {olAttrConversion} from './plugins/ol-attr-conversion';
15
15
  import {DEFAULT_LANG} from './constants';
16
+ import {createIDGeneratorByStrategy} from './plugins/utils';
16
17
 
17
18
  function initMarkdownIt(options: OptionsType) {
18
19
  const {
@@ -80,6 +81,7 @@ function getPluginOptions(options: OptionsType) {
80
81
  enabled: true,
81
82
  maxFileSize: 2 * 1024 * 1024,
82
83
  },
84
+ generateID,
83
85
  ...customOptions
84
86
  } = options;
85
87
 
@@ -93,6 +95,7 @@ function getPluginOptions(options: OptionsType) {
93
95
  log,
94
96
  lang,
95
97
  svgInline,
98
+ generateID: generateID ?? createIDGeneratorByStrategy('random'),
96
99
  } as MarkdownItPluginOpts;
97
100
  }
98
101
 
@@ -111,7 +111,10 @@ const index: MarkdownItPluginCb<Options> = (md, options) => {
111
111
  }
112
112
 
113
113
  if (level < 2 && extractTitle) {
114
- // if there are any custom ids in the level 1 heading we should clear them
114
+ const customIds = getCustomIds(inlineToken.content);
115
+ if (customIds) {
116
+ token.attrSet('id', customIds[0]);
117
+ }
115
118
  removeCustomIds(tokens[i + 1]);
116
119
  i += 3;
117
120
  continue;
@@ -1,8 +1,9 @@
1
1
  /* eslint-disable max-len */
2
2
 
3
3
  import type {MarkdownItPluginCb} from './typings';
4
+ import type {IDGenerator} from './utils';
4
5
 
5
- import {generateID} from './utils';
6
+ import {generateID as globalGenerateID} from './utils';
6
7
 
7
8
  const wrapInFloatingContainer = (
8
9
  element: string | undefined,
@@ -72,7 +73,12 @@ interface EnvTerm {
72
73
  };
73
74
  }
74
75
 
75
- function termReplace(str: string, env: EnvTerm, escape: (str: string) => string): string {
76
+ function termReplace(
77
+ str: string,
78
+ env: EnvTerm,
79
+ escape: (str: string) => string,
80
+ generateID: IDGenerator,
81
+ ): string {
76
82
  const regTerms = Object.keys(env.terms)
77
83
  .map((el) => el.slice(1))
78
84
  .map(escape)
@@ -83,7 +89,7 @@ function termReplace(str: string, env: EnvTerm, escape: (str: string) => string)
83
89
  const termCode = str.replace(
84
90
  reg,
85
91
  (_match: string, p1: string, _p2: string, p3: string) =>
86
- `<i class="yfm yfm-term_title" term-key=":${p3}" id="${generateID()}">${p1}</i>`,
92
+ `<i class="yfm yfm-term_title" term-key=":${p3}" id="${generateID(p3)}">${p1}</i>`,
87
93
  );
88
94
 
89
95
  return termCode || str;
@@ -100,6 +106,8 @@ const SPAN_TAG_RE = /<span[^>]*>|<\/span>/g;
100
106
  * `['<span class="hljs-string">\'line1', 'line2\'</span>']`
101
107
  * After:
102
108
  * `['<span class="hljs-string">\'line1</span>', '<span class="hljs-string">line2\'</span>']`
109
+ * @param lines - Highlighted HTML split into separate lines.
110
+ * @returns Lines with balanced `<span>` tags on each line.
103
111
  */
104
112
  function balanceSpansPerLine(lines: string[]): string[] {
105
113
  const openTagStack: string[] = [];
@@ -152,6 +160,7 @@ type CodeOptions = {
152
160
 
153
161
  const code: MarkdownItPluginCb<CodeOptions> = (md, opts) => {
154
162
  const lineWrapping = opts?.codeLineWrapping || false;
163
+ const generateID = opts.generateID ?? globalGenerateID;
155
164
 
156
165
  const superCodeRenderer = md.renderer.rules.fence;
157
166
  md.renderer.rules.fence = function (tokens, idx, options, env, self) {
@@ -173,7 +182,9 @@ const code: MarkdownItPluginCb<CodeOptions> = (md, opts) => {
173
182
  }
174
183
 
175
184
  const superCodeWithTerms =
176
- superCode && env?.terms ? termReplace(superCode, env, md.utils.escapeRE) : superCode;
185
+ superCode && env?.terms
186
+ ? termReplace(superCode, env, md.utils.escapeRE, generateID)
187
+ : superCode;
177
188
 
178
189
  return wrapInFloatingContainer(superCodeWithTerms, idx, {lineWrapping});
179
190
  };
@@ -1,6 +1,7 @@
1
1
  import type Token from 'markdown-it/lib/token';
2
2
  import type {MarkdownItPluginCb, MarkdownItPluginOpts} from '../typings';
3
3
  import type {ImageOptions, StateCore} from '../../typings';
4
+ import type {IDGenerator} from '../utils';
4
5
 
5
6
  import {join, sep} from 'path';
6
7
  import {bold} from 'chalk';
@@ -9,6 +10,7 @@ import {readFileSync} from 'fs';
9
10
 
10
11
  import {isFileExists, resolveRelativePath} from '../../utilsFS';
11
12
  import {filterTokens, getSrcTokenAttr, isExternalHref} from '../../utils';
13
+ import {generateID as globalGenerateID} from '../utils';
12
14
 
13
15
  const sanitizeAttribute = (value: string): string => value.replace(/(\d*[%a-z]{0,5}).*/gi, '$1');
14
16
 
@@ -166,7 +168,10 @@ const index: MarkdownItPluginCb<Opts> = (md, opts) => {
166
168
  );
167
169
  } else {
168
170
  const svgToken = new state.Token('image_svg', '', 0);
169
- svgToken.attrSet('content', replaceSvgContent(svgContent, imageOpts));
171
+ svgToken.attrSet(
172
+ 'content',
173
+ replaceSvgContent(svgContent, imageOpts, opts.generateID),
174
+ );
170
175
  childrenTokens[index] = svgToken;
171
176
  }
172
177
  }
@@ -193,7 +198,11 @@ const index: MarkdownItPluginCb<Opts> = (md, opts) => {
193
198
  };
194
199
  };
195
200
 
196
- function replaceSvgContent(content: string | null, options: ImageOptions) {
201
+ function replaceSvgContent(
202
+ content: string | null,
203
+ options: ImageOptions,
204
+ generateID: IDGenerator = globalGenerateID,
205
+ ) {
197
206
  if (!content) {
198
207
  return '';
199
208
  }
@@ -239,7 +248,7 @@ function replaceSvgContent(content: string | null, options: ImageOptions) {
239
248
  {
240
249
  name: 'prefixIds',
241
250
  params: {
242
- prefix: 'rnd-' + Math.floor(Math.random() * 1e9).toString(16),
251
+ prefix: generateID('svg'),
243
252
  prefixClassNames: false,
244
253
  },
245
254
  },
@@ -2,20 +2,21 @@ import type {MarkdownItPluginCb} from '../../typings';
2
2
 
3
3
  import {escapeHtml} from 'markdown-it/lib/common/utils';
4
4
 
5
- import {generateID} from '../utils';
5
+ import {generateID as globalGenerateID} from '../utils';
6
6
 
7
7
  import {LANG_TOKEN_DESCRIPTION, LANG_TOKEN_LABEL} from './constant';
8
8
 
9
9
  const inlineCode: MarkdownItPluginCb = (md, options) => {
10
10
  const lang = options.lang;
11
+ const generateID = options.generateID ?? globalGenerateID;
11
12
 
12
13
  md.renderer.rules.code_inline = function (tokens, idx) {
13
- const id = generateID();
14
+ const id = generateID('inline-code-id');
14
15
 
15
16
  const description = LANG_TOKEN_DESCRIPTION[lang] ?? LANG_TOKEN_DESCRIPTION.en;
16
17
  const label = LANG_TOKEN_LABEL[lang] ?? LANG_TOKEN_LABEL.en;
17
18
 
18
- return `<code class="yfm-clipboard-inline-code" role="button" aria-label="${label}" aria-description="${description}" tabindex='0' id="inline-code-id-${id}">${escapeHtml(tokens[idx].content)}</code>`;
19
+ return `<code class="yfm-clipboard-inline-code" role="button" aria-label="${label}" aria-description="${description}" tabindex='0' id="${id}">${escapeHtml(tokens[idx].content)}</code>`;
19
20
  };
20
21
  };
21
22
 
@@ -2,19 +2,20 @@ import type StateCore from 'markdown-it/lib/rules_core/state_core';
2
2
  import type StateInline from 'markdown-it/lib/rules_inline/state_inline';
3
3
  import type Token from 'markdown-it/lib/token';
4
4
  import type {MarkdownItPluginCb} from '../typings';
5
+ import type {IDGenerator} from '../utils';
5
6
 
6
- import {generateID} from '../utils';
7
+ import {generateID as globalGenerateID} from '../utils';
7
8
 
8
9
  import {termDefinitions} from './termDefinitions';
9
10
  import {BASIC_TERM_REGEXP} from './constants';
10
11
 
11
- function setTermAttrs(token: Token, termKey: string): void {
12
+ function setTermAttrs(token: Token, termKey: string, generateID: IDGenerator): void {
12
13
  token.attrSet('class', 'yfm yfm-term_title');
13
14
  token.attrSet('term-key', ':' + termKey);
14
15
  token.attrSet('role', 'button');
15
16
  token.attrSet('aria-controls', ':' + termKey + '_element');
16
17
  token.attrSet('tabindex', '0');
17
- token.attrSet('id', generateID());
18
+ token.attrSet('id', generateID(termKey));
18
19
  }
19
20
 
20
21
  interface TermMatch {
@@ -88,38 +89,41 @@ function matchTermPattern(src: string, start: number, max: number): TermMatch |
88
89
  }
89
90
 
90
91
  /**
91
- * Inline rule that matches [text](*termId) for defined terms.
92
+ * Creates an inline rule that matches [text](*termId) for defined terms.
92
93
  * Runs before the link rule so that * characters inside the
93
94
  * pattern are consumed and never trigger emphasis.
94
95
  *
95
- * @param state - inline parser state
96
- * @param silent - if true, only validate without creating tokens
97
- * @returns true if the pattern was matched
96
+ * @param generateID - per-file isolated ID generator
97
+ * @returns inline rule function for markdown-it
98
98
  */
99
- function termInlineRule(state: StateInline, silent: boolean): boolean {
100
- if (state.src.charCodeAt(state.pos) !== 0x5b /* [ */ || !state.env.terms) {
101
- return false;
102
- }
99
+ function termInlineRuleFactory(generateID: IDGenerator) {
100
+ return function termInlineRule(state: StateInline, silent: boolean): boolean {
101
+ if (state.src.charCodeAt(state.pos) !== 0x5b /* [ */ || !state.env.terms) {
102
+ return false;
103
+ }
103
104
 
104
- const match = matchTermPattern(state.src, state.pos, state.posMax);
105
- if (!match || !state.env.terms[':' + match.termId]) {
106
- return false;
107
- }
105
+ const match = matchTermPattern(state.src, state.pos, state.posMax);
106
+ if (!match || !state.env.terms[':' + match.termId]) {
107
+ return false;
108
+ }
108
109
 
109
- if (!silent) {
110
- const labelContent = state.src.slice(state.pos + 1, match.labelEnd).replace(/\\(.)/g, '$1');
110
+ if (!silent) {
111
+ const labelContent = state.src
112
+ .slice(state.pos + 1, match.labelEnd)
113
+ .replace(/\\(.)/g, '$1');
111
114
 
112
- const termOpen = state.push('term_open', 'i', 1);
113
- setTermAttrs(termOpen, match.termId);
115
+ const termOpen = state.push('term_open', 'i', 1);
116
+ setTermAttrs(termOpen, match.termId, generateID);
114
117
 
115
- const textToken = state.push('text', '', 0);
116
- textToken.content = labelContent;
118
+ const textToken = state.push('text', '', 0);
119
+ textToken.content = labelContent;
117
120
 
118
- state.push('term_close', 'i', -1);
119
- }
121
+ state.push('term_close', 'i', -1);
122
+ }
120
123
 
121
- state.pos = match.endPos;
122
- return true;
124
+ state.pos = match.endPos;
125
+ return true;
126
+ };
123
127
  }
124
128
 
125
129
  const RAW_CODE_TOKEN_TYPES = new Set(['fence', 'code_block', 'yfm_page-constructor']);
@@ -139,10 +143,21 @@ function collectTermsFromRawContent(tokens: Token[], reg: RegExp, out: Set<strin
139
143
  collectRawTermMatches(token.content, reg, out);
140
144
  }
141
145
 
142
- if (token.type === 'inline' && token.children) {
143
- for (const child of token.children) {
144
- if (child.type === 'code_inline' && child.content) {
145
- collectRawTermMatches(child.content, reg, out);
146
+ if (token.type === 'inline') {
147
+ // Scan the raw inline content to catch term references that
148
+ // may have been split across child tokens by emphasis parsing.
149
+ // When term definitions come from includes resolved after inline
150
+ // parsing, term_inline fails and `(*key)` gets fragmented by
151
+ // the `*` emphasis delimiter — making per-child scanning miss them.
152
+ if (token.content) {
153
+ collectRawTermMatches(token.content, reg, out);
154
+ }
155
+
156
+ if (token.children) {
157
+ for (const child of token.children) {
158
+ if (child.type === 'code_inline' && child.content) {
159
+ collectRawTermMatches(child.content, reg, out);
160
+ }
146
161
  }
147
162
  }
148
163
  }
@@ -241,6 +256,7 @@ const term: MarkdownItPluginCb = (md, options) => {
241
256
  const arrayReplaceAt = md.utils.arrayReplaceAt;
242
257
 
243
258
  const {isLintRun} = options;
259
+ const generateID = options.generateID ?? globalGenerateID;
244
260
 
245
261
  // Prevent * URLs from being parsed as regular links (backward compatibility)
246
262
  const defaultLinkValidation = md.validateLink;
@@ -253,7 +269,7 @@ const term: MarkdownItPluginCb = (md, options) => {
253
269
  };
254
270
 
255
271
  // Inline rule: handles [text](*termId) before emphasis can interfere with *
256
- md.inline.ruler.before('link', 'term_inline', termInlineRule);
272
+ md.inline.ruler.before('link', 'term_inline', termInlineRuleFactory(generateID));
257
273
 
258
274
  function termReplace(state: StateCore) {
259
275
  let i, j, l, tokens, token, text, nodes, pos, termMatch, currentToken;
@@ -347,7 +363,7 @@ const term: MarkdownItPluginCb = (md, options) => {
347
363
  }
348
364
 
349
365
  token = new state.Token('term_open', 'i', 1);
350
- setTermAttrs(token, termKey);
366
+ setTermAttrs(token, termKey, generateID);
351
367
  nodes.push(token);
352
368
 
353
369
  token = new state.Token('text', '', 0);
@@ -386,7 +402,15 @@ const term: MarkdownItPluginCb = (md, options) => {
386
402
  alt: ['paragraph', 'reference'],
387
403
  });
388
404
 
389
- md.core.ruler.after('linkify', 'termReplace', termReplace);
405
+ // Register termReplace after text_join so that emphasis-split text
406
+ // tokens (e.g. [text](*key) where * opens a delimiter) are merged
407
+ // before the term regex runs. Without this, terms defined in
408
+ // included files (resolved after inline parsing) would be missed.
409
+ try {
410
+ md.core.ruler.after('text_join', 'termReplace', termReplace);
411
+ } catch {
412
+ md.core.ruler.after('linkify', 'termReplace', termReplace);
413
+ }
390
414
  };
391
415
 
392
416
  export = term;
@@ -3,6 +3,8 @@ import type {Logger} from '../log';
3
3
 
4
4
  import {bold} from 'chalk';
5
5
  import {platform} from 'process';
6
+ export type {IDGenerator, IDGeneratorStrategy} from '@diplodoc/utils';
7
+ export {createIDGenerator, createIDGeneratorByStrategy} from '@diplodoc/utils';
6
8
 
7
9
  export type MatchTokenFunction = (
8
10
  tokens: Token[],
@@ -34,8 +36,19 @@ export const nestedCloseTokenIdxFactory =
34
36
 
35
37
  export const сarriage = platform === 'win32' ? '\r\n' : '\n';
36
38
 
37
- export function generateID() {
38
- return Math.random().toString(36).substr(2, 8);
39
+ /**
40
+ * Generates a legacy random ID.
41
+ * @deprecated Use `options.generateID` from plugin context instead.
42
+ * Fallback preserves legacy random behavior without shared counter state.
43
+ * @param prefix - Optional prefix to prepend to the generated ID.
44
+ * @returns Random ID string, optionally prefixed with `${prefix}-`.
45
+ */
46
+ export function generateID(prefix?: string): string {
47
+ if (!prefix) {
48
+ return Math.random().toString(36).substr(2, 8);
49
+ }
50
+
51
+ return `${prefix}-${Math.random().toString(36).substr(2, 8)}`;
39
52
  }
40
53
 
41
54
  export function append<T extends Record<string, []>, Key extends keyof T>(
@@ -5,6 +5,7 @@ import type DefaultStateCore from 'markdown-it/lib/rules_core/state_core';
5
5
  import type {SanitizeFunction, SanitizeOptions} from './sanitize';
6
6
  import type {LogLevels, Logger} from './log';
7
7
  import type {ChangelogItem} from './plugins/changelog/types';
8
+ import type {IDGenerator} from './plugins/utils';
8
9
 
9
10
  export interface MarkdownIt extends DefaultMarkdownIt {
10
11
  assets?: string[];
@@ -109,6 +110,15 @@ export interface OptionsType {
109
110
  * @default false
110
111
  */
111
112
  codeLineWrapping?: boolean;
113
+ /**
114
+ * Custom ID generator factory to use for this transform call.
115
+ * If provided, it will be called once per file to create a per-file isolated generator.
116
+ * If not provided, a new {@link IDGenerator} is created via {@link createIDGenerator}
117
+ * (deterministic per-file counters).
118
+ *
119
+ * Pass `() => () => Math.random().toString(36).substr(2, 8)` to restore legacy random behavior.
120
+ */
121
+ generateID?: IDGenerator;
112
122
  }
113
123
 
114
124
  export interface OutputType {
@@ -134,6 +144,7 @@ export interface MarkdownItPluginOpts {
134
144
  root: string;
135
145
  rootPublicPath: string;
136
146
  isLintRun: boolean;
147
+ generateID?: IDGenerator;
137
148
  cache?: CacheContext;
138
149
  conditionsInCode?: boolean;
139
150
  vars?: Record<string, string>;
@@ -57,7 +57,7 @@ export function headingInfo(tokens: Token[], idx: number) {
57
57
  while (inlineToken.children && i < inlineToken.children.length) {
58
58
  const token = inlineToken.children[i];
59
59
 
60
- if (token.type === 'text') {
60
+ if (token.type === 'text' || token.type === 'text_special') {
61
61
  title += token.content;
62
62
  }
63
63