(tree, { type: "code" });
55 |
56 | if (!code) return;
57 |
58 | // @ts-expect-error An argument for 'file' was not provided
59 | plugin(options)(tree);
60 |
61 | const code_ = find(tree, { type: "code" });
62 |
63 | if (!code_) return;
64 |
65 | const _lang = code_.lang;
66 | const _meta = code_.meta;
67 |
68 | const titleNode = find(tree, { type: "paragraph" })?.children[0];
69 | const title = titleNode ? (titleNode as Text).value : null;
70 |
71 | return { title, _lang, _meta };
72 | };
73 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "remark-flexible-code-titles",
3 | "version": "1.3.2",
4 | "description": "Remark plugin to add title or/and container for code blocks with customizable properties in markdown",
5 | "type": "module",
6 | "exports": "./dist/esm/index.js",
7 | "main": "./dist/esm/index.js",
8 | "types": "./dist/esm/index.d.ts",
9 | "scripts": {
10 | "build": "rimraf dist && tsc --build && type-coverage",
11 | "format": "npm run prettier && npm run lint",
12 | "prettier": "prettier --write .",
13 | "lint": "eslint .",
14 | "test": "vitest --watch=false",
15 | "test:watch": "vitest",
16 | "test:file": "vitest test.plugin.spec.ts",
17 | "prepack": "npm run build",
18 | "prepublishOnly": "npm run test && npm run format && npm run test-coverage",
19 | "test-coverage": "vitest run --coverage"
20 | },
21 | "files": [
22 | "dist/",
23 | "src/",
24 | "LICENSE",
25 | "README.md"
26 | ],
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/ipikuka/remark-flexible-code-titles.git"
30 | },
31 | "keywords": [
32 | "unified",
33 | "mdast",
34 | "markdown",
35 | "MDX",
36 | "remark",
37 | "plugin",
38 | "remark plugin",
39 | "code title",
40 | "code fence title",
41 | "remark code title",
42 | "remark code titles"
43 | ],
44 | "author": "ipikuka ",
45 | "license": "MIT",
46 | "homepage": "https://github.com/ipikuka/remark-flexible-code-titles#readme",
47 | "bugs": {
48 | "url": "https://github.com/ipikuka/remark-flexible-code-titles/issues"
49 | },
50 | "devDependencies": {
51 | "@eslint/js": "^9.38.0",
52 | "@types/node": "^24.9.2",
53 | "@vitest/coverage-v8": "^4.0.5",
54 | "@vitest/eslint-plugin": "^1.3.26",
55 | "dedent": "^1.7.0",
56 | "eslint": "^9.38.0",
57 | "eslint-config-prettier": "^10.1.8",
58 | "eslint-plugin-prettier": "^5.5.4",
59 | "globals": "^16.4.0",
60 | "mdast-util-from-markdown": "^2.0.2",
61 | "mdast-util-gfm": "^3.1.0",
62 | "micromark-extension-gfm": "^3.0.0",
63 | "prettier": "^3.6.2",
64 | "rehype-format": "^5.0.1",
65 | "rehype-stringify": "^10.0.1",
66 | "remark-gfm": "^4.0.1",
67 | "remark-parse": "^11.0.0",
68 | "remark-rehype": "^11.1.2",
69 | "rimraf": "^6.0.1",
70 | "type-coverage": "^2.29.7",
71 | "typescript": "^5.9.3",
72 | "typescript-eslint": "^8.46.2",
73 | "unified": "^11.0.5",
74 | "unist-util-find": "^3.0.0",
75 | "vfile": "^6.0.3",
76 | "vitest": "^4.0.5"
77 | },
78 | "dependencies": {
79 | "@types/mdast": "^4.0.4",
80 | "unist-util-visit": "^5.0.0"
81 | },
82 | "peerDependencies": {
83 | "unified": "^11"
84 | },
85 | "sideEffects": false,
86 | "typeCoverage": {
87 | "atLeast": 100,
88 | "detail": true,
89 | "ignoreAsAssertion": true,
90 | "ignoreCatch": true,
91 | "strict": true
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { visit, type Visitor } from "unist-util-visit";
2 | import type { Plugin, Transformer } from "unified";
3 | import type { Paragraph, Code, Root, Data, BlockContent, Parent } from "mdast";
4 |
5 | type Prettify = { [K in keyof T]: T[K] } & {};
6 |
7 | type PartiallyRequired = Omit & Required>;
8 |
9 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
10 | interface ContainerData extends Data {}
11 |
12 | interface Container extends Parent {
13 | /**
14 | * Node type of mdast Mark.
15 | */
16 | type: "container";
17 | /**
18 | * Children of paragraph.
19 | */
20 | children: BlockContent[];
21 | /**
22 | * Data associated with the mdast paragraph.
23 | */
24 | data?: ContainerData | undefined;
25 | }
26 |
27 | declare module "mdast" {
28 | interface BlockContentMap {
29 | container: Container;
30 | }
31 |
32 | interface RootContentMap {
33 | container: Container;
34 | }
35 | }
36 |
37 | type StringOrNull = string | null;
38 |
39 | type RestrictedRecord = Record & { className?: never };
40 | type PropertyFunction = (language?: string, title?: string) => RestrictedRecord;
41 |
42 | export type CodeTitleOptions = {
43 | title?: boolean;
44 | titleTagName?: string;
45 | titleClassName?: string;
46 | titleProperties?: PropertyFunction;
47 | container?: boolean;
48 | containerTagName?: string;
49 | containerClassName?: string;
50 | containerProperties?: PropertyFunction;
51 | handleMissingLanguageAs?: string;
52 | tokenForSpaceInTitle?: string;
53 | };
54 |
55 | const DEFAULT_SETTINGS: CodeTitleOptions = {
56 | title: true,
57 | titleTagName: "div",
58 | titleClassName: "remark-code-title",
59 | container: true,
60 | containerTagName: "div",
61 | containerClassName: "remark-code-container",
62 | };
63 |
64 | type PartiallyRequiredCodeTitleOptions = Prettify<
65 | PartiallyRequired<
66 | CodeTitleOptions,
67 | | "title"
68 | | "titleTagName"
69 | | "titleClassName"
70 | | "container"
71 | | "containerTagName"
72 | | "containerClassName"
73 | >
74 | >;
75 |
76 | /**
77 | *
78 | * This plugin adds a title element before the code element, if the title exists in the markdown code block;
79 | * and wraps them in a container.
80 | *
81 | * for example:
82 | * ```javascript:title.js
83 | * // some js code
84 | * ```
85 | */
86 | export const plugin: Plugin<[CodeTitleOptions?], Root> = (options) => {
87 | const settings = Object.assign(
88 | {},
89 | DEFAULT_SETTINGS,
90 | options,
91 | ) as PartiallyRequiredCodeTitleOptions;
92 |
93 | /** for creating mdx elements just in case (for archive)
94 | const titleNode = {
95 | type: "mdxJsxFlowElement",
96 | name: "div",
97 | attributes: [
98 | {
99 | type: "mdxJsxAttribute",
100 | name: "className",
101 | value: "remark-code-title",
102 | },
103 | { type: "mdxJsxAttribute", name: "data-language", value: language },
104 | ],
105 | children: [{ type: "text", value: title }],
106 | data: { _xdmExplicitJsx: true },
107 | };
108 |
109 | const containerNode = {
110 | type: "mdxJsxFlowElement",
111 | name: "div",
112 | attributes: [
113 | {
114 | type: "mdxJsxAttribute",
115 | name: "className",
116 | value: "remark-code-container",
117 | },
118 | ],
119 | children: [titleNode, node],
120 | data: { _xdmExplicitJsx: true },
121 | };
122 | */
123 |
124 | const constructTitle = (language: string, title: string): Paragraph => {
125 | const properties = settings.titleProperties?.(language, title) ?? {};
126 |
127 | Object.entries(properties).forEach(([k, v]) => {
128 | if (
129 | (typeof v === "string" && v === "") ||
130 | (Array.isArray(v) && (v as unknown[]).length === 0)
131 | ) {
132 | properties[k] = undefined;
133 | }
134 |
135 | if (k === "className") delete properties?.["className"];
136 | });
137 |
138 | return {
139 | type: "paragraph",
140 | children: [{ type: "text", value: title }],
141 | data: {
142 | hName: settings.titleTagName,
143 | hProperties: {
144 | className: [settings.titleClassName],
145 | ...(properties && { ...properties }),
146 | },
147 | },
148 | };
149 | };
150 |
151 | const constructContainer = (
152 | children: BlockContent[],
153 | language: string,
154 | title: string,
155 | ): Container => {
156 | const properties = settings.containerProperties?.(language, title) ?? {};
157 |
158 | Object.entries(properties).forEach(([k, v]) => {
159 | if (
160 | (typeof v === "string" && v === "") ||
161 | (Array.isArray(v) && (v as unknown[]).length === 0)
162 | ) {
163 | properties[k] = undefined;
164 | }
165 |
166 | if (k === "className") delete properties?.["className"];
167 | });
168 |
169 | return {
170 | type: "container",
171 | children,
172 | data: {
173 | hName: settings.containerTagName,
174 | hProperties: {
175 | className: [settings.containerClassName],
176 | ...(properties && { ...properties }),
177 | },
178 | },
179 | };
180 | };
181 |
182 | const extractLanguageAndTitle = (node: Code) => {
183 | const { lang: inputLang, meta: inputMeta } = node;
184 |
185 | if (!inputLang) {
186 | return { language: null, title: null, meta: null };
187 | }
188 |
189 | // we know that "lang" doesn't contain a space (gfm code fencing), but "meta" may consist.
190 |
191 | let title: StringOrNull = null;
192 | let language: StringOrNull = inputLang;
193 | let meta: StringOrNull = inputMeta ?? null;
194 |
195 | // move "showLineNumbers" into meta
196 | if (/showLineNumbers/.test(language)) {
197 | language = language.replace(/showLineNumbers/, "");
198 |
199 | meta = meta?.length ? meta + " showLineNumbers" : "showLineNumbers";
200 | }
201 |
202 | // move line range string like {1, 3-4} into meta (it may complete or nor)
203 | if (language.includes("{")) {
204 | const idxStart = language.search("{");
205 | const idxEnd = language.search("}");
206 |
207 | const metaPart =
208 | idxEnd >= 0
209 | ? language.substring(idxStart, idxEnd + 1)
210 | : language.slice(idxStart, language.length);
211 |
212 | language = language.replace(metaPart, "");
213 |
214 | meta = meta?.length ? metaPart + meta : metaPart;
215 | }
216 |
217 | // move colon+title into meta
218 | if (language.includes(":")) {
219 | const idx = language.search(":");
220 | const metaPart = language.slice(idx, language.length);
221 |
222 | language = language.slice(0, idx);
223 |
224 | meta = meta?.length ? metaPart + " " + meta : metaPart;
225 | }
226 |
227 | // another correctness for line ranges, removing all spaces within curly braces
228 | const RE = /{([\d\s,-]+)}/g; // finds {1, 2-4} like strings for line highlighting
229 | if (meta?.length && RE.test(meta)) {
230 | meta = meta.replace(RE, function (match) {
231 | return match.replace(/ /g, "");
232 | });
233 | }
234 |
235 | // correct if there is a non-space character before opening curly brace "{"
236 | meta = meta?.replace(/(?<=\S)\{/, " {") ?? null;
237 |
238 | // correct if there is a non-space character after closing curly brace "}"
239 | meta = meta?.replace(/\}(?=\S)/, "} ") ?? null;
240 |
241 | if (meta?.includes(":")) {
242 | const regex = /:\s*.*?(?=[\s{]|$)/; // to find :title with colon
243 | const match = meta?.match(regex);
244 |
245 | // classic V8 coverage false negative
246 | /* v8 ignore next -- @preserve */
247 | if (match) {
248 | const matched = match[0];
249 | title = matched.replace(/:\s*/, "");
250 | if (/^showLineNumbers$/i.test(title)) {
251 | title = null;
252 | meta = meta.replace(/:\s*/, "");
253 | } else {
254 | meta = meta.replace(matched, "");
255 | }
256 | }
257 | }
258 |
259 | // handle missing language
260 | if (
261 | !language &&
262 | settings.handleMissingLanguageAs &&
263 | typeof settings.handleMissingLanguageAs === "string"
264 | ) {
265 | language = settings.handleMissingLanguageAs;
266 | }
267 |
268 | // remove if there is more spaces in meta
269 | meta = meta?.replace(/\s+/g, " ").trim() ?? null;
270 |
271 | // employ the settings.tokenForSpaceInTitle
272 | if (title && settings.tokenForSpaceInTitle)
273 | title = title.replaceAll(settings.tokenForSpaceInTitle, " ");
274 |
275 | // if the title is empty, make it null
276 | if (title?.trim() === "") title = null;
277 |
278 | // if the language is empty, make it null
279 | if (language === "") language = null;
280 |
281 | // if the meta is empty, make it null
282 | if (meta === "") meta = null;
283 |
284 | return { title, language, meta };
285 | };
286 |
287 | const visitor: Visitor = function (node, index, parent) {
288 | /* v8 ignore next -- @preserve */
289 | if (!parent || typeof index === "undefined") return;
290 |
291 | const { title, language, meta } = extractLanguageAndTitle(node);
292 |
293 | // mutating the parent.children may effect the next iteration causing visit the same node "code"
294 | // so, it is important to normalize the language here, otherwise may cause infinite loop
295 | node.lang = language;
296 | node.meta = meta;
297 |
298 | let titleNode: Paragraph | undefined = undefined;
299 | let containerNode: Container | undefined = undefined;
300 |
301 | if (settings.title && title) {
302 | titleNode = constructTitle(language ?? "", title);
303 | }
304 |
305 | if (settings.container) {
306 | containerNode = constructContainer(
307 | titleNode ? [titleNode, node] : [node],
308 | language ?? "",
309 | title ?? "",
310 | );
311 | }
312 |
313 | if (containerNode) {
314 | // 1 is for replacing the "code" with the container which consists it already
315 | parent.children.splice(index, 1, containerNode);
316 | } else if (titleNode) {
317 | // 0 is for inserting the titleNode before the "code"
318 | parent.children.splice(index, 0, titleNode);
319 | }
320 | };
321 |
322 | const transformer: Transformer = (tree) => {
323 | visit(tree, "code", visitor);
324 | };
325 |
326 | return transformer;
327 | };
328 |
329 | export default plugin;
330 |
--------------------------------------------------------------------------------
/tests/test.plugin.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from "vitest";
2 | import dedent from "dedent";
3 |
4 | import { type CodeTitleOptions } from "../src/index";
5 |
6 | import { process } from "./util/index";
7 |
8 | const handleMissingLanguage: CodeTitleOptions = {
9 | handleMissingLanguageAs: "unknown",
10 | };
11 |
12 | const noContainer: CodeTitleOptions = { container: false };
13 |
14 | const noTitle: CodeTitleOptions = { title: false };
15 |
16 | const options: CodeTitleOptions = {
17 | titleTagName: "span",
18 | titleClassName: "custom-code-title",
19 | titleProperties(language, title) {
20 | return {
21 | ["data-language"]: language,
22 | title,
23 | dummy: "", // shouldn't be added
24 | empty: [], // shouldn't be added
25 | className: undefined, // shouldn't be taken account
26 | };
27 | },
28 | containerTagName: "section",
29 | containerClassName: "custom-code-wrapper",
30 | containerProperties(language, title) {
31 | return {
32 | ["data-language"]: language,
33 | title,
34 | dummy: "", // shouldn't be added
35 | empty: [], // shouldn't be added
36 | className: undefined, // shouldn't be taken account
37 | };
38 | },
39 | };
40 |
41 | describe("remark-flexible-code-title", () => {
42 | // ******************************************
43 | it("does nothing with code in paragraph", async () => {
44 | const input = dedent`
45 | \`Hi\`
46 | `;
47 |
48 | expect(await process(input)).toMatchInlineSnapshot(`
49 | "
50 | Hi
51 | "
52 | `);
53 | });
54 |
55 | // ******************************************
56 | it("considers there is no language or title, even if no coding phrase", async () => {
57 | const input = dedent`
58 | \`\`\`
59 | \`\`\`
60 | `;
61 |
62 | expect(await process(input)).toMatchInlineSnapshot(`
63 | "
64 |
65 |
66 |
67 | "
68 | `);
69 |
70 | expect(await process(input, handleMissingLanguage)).toMatchInlineSnapshot(`
71 | "
72 |
73 |
74 |
75 | "
76 | `);
77 |
78 | expect(await process(input, noContainer)).toMatchInlineSnapshot(`
79 | "
80 |
81 | "
82 | `);
83 |
84 | expect(await process(input, noTitle)).toMatchInlineSnapshot(`
85 | "
86 |
87 |
88 |
89 | "
90 | `);
91 |
92 | expect(await process(input, options)).toMatchInlineSnapshot(`
93 | "
94 |
95 |
96 |
97 | "
98 | `);
99 | });
100 |
101 | // ******************************************
102 | it("considers there is no language or title", async () => {
103 | const input = dedent`
104 | \`\`\`
105 | const a = 1;
106 | \`\`\`
107 | `;
108 |
109 | expect(await process(input)).toMatchInlineSnapshot(`
110 | "
111 |
112 | const a = 1;
113 |
114 |
115 | "
116 | `);
117 |
118 | expect(await process(input, handleMissingLanguage)).toMatchInlineSnapshot(`
119 | "
120 |
121 | const a = 1;
122 |
123 |
124 | "
125 | `);
126 |
127 | expect(await process(input, noContainer)).toMatchInlineSnapshot(`
128 | "
129 | const a = 1;
130 |
131 | "
132 | `);
133 |
134 | expect(await process(input, noTitle)).toMatchInlineSnapshot(`
135 | "
136 |
137 | const a = 1;
138 |
139 |
140 | "
141 | `);
142 |
143 | expect(await process(input, options)).toMatchInlineSnapshot(`
144 | "
145 |
146 | const a = 1;
147 |
148 |
149 | "
150 | `);
151 | });
152 |
153 | // ******************************************
154 | it("considers there is only language, no title", async () => {
155 | const input = dedent`
156 | \`\`\`javascript
157 | const a = 1;
158 | \`\`\`
159 | `;
160 |
161 | expect(await process(input)).toMatchInlineSnapshot(`
162 | "
163 |
164 | const a = 1;
165 |
166 |
167 | "
168 | `);
169 |
170 | expect(await process(input, handleMissingLanguage)).toMatchInlineSnapshot(`
171 | "
172 |
173 | const a = 1;
174 |
175 |
176 | "
177 | `);
178 |
179 | expect(await process(input, noContainer)).toMatchInlineSnapshot(`
180 | "
181 | const a = 1;
182 |
183 | "
184 | `);
185 |
186 | expect(await process(input, noTitle)).toMatchInlineSnapshot(`
187 | "
188 |
189 | const a = 1;
190 |
191 |
192 | "
193 | `);
194 |
195 | expect(await process(input, options)).toMatchInlineSnapshot(`
196 | "
197 |
198 | const a = 1;
199 |
200 |
201 | "
202 | `);
203 | });
204 |
205 | // ******************************************
206 | it("considers there is no language but only title", async () => {
207 | const input = dedent`
208 | \`\`\`:title.js
209 | const a = 1;
210 | \`\`\`
211 | `;
212 |
213 | expect(await process(input)).toMatchInlineSnapshot(`
214 | "
215 |
216 | title.js
217 | const a = 1;
218 |
219 |
220 | "
221 | `);
222 |
223 | expect(await process(input, handleMissingLanguage)).toMatchInlineSnapshot(`
224 | "
225 |
226 | title.js
227 | const a = 1;
228 |
229 |
230 | "
231 | `);
232 |
233 | expect(await process(input, noContainer)).toMatchInlineSnapshot(`
234 | "
235 | title.js
236 | const a = 1;
237 |
238 | "
239 | `);
240 |
241 | expect(await process(input, noTitle)).toMatchInlineSnapshot(`
242 | "
243 |
244 | const a = 1;
245 |
246 |
247 | "
248 | `);
249 |
250 | expect(await process(input, options)).toMatchInlineSnapshot(`
251 | "
252 | title.js
253 | const a = 1;
254 |
255 |
256 | "
257 | `);
258 | });
259 |
260 | // ******************************************
261 | it("considers there is a language and a title", async () => {
262 | const input = dedent`
263 | \`\`\`javascript:title.js
264 | const a = 1;
265 | \`\`\`
266 | `;
267 |
268 | expect(await process(input)).toMatchInlineSnapshot(`
269 | "
270 |
271 | title.js
272 | const a = 1;
273 |
274 |
275 | "
276 | `);
277 |
278 | expect(await process(input, handleMissingLanguage)).toMatchInlineSnapshot(`
279 | "
280 |
281 | title.js
282 | const a = 1;
283 |
284 |
285 | "
286 | `);
287 |
288 | expect(await process(input, noContainer)).toMatchInlineSnapshot(`
289 | "
290 | title.js
291 | const a = 1;
292 |
293 | "
294 | `);
295 |
296 | expect(await process(input, noTitle)).toMatchInlineSnapshot(`
297 | "
298 |
299 | const a = 1;
300 |
301 |
302 | "
303 | `);
304 |
305 | expect(await process(input, options)).toMatchInlineSnapshot(`
306 | "
307 | title.js
308 | const a = 1;
309 |
310 |
311 | "
312 | `);
313 | });
314 |
315 | // ******************************************
316 | it("considers there is no language or title when there is a syntax for line numbers", async () => {
317 | const input = dedent`
318 | \`\`\`{1,3-4} showLineNumbers
319 | const a = 1;
320 | \`\`\`
321 | `;
322 |
323 | expect(await process(input)).toMatchInlineSnapshot(`
324 | "
325 |
326 | const a = 1;
327 |
328 |
329 | "
330 | `);
331 |
332 | expect(await process(input, handleMissingLanguage)).toMatchInlineSnapshot(`
333 | "
334 |
335 | const a = 1;
336 |
337 |
338 | "
339 | `);
340 |
341 | expect(await process(input, noContainer)).toMatchInlineSnapshot(`
342 | "
343 | const a = 1;
344 |
345 | "
346 | `);
347 |
348 | expect(await process(input, noTitle)).toMatchInlineSnapshot(`
349 | "
350 |
351 | const a = 1;
352 |
353 |
354 | "
355 | `);
356 |
357 | expect(await process(input, options)).toMatchInlineSnapshot(`
358 | "
359 |
360 | const a = 1;
361 |
362 |
363 | "
364 | `);
365 | });
366 |
367 | // ******************************************
368 | it("considers there is only language, no title when there is a syntax for line numbers", async () => {
369 | const input = dedent`
370 | \`\`\`javascript {1,3-4} showLineNumbers
371 | const a = 1;
372 | \`\`\`
373 | `;
374 |
375 | expect(await process(input)).toMatchInlineSnapshot(`
376 | "
377 |
378 | const a = 1;
379 |
380 |
381 | "
382 | `);
383 |
384 | expect(await process(input, handleMissingLanguage)).toMatchInlineSnapshot(`
385 | "
386 |
387 | const a = 1;
388 |
389 |
390 | "
391 | `);
392 |
393 | expect(await process(input, noContainer)).toMatchInlineSnapshot(`
394 | "
395 | const a = 1;
396 |
397 | "
398 | `);
399 |
400 | expect(await process(input, noTitle)).toMatchInlineSnapshot(`
401 | "
402 |
403 | const a = 1;
404 |
405 |
406 | "
407 | `);
408 |
409 | expect(await process(input, options)).toMatchInlineSnapshot(`
410 | "
411 |
412 | const a = 1;
413 |
414 |
415 | "
416 | `);
417 | });
418 |
419 | // ******************************************
420 | it("considers there is no language but only title when there is a syntax for line numbers", async () => {
421 | const input = dedent`
422 | \`\`\`:title.js {1,3-4} showLineNumbers
423 | const a = 1;
424 | \`\`\`
425 | `;
426 |
427 | expect(await process(input)).toMatchInlineSnapshot(`
428 | "
429 |
430 | title.js
431 | const a = 1;
432 |
433 |
434 | "
435 | `);
436 |
437 | expect(await process(input, handleMissingLanguage)).toMatchInlineSnapshot(`
438 | "
439 |
440 | title.js
441 | const a = 1;
442 |
443 |
444 | "
445 | `);
446 |
447 | expect(await process(input, noContainer)).toMatchInlineSnapshot(`
448 | "
449 | title.js
450 | const a = 1;
451 |
452 | "
453 | `);
454 |
455 | expect(await process(input, noTitle)).toMatchInlineSnapshot(`
456 | "
457 |
458 | const a = 1;
459 |
460 |
461 | "
462 | `);
463 |
464 | expect(await process(input, options)).toMatchInlineSnapshot(`
465 | "
466 | title.js
467 | const a = 1;
468 |
469 |
470 | "
471 | `);
472 | });
473 |
474 | // ******************************************
475 | it("considers there is a language and a title when there is a syntax for line numbers", async () => {
476 | const input = dedent`
477 | \`\`\`javascript:title.js {1,3-4} showLineNumbers
478 | const a = 1;
479 | \`\`\`
480 | `;
481 |
482 | expect(await process(input)).toMatchInlineSnapshot(`
483 | "
484 |
485 | title.js
486 | const a = 1;
487 |
488 |
489 | "
490 | `);
491 |
492 | expect(await process(input, handleMissingLanguage)).toMatchInlineSnapshot(`
493 | "
494 |
495 | title.js
496 | const a = 1;
497 |
498 |
499 | "
500 | `);
501 |
502 | expect(await process(input, noContainer)).toMatchInlineSnapshot(`
503 | "
504 | title.js
505 | const a = 1;
506 |
507 | "
508 | `);
509 |
510 | expect(await process(input, noTitle)).toMatchInlineSnapshot(`
511 | "
512 |
513 | const a = 1;
514 |
515 |
516 | "
517 | `);
518 |
519 | expect(await process(input, options)).toMatchInlineSnapshot(`
520 | "
521 | title.js
522 | const a = 1;
523 |
524 |
525 | "
526 | `);
527 | });
528 | });
529 |
--------------------------------------------------------------------------------
/tests/test.fixture.spec.ts:
--------------------------------------------------------------------------------
1 | import { it, expect } from "vitest";
2 | import dedent from "dedent";
3 |
4 | import { processMDAST } from "./util/index";
5 |
6 | const fixture = dedent`
7 | :
8 |
9 | :
10 | :
11 | js
12 | js
13 | js:
14 | js:
15 | js :
16 | js :
17 | js:title
18 | js :title
19 | js: title
20 | js : title
21 | js{1,2}
22 | js {1,2}
23 | js { 1, 2 }
24 | js{ 1, 2}
25 | js{1 ,2}
26 | js{1 , 2 }
27 | js:title{1,2}
28 | js:title{ 1,2}
29 | js:title{1, 2 }
30 | js:title {1,2}
31 | js:title {1, 2 }
32 | js:title { 1, 2 }
33 | js :title{1,2}
34 | js :title{1 ,2 }
35 | js :title {1,2}
36 | js :title { 1 , 2 }
37 | js: title{1,2}
38 | js: title{1 ,2 }
39 | js: title {1,2}
40 | js: title { 1 , 2 }
41 | js : title{1,2}
42 | js : title{1 ,2 }
43 | js : title {1,2}
44 | js : title { 1 , 2 }
45 | js:title{1,2}showLineNumbers
46 | js:title{1,2} showLineNumbers
47 | js:title {1,2} showLineNumbers
48 | js:title { 1, 2} showLineNumbers
49 | js:title {1, 2}showLineNumbers
50 | js:{1,2}showLineNumbers
51 | js:{1,2} showLineNumbers
52 | js: {1,2} showLineNumbers
53 | js: { 1, 2} showLineNumbers
54 | js: {1, 2}showLineNumbers
55 | js :{1,2}showLineNumbers
56 | js :{1,2} showLineNumbers
57 | js : {1,2} showLineNumbers
58 | js : { 1, 2} showLineNumbers
59 | js : {1, 2}showLineNumbers
60 | js{1,2}showLineNumbers
61 | js{1,2} showLineNumbers
62 | js {1,2} showLineNumbers
63 | js { 1, 2} showLineNumbers
64 | js {1, 2}showLineNumbers
65 | js:showLineNumbers{1,2}
66 | js: showLineNumbers{1,2}
67 | js :showLineNumbers{1,2}
68 | js : showLineNumbers{1,2}
69 | js showLineNumbers {1,2}
70 | js showLineNumbers {1 , 2 }
71 | js:title showLineNumbers {1 , 2 }
72 | js :title showLineNumbers {1 , 2 }
73 | js : title showLineNumbers {1 , 2 }
74 | js :showLineNumbers {1 , 2 }
75 | js : showLineNumbers {1 , 2 }
76 | showLineNumbers{1,2}
77 | showLineNumbers {1 , 2 }
78 | {1,2}showLineNumbers
79 | {1 , 2}showLineNumbers
80 | showLineNumbers{1,2}:title
81 | showLineNumbers{1,2} : title
82 | showLineNumbers {1 , 2 }:title
83 | showLineNumbers {1 , 2 } :title
84 | {1,2}showLineNumbers:title
85 | {1,2}showLineNumbers : title
86 | {1 , 2}showLineNumbers:title
87 | {1, 2}showLineNumbers : title
88 | js:long@title@with@space{1,2}showLineNumbers
89 | js:long@title@with@space {1,2} showLineNumbers
90 | `;
91 |
92 | it("parses the language, title and meta correctly", async () => {
93 | const result = fixture.split("\n").map((input) => ({
94 | _____: input,
95 | ...processMDAST("```" + input + "\n```", {
96 | tokenForSpaceInTitle: "@",
97 | }),
98 | }));
99 |
100 | expect(result).toMatchInlineSnapshot(`
101 | [
102 | {
103 | "_____": ":",
104 | "_lang": null,
105 | "_meta": null,
106 | "title": null,
107 | },
108 | {
109 | "_____": "",
110 | "_lang": null,
111 | "_meta": null,
112 | "title": null,
113 | },
114 | {
115 | "_____": " :",
116 | "_lang": null,
117 | "_meta": null,
118 | "title": null,
119 | },
120 | {
121 | "_____": " :",
122 | "_lang": null,
123 | "_meta": null,
124 | "title": null,
125 | },
126 | {
127 | "_____": "js",
128 | "_lang": "js",
129 | "_meta": null,
130 | "title": null,
131 | },
132 | {
133 | "_____": " js",
134 | "_lang": "js",
135 | "_meta": null,
136 | "title": null,
137 | },
138 | {
139 | "_____": "js:",
140 | "_lang": "js",
141 | "_meta": null,
142 | "title": null,
143 | },
144 | {
145 | "_____": " js:",
146 | "_lang": "js",
147 | "_meta": null,
148 | "title": null,
149 | },
150 | {
151 | "_____": "js :",
152 | "_lang": "js",
153 | "_meta": null,
154 | "title": null,
155 | },
156 | {
157 | "_____": " js :",
158 | "_lang": "js",
159 | "_meta": null,
160 | "title": null,
161 | },
162 | {
163 | "_____": "js:title",
164 | "_lang": "js",
165 | "_meta": null,
166 | "title": "title",
167 | },
168 | {
169 | "_____": "js :title",
170 | "_lang": "js",
171 | "_meta": null,
172 | "title": "title",
173 | },
174 | {
175 | "_____": "js: title",
176 | "_lang": "js",
177 | "_meta": null,
178 | "title": "title",
179 | },
180 | {
181 | "_____": "js : title",
182 | "_lang": "js",
183 | "_meta": null,
184 | "title": "title",
185 | },
186 | {
187 | "_____": "js{1,2}",
188 | "_lang": "js",
189 | "_meta": "{1,2}",
190 | "title": null,
191 | },
192 | {
193 | "_____": "js {1,2}",
194 | "_lang": "js",
195 | "_meta": "{1,2}",
196 | "title": null,
197 | },
198 | {
199 | "_____": "js { 1, 2 }",
200 | "_lang": "js",
201 | "_meta": "{1,2}",
202 | "title": null,
203 | },
204 | {
205 | "_____": "js{ 1, 2}",
206 | "_lang": "js",
207 | "_meta": "{1,2}",
208 | "title": null,
209 | },
210 | {
211 | "_____": "js{1 ,2}",
212 | "_lang": "js",
213 | "_meta": "{1,2}",
214 | "title": null,
215 | },
216 | {
217 | "_____": "js{1 , 2 }",
218 | "_lang": "js",
219 | "_meta": "{1,2}",
220 | "title": null,
221 | },
222 | {
223 | "_____": "js:title{1,2}",
224 | "_lang": "js",
225 | "_meta": "{1,2}",
226 | "title": "title",
227 | },
228 | {
229 | "_____": "js:title{ 1,2}",
230 | "_lang": "js",
231 | "_meta": "{1,2}",
232 | "title": "title",
233 | },
234 | {
235 | "_____": "js:title{1, 2 }",
236 | "_lang": "js",
237 | "_meta": "{1,2}",
238 | "title": "title",
239 | },
240 | {
241 | "_____": "js:title {1,2}",
242 | "_lang": "js",
243 | "_meta": "{1,2}",
244 | "title": "title",
245 | },
246 | {
247 | "_____": "js:title {1, 2 }",
248 | "_lang": "js",
249 | "_meta": "{1,2}",
250 | "title": "title",
251 | },
252 | {
253 | "_____": "js:title { 1, 2 }",
254 | "_lang": "js",
255 | "_meta": "{1,2}",
256 | "title": "title",
257 | },
258 | {
259 | "_____": "js :title{1,2}",
260 | "_lang": "js",
261 | "_meta": "{1,2}",
262 | "title": "title",
263 | },
264 | {
265 | "_____": "js :title{1 ,2 }",
266 | "_lang": "js",
267 | "_meta": "{1,2}",
268 | "title": "title",
269 | },
270 | {
271 | "_____": "js :title {1,2}",
272 | "_lang": "js",
273 | "_meta": "{1,2}",
274 | "title": "title",
275 | },
276 | {
277 | "_____": "js :title { 1 , 2 }",
278 | "_lang": "js",
279 | "_meta": "{1,2}",
280 | "title": "title",
281 | },
282 | {
283 | "_____": "js: title{1,2}",
284 | "_lang": "js",
285 | "_meta": "{1,2}",
286 | "title": "title",
287 | },
288 | {
289 | "_____": "js: title{1 ,2 }",
290 | "_lang": "js",
291 | "_meta": "{1,2}",
292 | "title": "title",
293 | },
294 | {
295 | "_____": "js: title {1,2}",
296 | "_lang": "js",
297 | "_meta": "{1,2}",
298 | "title": "title",
299 | },
300 | {
301 | "_____": "js: title { 1 , 2 }",
302 | "_lang": "js",
303 | "_meta": "{1,2}",
304 | "title": "title",
305 | },
306 | {
307 | "_____": "js : title{1,2}",
308 | "_lang": "js",
309 | "_meta": "{1,2}",
310 | "title": "title",
311 | },
312 | {
313 | "_____": "js : title{1 ,2 }",
314 | "_lang": "js",
315 | "_meta": "{1,2}",
316 | "title": "title",
317 | },
318 | {
319 | "_____": "js : title {1,2}",
320 | "_lang": "js",
321 | "_meta": "{1,2}",
322 | "title": "title",
323 | },
324 | {
325 | "_____": "js : title { 1 , 2 }",
326 | "_lang": "js",
327 | "_meta": "{1,2}",
328 | "title": "title",
329 | },
330 | {
331 | "_____": "js:title{1,2}showLineNumbers",
332 | "_lang": "js",
333 | "_meta": "{1,2} showLineNumbers",
334 | "title": "title",
335 | },
336 | {
337 | "_____": "js:title{1,2} showLineNumbers",
338 | "_lang": "js",
339 | "_meta": "{1,2} showLineNumbers",
340 | "title": "title",
341 | },
342 | {
343 | "_____": "js:title {1,2} showLineNumbers",
344 | "_lang": "js",
345 | "_meta": "{1,2} showLineNumbers",
346 | "title": "title",
347 | },
348 | {
349 | "_____": "js:title { 1, 2} showLineNumbers",
350 | "_lang": "js",
351 | "_meta": "{1,2} showLineNumbers",
352 | "title": "title",
353 | },
354 | {
355 | "_____": "js:title {1, 2}showLineNumbers",
356 | "_lang": "js",
357 | "_meta": "{1,2} showLineNumbers",
358 | "title": "title",
359 | },
360 | {
361 | "_____": "js:{1,2}showLineNumbers",
362 | "_lang": "js",
363 | "_meta": "{1,2} showLineNumbers",
364 | "title": null,
365 | },
366 | {
367 | "_____": "js:{1,2} showLineNumbers",
368 | "_lang": "js",
369 | "_meta": "{1,2} showLineNumbers",
370 | "title": null,
371 | },
372 | {
373 | "_____": "js: {1,2} showLineNumbers",
374 | "_lang": "js",
375 | "_meta": "{1,2} showLineNumbers",
376 | "title": null,
377 | },
378 | {
379 | "_____": "js: { 1, 2} showLineNumbers",
380 | "_lang": "js",
381 | "_meta": "{1,2} showLineNumbers",
382 | "title": null,
383 | },
384 | {
385 | "_____": "js: {1, 2}showLineNumbers",
386 | "_lang": "js",
387 | "_meta": "{1,2} showLineNumbers",
388 | "title": null,
389 | },
390 | {
391 | "_____": "js :{1,2}showLineNumbers",
392 | "_lang": "js",
393 | "_meta": "{1,2} showLineNumbers",
394 | "title": null,
395 | },
396 | {
397 | "_____": "js :{1,2} showLineNumbers",
398 | "_lang": "js",
399 | "_meta": "{1,2} showLineNumbers",
400 | "title": null,
401 | },
402 | {
403 | "_____": "js : {1,2} showLineNumbers",
404 | "_lang": "js",
405 | "_meta": "{1,2} showLineNumbers",
406 | "title": null,
407 | },
408 | {
409 | "_____": "js : { 1, 2} showLineNumbers",
410 | "_lang": "js",
411 | "_meta": "{1,2} showLineNumbers",
412 | "title": null,
413 | },
414 | {
415 | "_____": "js : {1, 2}showLineNumbers",
416 | "_lang": "js",
417 | "_meta": "{1,2} showLineNumbers",
418 | "title": null,
419 | },
420 | {
421 | "_____": "js{1,2}showLineNumbers",
422 | "_lang": "js",
423 | "_meta": "{1,2} showLineNumbers",
424 | "title": null,
425 | },
426 | {
427 | "_____": "js{1,2} showLineNumbers",
428 | "_lang": "js",
429 | "_meta": "{1,2} showLineNumbers",
430 | "title": null,
431 | },
432 | {
433 | "_____": "js {1,2} showLineNumbers",
434 | "_lang": "js",
435 | "_meta": "{1,2} showLineNumbers",
436 | "title": null,
437 | },
438 | {
439 | "_____": "js { 1, 2} showLineNumbers",
440 | "_lang": "js",
441 | "_meta": "{1,2} showLineNumbers",
442 | "title": null,
443 | },
444 | {
445 | "_____": "js {1, 2}showLineNumbers",
446 | "_lang": "js",
447 | "_meta": "{1,2} showLineNumbers",
448 | "title": null,
449 | },
450 | {
451 | "_____": "js:showLineNumbers{1,2}",
452 | "_lang": "js",
453 | "_meta": "{1,2} showLineNumbers",
454 | "title": null,
455 | },
456 | {
457 | "_____": "js: showLineNumbers{1,2}",
458 | "_lang": "js",
459 | "_meta": "showLineNumbers {1,2}",
460 | "title": null,
461 | },
462 | {
463 | "_____": "js :showLineNumbers{1,2}",
464 | "_lang": "js",
465 | "_meta": "showLineNumbers {1,2}",
466 | "title": null,
467 | },
468 | {
469 | "_____": "js : showLineNumbers{1,2}",
470 | "_lang": "js",
471 | "_meta": "showLineNumbers {1,2}",
472 | "title": null,
473 | },
474 | {
475 | "_____": "js showLineNumbers {1,2}",
476 | "_lang": "js",
477 | "_meta": "showLineNumbers {1,2}",
478 | "title": null,
479 | },
480 | {
481 | "_____": "js showLineNumbers {1 , 2 }",
482 | "_lang": "js",
483 | "_meta": "showLineNumbers {1,2}",
484 | "title": null,
485 | },
486 | {
487 | "_____": "js:title showLineNumbers {1 , 2 }",
488 | "_lang": "js",
489 | "_meta": "showLineNumbers {1,2}",
490 | "title": "title",
491 | },
492 | {
493 | "_____": "js :title showLineNumbers {1 , 2 }",
494 | "_lang": "js",
495 | "_meta": "showLineNumbers {1,2}",
496 | "title": "title",
497 | },
498 | {
499 | "_____": "js : title showLineNumbers {1 , 2 }",
500 | "_lang": "js",
501 | "_meta": "showLineNumbers {1,2}",
502 | "title": "title",
503 | },
504 | {
505 | "_____": "js :showLineNumbers {1 , 2 }",
506 | "_lang": "js",
507 | "_meta": "showLineNumbers {1,2}",
508 | "title": null,
509 | },
510 | {
511 | "_____": "js : showLineNumbers {1 , 2 }",
512 | "_lang": "js",
513 | "_meta": "showLineNumbers {1,2}",
514 | "title": null,
515 | },
516 | {
517 | "_____": "showLineNumbers{1,2}",
518 | "_lang": null,
519 | "_meta": "{1,2} showLineNumbers",
520 | "title": null,
521 | },
522 | {
523 | "_____": "showLineNumbers {1 , 2 }",
524 | "_lang": null,
525 | "_meta": "{1,2} showLineNumbers",
526 | "title": null,
527 | },
528 | {
529 | "_____": "{1,2}showLineNumbers",
530 | "_lang": null,
531 | "_meta": "{1,2} showLineNumbers",
532 | "title": null,
533 | },
534 | {
535 | "_____": "{1 , 2}showLineNumbers",
536 | "_lang": null,
537 | "_meta": "{1,2} showLineNumbers",
538 | "title": null,
539 | },
540 | {
541 | "_____": "showLineNumbers{1,2}:title",
542 | "_lang": null,
543 | "_meta": "{1,2} showLineNumbers",
544 | "title": "title",
545 | },
546 | {
547 | "_____": "showLineNumbers{1,2} : title",
548 | "_lang": null,
549 | "_meta": "{1,2} showLineNumbers",
550 | "title": "title",
551 | },
552 | {
553 | "_____": "showLineNumbers {1 , 2 }:title",
554 | "_lang": null,
555 | "_meta": "{1,2} showLineNumbers",
556 | "title": "title",
557 | },
558 | {
559 | "_____": "showLineNumbers {1 , 2 } :title",
560 | "_lang": null,
561 | "_meta": "{1,2} showLineNumbers",
562 | "title": "title",
563 | },
564 | {
565 | "_____": "{1,2}showLineNumbers:title",
566 | "_lang": null,
567 | "_meta": "{1,2} showLineNumbers",
568 | "title": "title",
569 | },
570 | {
571 | "_____": "{1,2}showLineNumbers : title",
572 | "_lang": null,
573 | "_meta": "{1,2} showLineNumbers",
574 | "title": "title",
575 | },
576 | {
577 | "_____": "{1 , 2}showLineNumbers:title",
578 | "_lang": null,
579 | "_meta": "{1,2} showLineNumbers",
580 | "title": "title",
581 | },
582 | {
583 | "_____": "{1, 2}showLineNumbers : title",
584 | "_lang": null,
585 | "_meta": "{1,2} showLineNumbers",
586 | "title": "title",
587 | },
588 | {
589 | "_____": "js:long@title@with@space{1,2}showLineNumbers",
590 | "_lang": "js",
591 | "_meta": "{1,2} showLineNumbers",
592 | "title": "long title with space",
593 | },
594 | {
595 | "_____": "js:long@title@with@space {1,2} showLineNumbers",
596 | "_lang": "js",
597 | "_meta": "{1,2} showLineNumbers",
598 | "title": "long title with space",
599 | },
600 | ]
601 | `);
602 | });
603 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### [Become a sponsor](https://github.com/sponsors/ipikuka) 🚀
2 |
3 | If you find **`remark-flexible-code-titles`** useful in your projects, consider supporting my work.
4 | Your sponsorship means a lot 💖
5 |
6 | Be the **first sponsor** and get featured here and on [my sponsor wall](https://github.com/sponsors/ipikuka).
7 | Thank you for supporting open source! 🙌
8 |
9 | # remark-flexible-code-titles
10 |
11 | [![npm version][badge-npm-version]][url-npm-package]
12 | [![npm downloads][badge-npm-download]][url-npm-package]
13 | [![publish to npm][badge-publish-to-npm]][url-publish-github-actions]
14 | [![code-coverage][badge-codecov]][url-codecov]
15 | [![type-coverage][badge-type-coverage]][url-github-package]
16 | [![typescript][badge-typescript]][url-typescript]
17 | [![license][badge-license]][url-license]
18 |
19 | This package is a [**unified**][unified] ([**remark**][remark]) plugin **to add title or/and container for code blocks with customizable properties in markdown.**
20 |
21 | [**unified**][unified] is a project that transforms content with abstract syntax trees (ASTs) using the new parser [**micromark**][micromark]. [**remark**][remark] adds support for markdown to unified. [**mdast**][mdast] is the Markdown Abstract Syntax Tree (AST) which is a specification for representing markdown in a syntax tree.
22 |
23 | **This plugin is a remark plugin that transforms the mdast.**
24 |
25 | ## When should I use this?
26 |
27 | **`remark-flexible-code-titles`** is useful if you want to **add title and container or any of two** for code blocks in markdown. It is able to:
28 |
29 | - add `title` node above the `code` node, providing _custom tag name, custom class name and also additional properties_.
30 | - add `container` node for the `code` node, providing _custom tag name, custom class name and also additional properties_.
31 | - correct the syntax of code highligting directives on behalf of related rehype plugins (like [rehype-prism-plus][rehypeprismplus])
32 | - handle the titles even if there is no language provided,
33 | - handle the titles composed by more than one word (handle spaces in the title),
34 | - provide a fallback language as an option if the language is missing.
35 |
36 | ## Installation
37 |
38 | This package is suitable for ESM only. In Node.js (16.0+), install with npm:
39 |
40 | ```bash
41 | npm install remark-flexible-code-titles
42 | ```
43 |
44 | or
45 |
46 | ```bash
47 | yarn add remark-flexible-code-titles
48 | ```
49 |
50 | ## Usage
51 |
52 | Say we have the following file, `example.md`, which consists a code block. The code block's language is "javascript" and its title is "file.js" specified _after a colon_ **`:`**
53 |
54 | ````markdown
55 | ```javascript:file.js
56 |
57 | ```
58 | ````
59 |
60 | And our module, `example.js`, looks as follows:
61 |
62 | ```javascript
63 | import { read } from "to-vfile";
64 | import remark from "remark";
65 | import gfm from "remark-gfm";
66 | import remarkRehype from "remark-rehype";
67 | import rehypeStringify from "rehype-stringify";
68 | import remarkCodeTitles from "remark-flexible-code-titles";
69 |
70 | main();
71 |
72 | async function main() {
73 | const file = await remark()
74 | .use(gfm)
75 | .use(remarkCodeTitles)
76 | .use(remarkRehype)
77 | .use(rehypeStringify)
78 | .process(await read("example.md"));
79 |
80 | console.log(String(file));
81 | }
82 | ```
83 |
84 | Now, running `node example.js` yields:
85 |
86 | ```html
87 |
88 | file.js
89 |
90 |
91 |
92 |
93 |
94 |
95 | ```
96 |
97 | Without **`remark-flexible-code-titles`**, you’d get:
98 |
99 | ```html
100 |
101 |
102 |
103 |
104 |
105 | ```
106 |
107 | You can use **`remark-flexible-code-titles`** even **without a language**, _setting the title just after a colon_ **`:`**
108 |
109 | ````markdown
110 | ```:title
111 | This is a line of pseudo code.
112 | ```
113 | ````
114 |
115 | ## Options
116 |
117 | All options are **optional** and some of them have **default values**.
118 |
119 | ```tsx
120 | type RestrictedRecord = Record & { className?: never };
121 | type PropertyFunction = (language?: string, title?: string) => RestrictedRecord;
122 |
123 | use(remarkCodeTitles, {
124 | title?: boolean; // default is true
125 | titleTagName?: string; // default is "div"
126 | titleClassName?: string; // default is "remark-code-title"
127 | titleProperties?: PropertyFunction;
128 | container?: boolean; // default is true
129 | containerTagName?: string; // default is "div"
130 | containerClassName?: string; // default is "remark-code-container"
131 | containerProperties?: PropertyFunction;
132 | handleMissingLanguageAs?: string;
133 | tokenForSpaceInTitle?: string;
134 | } as CodeTitleOptions);
135 | ```
136 |
137 | #### `title`
138 |
139 | It is a **boolean** option for whether or not to add a `title` node.
140 |
141 | By default, it is `true`, meaningly adds a `title` node if a title is provided in the language part of the code block.
142 |
143 | ```javascript
144 | use(remarkCodeTitles, {
145 | title: false,
146 | });
147 | ```
148 |
149 | If the option is `false`, the plugin will not add any `title` node.
150 |
151 | ````markdown
152 | ```javascript:file.js
153 | console.log("Hi")
154 | ```
155 | ````
156 |
157 | ```html
158 |
159 |
160 |
161 | console.log("Hi")
162 |
163 |
164 | ```
165 |
166 | #### `titleTagName`
167 |
168 | It is a **string** option for providing custom HTML tag name for `title` nodes.
169 |
170 | By default, it is `div`.
171 |
172 | ```javascript
173 | use(remarkCodeTitles, {
174 | titleTagName: "span",
175 | });
176 | ```
177 |
178 | Now, the title element tag names will be `span`.
179 |
180 | ````markdown
181 | ```javascript:file.js
182 | console.log("Hi")
183 | ```
184 | ````
185 |
186 | ```html
187 |
188 | file.js
189 |
190 | console.log("Hi")
191 |
192 |
193 | ```
194 |
195 | #### `titleClassName`
196 |
197 | It is a **string** option for providing custom class name for `title` nodes.
198 |
199 | By default, it is `remark-code-title`, and all title elements' class names will contain `remark-code-title`.
200 |
201 | ```javascript
202 | use(remarkCodeTitles, {
203 | titleClassName: "custom-code-title",
204 | });
205 | ```
206 |
207 | Now, the title element class names will be `custom-code-title`.
208 |
209 | ````markdown
210 | ```javascript:file.js
211 | console.log("Hi")
212 | ```
213 | ````
214 |
215 | ```html
216 |
217 | file.js
218 |
219 | console.log("Hi")
220 |
221 |
222 | ```
223 |
224 | #### `titleProperties`
225 |
226 | It is a **callback** `(language?: string, title?: string) => Record & { className?: never }` option to set additional properties for the `title` node.
227 |
228 | The callback function that takes the `language` and the `title` as optional arguments and returns **object** which is going to be used for adding additional properties into the `title` node.
229 |
230 | **The `className` key is forbidden and effectless in the returned object.**
231 |
232 | ```javascript
233 | use(remarkCodeTitles, {
234 | titleProperties(language, title) {
235 | return {
236 | title,
237 | ["data-language"]: language,
238 | };
239 | },
240 | });
241 | ```
242 |
243 | Now, the title elements will contain `title` and `data-color` properties.
244 |
245 | ````markdown
246 | ```javascript:file.js
247 | console.log("Hi")
248 | ```
249 | ````
250 |
251 | ```html
252 |
253 | file.js
254 |
255 | console.log("Hi")
256 |
257 |
258 | ```
259 |
260 | #### `container`
261 |
262 | It is a **boolean** option for whether or not to add a `container` node.
263 |
264 | By default, it is `true`, meaningly adds a `container` node.
265 |
266 | ```javascript
267 | use(remarkCodeTitles, {
268 | container: false,
269 | });
270 | ```
271 |
272 | If the option is `false`, the plugin doesn't add any `container` node.
273 |
274 | ````markdown
275 | ```javascript:file.js
276 | console.log("Hi")
277 | ```
278 | ````
279 |
280 | ```html
281 |
282 | file.js
283 |
284 | console.log("Hi")
285 |
286 |
287 | ```
288 |
289 | #### `containerTagName`
290 |
291 | It is a **string** option for providing custom HTML tag name for `container` nodes.
292 |
293 | By default, it is `div`.
294 |
295 | ```javascript
296 | use(remarkCodeTitles, {
297 | containerTagName: "section",
298 | });
299 | ```
300 |
301 | Now, the container element tag names will be `section`.
302 |
303 | ````markdown
304 | ```javascript:file.js
305 | console.log("Hi")
306 | ```
307 | ````
308 |
309 | ```html
310 |
311 | file.js
312 |
313 | console.log("Hi")
314 |
315 |
316 | ```
317 |
318 | #### `containerClassName`
319 |
320 | It is a **string** option for providing custom class name for `container` nodes.
321 |
322 | By default, it is `remark-code-container`, and all container elements' class names will contain `remark-code-container`.
323 |
324 | ```javascript
325 | use(remarkCodeTitles, {
326 | containerClassName: "custom-code-container",
327 | });
328 | ```
329 |
330 | Now, the container element class names will be `custom-code-container`.
331 |
332 | ````markdown
333 | ```javascript:file.js
334 | console.log("Hi")
335 | ```
336 | ````
337 |
338 | ```html
339 |
340 | file.js
341 |
342 | console.log("Hi")
343 |
344 |
345 | ```
346 |
347 | #### `containerProperties`
348 |
349 | It is a **callback** `(language?: string, title?: string) => Record & { className?: never }` option to set additional properties for the `container` node.
350 |
351 | The callback function that takes the `language` and the `title` as optional arguments and returns **object** which is going to be used for adding additional properties into the `container` node.
352 |
353 | **The `className` key is forbidden and effectless in the returned object.**
354 |
355 | ```javascript
356 | use(remarkCodeTitles, {
357 | titleProperties(language, title) {
358 | return {
359 | title,
360 | ["data-language"]: language,
361 | };
362 | },
363 | });
364 | ```
365 |
366 | Now, the container elements will contain `title` and `data-color` properties.
367 |
368 | ````markdown
369 | ```javascript:file.js
370 | console.log("Hi")
371 | ```
372 | ````
373 |
374 | ```html
375 |
376 | file.js
377 |
378 | console.log("Hi")
379 |
380 |
381 | ```
382 |
383 | #### `handleMissingLanguageAs`
384 |
385 | It is a **string** option for providing a fallback language if the language is missing.
386 |
387 | ```javascript
388 | use(remarkCodeTitles, {
389 | handleMissingLanguageAs: "unknown",
390 | });
391 | ```
392 |
393 | Now, the class name of `` elements will contain `language-unknown` if the language is missing. If this option was not set, the `class` property would not be presented in the ``element.
394 |
395 | ````markdown
396 | ```
397 | Hello from code block
398 | ```
399 | ````
400 |
401 | ```html
402 |
403 |
404 | Hello from code block
405 |
406 |
407 | ```
408 |
409 | #### `tokenForSpaceInTitle`
410 |
411 | It is a **string** option for composing the title with more than one word.
412 |
413 | Normally, **`remark-flexible-code-titles`** can match a code title which is the word that comes after a colon and ends in the first space it encounters. This option is provided to replace a space with a token in order to specify a code title consisting of more than one word.
414 |
415 | ```javascript
416 | use(remarkCodeTitles, {
417 | tokenForSpaceInTitle: "@",
418 | });
419 | ```
420 |
421 | Now, the titles that have more than one word can be set using the token `@`.
422 |
423 | ````markdown
424 | ```bash:Useful@Bash@Commands
425 | mkdir project-directory
426 | ```
427 | ````
428 |
429 | ```html
430 |
431 | Useful Bash Commands
432 |
433 | mkdir project-directory
434 |
435 |
436 | ```
437 |
438 | ## Examples:
439 |
440 | #### Example for only container
441 |
442 | ````markdown
443 | ```javascript:file.js
444 | let me = "ipikuka";
445 | ```
446 | ````
447 |
448 | ```javascript
449 | use(remarkCodeTitles, {
450 | title: false,
451 | containerTagName: "section",
452 | containerClassName: "custom-code-wrapper",
453 | containerProperties(language, title) {
454 | return {
455 | ["data-language"]: language,
456 | title,
457 | };
458 | },
459 | });
460 | ```
461 |
462 | is going to produce the container `section` element like below:
463 |
464 | ```html
465 |
466 |
467 | let me = "ipikuka";
468 |
469 |
470 | ```
471 |
472 | #### Example for only title
473 |
474 | ````markdown
475 | ```javascript:file.js
476 | let me = "ipikuka";
477 | ```
478 | ````
479 |
480 | ```javascript
481 | use(remarkCodeTitles, {
482 | container: false,
483 | titleTagName: "span",
484 | titleClassName: "custom-code-title",
485 | titleProperties: (language, title) => {
486 | ["data-language"]: language,
487 | title,
488 | },
489 | });
490 | ```
491 |
492 | is going to produce the title `span` element just before the code block, like below:
493 |
494 | ```html
495 | file.js
496 |
497 | let me = "ipikuka";
498 |
499 | ```
500 |
501 | #### Example for line highlighting and numbering
502 |
503 | > [!NOTE]
504 | > You need a rehype plugin like [**rehype-prism-plus**][rehypeprismplus] or [**rehype-highlight-code-lines**][rehypehighlightcodelines] for line highlighting and numbering features.
505 |
506 | ````markdown
507 | ```javascript:file.js {1,3-6} showLineNumbers
508 | let me = "ipikuka";
509 | ```
510 | ````
511 |
512 | **`remark-flexible-code-titles`** takes the line highlighting and numbering syntax into consideration, and passes that information to other remark and rehype plugins.
513 |
514 | But, if you want to highlight and number the lines **without specifying language**, you will get the language of the code block as for example `language-{2}` like strings. Let me give an example:
515 |
516 | ````markdown
517 | ```{2} showLineNumbers
518 | This is a line which is going to be numbered with rehype-prism-plus.
519 | This is a line which is going to be highlighted and numbered with rehype-prism-plus.
520 | ```
521 | ````
522 |
523 | The above markdown, with no language provided, will lead to produce a mdast "code" node as follows:
524 |
525 | ```json
526 | {
527 | "type": "code",
528 | "lang": "{2}",
529 | "meta": "showLineNumbers"
530 | }
531 | ```
532 |
533 | As a result, the html `code` element will have wrong language `language-{2}`:
534 | _(The class `code-highlight` in the `code` element is added by the rehype plugin `rehype-prism-plus`)_
535 |
536 | ```html
537 | ...
538 | ```
539 |
540 | **`remark-flexible-code-titles`** not only adds `title` and `container` elements but also **corrects the language** producing the below `mdast` which will lead the `` element has accurate language or not have language as it sould be.
541 |
542 | ```json
543 | {
544 | "type": "code",
545 | "lang": null,
546 | "meta": "{2} showLineNumbers"
547 | }
548 | ```
549 |
550 | ```html
551 |
552 |
553 |
554 | ```
555 |
556 | If there is no space between the parts (_title, line range string and "showLineNumbers"_), or there is extra spaces inside the _line range string_, line highlighting or numbering features by the rehype plugin will not work. **`remark-flexible-code-titles` can handles and corrects this kind of mis-typed situations**.
557 |
558 | ````markdown
559 | ```typescript:title{ 1, 3 - 6 }showLineNumbers
560 | content
561 | ```
562 | ````
563 |
564 | There is mis-typed syntax in above markdown example; and without **`remark-flexible-code-titles`** will cause to produce the following `mdast`; and the rehype plugin not to work properly:
565 |
566 | ```json
567 | {
568 | "type": "code",
569 | "lang": "typescript:title{",
570 | "meta": " 1, 3 - 6 }showLineNumbers"
571 | }
572 | ```
573 |
574 | **`remark-flexible-code-titles`** will correct the syntax and ensure to produce the following `mdast` and `html`:
575 |
576 | ```json
577 | {
578 | "type": "code",
579 | "lang": "typescript",
580 | "meta": "{1,3-6} showLineNumbers"
581 | }
582 | ```
583 |
584 | ```html
585 |
586 | title
587 |
588 |
589 |
590 |
591 |
592 |
593 | ```
594 |
595 | #### Example for providing a title without any language
596 |
597 | You can provide a `title` without any language just using a colon **`:`** at the beginning.
598 |
599 | ````markdown
600 | ```:title
601 | content
602 | ```
603 | ````
604 |
605 | ```html
606 |
607 | title
608 |
609 | content
610 |
611 |
612 | ```
613 |
614 | ### Another flexible usage
615 |
616 | You can use **`remark-flexible-code-titles**` **just for only correcting language, line highlighting and numbering syntax** on behalf of related rehype plugins.
617 |
618 | ```javascript
619 | use(remarkCodeTitles, {
620 | container: false,
621 | title: false,
622 | });
623 | ```
624 |
625 | Now, **`remark-flexible-code-titles`** will not add any node, but will correct language, line highlighting and numbering syntax.
626 |
627 | ## Syntax tree
628 |
629 | This plugin only modifies the mdast (markdown abstract syntax tree) as explained.
630 |
631 | ## Types
632 |
633 | This package is fully typed with [TypeScript][url-typescript]. The plugin options' type is exported as `CodeTitleOptions`.
634 |
635 | ## Compatibility
636 |
637 | This plugin works with `unified` version 6+ and `remark` version 7+. It is compatible with `mdx` version 2+.
638 |
639 | ## Security
640 |
641 | Use of **`remark-flexible-code-titles`** does not involve rehype (hast) or user content so there are no openings for cross-site scripting (XSS) attacks.
642 |
643 | ## My Plugins
644 |
645 | I like to contribute the Unified / Remark / MDX ecosystem, so I recommend you to have a look my plugins.
646 |
647 | ### My Remark Plugins
648 |
649 | - [`remark-flexible-code-titles`](https://www.npmjs.com/package/remark-flexible-code-titles)
650 | – Remark plugin to add titles or/and containers for the code blocks with customizable properties
651 | - [`remark-flexible-containers`](https://www.npmjs.com/package/remark-flexible-containers)
652 | – Remark plugin to add custom containers with customizable properties in markdown
653 | - [`remark-ins`](https://www.npmjs.com/package/remark-ins)
654 | – Remark plugin to add `ins` element in markdown
655 | - [`remark-flexible-paragraphs`](https://www.npmjs.com/package/remark-flexible-paragraphs)
656 | – Remark plugin to add custom paragraphs with customizable properties in markdown
657 | - [`remark-flexible-markers`](https://www.npmjs.com/package/remark-flexible-markers)
658 | – Remark plugin to add custom `mark` element with customizable properties in markdown
659 | - [`remark-flexible-toc`](https://www.npmjs.com/package/remark-flexible-toc)
660 | – Remark plugin to expose the table of contents via `vfile.data` or via an option reference
661 | - [`remark-mdx-remove-esm`](https://www.npmjs.com/package/remark-mdx-remove-esm)
662 | – Remark plugin to remove import and/or export statements (mdxjsEsm)
663 |
664 | ### My Rehype Plugins
665 |
666 | - [`rehype-pre-language`](https://www.npmjs.com/package/rehype-pre-language)
667 | – Rehype plugin to add language information as a property to `pre` element
668 | - [`rehype-highlight-code-lines`](https://www.npmjs.com/package/rehype-highlight-code-lines)
669 | – Rehype plugin to add line numbers to code blocks and allow highlighting of desired code lines
670 | - [`rehype-code-meta`](https://www.npmjs.com/package/rehype-code-meta)
671 | – Rehype plugin to copy `code.data.meta` to `code.properties.metastring`
672 | - [`rehype-image-toolkit`](https://www.npmjs.com/package/rehype-image-toolkit)
673 | – Rehype plugin to enhance Markdown image syntax `![]()` and Markdown/MDX media elements (`
`, `