origam 2.34.0 → 2.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -18,6 +18,24 @@ This project follows [Semantic Versioning](https://semver.org).
18
18
 
19
19
  ## [Unreleased]
20
20
 
21
+ ## [2.35.1] - 2026-10-19
22
+
23
+ ### Fixed — `OrigamCode` : un 2e bloc monté pendant un import de `shiki` en échec restait vide (#1189)
24
+
25
+ Quand `shiki` n'est pas résolvable, deux `<origam-code>` montés dans le même tick : le premier s'affichait en texte brut, le second restait **vide** (zone défilante de hauteur 0) avec un rejet de promesse non capturé. `loadHighlighter()` ne renvoyait la promesse munie de son repli qu'au premier appelant ; un appelant concurrent recevait la promesse brute, qui rejetait, et `rebuild()` n'avait qu'un `try/finally`.
26
+
27
+ - `loadHighlighter()` renvoie désormais la même promesse, déjà munie de son repli (`null` → code brut), à tous les appelants.
28
+ - `rebuild()` capture en plus un éventuel rejet et peint le code en clair : aucun bloc ne reste vide.
29
+ - Prouvé par `packages/tests/TU/components/Code/OrigamCode-concurrent-load-failure.spec.ts` (rouge sur le parent, vert après).
30
+
31
+ ## [2.35.0] - 2026-10-19
32
+
33
+ ### Added — `OrigamCode` : `lang` ouvert, `createOrigam({ code: { langs } })` (#1180)
34
+
35
+ - `lang` n'est plus limité aux 14 grammaires de `SUPPORTED_LANGS` quand un `code.highlighter` est fourni : la valeur est transmise telle quelle au highlighter, qui sait seul ce qu'il peut servir. S'il échoue, le bloc retombe sur `plaintext` (le `try/catch` existant autour de `codeToHtml`), avec un avertissement dev-only, une fois par langage. Avant, `resolveLang()` repliait tout `lang` inconnu sur `plaintext` avant de consulter le highlighter, donc `dockerfile` ne pouvait jamais être servi.
36
+ - Nouvelle option `createOrigam({ code: { langs } })` : liste passée à `createHighlighter` (défaut : `SUPPORTED_LANGS`). Avec le highlighter intégré, un `lang` présent dans `langs` est accepté.
37
+ - Typage : la prop `lang` devient `TCodeLangOpen` (`TCodeLang | (string & {})`), l'autocomplétion des 14 valeurs est conservée. `TCodeLang` reste la liste fermée.
38
+
21
39
  ## [2.34.0] - 2026-10-19
22
40
 
23
41
  ### Added — `OrigamCommandPalette` : prop `filter` pour afficher des résultats classés en externe (#1184)
@@ -266,6 +266,13 @@ async function rebuild() {
266
266
  const html = await highlight(formattedCode.value, props.lang);
267
267
  if (token !== _renderToken) return;
268
268
  paintIntoDom(html);
269
+ } catch (err) {
270
+ console.warn("[origam] OrigamCode: highlighting failed \u2014 rendering plain code.", err);
271
+ if (token === _renderToken) {
272
+ const plain = document.createElement("div");
273
+ plain.textContent = formattedCode.value;
274
+ paintIntoDom(plain.innerHTML);
275
+ }
269
276
  } finally {
270
277
  if (token === _renderToken) isHighlighting.value = false;
271
278
  }
@@ -34,19 +34,19 @@ function lruSet(key, value) {
34
34
  }
35
35
  _cache.set(key, value);
36
36
  }
