comark 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +25 -1
  2. package/dist/context.d.ts +78 -0
  3. package/dist/context.js +127 -0
  4. package/dist/devtools/bridge.d.ts +1 -0
  5. package/dist/devtools/bridge.js +1 -0
  6. package/dist/devtools/constants.d.ts +1 -0
  7. package/dist/devtools/constants.js +1 -0
  8. package/dist/devtools/renderer/dom.d.ts +1 -0
  9. package/dist/devtools/renderer/dom.js +1 -0
  10. package/dist/devtools/renderer/index.d.ts +2 -0
  11. package/dist/devtools/renderer/index.js +2 -0
  12. package/dist/devtools/renderer/output.d.ts +1 -0
  13. package/dist/devtools/renderer/output.js +1 -0
  14. package/dist/devtools/renderer/panel.d.ts +1 -0
  15. package/dist/devtools/renderer/panel.js +1 -0
  16. package/dist/devtools/renderer/styles.d.ts +1 -0
  17. package/dist/devtools/renderer/styles.js +1 -0
  18. package/dist/devtools/renderer/theme.d.ts +1 -0
  19. package/dist/devtools/renderer/theme.js +1 -0
  20. package/dist/devtools/types.d.ts +1 -0
  21. package/dist/devtools/types.js +1 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +2 -0
  24. package/dist/internal/parse/auto-close/index.js +96 -31
  25. package/dist/internal/parse/auto-unwrap.js +5 -1
  26. package/dist/internal/parse/html/html_block_rule.js +9 -15
  27. package/dist/internal/parse/html/index.d.ts +1 -0
  28. package/dist/internal/parse/html/index.js +1 -1
  29. package/dist/internal/parse/token-processor.js +70 -32
  30. package/dist/internal/stringify/attributes.d.ts +8 -1
  31. package/dist/internal/stringify/attributes.js +53 -0
  32. package/dist/internal/stringify/handlers/blockquote.js +17 -0
  33. package/dist/internal/stringify/handlers/heading.js +6 -1
  34. package/dist/internal/stringify/handlers/html.js +8 -2
  35. package/dist/internal/stringify/handlers/li.js +19 -9
  36. package/dist/internal/stringify/handlers/mdc.js +1 -1
  37. package/dist/internal/stringify/handlers/ol.js +15 -2
  38. package/dist/internal/stringify/handlers/p.js +4 -0
  39. package/dist/internal/stringify/handlers/pre.js +11 -2
  40. package/dist/internal/stringify/handlers/table.js +7 -0
  41. package/dist/internal/stringify/handlers/template.js +4 -1
  42. package/dist/internal/stringify/handlers/ul.js +11 -1
  43. package/dist/internal/stringify/state.js +13 -1
  44. package/dist/parse.d.ts +4 -4
  45. package/dist/parse.js +7 -3
  46. package/dist/plugins/alert.d.ts +1 -1
  47. package/dist/plugins/binding.d.ts +1 -1
  48. package/dist/plugins/breaks.d.ts +1 -1
  49. package/dist/plugins/emoji.d.ts +1 -1
  50. package/dist/plugins/footnotes.d.ts +1 -1
  51. package/dist/plugins/headings.d.ts +19 -8
  52. package/dist/plugins/headings.js +25 -15
  53. package/dist/plugins/highlight.d.ts +1 -1
  54. package/dist/plugins/highlight.js +4 -2
  55. package/dist/plugins/json-render.d.ts +1 -1
  56. package/dist/plugins/math.d.ts +1 -1
  57. package/dist/plugins/mermaid.d.ts +1 -1
  58. package/dist/plugins/punctuation.d.ts +1 -1
  59. package/dist/plugins/security.d.ts +12 -1
  60. package/dist/plugins/security.js +13 -6
  61. package/dist/plugins/summary.d.ts +4 -1
  62. package/dist/plugins/syntax.d.ts +1 -1
  63. package/dist/plugins/syntax.js +95 -36
  64. package/dist/plugins/task-list.d.ts +1 -1
  65. package/dist/plugins/toc.d.ts +3 -1
  66. package/dist/render.d.ts +6 -2
  67. package/dist/render.js +2 -2
  68. package/dist/types.d.ts +61 -12
  69. package/dist/utils/helpers.d.ts +16 -4
  70. package/dist/utils/helpers.js +15 -3
  71. package/dist/utils/index.d.ts +3 -1
  72. package/dist/utils/index.js +30 -14
  73. package/package.json +6 -3
  74. package/skills/comark/references/rendering-svelte.md +51 -7
  75. package/dist/internal/stringify/indent.d.ts +0 -1
  76. package/dist/internal/stringify/indent.js +0 -1
  77. package/dist/vite.d.ts +0 -1
  78. package/dist/vite.js +0 -1
