@intlayer/core 9.4.1 → 9.4.2

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 (72) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/index.cjs +8 -4
  3. package/dist/cjs/localization/getCanonicalPath.cjs +52 -0
  4. package/dist/cjs/localization/getCanonicalPath.cjs.map +1 -0
  5. package/dist/cjs/localization/getCanonicalPath.test-d.cjs +12 -0
  6. package/dist/cjs/localization/getCanonicalPath.test-d.cjs.map +1 -0
  7. package/dist/cjs/localization/getLocalizedPath.cjs +4 -2
  8. package/dist/cjs/localization/getLocalizedPath.cjs.map +1 -1
  9. package/dist/cjs/localization/index.cjs +4 -2
  10. package/dist/cjs/localization/rewriteMatch.cjs +52 -0
  11. package/dist/cjs/localization/rewriteMatch.cjs.map +1 -0
  12. package/dist/cjs/localization/rewriteUtils.cjs +6 -68
  13. package/dist/cjs/localization/rewriteUtils.cjs.map +1 -1
  14. package/dist/cjs/localization/rewriteUtils.test-d.cjs +0 -5
  15. package/dist/cjs/localization/rewriteUtils.test-d.cjs.map +1 -1
  16. package/dist/cjs/markdown/compiler.cjs +100 -75
  17. package/dist/cjs/markdown/compiler.cjs.map +1 -1
  18. package/dist/cjs/markdown/constants.cjs +14 -5
  19. package/dist/cjs/markdown/constants.cjs.map +1 -1
  20. package/dist/cjs/markdown/index.cjs +4 -2
  21. package/dist/cjs/markdown/parser.cjs +70 -44
  22. package/dist/cjs/markdown/parser.cjs.map +1 -1
  23. package/dist/cjs/markdown/renderer.cjs +7 -19
  24. package/dist/cjs/markdown/renderer.cjs.map +1 -1
  25. package/dist/cjs/markdown/utils.cjs +33 -37
  26. package/dist/cjs/markdown/utils.cjs.map +1 -1
  27. package/dist/esm/index.mjs +5 -3
  28. package/dist/esm/localization/getCanonicalPath.mjs +51 -0
  29. package/dist/esm/localization/getCanonicalPath.mjs.map +1 -0
  30. package/dist/esm/localization/getCanonicalPath.test-d.mjs +12 -0
  31. package/dist/esm/localization/getCanonicalPath.test-d.mjs.map +1 -0
  32. package/dist/esm/localization/getLocalizedPath.mjs +3 -1
  33. package/dist/esm/localization/getLocalizedPath.mjs.map +1 -1
  34. package/dist/esm/localization/index.mjs +3 -1
  35. package/dist/esm/localization/rewriteMatch.mjs +49 -0
  36. package/dist/esm/localization/rewriteMatch.mjs.map +1 -0
  37. package/dist/esm/localization/rewriteUtils.mjs +3 -63
  38. package/dist/esm/localization/rewriteUtils.mjs.map +1 -1
  39. package/dist/esm/localization/rewriteUtils.test-d.mjs +1 -6
  40. package/dist/esm/localization/rewriteUtils.test-d.mjs.map +1 -1
  41. package/dist/esm/markdown/compiler.mjs +101 -55
  42. package/dist/esm/markdown/compiler.mjs.map +1 -1
  43. package/dist/esm/markdown/constants.mjs +11 -4
  44. package/dist/esm/markdown/constants.mjs.map +1 -1
  45. package/dist/esm/markdown/index.mjs +2 -2
  46. package/dist/esm/markdown/parser.mjs +70 -44
  47. package/dist/esm/markdown/parser.mjs.map +1 -1
  48. package/dist/esm/markdown/renderer.mjs +7 -19
  49. package/dist/esm/markdown/renderer.mjs.map +1 -1
  50. package/dist/esm/markdown/utils.mjs +34 -38
  51. package/dist/esm/markdown/utils.mjs.map +1 -1
  52. package/dist/types/index.d.ts +5 -3
  53. package/dist/types/localization/getCanonicalPath.d.ts +19 -0
  54. package/dist/types/localization/getCanonicalPath.d.ts.map +1 -0
  55. package/dist/types/localization/getCanonicalPath.test-d.d.ts +1 -0
  56. package/dist/types/localization/getLocalizedPath.d.ts.map +1 -1
  57. package/dist/types/localization/index.d.ts +3 -1
  58. package/dist/types/localization/rewriteMatch.d.ts +20 -0
  59. package/dist/types/localization/rewriteMatch.d.ts.map +1 -0
  60. package/dist/types/localization/rewriteUtils.d.ts +2 -14
  61. package/dist/types/localization/rewriteUtils.d.ts.map +1 -1
  62. package/dist/types/markdown/compiler.d.ts.map +1 -1
  63. package/dist/types/markdown/constants.d.ts +11 -4
  64. package/dist/types/markdown/constants.d.ts.map +1 -1
  65. package/dist/types/markdown/index.d.ts +2 -2
  66. package/dist/types/markdown/parser.d.ts.map +1 -1
  67. package/dist/types/markdown/renderer.d.ts.map +1 -1
  68. package/dist/types/markdown/types.d.ts +8 -2
  69. package/dist/types/markdown/types.d.ts.map +1 -1
  70. package/dist/types/markdown/utils.d.ts +0 -4
  71. package/dist/types/markdown/utils.d.ts.map +1 -1
  72. package/package.json +7 -7
@@ -1,69 +1,9 @@
1
+ import { extractParams, fillPath, getRewriteRules } from "./rewriteMatch.mjs";
2
+ import { getCanonicalPath } from "./getCanonicalPath.mjs";
1
3
  import { internationalization } from "@intlayer/config/built";
2
4
 
3
5
  //#region src/localization/rewriteUtils.ts
