@diplodoc/transform 4.76.9 → 4.77.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.
- package/dist/css/_yfm-only.css.map +1 -1
- package/dist/css/_yfm-only.min.css.map +1 -1
- package/dist/css/base.css +7 -0
- package/dist/css/base.css.map +3 -3
- package/dist/css/base.min.css +1 -1
- package/dist/css/base.min.css.map +3 -3
- package/dist/css/print.css.map +1 -1
- package/dist/css/yfm.css +7 -0
- package/dist/css/yfm.css.map +3 -3
- package/dist/css/yfm.min.css +1 -1
- package/dist/css/yfm.min.css.map +3 -3
- package/dist/js/base.js +19 -6
- package/dist/js/base.js.map +2 -2
- package/dist/js/base.min.js +3 -1
- package/dist/js/base.min.js.map +3 -3
- package/dist/js/yfm.js +19 -6
- package/dist/js/yfm.js.map +2 -2
- package/dist/js/yfm.min.js +3 -1
- package/dist/js/yfm.min.js.map +3 -3
- package/dist/scss/_code.scss +6 -0
- package/lib/plugins/code.js +110 -17
- package/lib/plugins/code.js.map +1 -1
- package/lib/plugins/links/collect.js +6 -6
- package/lib/plugins/links/collect.js.map +1 -1
- package/lib/plugins/links/index.js +11 -14
- package/lib/plugins/links/index.js.map +1 -1
- package/lib/utils.js +7 -7
- package/lib/utils.js.map +1 -1
- package/package.json +4 -3
- package/src/js/code.ts +28 -11
- package/src/scss/_code.scss +6 -0
- package/src/transform/plugins/code.ts +140 -17
- package/src/transform/plugins/links/collect.ts +2 -2
- package/src/transform/plugins/links/index.ts +4 -4
- package/src/transform/utils.ts +7 -4
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import type {MarkdownItPluginCb} from './typings';
|
|
4
4
|
import type {IDGenerator} from './utils';
|
|
5
5
|
|
|
6
|
+
import {escapeHtml} from 'markdown-it/lib/common/utils';
|
|
7
|
+
|
|
6
8
|
import {generateID as globalGenerateID} from './utils';
|
|
7
9
|
|
|
8
10
|
const wrapInFloatingContainer = (
|
|
@@ -134,7 +136,57 @@ function balanceSpansPerLine(lines: string[]): string[] {
|
|
|
134
136
|
});
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Strips the prompt prefix from the raw fence content BEFORE it reaches the highlighter.
|
|
141
|
+
*
|
|
142
|
+
* @param content - Raw fence content.
|
|
143
|
+
* @param prompt - Raw prompt string (e.g. `$`, `#`, `>>>`).
|
|
144
|
+
* @returns Stripped content (same number of lines) and a per-line prompt flag array.
|
|
145
|
+
*/
|
|
146
|
+
function stripPrompt(content: string, prompt: string): {content: string; flags: boolean[]} {
|
|
147
|
+
const hasTrailingNewline = content.endsWith('\n');
|
|
148
|
+
const lines = content.split('\n');
|
|
149
|
+
const linesToProcess = hasTrailingNewline ? lines.slice(0, -1) : lines;
|
|
150
|
+
|
|
151
|
+
const flags: boolean[] = [];
|
|
152
|
+
const stripped = linesToProcess.map((line) => {
|
|
153
|
+
const trimmed = line.trimStart();
|
|
154
|
+
const leadingWs = line.slice(0, line.length - trimmed.length);
|
|
155
|
+
|
|
156
|
+
if (trimmed.startsWith(prompt + ' ')) {
|
|
157
|
+
flags.push(true);
|
|
158
|
+
return leadingWs + trimmed.slice(prompt.length + 1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (trimmed === prompt) {
|
|
162
|
+
flags.push(true);
|
|
163
|
+
return leadingWs;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
flags.push(false);
|
|
167
|
+
return line;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
content: stripped.join('\n') + (hasTrailingNewline ? '\n' : ''),
|
|
172
|
+
flags,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function postProcessCode(
|
|
177
|
+
code: string,
|
|
178
|
+
{
|
|
179
|
+
showLineNumbers,
|
|
180
|
+
lineWrapping,
|
|
181
|
+
promptFlags,
|
|
182
|
+
promptEscaped,
|
|
183
|
+
}: {
|
|
184
|
+
showLineNumbers: boolean;
|
|
185
|
+
lineWrapping: boolean;
|
|
186
|
+
promptFlags?: boolean[];
|
|
187
|
+
promptEscaped?: string;
|
|
188
|
+
},
|
|
189
|
+
): string {
|
|
138
190
|
const hasTrailingNewline = code.endsWith('\n');
|
|
139
191
|
const lines = code.split('\n');
|
|
140
192
|
const linesToProcess = hasTrailingNewline ? lines.slice(0, -1) : lines;
|
|
@@ -144,15 +196,53 @@ function addLineNumbers(code: string, {lineWrapping}: {lineWrapping: boolean}):
|
|
|
144
196
|
return (
|
|
145
197
|
normalized
|
|
146
198
|
.map((line, index) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
// The prompt was already stripped from the raw content before
|
|
200
|
+
// highlighting, so `line` here is the highlighted command only.
|
|
201
|
+
// Re-insert the prompt as a dedicated, non-selectable span after
|
|
202
|
+
// any leading whitespace. `promptFlags` marks which lines carried
|
|
203
|
+
// a prompt in the original source.
|
|
204
|
+
if (promptFlags?.[index] && promptEscaped !== undefined) {
|
|
205
|
+
const trimmed = line.trimStart();
|
|
206
|
+
const leadingWs = line.slice(0, line.length - trimmed.length);
|
|
207
|
+
line =
|
|
208
|
+
`${leadingWs}` +
|
|
209
|
+
`<span class="yfm-code-prompt" aria-hidden="true">${promptEscaped} </span>` +
|
|
210
|
+
`${trimmed}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (showLineNumbers) {
|
|
214
|
+
const lineNumber = String(index + 1).padStart(maxDigits, ' ');
|
|
215
|
+
line = lineWrapping
|
|
216
|
+
? `<span class="yfm-line-number">${lineNumber}</span><span class="yfm-line">${line}</span>`
|
|
217
|
+
: `<span class="yfm-line-number">${lineNumber}</span>${line}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return line;
|
|
151
221
|
})
|
|
152
222
|
.join('\n') + (hasTrailingNewline ? '\n' : '')
|
|
153
223
|
);
|
|
154
224
|
}
|
|
155
225
|
|
|
226
|
+
/**
|
|
227
|
+
* @param info - Raw fence info string (e.g. `js showLineNumbers prompt="$"`).
|
|
228
|
+
* @returns Parsed flags and the optional prompt value.
|
|
229
|
+
*/
|
|
230
|
+
function parseFenceInfo(info: string): {
|
|
231
|
+
showLineNumbers: boolean;
|
|
232
|
+
wrapLines: boolean;
|
|
233
|
+
prompt?: string;
|
|
234
|
+
} {
|
|
235
|
+
const promptMatch = /(?:^|\s)prompt=(?:"([^"]*)"|'([^']*)')/.exec(info);
|
|
236
|
+
const prompt = promptMatch?.[1] ?? promptMatch?.[2];
|
|
237
|
+
const rest = promptMatch ? info.replace(promptMatch[0], ' ') : info;
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
showLineNumbers: /\bshowLineNumbers\b/.test(rest),
|
|
241
|
+
wrapLines: /\bwrap\b/.test(rest),
|
|
242
|
+
prompt,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
156
246
|
type CodeOptions = {
|
|
157
247
|
/**
|
|
158
248
|
* Show button to toggle line wrapping of code.
|
|
@@ -164,36 +254,69 @@ type CodeOptions = {
|
|
|
164
254
|
|
|
165
255
|
const code: MarkdownItPluginCb<CodeOptions> = (md, opts) => {
|
|
166
256
|
const lineWrapping = opts?.codeLineWrapping || false;
|
|
167
|
-
const generateID = opts
|
|
257
|
+
const generateID = opts?.generateID ?? globalGenerateID;
|
|
168
258
|
|
|
169
259
|
const superCodeRenderer = md.renderer.rules.fence;
|
|
170
260
|
md.renderer.rules.fence = function (tokens, idx, options, env, self) {
|
|
171
261
|
const token = tokens[idx];
|
|
172
|
-
const showLineNumbers = token.info
|
|
173
|
-
const wrapLines = token.info.includes('wrap');
|
|
174
|
-
|
|
262
|
+
const {showLineNumbers, prompt, wrapLines} = parseFenceInfo(token.info);
|
|
175
263
|
const shouldWrap = wrapLines;
|
|
176
264
|
|
|
265
|
+
// Strip the prompt prefix from the raw content BEFORE handing it
|
|
266
|
+
// to the highlighter, so language grammars never misinterpret prompt
|
|
267
|
+
// symbols (e.g. `#` as a shell comment, `$` as a variable). The prompt is
|
|
268
|
+
// re-inserted as a span in postProcessCode, driven by `promptFlags`.
|
|
269
|
+
let promptFlags: boolean[] | undefined;
|
|
270
|
+
let promptEscaped: string | undefined;
|
|
271
|
+
const originalContent = token.content;
|
|
272
|
+
if (prompt) {
|
|
273
|
+
const stripped = stripPrompt(token.content, prompt);
|
|
274
|
+
token.content = stripped.content;
|
|
275
|
+
promptFlags = stripped.flags;
|
|
276
|
+
promptEscaped = escapeHtml(prompt);
|
|
277
|
+
}
|
|
278
|
+
|
|
177
279
|
let superCode = superCodeRenderer?.(tokens, idx, options, env, self);
|
|
178
280
|
|
|
179
|
-
|
|
281
|
+
// Restore the original content so the mutation does not leak to other
|
|
282
|
+
// consumers of the token.
|
|
283
|
+
token.content = originalContent;
|
|
284
|
+
|
|
285
|
+
if (superCode && (showLineNumbers || prompt)) {
|
|
180
286
|
// Extract the code content from the pre/code tags
|
|
181
287
|
const codeMatch = superCode.match(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/);
|
|
182
288
|
if (codeMatch) {
|
|
183
289
|
const codeContent = codeMatch[1];
|
|
184
|
-
const
|
|
290
|
+
const processedCode = postProcessCode(codeContent, {
|
|
291
|
+
showLineNumbers,
|
|
292
|
+
lineWrapping,
|
|
293
|
+
promptFlags,
|
|
294
|
+
promptEscaped,
|
|
295
|
+
});
|
|
185
296
|
// Escape $ in replacement string: $$ becomes a literal $ in String.replace()
|
|
186
|
-
const escapedReplacement =
|
|
297
|
+
const escapedReplacement = processedCode.replace(/\$/g, '$$$$');
|
|
187
298
|
superCode = superCode.replace(codeContent, escapedReplacement);
|
|
188
299
|
}
|
|
189
300
|
}
|
|
190
301
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
302
|
+
// Attach the raw prompt value as a data attribute on <code> so that the
|
|
303
|
+
// copy widget can strip it from the plain-text content without relying on
|
|
304
|
+
// any class names or DOM structure introduced by the plugin.
|
|
305
|
+
if (superCode && (prompt || shouldWrap)) {
|
|
306
|
+
superCode = superCode.replace(/(<pre[^>]*><code)([^>]*)>/, (_match, open, attrs) => {
|
|
307
|
+
// Add the wrap class
|
|
308
|
+
if (shouldWrap) {
|
|
309
|
+
attrs = /class="[^"]*"/.test(attrs)
|
|
310
|
+
? attrs.replace(/class="([^"]*)"/, 'class="$1 wrap"')
|
|
311
|
+
: ` class="wrap"${attrs}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Add the data-prompt attribute
|
|
315
|
+
if (prompt) {
|
|
316
|
+
attrs = ` data-prompt="${escapeHtml(prompt)}"${attrs}`;
|
|
195
317
|
}
|
|
196
|
-
|
|
318
|
+
|
|
319
|
+
return `${open}${attrs}>`;
|
|
197
320
|
});
|
|
198
321
|
}
|
|
199
322
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import MarkdownIt from 'markdown-it';
|
|
2
2
|
import {sep} from 'path';
|
|
3
|
-
import
|
|
3
|
+
import {parseHref} from '@diplodoc/utils';
|
|
4
4
|
|
|
5
5
|
import {PAGE_LINK_REGEXP, getHrefTokenAttr, isLocalUrl} from '../../utils';
|
|
6
6
|
import {getSinglePageAnchorId, resolveRelativePath} from '../../utilsFS';
|
|
@@ -62,7 +62,7 @@ const collect = (input: string, options: Options) => {
|
|
|
62
62
|
continue;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
const {pathname, hash} =
|
|
65
|
+
const {pathname, hash} = parseHref(href);
|
|
66
66
|
if (pathname) {
|
|
67
67
|
const isPageFile = PAGE_LINK_REGEXP.test(pathname);
|
|
68
68
|
if (isPageFile) {
|
|
@@ -4,7 +4,7 @@ import type {CacheContext, StateCore} from '../../typings';
|
|
|
4
4
|
import type {MarkdownItPluginCb, MarkdownItPluginOpts} from '../typings';
|
|
5
5
|
import type {MarkdownItIncluded} from '../includes/types';
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import {formatHref, parseHref} from '@diplodoc/utils';
|
|
8
8
|
import {bold} from 'chalk';
|
|
9
9
|
import path, {isAbsolute, parse, relative, resolve} from 'path';
|
|
10
10
|
|
|
@@ -153,7 +153,7 @@ function processLink(
|
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
const {pathname, hash} =
|
|
156
|
+
const {pathname, hash} = parseHref(originalHref);
|
|
157
157
|
let file;
|
|
158
158
|
let fileExists;
|
|
159
159
|
let isPageFile;
|
|
@@ -219,8 +219,8 @@ function processLink(
|
|
|
219
219
|
|
|
220
220
|
const patchedHref =
|
|
221
221
|
!isAbsolute(originalHref) && !originalHref.includes('//')
|
|
222
|
-
?
|
|
223
|
-
...
|
|
222
|
+
? formatHref({
|
|
223
|
+
...parseHref(originalHref),
|
|
224
224
|
pathname: getPublicPath(opts, file),
|
|
225
225
|
})
|
|
226
226
|
: originalHref;
|
package/src/transform/utils.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type Token from 'markdown-it/lib/token';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import {formatHref, parseHref} from '@diplodoc/utils';
|
|
4
4
|
|
|
5
5
|
export function isLocalUrl(url: string) {
|
|
6
|
-
|
|
6
|
+
// A URL is local unless it has an external scheme: either `scheme://...`
|
|
7
|
+
// / `//host...` (protocol-relative), or an opaque scheme URI without
|
|
8
|
+
// a slash-slash part (e.g. `mailto:`, `tel:`, `custom:`).
|
|
9
|
+
return !isExternalHref(url);
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export function findBlockTokens(tokens: Token[], id: string) {
|
|
@@ -105,8 +108,8 @@ export function getSrcTokenAttr(token: Token) {
|
|
|
105
108
|
export const PAGE_LINK_REGEXP = /\.(md|ya?ml)$/i;
|
|
106
109
|
|
|
107
110
|
export function defaultTransformLink(href: string) {
|
|
108
|
-
const parsed =
|
|
109
|
-
href =
|
|
111
|
+
const parsed = parseHref(href);
|
|
112
|
+
href = formatHref({
|
|
110
113
|
...parsed,
|
|
111
114
|
pathname: parsed.pathname?.replace(PAGE_LINK_REGEXP, '.html'),
|
|
112
115
|
});
|