package/dist/types.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import type { DumpOptions } from 'js-yaml';
2
2
  import type MarkdownExit from 'markdown-exit';
3
3
  import type MarkdownIt from 'markdown-it';
4
+ /**
5
+ * The `[keyof T] extends [never]` form (rather than `keyof T extends never`)
6
+ * is the standard trick to prevent TS from distributing the check over a
7
+ * union — we want to test "is T's keyset empty?" as one yes/no question.
8
+ */
9
+ type Writable<T> = [keyof T] extends [never] ? Record<string, any> : T;
4
10
  /**
5
11
  * The Comark text
6
12
  * @param string - The text content
@@ -43,11 +49,14 @@ export type ComarkNode = ComarkElement | ComarkText | ComarkComment;
43
49
  * @param nodes - The nodes of the tree
44
50
  * @param frontmatter - The frontmatter data which is the data at the top of the file
45
51
  * @param meta - The meta data of tree, it can be used to store additional data for the tree
52
+ *
53
+ * The `TMeta` and `TFrontmatter` type parameters allow `parse` / `createParse`
54
+ * to surface plugin-contributed keys with narrow types (see `MergePluginMeta`).
46
55
  */
47
- export interface ComarkTree {
56
+ export interface ComarkTree<TMeta = Record<string, any>, TFrontmatter = Record<string, any>> {
48
57
  nodes: ComarkNode[];
49
- frontmatter: Record<string, any>;
50
- meta: Record<string, any>;
58
+ frontmatter: TFrontmatter;
59
+ meta: TMeta;
51
60
  }
52
61
  export interface ContextBase {
53
62
  /**
@@ -234,26 +243,60 @@ export type ComarkParsePreState = {
234
243
  options: ParseOptions;
235
244
  [key: string]: any;
236
245
  };
237
- export type ComarkParsePostState = {
246
+ export type ComarkParsePostState<TMeta = Record<string, any>, TFrontmatter = Record<string, any>> = {
238
247
  markdown: string;
239
- tree: ComarkTree;
248
+ tree: ComarkTree<TMeta, TFrontmatter>;
240
249
  options: ParseOptions;
241
250
  tokens: unknown[];
242
251
  [key: string]: any;
243
252
  };
244
- export type ComarkPlugin = {
253
+ /**
254
+ * A Comark plugin.
255
+ *
256
+ * `TMeta` / `TFrontmatter` are phantom type parameters that record what this
257
+ * plugin contributes to `tree.meta` / `tree.frontmatter`. They are surfaced
258
+ * only via the optional `__meta` / `__frontmatter` markers — implementations
259
+ * never set these at runtime; they exist purely so the contribution survives
260
+ * `ReturnType<typeof factory>` inference and can be merged in `createParse`.
261
+ */
262
+ export type ComarkPlugin<TMeta = {}, TFrontmatter = {}> = {
245
263
  name: string;
246
264
  markdownItPlugins?: MarkdownItPlugin[];
247
265
  pre?: (state: ComarkParsePreState) => Promise<void> | void;
248
- post?: (state: ComarkParsePostState) => Promise<void> | void;
266
+ post?: (state: ComarkParsePostState<Writable<TMeta>, Writable<TFrontmatter>>) => Promise<void> | void;
267
+ /** Phantom — used for type inference only. Never set at runtime. */
268
+ __meta?: TMeta;
269
+ /** Phantom — used for type inference only. Never set at runtime. */
270
+ __frontmatter?: TFrontmatter;
249
271
  };
250
- export type ComarkPluginFactory<Options> = (opts?: Options) => ComarkPlugin;
251
- export type ComponentManifest = (name: string) => Promise<unknown> | undefined | null;
272
+ export type ComarkPluginFactory<Options, TMeta = {}, TFrontmatter = {}> = (opts?: Options) => ComarkPlugin<TMeta, TFrontmatter>;
273
+ type PluginMetaOf<P> = P extends ComarkPlugin<infer M, any> ? M : {};
274
+ type PluginFrontmatterOf<P> = P extends ComarkPlugin<any, infer F> ? F : {};
275
+ /**
276
+ * Walk a tuple of plugins and intersect their meta contributions.
277
+ * Returns `{}` when the tuple is empty or when nothing was contributed.
278
+ */
279
+ export type MergePluginMeta<TPlugins extends readonly unknown[]> = TPlugins extends readonly [infer Head, ...infer Rest] ? PluginMetaOf<Head> & MergePluginMeta<Rest extends readonly unknown[] ? Rest : []> : {};
280
+ /**
281
+ * Walk a tuple of plugins and intersect their frontmatter contributions.
282
+ */
283
+ export type MergePluginFrontmatter<TPlugins extends readonly unknown[]> = TPlugins extends readonly [
284
+ infer Head,
285
+ ...infer Rest
286
+ ] ? PluginFrontmatterOf<Head> & MergePluginFrontmatter<Rest extends readonly unknown[] ? Rest : []> : {};
287
+ /**
288
+ * When no plugin contributed meta keys, fall back to the permissive
289
+ * `Record<string, any>` (backwards-compatible). Otherwise, preserve narrow
290
+ * keys and type unknown accesses as `unknown` (safer than `any`).
291
+ */
292
+ export type ResolvedMeta<T> = [keyof T] extends [never] ? Record<string, any> : T & Record<string, unknown>;
293
+ export type ResolvedFrontmatter<T> = [keyof T] extends [never] ? Record<string, any> : T & Record<string, unknown>;
294
+ export type ComponentManifest = (name: string) => unknown | Promise<unknown> | undefined | null;
252
295
  export interface ComarkContextProvider {
253
296
  components: Record<string, any>;
254
297
  componentManifest: ComponentManifest;
255
298
  }
256
- export interface ParseOptions {
299
+ export interface ParseOptions<TPlugins extends readonly ComarkPlugin<any, any>[] = readonly ComarkPlugin<any, any>[]> {
257
300
  /**
258
301
  * Whether to automatically unwrap single paragraphs in container components.
259
302
  * When enabled, if a container component (alert, card, callout, note, warning, tip, info)
@@ -296,11 +339,16 @@ export interface ParseOptions {
296
339
  * // With html: false — HTML tags are left as raw text / ignored
297
340
  */
298
341
  html?: boolean;
342
+ /**
343
+ * Set `false` to disable autoconvert URL-like text to links.
344
+ * @default true
345
+ */
346
+ linkify?: boolean;
299
347
  /**
300
348
  * Additional plugins to use
301
349
  * @default []
302
350
  */
303
- plugins?: ComarkPlugin[];
351
+ plugins?: TPlugins;
304
352
  }
305
353
  /**
306
354
  * Type signature for the options object passed to the Comark parser function returned by createParse().
@@ -312,4 +360,5 @@ export type ComarkParseFnOptions = {
312
360
  * Type signature for the async Comark parser function returned by createParse().
313
361
  * Accepts a markdown string and optional parsing options, and returns a Promise of ComarkTree.
314
362
  */
315
- export type ComarkParseFn = (markdown: string, opts?: ComarkParseFnOptions) => Promise<ComarkTree>;
363
+ export type ComarkParseFn<TMeta = Record<string, any>, TFrontmatter = Record<string, any>> = (markdown: string, opts?: ComarkParseFnOptions) => Promise<ComarkTree<TMeta, TFrontmatter>>;
364
+ export {};
@@ -5,8 +5,20 @@ import type { ComarkPluginFactory } from '../types.ts';
5
5
  */
6
6
  export declare function createSerializedTask<TArgs extends unknown[], TResult>(fn: (...args: TArgs) => Promise<TResult>): (...args: TArgs) => Promise<TResult>;
7
7
  /**
8
- * Define a Comark plugin
9
- * @param fn - The plugin factory function
10
- * @returns The defined plugin
8
+ * Define a Comark plugin.
9
+ *
10
+ * `TMeta` and `TFrontmatter` declare what the plugin contributes to
11
+ * `tree.meta` / `tree.frontmatter`. They are inferred from the factory's
12
+ * return type when set via the `__meta` / `__frontmatter` phantom markers,
13
+ * or can be passed explicitly. Plugins that don't contribute typed keys can
14
+ * omit them entirely.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * defineComarkPlugin<Options, { toc: Toc }>((opts) => ({
19
+ * name: 'toc',
20
+ * post(state) { state.tree.meta.toc = ... },
21
+ * }))
22
+ * ```
11
23
  */
12
- export declare function defineComarkPlugin<Options>(fn: ComarkPluginFactory<Options>): ComarkPluginFactory<Options>;
24
+ export declare function defineComarkPlugin<Options, TMeta = {}, TFrontmatter = {}>(fn: ComarkPluginFactory<Options, TMeta, TFrontmatter>): ComarkPluginFactory<Options, TMeta, TFrontmatter>;
@@ -11,9 +11,21 @@ export function createSerializedTask(fn) {
11
11
  }
12
12
  // #region define plugin
13
13
  /**
14
- * Define a Comark plugin
15
- * @param fn - The plugin factory function
16
- * @returns The defined plugin
14
+ * Define a Comark plugin.
15
+ *
16
+ * `TMeta` and `TFrontmatter` declare what the plugin contributes to
17
+ * `tree.meta` / `tree.frontmatter`. They are inferred from the factory's
18
+ * return type when set via the `__meta` / `__frontmatter` phantom markers,
19
+ * or can be passed explicitly. Plugins that don't contribute typed keys can
20
+ * omit them entirely.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * defineComarkPlugin<Options, { toc: Toc }>((opts) => ({
25
+ * name: 'toc',
26
+ * post(state) { state.tree.meta.toc = ... },
27
+ * }))
28
+ * ```
17
29
  */
18
30
  export function defineComarkPlugin(fn) {
19
31
  return fn;
@@ -1,4 +1,5 @@
1
1
  import type { ComarkNode, ComarkTree } from 'comark';
2
+ type VisitResult = ComarkNode | false | undefined | void;
2
3
  /**
3
4
  * Get the text content of a Comark node
4
5
  *
@@ -17,7 +18,8 @@ export declare function textContent(node: ComarkNode, options?: {
17
18
  * @param checker - A function that checks if a node should be visited
18
19
  * @param visitor - A function that visits a node
19
20
  */
20
- export declare function visit(tree: ComarkTree, checker: (node: ComarkNode) => boolean, visitor: (node: ComarkNode) => ComarkNode | false | undefined | void): void;
21
+ export declare function visit(tree: ComarkTree, checker: (node: ComarkNode) => boolean, visitor: (node: ComarkNode) => VisitResult): void;
22
+ export declare function visitAsync(tree: ComarkTree, checker: (node: ComarkNode) => boolean, visitor: (node: ComarkNode) => Promise<VisitResult> | VisitResult): Promise<void>;
21
23
  export declare function indent(text: string, { ignoreFirstLine, level, width }?: {
22
24
  ignoreFirstLine?: boolean;
23
25
  level?: number;
@@ -22,20 +22,11 @@ export function textContent(node, options = {}) {
22
22
  }
23
23
  return out;
24
24
  }
25
- /**
26
- * Visit a Comark tree and apply a visitor function to each node
27
- *
28
- * @param tree - The Comark tree
29
- * @param checker - A function that checks if a node should be visited
30
- * @param visitor - A function that visits a node
31
- */
32
- export function visit(tree, checker,
33
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
34
- visitor) {
35
- function walk(node, parent, index) {
25
+ function* walkGenerator(tree, checker) {
26
+ function* walk(node, parent, index) {
36
27
  let currentNode = node;
37
28
  if (checker(node)) {
38
- const res = visitor(node);
29
+ const res = yield node;
39
30
  if (res === false) {
40
31
  // remove the node from the parent
41
32
  ;
@@ -52,7 +43,7 @@ visitor) {
52
43
  // Use a while loop to handle removals correctly - don't increment if node was removed
53
44
  let i = 2;
54
45
  while (i < currentNode.length) {
55
- const childRemoved = walk(currentNode[i], currentNode, i);
46
+ const childRemoved = yield* walk(currentNode[i], currentNode, i);
56
47
  if (childRemoved) {
57
48
  // If removed, i stays the same (next node is now at this index)
58
49
  continue;
@@ -65,7 +56,7 @@ visitor) {
65
56
  // Use a while loop to handle removals correctly - don't increment if node was removed
66
57
  let i = 0;
67
58
  while (i < tree.nodes.length) {
68
- const removed = walk(tree.nodes[i], tree.nodes, i);
59
+ const removed = yield* walk(tree.nodes[i], tree.nodes, i);
69
60
  if (removed) {
70
61
  // If removed, i stays the same (next node is now at this index)
71
62
  continue;
@@ -73,6 +64,31 @@ visitor) {
73
64
  i += 1;
74
65
  }
75
66
  }
67
+ /**
68
+ * Visit a Comark tree and apply a visitor function to each node
69
+ *
70
+ * @param tree - The Comark tree
71
+ * @param checker - A function that checks if a node should be visited
72
+ * @param visitor - A function that visits a node
73
+ */
74
+ export function visit(tree, checker,
75
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
76
+ visitor) {
77
+ const iterator = walkGenerator(tree, checker);
78
+ let step = iterator.next();
79
+ while (!step.done) {
80
+ const res = visitor(step.value);
81
+ step = iterator.next(res);
82
+ }
83
+ }
84
+ export async function visitAsync(tree, checker, visitor) {
85
+ const iterator = walkGenerator(tree, checker);
86
+ let step = iterator.next();
87
+ while (!step.done) {
88
+ const res = await visitor(step.value);
89
+ step = iterator.next(res);
90
+ }
91
+ }
76
92
  // #region String Utils
77
93
  export function indent(text, { ignoreFirstLine = false, level = 1, width } = {}) {
78
94
  const pad = width ? ' '.repeat(width) : ' '.repeat(level);
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "comark",
3
- "version": "0.3.2",
4
- "description": "Components in Markdown (Comark) parser with streaming support for Vue, React, Svelte and HTML",
3
+ "version": "0.5.0",
4
+ "description": "Components in Markdown (Comark) parser with streaming support for Vue, React, Svelte, Angular and HTML",
5
5
  "keywords": [
6
+ "angular",
6
7
  "markdown",
7
8
  "mdc",
8
9
  "parser",
9
10
  "react",
10
11
  "streaming",
12
+ "svelte",
11
13
  "vue"
12
14
  ],
13
15
  "homepage": "https://comark.dev",
@@ -25,6 +27,7 @@
25
27
  "skills"
26
28
  ],
27
29
  "type": "module",
30
+ "sideEffects": false,
28
31
  "main": "./dist/index.js",
29
32
  "module": "./dist/index.js",
30
33
  "types": "./dist/index.d.ts",
@@ -52,7 +55,7 @@
52
55
  "github-slugger": "^2.0.0",
53
56
  "hast-util-to-string": "^3.0.1",
54
57
  "minimark": "0.2.0",
55
- "tsx": "^4.21.0",
58
+ "tsx": "^4.22.4",
56
59
  "twoslash": "^0.3.6",
57
60
  "vitest": "^4.1.4"
58
61
  },
@@ -48,9 +48,9 @@ Map custom Svelte components to Comark elements:
48
48
  ```svelte
49
49
  <script lang="ts">
50
50
  import { Comark } from '@comark/svelte'
51
- import CustomHeading from './CustomHeading.svelte'
52
- import CustomAlert from './CustomAlert.svelte'
53
- import CustomCard from './CustomCard.svelte'
51
+ import CustomHeading from './components/comark/CustomHeading.svelte'
52
+ import CustomAlert from './components/comark/CustomAlert.svelte'
53
+ import CustomCard from './components/comark/CustomCard.svelte'
54
54
 
55
55
  const customComponents = {
56
56
  h1: CustomHeading,
@@ -127,9 +127,9 @@ Load components dynamically using `componentsManifest`:
127
127
  import { Comark } from '@comark/svelte'
128
128
 
129
129
  const componentMap: Record<string, () => Promise<any>> = {
130
- 'alert': () => import('./Alert.svelte'),
131
- 'card': () => import('./Card.svelte'),
132
- 'button': () => import('./Button.svelte'),
130
+ 'alert': () => import('./components/comark/Alert.svelte'),
131
+ 'card': () => import('./components/comark/Card.svelte'),
132
+ 'button': () => import('./components/comark/Button.svelte'),
133
133
  }
134
134
 
135
135
  async function loadComponent(name: string) {
@@ -144,6 +144,50 @@ Load components dynamically using `componentsManifest`:
144
144
  <Comark markdown={content} componentsManifest={loadComponent} />
145
145
  ```
146
146
 
147
+ In SvelteKit projects, keep components rendered from Markdown in a dedicated folder such as `$lib/components/comark/`. This keeps Comark-rendered components separate from normal app UI components and makes `componentsManifest` globs easier to audit.
148
+
149
+ For SvelteKit SSR with non-eager lazy components, use `ComarkAsync` and a manifest that returns dynamic imports. An explicit map is the easiest option to audit:
150
+
151
+ ```svelte
152
+ <script lang="ts">
153
+ import { ComarkAsync } from '@comark/svelte/async'
154
+
155
+ const componentMap: Record<string, () => Promise<any>> = {
156
+ 'alert': () => import('$lib/components/comark/Alert.svelte'),
157
+ 'lazy-card': () => import('$lib/components/comark/LazyCard.svelte'),
158
+ }
159
+
160
+ const componentsManifest = (name: string) => componentMap[name]?.()
161
+ </script>
162
+
163
+ <svelte:boundary>
164
+ <ComarkAsync markdown={content} {componentsManifest} />
165
+ </svelte:boundary>
166
+ ```
167
+
168
+ Use `import.meta.glob` when you want the manifest to cover every Svelte component in a folder:
169
+
170
+ ```svelte
171
+ <script lang="ts">
172
+ import { ComarkAsync } from '@comark/svelte/async'
173
+ import { pascalCase } from '@comark/svelte/utils'
174
+
175
+ const modules = import.meta.glob('../lib/components/comark/*.svelte')
176
+
177
+ const componentsManifest = (name: string) => {
178
+ return modules[`../lib/components/comark/${pascalCase(name)}.svelte`]?.()
179
+ }
180
+ </script>
181
+
182
+ <svelte:boundary>
183
+ <ComarkAsync markdown={content} {componentsManifest} />
184
+ </svelte:boundary>
185
+ ```
186
+
187
+ Omit the boundary `pending` snippet when you want SvelteKit SSR to wait and include the resolved lazy components in the initial HTML.
188
+
189
+ Use eager/static components with `ComarkRenderer` when you need stable SSR without Svelte's experimental async support.
190
+
147
191
  ---
148
192
 
149
193
  ## Slots Support
@@ -319,7 +363,7 @@ Override native HTML elements using the `Prose` prefix:
319
363
 
320
364
  ## Experimental Async
321
365
 
322
- The `ComarkAsync` component uses Svelte's experimental `await` in `$derived` for a declarative approach. Requires `experimental.async` in your Svelte config:
366
+ The `ComarkAsync` component uses Svelte's experimental `await` in `$derived` for a declarative approach. It can also await async `componentsManifest` entries during SSR, so lazy dynamic imports render into SvelteKit server HTML. Requires `experimental.async` in your Svelte config:
323
367
 
324
368
  ```js
325
369
  // svelte.config.js
@@ -1 +0,0 @@
1
- export * from '../../../src/internal/stringify/indent'
@@ -1 +0,0 @@
1
- export * from '../../../src/internal/stringify/indent.ts'
package/dist/vite.d.ts DELETED
@@ -1 +0,0 @@
1
- export * from '../src/vite'
package/dist/vite.js DELETED
@@ -1 +0,0 @@
1
- export * from '../src/vite.ts'