4
6
  /**
5
- * Normalizes legacy Record format or extracts specialized rules from RewriteObject.
6
- *
7
- * Idempotent: already-normalized rules are handed back untouched, so a caller
8
- * holding resolved rules can pass them on without re-normalizing.
9
- */
10
- const getRewriteRules = (rewrite, context = "url") => {
11
- if (!rewrite || process.env.INTLAYER_ROUTING_REWRITE_RULES && process.env.INTLAYER_ROUTING_REWRITE_RULES === "false") return void 0;
12
- if (Array.isArray(rewrite.rules)) return rewrite;
13
- if ("url" in rewrite) return rewrite[context];
14
- return { rules: Object.entries(rewrite).map(([canonical, localized]) => ({
15
- canonical: canonical.startsWith("/") ? canonical.replace(/\[([^\]]+)\]/g, ":$1") : `/${canonical.replace(/\[([^\]]+)\]/g, ":$1")}`,
16
- localized: Object.fromEntries(Object.entries(localized).map(([locale, pattern]) => {
17
- return [locale, pattern?.startsWith("/") ? pattern.replace(/\[([^\]]+)\]/g, ":$1") : `/${(pattern || "").replace(/\[([^\]]+)\]/g, ":$1")}`];
18
- }))
19
- })) };
20
- };
21
- /**
22
- * Converts normalized pattern to Regex.
23
- * Internal syntax supports:
24
- * - :param -> ([^/]+) (one segment)
25
- * - :param* -> (.*) (zero or more segments)
26
- * - :param+ -> (.+) (one or more segments)
27
- * - :param? -> ([^/]*) (zero or one segment)
28
- */
29
- const patternToRegex = (pattern) => {
30
- const regexString = pattern.replace(/\//g, "\\/").replace(/\\\/:(?:[^/\\*+?]+)\*/g, "(?:\\/(.*))?").replace(/\\\/:(?:[^/\\*+?]+)\?/g, "(?:\\/([^\\/]+))?").replace(/:([^/\\*+?]+)\*/g, "(.*)").replace(/:([^/\\*+?]+)\?/g, "([^\\/]*)").replace(/:([^/\\*+?]+)\+/g, "(.+)").replace(/:([^/\\*+?]+)/g, "([^\\/]+)");
31
- return new RegExp(`^${regexString}$`);
32
- };
33
- /**
34
- * Replaces route parameters in a path with provided values.
35
- */
36
- const fillPath = (pattern, params) => {
37
- let index = 0;
38
- return pattern.replace(/:([^/\\*+?]+)[*+?]?/g, () => params[index++] ?? "").replace(/\/+/g, "/").replace(/\/$/, "") || "/";
39
- };
40
- /**
41
- * Extract values from a URL based on a pattern.
42
- */
43
- const extractParams = (url, pattern) => {
44
- const regex = patternToRegex(pattern);
45
- const match = url.match(regex);
46
- return match ? match.slice(1) : null;
47
- };
48
- /**
49
- * Given a localized URL (e.g., "/produits/123"), finds the canonical internal path (e.g., "/products/123").
50
- * If locale is provided, only check for that locale. Otherwise, check for all locales.
51
- */
52
- const getCanonicalPath = (localizedPath, locale, rewriteRules) => {
53
- if (!rewriteRules || process.env.INTLAYER_ROUTING_REWRITE_RULES && process.env.INTLAYER_ROUTING_REWRITE_RULES === "false") return localizedPath;
54
- for (const rule of rewriteRules.rules) {
55
- const { canonical, localized } = rule;
56
- const localesToCheck = locale ? [locale] : Object.keys(localized);
57
- for (const loc of localesToCheck) {
58
- const localizedPattern = localized[loc];
59
- if (!localizedPattern) continue;
60
- const params = extractParams(localizedPath, localizedPattern);
61
- if (params) return fillPath(canonical, params);
62
- }
63
- }
64
- return localizedPath;
65
- };
66
- /**
67
7
  * Given a canonical path (e.g., "/products/123"), finds the localized URL pattern
68
8
  * (e.g., "/produits/123") and reports whether a rule actually matched.
69
9
  *
@@ -120,5 +60,5 @@ const getRewritePath = (pathname, locale, rewrite) => {
120
60
  };
121
61
 
122
62
  //#endregion
123
- export { getCanonicalPath, getInternalPath, getRewritePath, getRewriteRules, resolveLocalizedPath };
63
+ export { getInternalPath, getRewritePath, resolveLocalizedPath };
124
64
  //# sourceMappingURL=rewriteUtils.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rewriteUtils.mjs","names":["legacyRules"],"sources":["../../../src/localization/rewriteUtils.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\nimport type {\n RewriteObject,\n RewriteRules,\n RoutingConfig,\n} from '@intlayer/types/config';\nimport type {\n LocalesValues,\n ResolvedDefaultLocale,\n} from '@intlayer/types/module_augmentation';\n\n/**\n * Outcome of resolving a canonical path against the rewrite rules.\n *\n * Discriminated on `isRewritten`: when no rule matched, `path` is the very\n * input that was passed in, so a literal input keeps its literal type. Only a\n * rewritten path is computed at runtime and therefore widens to `string`.\n */\nexport type LocalizedPathResult<CanonicalPath extends string = string> =\n | { path: CanonicalPath; isRewritten: false }\n | { path: string; isRewritten: true };\n\n/** Adds the leading slash a path may be missing. */\ntype WithLeadingSlash<Path extends string> = Path extends `/${string}`\n ? Path\n : `/${Path}`;\n\n/**\n * Computes the locale-prefixed internal path for a path and a locale, mirroring\n * `getInternalPath`'s runtime behaviour: an already-prefixed path is returned\n * untouched, the root collapses to `/${Locale}` (never a trailing slash), and\n * anything else gets the prefix prepended.\n *\n * Falls back to `string` for a non-literal path, where the prefixed and\n * already-prefixed cases cannot be told apart.\n *\n * @example\n * type A = InternalPath<'/about', 'fr'>; // '/fr/about'\n * type B = InternalPath<'/', 'fr'>; // '/fr'\n * type C = InternalPath<'/fr/about', 'fr'>; // '/fr/about' (already prefixed)\n */\nexport type InternalPath<\n Path extends string,\n Locale extends string,\n> = string extends Path\n ? string\n : WithLeadingSlash<Path> extends infer Prefixed extends string\n ? Prefixed extends `/${Locale}` | `/${Locale}/${string}`\n ? Prefixed\n : Prefixed extends '/'\n ? `/${Locale}`\n : `/${Locale}${Prefixed}`\n : never;\n\n/**\n * Normalizes legacy Record format or extracts specialized rules from RewriteObject.\n *\n * Idempotent: already-normalized rules are handed back untouched, so a caller\n * holding resolved rules can pass them on without re-normalizing.\n */\nexport const getRewriteRules = <\n const Context extends keyof RewriteObject = 'url',\n>(\n rewrite: RoutingConfig['rewrite'] | RewriteRules,\n context: Context = 'url' as Context\n): RewriteRules | undefined => {\n if (\n !rewrite ||\n (process.env.INTLAYER_ROUTING_REWRITE_RULES &&\n process.env.INTLAYER_ROUTING_REWRITE_RULES === 'false')\n )\n return undefined;\n\n if (Array.isArray((rewrite as RewriteRules).rules)) {\n return rewrite as RewriteRules;\n }\n\n if ('url' in rewrite) {\n return (rewrite as RewriteObject)[context];\n }\n\n // Normalize legacy format\n const legacyRules = rewrite as Record<string, Record<string, string>>;\n\n return {\n rules: Object.entries(legacyRules).map(([canonical, localized]) => ({\n // Normalize canonical path\n canonical: canonical.startsWith('/')\n ? canonical.replace(/\\[([^\\]]+)\\]/g, ':$1')\n : `/${canonical.replace(/\\[([^\\]]+)\\]/g, ':$1')}`,\n\n // Normalize localized path\n localized: Object.fromEntries(\n Object.entries(localized).map(([locale, pattern]) => {\n const normalizedPattern = pattern?.startsWith('/')\n ? pattern.replace(/\\[([^\\]]+)\\]/g, ':$1')\n : `/${(pattern || '').replace(/\\[([^\\]]+)\\]/g, ':$1')}`;\n return [locale, normalizedPattern];\n })\n ),\n })),\n };\n};\n\n/**\n * Converts normalized pattern to Regex.\n * Internal syntax supports:\n * - :param -> ([^/]+) (one segment)\n * - :param* -> (.*) (zero or more segments)\n * - :param+ -> (.+) (one or more segments)\n * - :param? -> ([^/]*) (zero or one segment)\n */\nconst patternToRegex = (pattern: string) => {\n const regexString = pattern\n .replace(/\\//g, '\\\\/') // Escape slashes\n .replace(/\\\\\\/:(?:[^/\\\\*+?]+)\\*/g, '(?:\\\\/(.*))?') // /:param*\n .replace(/\\\\\\/:(?:[^/\\\\*+?]+)\\?/g, '(?:\\\\/([^\\\\/]+))?') // /:param?\n .replace(/:([^/\\\\*+?]+)\\*/g, '(.*)') // :param* (if no leading slash)\n .replace(/:([^/\\\\*+?]+)\\?/g, '([^\\\\/]*)') // :param? (if no leading slash)\n .replace(/:([^/\\\\*+?]+)\\+/g, '(.+)') // :param+\n .replace(/:([^/\\\\*+?]+)/g, '([^\\\\/]+)'); // :param\n\n return new RegExp(`^${regexString}$`);\n};\n\n/**\n * Replaces route parameters in a path with provided values.\n */\nconst fillPath = (pattern: string, params: string[]) => {\n let index = 0;\n return (\n pattern\n .replace(/:([^/\\\\*+?]+)[*+?]?/g, () => params[index++] ?? '')\n .replace(/\\/+/g, '/')\n .replace(/\\/$/, '') || '/'\n );\n};\n\n/**\n * Extract values from a URL based on a pattern.\n */\nconst extractParams = (url: string, pattern: string): string[] | null => {\n const regex = patternToRegex(pattern);\n const match = url.match(regex);\n return match ? match.slice(1) : null;\n};\n\n/**\n * Given a localized URL (e.g., \"/produits/123\"), finds the canonical internal path (e.g., \"/products/123\").\n * If locale is provided, only check for that locale. Otherwise, check for all locales.\n */\nexport const getCanonicalPath = <\n const Path extends string,\n const Locale extends LocalesValues = LocalesValues,\n>(\n localizedPath: Path,\n locale?: Locale,\n rewriteRules?: RewriteRules\n): string => {\n if (\n !rewriteRules ||\n (process.env.INTLAYER_ROUTING_REWRITE_RULES &&\n process.env.INTLAYER_ROUTING_REWRITE_RULES === 'false')\n )\n return localizedPath;\n\n for (const rule of rewriteRules.rules) {\n const { canonical, localized } = rule;\n const localesToCheck = locale ? [locale] : Object.keys(localized);\n\n for (const loc of localesToCheck) {\n const localizedPattern = localized[loc as keyof typeof localized];\n\n if (!localizedPattern) continue;\n\n const params = extractParams(localizedPath, localizedPattern);\n\n if (params) {\n return fillPath(canonical, params);\n }\n }\n }\n\n return localizedPath;\n};\n\n/**\n * Given a canonical path (e.g., \"/products/123\"), finds the localized URL pattern\n * (e.g., \"/produits/123\") and reports whether a rule actually matched.\n *\n * Takes already-normalized rules, so callers that resolve the rules once (proxies,\n * static emitters) can reuse them. {@link getLocalizedPath} is the user-facing\n * counterpart: it accepts the raw `routing.rewrite` configuration, defaults to the\n * project configuration, and returns the path alone.\n *\n * @param canonicalPath - The internal application path (e.g. `/product/:id`, `/about`).\n * @param locale - The target locale. Defaults to the configured default locale.\n * @param rewriteRules - Normalized rules, as returned by {@link getRewriteRules}.\n * @returns The localized path, discriminated on whether a rewrite rule matched.\n */\nexport const resolveLocalizedPath = <\n const CanonicalPath extends string,\n const Locale extends LocalesValues = ResolvedDefaultLocale,\n>(\n canonicalPath: CanonicalPath,\n locale?: Locale,\n rewriteRules?: RewriteRules\n): LocalizedPathResult<CanonicalPath> => {\n if (\n !rewriteRules ||\n (process.env.INTLAYER_ROUTING_REWRITE_RULES &&\n process.env.INTLAYER_ROUTING_REWRITE_RULES === 'false')\n )\n return { path: canonicalPath, isRewritten: false };\n\n for (const rule of rewriteRules.rules) {\n const { canonical, localized } = rule;\n\n // Check if the input path matches a configured canonical pattern\n const params = extractParams(canonicalPath, canonical);\n\n if (params) {\n const targetLocale = locale ?? internationalization.defaultLocale;\n const targetPattern = localized[targetLocale as keyof typeof localized];\n\n if (targetPattern) {\n return {\n path: fillPath(targetPattern, params),\n isRewritten: true,\n };\n }\n }\n }\n\n return { path: canonicalPath, isRewritten: false };\n};\n\n/**\n * Returns the internal path for a given canonical path and locale.\n * Ensures the locale prefix is present exactly once.\n */\nexport const getInternalPath = <\n const Path extends string,\n const Locale extends LocalesValues,\n>(\n canonicalPath: Path,\n locale: Locale\n): InternalPath<Path, Locale> => {\n const pathWithLeadingSlash = canonicalPath.startsWith('/')\n ? canonicalPath\n : `/${canonicalPath}`;\n\n if (\n pathWithLeadingSlash.startsWith(`/${locale}/`) ||\n pathWithLeadingSlash === `/${locale}`\n ) {\n return pathWithLeadingSlash as InternalPath<Path, Locale>;\n }\n\n return `/${locale}${\n pathWithLeadingSlash === '/' ? '' : pathWithLeadingSlash\n }` as InternalPath<Path, Locale>;\n};\n\n/**\n * Given a current pathname and locale, returns the pretty localized path if a rewrite rule exists and the path is not already localized.\n */\nexport const getRewritePath = <\n const Path extends string,\n const Locale extends LocalesValues = ResolvedDefaultLocale,\n>(\n pathname: Path,\n locale: Locale,\n rewrite?: RoutingConfig['rewrite']\n): string | undefined => {\n if (\n process.env.INTLAYER_ROUTING_REWRITE_RULES &&\n process.env.INTLAYER_ROUTING_REWRITE_RULES === 'false'\n )\n return undefined;\n const rules = getRewriteRules(rewrite, 'url');\n if (!rules) return undefined;\n\n // Identify canonical path (relative to root, no locale prefix expected in 'url' context)\n const canonicalPath = getCanonicalPath(pathname, undefined, rules);\n\n // Get the localized path for the current locale\n const { path: localizedPath, isRewritten } = resolveLocalizedPath(\n canonicalPath,\n locale,\n rules\n );\n\n if (isRewritten && localizedPath !== pathname) {\n return localizedPath;\n }\n\n return undefined;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"rewriteUtils.mjs","names":[],"sources":["../../../src/localization/rewriteUtils.ts"],"sourcesContent":["import { internationalization } from '@intlayer/config/built';\nimport type { RewriteRules, RoutingConfig } from '@intlayer/types/config';\nimport type {\n LocalesValues,\n ResolvedDefaultLocale,\n} from '@intlayer/types/module_augmentation';\nimport { getCanonicalPath } from './getCanonicalPath';\nimport { extractParams, fillPath, getRewriteRules } from './rewriteMatch';\n\n/**\n * Outcome of resolving a canonical path against the rewrite rules.\n *\n * Discriminated on `isRewritten`: when no rule matched, `path` is the very\n * input that was passed in, so a literal input keeps its literal type. Only a\n * rewritten path is computed at runtime and therefore widens to `string`.\n */\nexport type LocalizedPathResult<CanonicalPath extends string = string> =\n | { path: CanonicalPath; isRewritten: false }\n | { path: string; isRewritten: true };\n\n/** Adds the leading slash a path may be missing. */\ntype WithLeadingSlash<Path extends string> = Path extends `/${string}`\n ? Path\n : `/${Path}`;\n\n/**\n * Computes the locale-prefixed internal path for a path and a locale, mirroring\n * `getInternalPath`'s runtime behaviour: an already-prefixed path is returned\n * untouched, the root collapses to `/${Locale}` (never a trailing slash), and\n * anything else gets the prefix prepended.\n *\n * Falls back to `string` for a non-literal path, where the prefixed and\n * already-prefixed cases cannot be told apart.\n *\n * @example\n * type A = InternalPath<'/about', 'fr'>; // '/fr/about'\n * type B = InternalPath<'/', 'fr'>; // '/fr'\n * type C = InternalPath<'/fr/about', 'fr'>; // '/fr/about' (already prefixed)\n */\nexport type InternalPath<\n Path extends string,\n Locale extends string,\n> = string extends Path\n ? string\n : WithLeadingSlash<Path> extends infer Prefixed extends string\n ? Prefixed extends `/${Locale}` | `/${Locale}/${string}`\n ? Prefixed\n : Prefixed extends '/'\n ? `/${Locale}`\n : `/${Locale}${Prefixed}`\n : never;\n\n/**\n * Given a canonical path (e.g., \"/products/123\"), finds the localized URL pattern\n * (e.g., \"/produits/123\") and reports whether a rule actually matched.\n *\n * Takes already-normalized rules, so callers that resolve the rules once (proxies,\n * static emitters) can reuse them. {@link getLocalizedPath} is the user-facing\n * counterpart: it accepts the raw `routing.rewrite` configuration, defaults to the\n * project configuration, and returns the path alone.\n *\n * @param canonicalPath - The internal application path (e.g. `/product/:id`, `/about`).\n * @param locale - The target locale. Defaults to the configured default locale.\n * @param rewriteRules - Normalized rules, as returned by {@link getRewriteRules}.\n * @returns The localized path, discriminated on whether a rewrite rule matched.\n */\nexport const resolveLocalizedPath = <\n const CanonicalPath extends string,\n const Locale extends LocalesValues = ResolvedDefaultLocale,\n>(\n canonicalPath: CanonicalPath,\n locale?: Locale,\n rewriteRules?: RewriteRules\n): LocalizedPathResult<CanonicalPath> => {\n if (\n !rewriteRules ||\n (process.env.INTLAYER_ROUTING_REWRITE_RULES &&\n process.env.INTLAYER_ROUTING_REWRITE_RULES === 'false')\n )\n return { path: canonicalPath, isRewritten: false };\n\n for (const rule of rewriteRules.rules) {\n const { canonical, localized } = rule;\n\n // Check if the input path matches a configured canonical pattern\n const params = extractParams(canonicalPath, canonical);\n\n if (params) {\n const targetLocale = locale ?? internationalization.defaultLocale;\n const targetPattern = localized[targetLocale as keyof typeof localized];\n\n if (targetPattern) {\n return {\n path: fillPath(targetPattern, params),\n isRewritten: true,\n };\n }\n }\n }\n\n return { path: canonicalPath, isRewritten: false };\n};\n\n/**\n * Returns the internal path for a given canonical path and locale.\n * Ensures the locale prefix is present exactly once.\n */\nexport const getInternalPath = <\n const Path extends string,\n const Locale extends LocalesValues,\n>(\n canonicalPath: Path,\n locale: Locale\n): InternalPath<Path, Locale> => {\n const pathWithLeadingSlash = canonicalPath.startsWith('/')\n ? canonicalPath\n : `/${canonicalPath}`;\n\n if (\n pathWithLeadingSlash.startsWith(`/${locale}/`) ||\n pathWithLeadingSlash === `/${locale}`\n ) {\n return pathWithLeadingSlash as InternalPath<Path, Locale>;\n }\n\n return `/${locale}${\n pathWithLeadingSlash === '/' ? '' : pathWithLeadingSlash\n }` as InternalPath<Path, Locale>;\n};\n\n/**\n * Given a current pathname and locale, returns the pretty localized path if a rewrite rule exists and the path is not already localized.\n */\nexport const getRewritePath = <\n const Path extends string,\n const Locale extends LocalesValues = ResolvedDefaultLocale,\n>(\n pathname: Path,\n locale: Locale,\n rewrite?: RoutingConfig['rewrite']\n): string | undefined => {\n if (\n process.env.INTLAYER_ROUTING_REWRITE_RULES &&\n process.env.INTLAYER_ROUTING_REWRITE_RULES === 'false'\n )\n return undefined;\n const rules = getRewriteRules(rewrite, 'url');\n if (!rules) return undefined;\n\n // Identify canonical path (relative to root, no locale prefix expected in 'url' context)\n const canonicalPath = getCanonicalPath(pathname, undefined, rules);\n\n // Get the localized path for the current locale\n const { path: localizedPath, isRewritten } = resolveLocalizedPath(\n canonicalPath,\n locale,\n rules\n );\n\n if (isRewritten && localizedPath !== pathname) {\n return localizedPath;\n }\n\n return undefined;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkEA,MAAa,wBAIX,eACA,QACA,iBACuC;CACvC,IACE,CAAC,gBACA,QAAQ,IAAI,kCACX,QAAQ,IAAI,mCAAmC,SAEjD,OAAO;EAAE,MAAM;EAAe,aAAa;CAAM;CAEnD,KAAK,MAAM,QAAQ,aAAa,OAAO;EACrC,MAAM,EAAE,WAAW,cAAc;EAGjC,MAAM,SAAS,cAAc,eAAe,SAAS;EAErD,IAAI,QAAQ;GAEV,MAAM,gBAAgB,UADD,UAAU,qBAAqB;GAGpD,IAAI,eACF,OAAO;IACL,MAAM,SAAS,eAAe,MAAM;IACpC,aAAa;GACf;EAEJ;CACF;CAEA,OAAO;EAAE,MAAM;EAAe,aAAa;CAAM;AACnD;;;;;AAMA,MAAa,mBAIX,eACA,WAC+B;CAC/B,MAAM,uBAAuB,cAAc,WAAW,GAAG,IACrD,gBACA,IAAI;CAER,IACE,qBAAqB,WAAW,IAAI,OAAO,EAAE,KAC7C,yBAAyB,IAAI,UAE7B,OAAO;CAGT,OAAO,IAAI,SACT,yBAAyB,MAAM,KAAK;AAExC;;;;AAKA,MAAa,kBAIX,UACA,QACA,YACuB;CACvB,IACE,QAAQ,IAAI,kCACZ,QAAQ,IAAI,mCAAmC,SAE/C,OAAO;CACT,MAAM,QAAQ,gBAAgB,SAAS,KAAK;CAC5C,IAAI,CAAC,OAAO,OAAO;CAGnB,MAAM,gBAAgB,iBAAiB,UAAU,QAAW,KAAK;CAGjE,MAAM,EAAE,MAAM,eAAe,gBAAgB,qBAC3C,eACA,QACA,KACF;CAEA,IAAI,eAAe,kBAAkB,UACnC,OAAO;AAIX"}
@@ -1,4 +1,4 @@
1
- import { getCanonicalPath, getInternalPath, resolveLocalizedPath } from "./rewriteUtils.mjs";
1
+ import { getInternalPath, resolveLocalizedPath } from "./rewriteUtils.mjs";
2
2
  import { describe, expectTypeOf, it } from "vitest";
3
3
 
4
4
  //#region src/localization/rewriteUtils.test-d.ts
@@ -38,11 +38,6 @@ describe("resolveLocalizedPath", () => {
38
38
  expectTypeOf(resolveLocalizedPath("/about")).toEqualTypeOf();
39
39
  });
40
40
  });
