@diplodoc/transform 4.73.3 → 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.
@@ -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']);
@@ -252,6 +256,7 @@ const term: MarkdownItPluginCb = (md, options) => {
252
256
  const arrayReplaceAt = md.utils.arrayReplaceAt;
253
257
 
254
258
  const {isLintRun} = options;
259
+ const generateID = options.generateID ?? globalGenerateID;
255
260
 
256
261
  // Prevent * URLs from being parsed as regular links (backward compatibility)
257
262
  const defaultLinkValidation = md.validateLink;
@@ -264,7 +269,7 @@ const term: MarkdownItPluginCb = (md, options) => {
264
269
  };
265
270
 
266
271
  // Inline rule: handles [text](*termId) before emphasis can interfere with *
267
- md.inline.ruler.before('link', 'term_inline', termInlineRule);
272
+ md.inline.ruler.before('link', 'term_inline', termInlineRuleFactory(generateID));
268
273
 
269
274
  function termReplace(state: StateCore) {
270
275
  let i, j, l, tokens, token, text, nodes, pos, termMatch, currentToken;
@@ -358,7 +363,7 @@ const term: MarkdownItPluginCb = (md, options) => {
358
363
  }
359
364
 
360
365
  token = new state.Token('term_open', 'i', 1);
361
- setTermAttrs(token, termKey);
366
+ setTermAttrs(token, termKey, generateID);
362
367
  nodes.push(token);
363
368
 
364
369
  token = new state.Token('text', '', 0);
@@ -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>;