37
- async function loadHighlighter(loader) {
37
+ async function loadHighlighter(loader, langs) {
38
38
  if (_highlighterUnavailable) return null;
39
39
  if (_highlighterPromise) return _highlighterPromise;
40
- _highlighterPromise = (async () => {
40
+ const raw = (async () => {
41
41
  const shiki = loader ? await loader() : await Promise.resolve().then(() => require("shiki"));
42
42
  const highlighter = await shiki.createHighlighter({
43
43
  themes: [_code.CODE_LIGHT_THEME, _code.CODE_DARK_THEME],
44
- langs: [..._code.SUPPORTED_LANGS]
44
+ langs: [...(langs ?? _code.SUPPORTED_LANGS)]
45
45
  });
46
46
  _highlighterReady = true;
47
47
  return highlighter;
48
48
  })();
49
- return _highlighterPromise.catch(err => {
49
+ _highlighterPromise = raw.catch(err => {
50
50
  _highlighterUnavailable = true;
51
51
  _highlighterPromise = null;
52
52
  if (!_unavailableWarned) {
@@ -55,23 +55,28 @@ async function loadHighlighter(loader) {
55
55
  }
56
56
  return null;
57
57
  });
58
+ return _highlighterPromise;
58
59
  }
59
- function resolveLang(lang) {
60
+ function resolveLang(lang, hasCustomHighlighter, extraLangs) {
61
+ if (hasCustomHighlighter) return lang;
60
62
  if (_code.SUPPORTED_LANGS.includes(lang)) return lang;
63
+ if (extraLangs?.includes(lang)) return lang;
61
64
  if (!_unsupportedLangWarned.has(lang)) {
62
65
  _unsupportedLangWarned.add(lang);
63
- console.warn(`[origam] OrigamCode: lang="${lang}" is not in the bundled grammar set. Falling back to "${_enums.CODE_LANG.PLAINTEXT}". Supported: ${_code.SUPPORTED_LANGS.join(", ")}.`);
66
+ console.warn(`[origam] OrigamCode: lang="${lang}" is not in the bundled grammar set. Falling back to "${_enums.CODE_LANG.PLAINTEXT}". Supported: ${[..._code.SUPPORTED_LANGS, ...(extraLangs ?? [])].join(", ")}. Add it with createOrigam({ code: { langs } }) or supply a code.highlighter.`);
64
67
  }
65
68
  return _enums.CODE_LANG.PLAINTEXT;
66
69
  }
67
70
  function useCode() {
68
- const highlighterLoader = (0, _vue.inject)(_code.ORIGAM_CODE_KEY)?.highlighter;
71
+ const codeOptions = (0, _vue.inject)(_code.ORIGAM_CODE_KEY);
72
+ const highlighterLoader = codeOptions?.highlighter;
73
+ const customLangs = codeOptions?.langs;
69
74
  async function highlight(code, lang) {
70
- const resolvedLang = resolveLang(lang);
75
+ const resolvedLang = resolveLang(lang, highlighterLoader !== void 0, customLangs);
71
76
  const key = makeCacheKey(code, resolvedLang);
72
77
  const cached = lruGet(key);
73
78
  if (cached !== void 0) return cached;
74
- const highlighter = await loadHighlighter(highlighterLoader);
79
+ const highlighter = await loadHighlighter(highlighterLoader, customLangs);
75
80
  if (!highlighter) {
76
81
  const plain = escapeHtml(code);
77
82
  lruSet(key, plain);
@@ -89,7 +94,10 @@ function useCode() {
89
94
  try {
90
95
  html = highlighter.codeToHtml(code, opts);
91
96
  } catch (err) {
92
- console.warn("[origam] OrigamCode: shiki failed to tokenise, falling back to plaintext.", err);
97
+ if (process.env?.DEV && !_unsupportedLangWarned.has(resolvedLang)) {
98
+ _unsupportedLangWarned.add(resolvedLang);
99
+ console.warn(`[origam] OrigamCode: shiki could not tokenise lang="${resolvedLang}", falling back to plaintext.`, err);
100
+ }
93
101
  html = highlighter.codeToHtml(code, {
94
102
  ...opts,
95
103
  lang: _enums.CODE_LANG.PLAINTEXT
@@ -99,7 +107,7 @@ function useCode() {
99
107
  return html;
100
108
  }
101
109
  async function prime() {
102
- await loadHighlighter(highlighterLoader);
110
+ await loadHighlighter(highlighterLoader, customLangs);
103
111
  }
104
112
  function isReady() {
105
113
  return _highlighterReady;
@@ -27,22 +27,22 @@ function lruSet(key, value) {
27
27
  }
28
28
  _cache.set(key, value);
29
29
  }
30
- async function loadHighlighter(loader) {
30
+ async function loadHighlighter(loader, langs) {
31
31
  if (_highlighterUnavailable) return null;
32
32
  if (_highlighterPromise) return _highlighterPromise;
33
- _highlighterPromise = (async () => {
33
+ const raw = (async () => {
34
34
  const shiki = loader ? await loader() : await import(
35
35
  /* @vite-ignore */
36
36
  "shiki"
37
37
  );
38
38
  const highlighter = await shiki.createHighlighter({
39
39
  themes: [LIGHT_THEME, DARK_THEME],
40
- langs: [...SUPPORTED_LANGS]
40
+ langs: [...langs ?? SUPPORTED_LANGS]
41
41
  });
42
42
  _highlighterReady = true;
43
43
  return highlighter;
44
44
  })();
45
- return _highlighterPromise.catch((err) => {
45
+ _highlighterPromise = raw.catch((err) => {
46
46
  _highlighterUnavailable = true;
47
47
  _highlighterPromise = null;
48
48
  if (!_unavailableWarned) {
@@ -54,25 +54,30 @@ async function loadHighlighter(loader) {
54
54
  }
55
55
  return null;
56
56
  });
57
+ return _highlighterPromise;
57
58
  }
58
- function resolveLang(lang) {
59
+ function resolveLang(lang, hasCustomHighlighter, extraLangs) {
60
+ if (hasCustomHighlighter) return lang;
59
61
  if (SUPPORTED_LANGS.includes(lang)) return lang;
62
+ if (extraLangs?.includes(lang)) return lang;
60
63
  if (!_unsupportedLangWarned.has(lang)) {
61
64
  _unsupportedLangWarned.add(lang);
62
65
  console.warn(
63
- `[origam] OrigamCode: lang="${lang}" is not in the bundled grammar set. Falling back to "${CODE_LANG.PLAINTEXT}". Supported: ${SUPPORTED_LANGS.join(", ")}.`
66
+ `[origam] OrigamCode: lang="${lang}" is not in the bundled grammar set. Falling back to "${CODE_LANG.PLAINTEXT}". Supported: ${[...SUPPORTED_LANGS, ...extraLangs ?? []].join(", ")}. Add it with createOrigam({ code: { langs } }) or supply a code.highlighter.`
64
67
  );
65
68
  }
66
69
  return CODE_LANG.PLAINTEXT;
67
70
  }
68
71
  export function useCode() {
69
- const highlighterLoader = inject(ORIGAM_CODE_KEY)?.highlighter;
72
+ const codeOptions = inject(ORIGAM_CODE_KEY);
73
+ const highlighterLoader = codeOptions?.highlighter;
74
+ const customLangs = codeOptions?.langs;
70
75
  async function highlight(code, lang) {
71
- const resolvedLang = resolveLang(lang);
76
+ const resolvedLang = resolveLang(lang, highlighterLoader !== void 0, customLangs);
72
77
  const key = makeCacheKey(code, resolvedLang);
73
78
  const cached = lruGet(key);
74
79
  if (cached !== void 0) return cached;
75
- const highlighter = await loadHighlighter(highlighterLoader);
80
+ const highlighter = await loadHighlighter(highlighterLoader, customLangs);
76
81
  if (!highlighter) {
77
82
  const plain = escapeHtml(code);
78
83
  lruSet(key, plain);
@@ -87,14 +92,17 @@ export function useCode() {
87
92
  try {
88
93
  html = highlighter.codeToHtml(code, opts);
89
94
  } catch (err) {
90
- console.warn("[origam] OrigamCode: shiki failed to tokenise, falling back to plaintext.", err);
95
+ if (import.meta.env?.DEV && !_unsupportedLangWarned.has(resolvedLang)) {
96
+ _unsupportedLangWarned.add(resolvedLang);
97
+ console.warn(`[origam] OrigamCode: shiki could not tokenise lang="${resolvedLang}", falling back to plaintext.`, err);
98
+ }
91
99
  html = highlighter.codeToHtml(code, { ...opts, lang: CODE_LANG.PLAINTEXT });
92
100
  }
93
101
  lruSet(key, html);
94
102
  return html;
95
103
  }
96
104
  async function prime() {
97
- await loadHighlighter(highlighterLoader);
105
+ await loadHighlighter(highlighterLoader, customLangs);
98
106
  }
99
107
  function isReady() {
100
108
  return _highlighterReady;
@@ -7,7 +7,7 @@ import type { IMarginProps } from '../Commons/margin.interface';
7
7
  import type { IPaddingProps } from '../Commons/padding.interface';
8
8
  import type { IRoundedProps } from '../Commons/rounded.interface';
9
9
  import type { ITypographyProps } from '../Commons/typography.interface';
10
- import type { TCodeLang, TShikiHighlighterLoader } from '../../types/Code/code.type';
10
+ import type { TCodeLangOpen, TShikiHighlighterLoader } from '../../types/Code/code.type';
11
11
  /**
12
12
  * Props for `<OrigamCode>` — a shiki-powered code block with line numbers,
13
13
  * line highlighting, copy button and theme awareness.
@@ -26,11 +26,15 @@ export interface ICodeProps extends ICommonsComponentProps, ITagProps, IBorderPr
26
26
  */
27
27
  code?: string;
28
28
  /**
29
- * Language grammar to apply. One of the values in `SUPPORTED_LANGS`.
30
- * Unknown values fall back to `plaintext` (with a one-shot console
31
- * warning).
29
+ * Language grammar to apply. Autocompletes the 14 values of
30
+ * `SUPPORTED_LANGS` but accepts any string (#1180). Without a custom
31
+ * `code.highlighter`, a value outside `SUPPORTED_LANGS` and outside
32
+ * `createOrigam({ code: { langs } })` falls back to `plaintext` (one-shot
33
+ * console warning). With a custom `code.highlighter`, the value is handed
34
+ * to it as-is; if it cannot tokenise it, the block falls back to
35
+ * `plaintext` (dev-only warning).
32
36
  */
33
- lang?: TCodeLang;
37
+ lang?: TCodeLangOpen;
34
38
  /**
35
39
  * Show a left gutter with line numbers. Pure CSS — uses a CSS counter
36
40
  * incremented per `.origam-code__row`, no JS layout work.
@@ -90,7 +94,7 @@ export interface ICodeProps extends ICommonsComponentProps, ITagProps, IBorderPr
90
94
  *
91
95
  * @description
92
96
  * App-level config for `<OrigamCode>`, passed via `createOrigam({ code })`.
93
- * Today it carries a single knob — `highlighter` — but it is its own
97
+ * Today it carries two knobs — `highlighter` and `langs` — and it is its own
94
98
  * interface (rather than a bare function on `IOrigamOptions`) so a future
95
99
  * `OrigamCode`-wide setting (e.g. a default theme pair) has somewhere to
96
100
  * land without another breaking shape change.
@@ -116,6 +120,23 @@ export interface ICodeProps extends ICommonsComponentProps, ITagProps, IBorderPr
116
120
  ********************************************************/
117
121
  export interface ICodeOptions {
118
122
  highlighter?: TShikiHighlighterLoader;
123
+ /*********************************************************
124
+ * langs
125
+ *
126
+ * @description
127
+ * Grammars handed to `createHighlighter({ langs })`. Default:
128
+ * `SUPPORTED_LANGS` (the 14 bundled ones). Replaces the default list when
129
+ * set — include `plaintext` if you rely on it.
130
+ *
131
+ * @description
132
+ * With the built-in highlighter, a `lang` listed here is accepted instead
133
+ * of being folded onto `plaintext`; with a custom `highlighter`, `lang` is
134
+ * never filtered, and this only controls what is loaded up front.
135
+ *
136
+ * @description
137
+ * Like `highlighter`, read once (first `<OrigamCode>` to mount wins).
138
+ ********************************************************/
139
+ langs?: ReadonlyArray<string>;
119
140
  }
120
141
  /** Emits fired by `<OrigamCode>` — copy-to-clipboard confirmation. */
121
142
  export interface ICodeEmits {
@@ -127,7 +148,7 @@ export interface ICodeSlots {
127
148
  * consumer can rebuild the default filename/lang badge + copy button. */
128
149
  header?: (data: {
129
150
  filename: string | undefined;
130
- langName: TCodeLang | undefined;
151
+ langName: TCodeLangOpen | undefined;
131
152
  copy: () => Promise<void>;
132
153
  copied: boolean;
133
154
  }) => any;
@@ -146,7 +167,7 @@ export interface IUseCodeReturn {
146
167
  * underlying shiki highlighter has loaded the requested language /
147
168
  * theme (lazy; ~200 ms cold, instant on subsequent calls).
148
169
  */
149
- highlight(code: string, lang: TCodeLang): Promise<string>;
170
+ highlight(code: string, lang: TCodeLangOpen): Promise<string>;
150
171
  /**
151
172
  * Force the highlighter to load. Useful for stories / tests that want
152
173
  * to warm the cache before mounting consumers.
@@ -1,7 +1,21 @@
1
1
  import { CODE_LANG, CODE_THEME } from '../../enums/Code/code.enum';
2
2
  import OrigamCode from '../../components/Code/OrigamCode.vue';
3
+ import type { TAnyString } from '../Commons/commons.type';
3
4
  export type TOrigamCode = InstanceType<typeof OrigamCode>;
4
5
  export type TCodeLang = `${CODE_LANG}`;
6
+ /*********************************************************
7
+ * TCodeLangOpen
8
+ *
9
+ * @description
10
+ * Open form of `TCodeLang` accepted by the `lang` prop (#1180): the 14 bundled
11
+ * grammars keep their autocompletion, any other string is allowed because a
12
+ * `createOrigam({ code: { highlighter, langs } })` highlighter can serve it.
13
+ *
14
+ * @description
15
+ * `TAnyString` (Commons) stops the union from collapsing to plain `string`,
16
+ * and avoids the empty-object intersection Sonar flags (#771).
17
+ ********************************************************/
18
+ export type TCodeLangOpen = TCodeLang | TAnyString;
5
19
  export type TCodeTheme = `${CODE_THEME}`;
6
20
  /**
7
21
  * The slice of shiki's highlighter API that `useCode()` actually calls.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "origam",
3
- "version": "2.34.0",
3
+ "version": "2.35.1",
4
4
  "type": "module",
5
5
  "description": "origam — a Vue 3 design system with multi-theme design tokens, CSS-first components and TypeScript types.",
6
6
  "keywords": [