41
- describe("getCanonicalPath", () => {
42
- it("should accept an omitted locale (checks every locale)", () => {
43
- expectTypeOf(getCanonicalPath("/a-propos")).toEqualTypeOf();
44
- });
45
- });
46
41
 
47
42
  //#endregion
48
43
  //# sourceMappingURL=rewriteUtils.test-d.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rewriteUtils.test-d.mjs","names":[],"sources":["../../../src/localization/rewriteUtils.test-d.ts"],"sourcesContent":["import { describe, expectTypeOf, it } from 'vitest';\nimport {\n getCanonicalPath,\n getInternalPath,\n resolveLocalizedPath,\n} from './rewriteUtils';\n\n/**\n * `getInternalPath` is pure string surgery — no rewrite rules involved — so its\n * result is fully derivable from the arguments whenever both are literals.\n */\ndescribe('getInternalPath', () => {\n it('should prefix a literal path with the locale', () => {\n expectTypeOf(getInternalPath('/about', 'fr')).toEqualTypeOf<'/fr/about'>();\n });\n\n it('should collapse the root to a bare prefix, never a trailing slash', () => {\n expectTypeOf(getInternalPath('/', 'fr')).toEqualTypeOf<'/fr'>();\n });\n\n it('should leave an already-prefixed path untouched', () => {\n expectTypeOf(\n getInternalPath('/fr/about', 'fr')\n ).toEqualTypeOf<'/fr/about'>();\n expectTypeOf(getInternalPath('/fr', 'fr')).toEqualTypeOf<'/fr'>();\n });\n\n it('should add the leading slash a path is missing', () => {\n expectTypeOf(getInternalPath('about', 'fr')).toEqualTypeOf<'/fr/about'>();\n });\n\n it('should widen to string when the path is not a literal', () => {\n const dynamicPath = '/about' as string;\n\n expectTypeOf(getInternalPath(dynamicPath, 'fr')).toEqualTypeOf<string>();\n });\n});\n\n/**\n * `resolveLocalizedPath` only computes a new path when a rule matched, so the\n * `isRewritten: false` branch still carries the exact input it was given.\n */\ndescribe('resolveLocalizedPath', () => {\n it('should keep the input literal on the non-rewritten branch', () => {\n const result = resolveLocalizedPath('/about', 'fr');\n\n if (!result.isRewritten) {\n expectTypeOf(result.path).toEqualTypeOf<'/about'>();\n } else {\n expectTypeOf(result.path).toEqualTypeOf<string>();\n }\n });\n\n it('should discriminate on isRewritten', () => {\n expectTypeOf(resolveLocalizedPath('/about')).toEqualTypeOf<\n | { path: '/about'; isRewritten: false }\n | { path: string; isRewritten: true }\n >();\n });\n});\n\ndescribe('getCanonicalPath', () => {\n it('should accept an omitted locale (checks every locale)', () => {\n expectTypeOf(getCanonicalPath('/a-propos')).toEqualTypeOf<string>();\n });\n});\n"],"mappings":";;;;;;;;AAWA,SAAS,yBAAyB;CAChC,GAAG,sDAAsD;EACvD,aAAa,gBAAgB,UAAU,IAAI,CAAC,CAAC,CAAC,cAA2B;CAC3E,CAAC;CAED,GAAG,2EAA2E;EAC5E,aAAa,gBAAgB,KAAK,IAAI,CAAC,CAAC,CAAC,cAAqB;CAChE,CAAC;CAED,GAAG,yDAAyD;EAC1D,aACE,gBAAgB,aAAa,IAAI,CACnC,CAAC,CAAC,cAA2B;EAC7B,aAAa,gBAAgB,OAAO,IAAI,CAAC,CAAC,CAAC,cAAqB;CAClE,CAAC;CAED,GAAG,wDAAwD;EACzD,aAAa,gBAAgB,SAAS,IAAI,CAAC,CAAC,CAAC,cAA2B;CAC1E,CAAC;CAED,GAAG,+DAA+D;EAGhE,aAAa,gBAAgB,UAAa,IAAI,CAAC,CAAC,CAAC,cAAsB;CACzE,CAAC;AACH,CAAC;;;;;AAMD,SAAS,8BAA8B;CACrC,GAAG,mEAAmE;EACpE,MAAM,SAAS,qBAAqB,UAAU,IAAI;EAElD,IAAI,CAAC,OAAO,aACV,aAAa,OAAO,IAAI,CAAC,CAAC,cAAwB;OAElD,aAAa,OAAO,IAAI,CAAC,CAAC,cAAsB;CAEpD,CAAC;CAED,GAAG,4CAA4C;EAC7C,aAAa,qBAAqB,QAAQ,CAAC,CAAC,CAAC,cAG3C;CACJ,CAAC;AACH,CAAC;AAED,SAAS,0BAA0B;CACjC,GAAG,+DAA+D;EAChE,aAAa,iBAAiB,WAAW,CAAC,CAAC,CAAC,cAAsB;CACpE,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"rewriteUtils.test-d.mjs","names":[],"sources":["../../../src/localization/rewriteUtils.test-d.ts"],"sourcesContent":["import { describe, expectTypeOf, it } from 'vitest';\nimport { getInternalPath, resolveLocalizedPath } from './rewriteUtils';\n\n/**\n * `getInternalPath` is pure string surgery — no rewrite rules involved — so its\n * result is fully derivable from the arguments whenever both are literals.\n */\ndescribe('getInternalPath', () => {\n it('should prefix a literal path with the locale', () => {\n expectTypeOf(getInternalPath('/about', 'fr')).toEqualTypeOf<'/fr/about'>();\n });\n\n it('should collapse the root to a bare prefix, never a trailing slash', () => {\n expectTypeOf(getInternalPath('/', 'fr')).toEqualTypeOf<'/fr'>();\n });\n\n it('should leave an already-prefixed path untouched', () => {\n expectTypeOf(\n getInternalPath('/fr/about', 'fr')\n ).toEqualTypeOf<'/fr/about'>();\n expectTypeOf(getInternalPath('/fr', 'fr')).toEqualTypeOf<'/fr'>();\n });\n\n it('should add the leading slash a path is missing', () => {\n expectTypeOf(getInternalPath('about', 'fr')).toEqualTypeOf<'/fr/about'>();\n });\n\n it('should widen to string when the path is not a literal', () => {\n const dynamicPath = '/about' as string;\n\n expectTypeOf(getInternalPath(dynamicPath, 'fr')).toEqualTypeOf<string>();\n });\n});\n\n/**\n * `resolveLocalizedPath` only computes a new path when a rule matched, so the\n * `isRewritten: false` branch still carries the exact input it was given.\n */\ndescribe('resolveLocalizedPath', () => {\n it('should keep the input literal on the non-rewritten branch', () => {\n const result = resolveLocalizedPath('/about', 'fr');\n\n if (!result.isRewritten) {\n expectTypeOf(result.path).toEqualTypeOf<'/about'>();\n } else {\n expectTypeOf(result.path).toEqualTypeOf<string>();\n }\n });\n\n it('should discriminate on isRewritten', () => {\n expectTypeOf(resolveLocalizedPath('/about')).toEqualTypeOf<\n | { path: '/about'; isRewritten: false }\n | { path: string; isRewritten: true }\n >();\n });\n});\n"],"mappings":";;;;;;;;AAOA,SAAS,yBAAyB;CAChC,GAAG,sDAAsD;EACvD,aAAa,gBAAgB,UAAU,IAAI,CAAC,CAAC,CAAC,cAA2B;CAC3E,CAAC;CAED,GAAG,2EAA2E;EAC5E,aAAa,gBAAgB,KAAK,IAAI,CAAC,CAAC,CAAC,cAAqB;CAChE,CAAC;CAED,GAAG,yDAAyD;EAC1D,aACE,gBAAgB,aAAa,IAAI,CACnC,CAAC,CAAC,cAA2B;EAC7B,aAAa,gBAAgB,OAAO,IAAI,CAAC,CAAC,CAAC,cAAqB;CAClE,CAAC;CAED,GAAG,wDAAwD;EACzD,aAAa,gBAAgB,SAAS,IAAI,CAAC,CAAC,CAAC,cAA2B;CAC1E,CAAC;CAED,GAAG,+DAA+D;EAGhE,aAAa,gBAAgB,UAAa,IAAI,CAAC,CAAC,CAAC,cAAsB;CACzE,CAAC;AACH,CAAC;;;;;AAMD,SAAS,8BAA8B;CACrC,GAAG,mEAAmE;EACpE,MAAM,SAAS,qBAAqB,UAAU,IAAI;EAElD,IAAI,CAAC,OAAO,aACV,aAAa,OAAO,IAAI,CAAC,CAAC,cAAwB;OAElD,aAAa,OAAO,IAAI,CAAC,CAAC,cAAsB;CAEpD,CAAC;CAED,GAAG,4CAA4C;EAC7C,aAAa,qBAAqB,QAAQ,CAAC,CAAC,CAAC,cAG3C;CACJ,CAAC;AACH,CAAC"}
@@ -1,4 +1,4 @@
1
- import { ATTRIBUTE_TO_NODE_PROP_MAP, ATTR_EXTRACTOR_R, BLOCKQUOTE_ALERT_R, BLOCKQUOTE_R, BLOCKQUOTE_TRIM_LEFT_MULTILINE_R, BLOCK_END_R, BREAK_LINE_R, BREAK_THEMATIC_R, CODE_BLOCK_FENCED_R, CODE_BLOCK_R, CODE_INLINE_R, CONSECUTIVE_NEWLINE_R, CUSTOM_COMPONENT_R, DO_NOT_PROCESS_HTML_ELEMENTS, FOOTNOTE_R, FOOTNOTE_REFERENCE_R, FRONT_MATTER_R, GFM_TASK_R, HEADING_ATX_COMPLIANT_R, HEADING_R, HEADING_SETEXT_R, HTML_BLOCK_ELEMENT_R, HTML_CHAR_CODE_R, HTML_COMMENT_R, HTML_LEFT_TRIM_AMOUNT_R, HTML_SELF_CLOSING_ELEMENT_R, LINK_AUTOLINK_BARE_URL_R, LINK_AUTOLINK_R, LIST_LOOKBEHIND_R, NAMED_CODES_TO_UNICODE, NP_TABLE_R, ORDERED, ORDERED_LIST_ITEM_PREFIX_R, ORDERED_LIST_ITEM_R, ORDERED_LIST_R, PARAGRAPH_R, Priority, REFERENCE_IMAGE_OR_LINK, REFERENCE_IMAGE_R, REFERENCE_LINK_R, RuleType, SHORTCODE_R, SHOULD_RENDER_AS_BLOCK_R, TEXT_BOLD_R, TEXT_EMPHASIZED_R, TEXT_ESCAPED_R, TEXT_MARKED_R, TEXT_PLAIN_R, TEXT_STRIKETHROUGHED_R, TRIM_STARTING_NEWLINES, UNORDERED, UNORDERED_LIST_ITEM_PREFIX_R, UNORDERED_LIST_ITEM_R, UNORDERED_LIST_R } from "./constants.mjs";
1
+ import { ATTRIBUTE_TO_NODE_PROP_MAP, ATTR_EXTRACTOR_R, BLOCKQUOTE_ALERT_R, BLOCKQUOTE_R, BLOCKQUOTE_TRIM_LEFT_MULTILINE_R, BLOCK_END_R, BREAK_LINE_R, BREAK_THEMATIC_R, CODE_BLOCK_FENCED_R, CODE_BLOCK_R, CODE_INLINE_R, CONSECUTIVE_NEWLINE_R, CUSTOM_COMPONENT_OPENING_TAG_R, CUSTOM_COMPONENT_R, DO_NOT_PROCESS_HTML_ELEMENTS, FOOTNOTE_R, FOOTNOTE_REFERENCE_R, FRONT_MATTER_R, GFM_TASK_R, HEADING_ATX_COMPLIANT_R, HEADING_R, HEADING_SETEXT_R, HTML_BLOCK_ELEMENT_R, HTML_BLOCK_OPENING_TAG_R, HTML_CHAR_CODE_R, HTML_COMMENT_R, HTML_LEFT_TRIM_AMOUNT_R, HTML_SELF_CLOSING_ELEMENT_R, HTML_SELF_CLOSING_OPENING_TAG_R, LINK_AUTOLINK_BARE_URL_R, LINK_AUTOLINK_R, NAMED_CODES_TO_UNICODE, NP_TABLE_R, ORDERED, ORDERED_LIST_ITEM_PREFIX_R, ORDERED_LIST_ITEM_R, ORDERED_LIST_R, PARAGRAPH_R, Priority, REFERENCE_IMAGE_OR_LINK, REFERENCE_IMAGE_R, REFERENCE_LINK_R, RuleType, SHORTCODE_R, SHOULD_RENDER_AS_BLOCK_R, TEXT_BOLD_R, TEXT_EMPHASIZED_R, TEXT_ESCAPED_R, TEXT_MARKED_R, TEXT_PLAIN_R, TEXT_STRIKETHROUGHED_R, TRIM_STARTING_NEWLINES, UNORDERED, UNORDERED_LIST_ITEM_PREFIX_R, UNORDERED_LIST_ITEM_R, UNORDERED_LIST_R } from "./constants.mjs";
2
2
  import { allowInline, anyScopeRegex, attributeValueToNodePropValue, blockRegex, captureNothing, cx, get, inlineRegex, normalizeAttributeKey, parseBlock, parseCaptureInline, parseInline, parseSimpleInline, parseTableAlign, parseTableCells, parseTableRow, renderNothing, sanitizer, simpleInlineRegex, slugify, some, startsWith, trimEnd, trimLeadingWhitespaceOutsideFences, unescapeString, unquote } from "./utils.mjs";
3
3
  import { parserFor } from "./parser.mjs";
4
4
  import { createRenderer, renderFor } from "./renderer.mjs";
@@ -6,19 +6,49 @@ import { createRenderer, renderFor } from "./renderer.mjs";
6
6
  //#region src/markdown/compiler.ts
7
7
  const IMAGE_R = /^!\[(.*?)\]\( *((?:\([^)]*\)|[^() ])*) *"?([^)"]*)?"?\)/;
8
8
  const LINK_R = new RegExp(`^\\[((?:\\[[^\\[\\]]*(?:\\[[^\\[\\]]*\\][^\\[\\]]*)*\\]|[^\\[\\]])*)\\]\\(\\s*<?((?:\\([^)]*\\)|[^\\s\\\\]|\\\\.)*?)>?(?:\\s+['"]([\\s\\S]*?)['"])?\\s*\\)`);
9
- const getTag = (tag, components) => {
10
- if (typeof tag !== "string") return tag;
11
- let override = get(components, tag);
12
- if (!override && typeof tag === "string") {
13
- const lowercaseTag = tag.toLowerCase();
14
- const key = Object.keys(components).find((k) => k.toLowerCase() === lowercaseTag);
15
- if (key) override = get(components, key);
16
- }
17
- if (!override) return tag;
18
- return override;
9
+ /**
10
+ * Case-insensitive `</tag>` probes, cached per tag name.
11
+ *
12
+ * The `htmlBlock` rule is qualified at every candidate position, and it used to
13
+ * lowercase the whole remaining document to look its closing tag up — copying
14
+ * the rest of the file on each probe.
15
+ */
16
+ const closingTagRegexCache = /* @__PURE__ */ new Map();
17
+ const getClosingTagRegex = (tag) => {
18
+ const cached = closingTagRegexCache.get(tag);
19
+ if (cached) return cached;
20
+ const regex = new RegExp(`</${tag}>`, "i");
21
+ closingTagRegexCache.set(tag, regex);
22
+ return regex;
23
+ };
24
+ /**
25
+ * Builds a tag -> component resolver for one set of overrides.
26
+ *
27
+ * The case-insensitive fallback used to rescan every override key on each
28
+ * element created; the lowercase index is now built once per document instead.
29
+ */
30
+ const createTagResolver = (components) => {
31
+ let lowercaseKeys = null;
32
+ return (tag) => {
33
+ if (typeof tag !== "string") return tag;
34
+ const override = get(components, tag);
35
+ if (override) return override;
36
+ if (!lowercaseKeys) {
37
+ lowercaseKeys = /* @__PURE__ */ new Map();
38
+ for (const key of Object.keys(components)) {
39
+ const lowercased = key.toLowerCase();
40
+ if (!lowercaseKeys.has(lowercased)) lowercaseKeys.set(lowercased, key);
41
+ }
42
+ }
43
+ const key = lowercaseKeys.get(tag.toLowerCase());
44
+ return (key ? get(components, key) : void 0) || tag;
45
+ };
19
46
  };
47
+ /** Resolves the slugifier for a context, falling back to the built-in one. */
48
+ const createSlugger = (ctx) => (input) => ctx.slugify ? ctx.slugify(input, slugify) : slugify(input);
20
49
  const createElementFactory = (ctx, options) => {
21
50
  const { runtime, components = {} } = ctx;
51
+ const resolveTag = createTagResolver(components);
22
52
  const filteredTags = options.tagfilter ? [
23
53
  "title",
24
54
  "textarea",
@@ -49,14 +79,12 @@ const createElementFactory = (ctx, options) => {
49
79
  if (!classNameHandled && className) mergedProps.className = className;
50
80
  let finalProps = mergedProps;
51
81
  if (runtime.normalizeProps && isStringTag) finalProps = runtime.normalizeProps(tag, mergedProps);
52
- const component = getTag(tag, components);
82
+ const component = resolveTag(tag);
53
83
  return runtime.createElement(component, finalProps, ...children.length === 1 ? [children[0]] : children);
54
84
  };
55
85
  };
56
86
  const createRules = (createElement, ctx, options, footnotes, refs, attrStringToMap, containsBlockSyntax, nonParagraphBlockSyntaxes) => {
57
- const slug = (input) => {
58
- return ctx.slugify ? ctx.slugify(input, slugify) : slugify(input);
59
- };
87
+ const slug = createSlugger(ctx);
60
88
  const sanitize = ctx.sanitizer ?? sanitizer;
61
89
  const namedCodesToUnicode = ctx.namedCodesToUnicode ? {
62
90
  ...NAMED_CODES_TO_UNICODE,
@@ -70,10 +98,11 @@ const createRules = (createElement, ctx, options, footnotes, refs, attrStringToM
70
98
  return {
71
99
  _qualify: (source) => LIST_ITEM_PREFIX_R.test(source),
72
100
  _match: allowInline((source, state) => {
73
- const isStartOfLine = LIST_LOOKBEHIND_R.exec(state.prevCapture ?? "");
101
+ const lineIndent = state.prevCaptureIndent;
102
+ const isStartOfLine = lineIndent !== void 0;
74
103
  const isListAllowed = state.list ?? (!state.inline && !state.simple);
75
104
  if (isStartOfLine && isListAllowed) {
76
- const matchSource = (isStartOfLine[1] || "") + source;
105
+ const matchSource = lineIndent ? lineIndent + source : source;
77
106
  return LIST_R.exec(matchSource);
78
107
  }
79
108
  return null;
@@ -127,7 +156,7 @@ const createRules = (createElement, ctx, options, footnotes, refs, attrStringToM
127
156
  };
128
157
  };
129
158
  const matchParagraph = (source, state) => {
130
- if (state.inline || state.simple || state.inHTML && source.indexOf("\n\n") === -1 && state.prevCapture?.indexOf("\n\n") === -1) return null;
159
+ if (state.inline || state.simple || state.inHTML && source.indexOf("\n\n") === -1 && !state.prevCaptureHasBlankLine) return null;
131
160
  let start = 0;
132
161
  while (true) {
133
162
  const newlineIndex = source.indexOf("\n", start);
@@ -327,10 +356,9 @@ const createRules = (createElement, ctx, options, footnotes, refs, attrStringToM
327
356
  [RuleType.htmlBlock]: {
328
357
  _qualify: (source) => {
329
358
  if (options.disableParsingRawHTML) return false;
330
- const match = source.match(/^ *<([a-z][a-z0-9:-]*)\b/i);
359
+ const match = HTML_BLOCK_OPENING_TAG_R.exec(source);
331
360
  if (!match) return false;
332
- const tag = match[1];
333
- return source.toLowerCase().indexOf(`</${tag.toLowerCase()}>`) !== -1;
361
+ return getClosingTagRegex(match[1]).test(source);
334
362
  },
335
363
  _match: anyScopeRegex(HTML_BLOCK_ELEMENT_R),
336
364
  _order: Priority.HIGH,
@@ -374,7 +402,7 @@ const createRules = (createElement, ctx, options, footnotes, refs, attrStringToM
374
402
  [RuleType.htmlSelfClosing]: {
375
403
  _qualify: (source) => {
376
404
  if (options.disableParsingRawHTML) return false;
377
- return /^ *<([a-zA-Z][a-zA-Z0-9:]*)[\s>/]/.test(source);
405
+ return HTML_SELF_CLOSING_OPENING_TAG_R.test(source);
378
406
  },
379
407
  _match: anyScopeRegex(HTML_SELF_CLOSING_ELEMENT_R),
380
408
  _order: Priority.HIGH,
@@ -393,7 +421,7 @@ const createRules = (createElement, ctx, options, footnotes, refs, attrStringToM
393
421
  }
394
422
  },
395
423
  [RuleType.customComponent]: {
396
- _qualify: (source) => /^ *<([A-Z][a-zA-Z0-9]*)/.test(source),
424
+ _qualify: (source) => CUSTOM_COMPONENT_OPENING_TAG_R.test(source),
397
425
  _match: anyScopeRegex(CUSTOM_COMPONENT_R),
398
426
  _order: Priority.MAX,
399
427
  _parse(capture, parse, state) {
@@ -682,10 +710,19 @@ const createRules = (createElement, ctx, options, footnotes, refs, attrStringToM
682
710
  }
683
711
  };
684
712
  };
685
- const parseMarkdown = (markdown = "", ctx, options = {}) => {
686
- const dummyCreateElement = () => null;
687
- const footnotes = [];
688
- const refs = {};
713
+ /**
714
+ * `createElement` is only ever reached from a rule's `_render`, so a parse-only
715
+ * caller can hand over this stub instead of building an element factory.
716
+ */
717
+ const noopCreateElement = () => null;
718
+ /**
719
+ * Builds the rule set backing one document.
720
+ *
721
+ * Rules close over the document's footnote and reference tables, so they cannot
722
+ * be shared across documents — but a single document's parse and render phases
723
+ * can share one set, which is what `compile` does.
724
+ */
725
+ const createDocumentRules = (createElement, ctx, options, footnotes, refs) => {
689
726
  const attrStringToMap = (tag, str) => {
690
727
  if (!str?.trim()) return null;
691
728
  const attributes = str.match(ATTR_EXTRACTOR_R);
@@ -714,29 +751,33 @@ const parseMarkdown = (markdown = "", ctx, options = {}) => {
714
751
  UNORDERED_LIST_R,
715
752
  CUSTOM_COMPONENT_R
716
753
  ];
754
+ const blockSyntaxes = options.disableParsingRawHTML ? nonParagraphBlockSyntaxes : [
755
+ ...nonParagraphBlockSyntaxes,
756
+ PARAGRAPH_R,
757
+ HTML_BLOCK_ELEMENT_R,
758
+ HTML_COMMENT_R,
759
+ HTML_SELF_CLOSING_ELEMENT_R,
760
+ CUSTOM_COMPONENT_R
761
+ ];
717
762
  const containsBlockSyntax = (input) => {
718
763
  const cleaned = input.replace(TRIM_STARTING_NEWLINES, "");
719
764
  const slice = cleaned.length > 2048 ? cleaned.slice(0, 2048) : cleaned;
720
765
  if (slice.indexOf("\n\n") !== -1) return true;
721
- const syntaxes = options.disableParsingRawHTML ? nonParagraphBlockSyntaxes : [
722
- ...nonParagraphBlockSyntaxes,
723
- PARAGRAPH_R,
724
- HTML_BLOCK_ELEMENT_R,
725
- HTML_COMMENT_R,
726
- HTML_SELF_CLOSING_ELEMENT_R,
727
- CUSTOM_COMPONENT_R
728
- ];
729
- return some(syntaxes, slice);
766
+ return some(blockSyntaxes, slice);
730
767
  };
731
- const baseRules = createRules(dummyCreateElement, ctx, options, footnotes, refs, attrStringToMap, containsBlockSyntax, nonParagraphBlockSyntaxes);
732
- const rules = options.disableParsingRawHTML ? Object.keys(baseRules).reduce((acc, key) => {
768
+ const baseRules = createRules(createElement, ctx, options, footnotes, refs, attrStringToMap, containsBlockSyntax, nonParagraphBlockSyntaxes);
769
+ return options.disableParsingRawHTML ? Object.keys(baseRules).reduce((acc, key) => {
733
770
  if (key !== RuleType.htmlBlock && key !== RuleType.htmlSelfClosing) acc[key] = baseRules[key];
734
771
  return acc;
735
772
  }, {}) : baseRules;
773
+ };
774
+ /** Runs the parse phase against an already-built rule set. */
775
+ const parseWithRules = (markdown, options, rules, footnotes, refs) => {
736
776
  const parser = parserFor(rules);
737
777
  const result = options.preserveFrontmatter ? markdown : markdown.replace(FRONT_MATTER_R, "");
738
- const inline = options.forceInline || !options.forceBlock && SHOULD_RENDER_AS_BLOCK_R.test(result.replace(TRIM_STARTING_NEWLINES, "")) === false;
739
- const ast = parser(inline ? result : `${trimEnd(result).replace(TRIM_STARTING_NEWLINES, "")}\n\n`, { inline });
778
+ const trimmedStart = result.replace(TRIM_STARTING_NEWLINES, "");
779
+ const inline = options.forceInline || !options.forceBlock && SHOULD_RENDER_AS_BLOCK_R.test(trimmedStart) === false;
780
+ const ast = parser(inline ? result : `${trimEnd(trimmedStart)}\n\n`, { inline });
740
781
  if (footnotes.length > 0) for (const def of footnotes) def.parsedAst = parser(def.footnote, { inline: true });
741
782
  return {
742
783
  ast,
@@ -745,21 +786,11 @@ const parseMarkdown = (markdown = "", ctx, options = {}) => {
745
786
  inline
746
787
  };
747
788
  };
748
- const renderMarkdownAst = (parsed, ctx, options = {}) => {
789
+ /** Runs the render phase against an already-built rule set. */
790
+ const renderWithRules = (parsed, ctx, options, rules, createElement) => {
749
791
  const components = ctx.components ?? {};
750
- const slug = (input) => {
751
- return ctx.slugify ? ctx.slugify(input, slugify) : slugify(input);
752
- };
753
- const createElement = createElementFactory(ctx, options);
792
+ const slug = createSlugger(ctx);
754
793
  const footnotes = parsed.footnotes || [];
755
- const refs = parsed.references ?? {};
756
- const attrStringToMap = () => null;
757
- const containsBlockSyntax = () => false;
758
- const baseRules = createRules(createElement, ctx, options, footnotes, refs, attrStringToMap, containsBlockSyntax, []);
759
- const rules = options.disableParsingRawHTML ? Object.keys(baseRules).reduce((acc, key) => {
760
- if (key !== RuleType.htmlBlock && key !== RuleType.htmlSelfClosing) acc[key] = baseRules[key];
761
- return acc;
762
- }, {}) : baseRules;
763
794
  const emitter = renderFor(createRenderer(rules, options.renderRule));
764
795
  const inline = parsed.inline;
765
796
  const arr = emitter(parsed.ast, { inline });
@@ -794,13 +825,28 @@ const renderMarkdownAst = (parsed, ctx, options = {}) => {
794
825
  key: def.identifier
795
826
  }, def.identifier, emitter(def.parsedAst || def.footnote, { inline: true }))))) : node;
796
827
  };
828
+ const parseMarkdown = (markdown = "", ctx, options = {}) => {
829
+ const footnotes = [];
830
+ const refs = {};
831
+ const rules = createDocumentRules(noopCreateElement, ctx, options, footnotes, refs);
832
+ return parseWithRules(markdown, options, rules, footnotes, refs);
833
+ };
834
+ const renderMarkdownAst = (parsed, ctx, options = {}) => {
835
+ const createElement = createElementFactory(ctx, options);
836
+ const rules = createDocumentRules(createElement, ctx, options, parsed.footnotes ?? [], parsed.references ?? {});
837
+ return renderWithRules(parsed, ctx, options, rules, createElement);
838
+ };
797
839
  const compile = (markdown = "", ctx, options = {}) => {
798
840
  if (typeof markdown !== "string") {
799
841
  console.error("intlayer: the first argument must be a string. Received", typeof markdown);
800
842
  throw new Error("intlayer: the first argument must be a string");
801
843
  }
802
- const parsed = parseMarkdown(markdown, ctx, options);
803
- return renderMarkdownAst(parsed, ctx, options);
844
+ const footnotes = [];
845
+ const refs = {};
846
+ const createElement = createElementFactory(ctx, options);
847
+ const rules = createDocumentRules(createElement, ctx, options, footnotes, refs);
848
+ const parsed = parseWithRules(markdown, options, rules, footnotes, refs);
849
+ return renderWithRules(parsed, ctx, options, rules, createElement);
804
850
  };
805
851
  const createCompiler = (ctx) => (markdown, options) => compile(markdown, ctx, options);
806
852
  const compileWithOptions = (markdown, runtime, options = {}) => {