@markii/stdlib 0.9.0 → 0.11.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.
@@ -1,18 +1,3 @@
1
- /**
2
- * The neutral, framework-agnostic definition of Mark's standard component
3
- * *contracts* (docs/integration.md/§13.6): name + kind + attribute schema +
4
- * semantics, as pure data — zero React, zero dependency on any other
5
- * `@markii/*` package. A renderer (today `@markii/react`, tomorrow a
6
- * hypothetical `@markii/gnome`) implements these same contracts against its
7
- * own component framework; this module is the seam that makes "one format,
8
- * many renderers" a checkable fact rather than a hope.
9
- *
10
- * A contract never describes markup, styling, or a component
11
- * implementation — only what a directive named `name` means: how it is
12
- * written (`kind`, matching the three directive forms from docs/format.md/§13:
13
- * `inline` -> `:name[...]`, `leaf` -> `::name{...}`, `container` ->
14
- * `:::name{...} ... :::`) and what attributes it accepts.
15
- */
16
1
  /**
17
2
  * Which of the three directive forms a component is written as. Matches
18
3
  * `remark-directive`'s three node types one-to-one:
package/dist/contracts.js CHANGED
@@ -13,32 +13,48 @@
13
13
  * `inline` -> `:name[...]`, `leaf` -> `::name{...}`, `container` ->
14
14
  * `:::name{...} ... :::`) and what attributes it accepts.
15
15
  */
16
+ import { LAYOUT_ATTRIBUTES, layoutWrapperAxis, otherLayoutAxis, } from './layout.js';
17
+ import { TEXT_ALIGN_ATTRIBUTE } from './text-align.js';
16
18
  /**
17
- * Builds one of docs/format.md's five layout-wrapper container contracts —
18
- * `center`/`right`/`wide`/`narrow`/`full` — which are otherwise identical in
19
- * shape (`kind: 'container'`, no attributes) and differ only in `name` and
20
- * the preset-specific half of `description`. A tiny helper rather than five
21
- * hand-copied literals, since a typo in one copy's `kind`/`attributes` would
22
- * otherwise be easy to miss across five near-duplicate blocks.
19
+ * Builds one of docs/format.md's layout-wrapper container contracts,
20
+ * `center`/`left`/`right`/`wide`/`narrow`/`full`/`fit`, which are otherwise
21
+ * identical in shape and differ only in `name`, the preset-specific half of
22
+ * `description`, and which of the two layout axes they leave open. A tiny
23
+ * helper rather than one hand-copied literal per preset, since a typo in
24
+ * one copy's `kind`/`attributes` would otherwise be easy to miss across a
25
+ * run of near-duplicate blocks.
26
+ *
27
+ * A wrapper decides ONE axis by its name and accepts the OTHER as an
28
+ * attribute (docs/spec.md §3), so an alignment wrapper declares `width` and
29
+ * a width wrapper declares `align`, taken from `./layout.ts`'s one
30
+ * definition of those two schemas. An attribute for the wrapper's own axis
31
+ * is deliberately absent from the contract: the name already decided it,
32
+ * and writing it is ignored rather than an error.
23
33
  *
24
34
  * `whatItDoes` is the preset-specific clause (e.g. "Centers narrower-than-
25
35
  * column plain markdown..."); the shared clause about why these exist (the
26
- * attribute mechanism cannot reach plain markdown) and about composing with
27
- * a nested wrapper is appended identically for all five.
36
+ * attribute mechanism cannot reach plain markdown) is appended identically
37
+ * for all of them.
28
38
  */
29
39
  function layoutWrapperContract(name, whatItDoes) {
40
+ const ownAxis = layoutWrapperAxis(name);
41
+ if (ownAxis === undefined) {
42
+ throw new Error(`"${name}" is not a layout-wrapper name`);
43
+ }
44
+ const openAxis = otherLayoutAxis(ownAxis);
30
45
  return {
31
46
  name,
32
47
  kind: 'container',
33
- attributes: {},
34
- description: `${whatItDoes} Takes no attributes — docs/format.md gives these five ` +
35
- 'wrapper names a closed, attribute-free form; unlike a `width`/`align` ' +
36
- 'attribute on a directive, a wrapper also reaches plain markdown a ' +
37
- 'directive attribute mechanism structurally cannot (a GFM table or a ' +
38
- 'bare image has no `{...}` to write `width=`/`align=` into). Nesting a ' +
39
- 'width wrapper (`wide`/`narrow`/`full`) inside an alignment wrapper ' +
40
- '(`center`/`right`) composes, e.g. `::::center :::narrow ... ::: ::::` ' +
41
- '(the outer fence needs more colons than the inner one).',
48
+ attributes: { [openAxis]: LAYOUT_ATTRIBUTES[openAxis] },
49
+ description: `${whatItDoes} A wrapper reaches plain markdown that a directive ` +
50
+ 'attribute structurally cannot: a GFM table or a bare image has no ' +
51
+ '`{...}` to write `width=`/`align=` into. It sets one of the two ' +
52
+ `layout axes by its name and takes the other, \`${openAxis}\`, as an ` +
53
+ `attribute, so \`:::${name}{${openAxis}=...}\` applies both to the ` +
54
+ `one scope. An \`${ownAxis}\` attribute is ignored here, since the ` +
55
+ 'name already decided that axis. Nesting two wrappers composes the ' +
56
+ 'same way, e.g. `::::center :::narrow ... ::: ::::` (the outer fence ' +
57
+ 'needs more colons than the inner one).',
42
58
  };
43
59
  }
44
60
  /**
@@ -69,6 +85,7 @@ export const STANDARD_COMPONENTS = {
69
85
  required: false,
70
86
  description: 'Optional header text shown above the body. Absent means no header line is rendered.',
71
87
  },
88
+ text: TEXT_ALIGN_ATTRIBUTE,
72
89
  },
73
90
  description: "A colored box for an aside, warning, or danger note. Body is the directive's inner markdown, rendered as-is.",
74
91
  },
@@ -102,7 +119,7 @@ export const STANDARD_COMPONENTS = {
102
119
  label: {
103
120
  type: 'string',
104
121
  required: false,
105
- description: 'Optional short text shown centered in the line. Absent or empty means an unlabeled divider.',
122
+ description: 'Optional short text shown in the break, centered unless `label-align` says otherwise. Absent or empty means an unlabeled divider.',
106
123
  },
107
124
  variant: {
108
125
  type: 'string',
@@ -110,8 +127,14 @@ export const STANDARD_COMPONENTS = {
110
127
  enum: ['line', 'dots', 'ornament'],
111
128
  description: 'Which break to draw: a solid hairline, a dotted hairline, or a centered ornament with no hairline. Defaults to `line` when absent or not one of the allowed values.',
112
129
  },
130
+ 'label-align': {
131
+ type: 'string',
132
+ required: false,
133
+ enum: ['left', 'center', 'right'],
134
+ description: 'Where the label sits along the rule. Defaults to `center` when absent or not one of the allowed values.',
135
+ },
113
136
  },
114
- description: 'A leaf directive drawing a section break, e.g. `::divider{label="Part 2" variant="dots"}`. Has no body, and both attributes are optional. A plain `---` thematic break keeps its own CommonMark meaning; this directive is for a break that carries a label or a chosen look.',
137
+ description: 'A leaf directive drawing a section break, e.g. `::divider{label="Part 2" variant="dots"}`. Has no body, and every attribute is optional. A plain `---` thematic break keeps its own CommonMark meaning; this directive is for a break that carries a label or a chosen look.',
115
138
  },
116
139
  details: {
117
140
  name: 'details',
@@ -139,6 +162,7 @@ export const STANDARD_COMPONENTS = {
139
162
  required: false,
140
163
  description: 'Optional header text shown above the body. Absent means no title line is rendered.',
141
164
  },
165
+ text: TEXT_ALIGN_ATTRIBUTE,
142
166
  },
143
167
  description: 'A titled panel, e.g. `:::card{title="Notes"} ... :::`. Body is the directive\'s inner markdown, rendered as-is.',
144
168
  },
@@ -268,21 +292,23 @@ export const STANDARD_COMPONENTS = {
268
292
  enum: ['2', '3', '4'],
269
293
  description: 'Fixed column count for the row. Defaults to auto-fit (a responsive number of equal-width columns) when absent or not one of the allowed values — an invalid `cols` is never an error.',
270
294
  },
295
+ text: TEXT_ALIGN_ATTRIBUTE,
271
296
  },
272
- description: "docs/format.md's one layout container, e.g. `:::row{cols=3} ... :::`. Its block children become equal-width cells that wrap responsively and stack on narrow viewports — and simply stack in a plain markdown viewer. No spans, no per-cell sizing, no other knobs.",
297
+ description: "docs/format.md's one layout container, e.g. `:::row{cols=3} ... :::`. Its block children become equal-width cells that wrap responsively and stack on narrow viewports — and simply stack in a plain markdown viewer. No spans, no per-cell sizing, no other knobs. A row always fills the column, so the reserved `align` has nothing to place on it; `text` is what aligns the content inside its cells.",
273
298
  },
274
299
  cell: {
275
300
  name: 'cell',
276
301
  kind: 'container',
277
- attributes: {},
278
- description: "A transparent grouping container, e.g. `:::cell ... :::`. Its only job is making several blocks count as ONE cell of a `row`: a row's cells are its direct block children, so two blocks are two cells unless a `cell` groups them — and two adjacent task lists, which markdown merges into a single list, can only become two cells by putting one `cell` around each. Takes no attributes and adds no look of its own; outside a `row` it is inert. The enclosing `row` fence must use MORE colons than its `cell` children (directive container nesting rule), hence `::::row` wrapping `:::cell`.",
302
+ attributes: { text: TEXT_ALIGN_ATTRIBUTE },
303
+ description: "A transparent grouping container, e.g. `:::cell ... :::`. Its only job is making several blocks count as ONE cell of a `row`: a row's cells are its direct block children, so two blocks are two cells unless a `cell` groups them — and two adjacent task lists, which markdown merges into a single list, can only become two cells by putting one `cell` around each. Its only presentation of its own is `text`, which overrides the enclosing row's; outside a `row` it is inert. The enclosing `row` fence must use MORE colons than its `cell` children (directive container nesting rule), hence `::::row` wrapping `:::cell`.",
279
304
  },
280
305
  center: layoutWrapperContract('center', 'Centers narrower-than-column plain markdown (a table, an image) within its scope and sets text alignment for everything in scope, e.g. `:::center ... :::`.'),
281
- left: layoutWrapperContract('left', 'Left-aligns plain markdown within its scope and sets text alignment for everything in scope, e.g. `:::left ... :::`. Mostly matches the ambient default; it exists to override an alignment inherited from an enclosing scope, such as opting one cell back out of `:::row{align=center}`.'),
306
+ left: layoutWrapperContract('left', 'Left-aligns plain markdown within its scope and sets text alignment for everything in scope, e.g. `:::left ... :::`. Mostly matches the ambient default; it exists to override an alignment inherited from an enclosing scope, such as opting one cell back out of `:::row{text=center}`.'),
282
307
  right: layoutWrapperContract('right', 'Right-aligns narrower-than-column plain markdown (a table, an image) within its scope and sets text alignment for everything in scope, e.g. `:::right ... :::`.'),
283
308
  wide: layoutWrapperContract('wide', "Sizes its scope's content to docs/format.md's wide-column width preset, e.g. `:::wide ... :::`."),
284
309
  narrow: layoutWrapperContract('narrow', "Sizes its scope's content to docs/format.md's narrow-column width preset, e.g. `:::narrow ... :::`."),
285
310
  full: layoutWrapperContract('full', "Sizes its scope's content to the full available document-column width, e.g. `:::full ... :::`."),
311
+ fit: layoutWrapperContract('fit', 'Shrinks its scope to the width of its own content rather than filling the column, e.g. `:::fit ... :::`. Pair it with an alignment to place the result, as in `:::fit{align=right} ... :::`.'),
286
312
  };
287
313
  /**
288
314
  * Looks `name` up in `STANDARD_COMPONENTS`, returning `undefined` if it
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { getContract, STANDARD_COMPONENTS, type AttributeSchema, type ComponentContract, type ComponentKind, } from './contracts.js';
2
- export { ALIGN_PRESETS, LAYOUT_ATTRIBUTE_KEYS, LAYOUT_ATTRIBUTES, WIDTH_PRESETS, } from './layout.js';
2
+ export { ALIGN_PRESETS, LAYOUT_ATTRIBUTE_KEYS, LAYOUT_ATTRIBUTES, WIDTH_PRESETS, layoutWrapperAxis, otherLayoutAxis, type LayoutAxis, } from './layout.js';
3
+ export { TEXT_ALIGN_ATTRIBUTE, TEXT_ALIGN_COMPONENTS, TEXT_ALIGN_PRESETS, type TextAlignPreset, } from './text-align.js';
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { getContract, STANDARD_COMPONENTS, } from './contracts.js';
2
- export { ALIGN_PRESETS, LAYOUT_ATTRIBUTE_KEYS, LAYOUT_ATTRIBUTES, WIDTH_PRESETS, } from './layout.js';
2
+ export { ALIGN_PRESETS, LAYOUT_ATTRIBUTE_KEYS, LAYOUT_ATTRIBUTES, WIDTH_PRESETS, layoutWrapperAxis, otherLayoutAxis, } from './layout.js';
3
+ export { TEXT_ALIGN_ATTRIBUTE, TEXT_ALIGN_COMPONENTS, TEXT_ALIGN_PRESETS, } from './text-align.js';
package/dist/layout.d.ts CHANGED
@@ -16,10 +16,36 @@
16
16
  import type { AttributeSchema } from './contracts.js';
17
17
  /** The two attribute names docs/format.md reserves on every block directive. */
18
18
  export declare const LAYOUT_ATTRIBUTE_KEYS: readonly ["width", "align"];
19
- /** The closed `width=` preset vocabulary (docs/format.md), in the order the docs list them. */
20
- export declare const WIDTH_PRESETS: readonly ["normal", "narrow", "wide", "full"];
19
+ /**
20
+ * One of the two layout axes. `width` sizes a block's box; `align` places
21
+ * that box within the column. They are independent: every block has a
22
+ * value on both, and setting one never implies the other.
23
+ */
24
+ export type LayoutAxis = (typeof LAYOUT_ATTRIBUTE_KEYS)[number];
25
+ /** The axis a wrapper of the other axis takes as an attribute (docs/spec.md §3). */
26
+ export declare function otherLayoutAxis(axis: LayoutAxis): LayoutAxis;
27
+ /**
28
+ * The closed `width=` preset vocabulary (docs/format.md), listed narrowest
29
+ * to widest so the list reads as the scale it is: `fit` hugs the block's
30
+ * own content, `normal` is the default column width, and `full` fills the
31
+ * column. Order is user-visible: a completion popup offers the values in
32
+ * this order.
33
+ */
34
+ export declare const WIDTH_PRESETS: readonly ["fit", "narrow", "normal", "wide", "full"];
21
35
  /** The closed `align=` preset vocabulary (docs/format.md), in the order the docs list them. */
22
36
  export declare const ALIGN_PRESETS: readonly ["left", "center", "right"];
37
+ /**
38
+ * The layout axis the wrapper container named `name` sets by its NAME
39
+ * (docs/format.md, "The same presets as wrappers"), or `undefined` when
40
+ * `name` is not one of the seven wrapper names.
41
+ *
42
+ * This is the one place the seven names are classified. A wrapper owns its
43
+ * own axis and takes the OTHER axis as an attribute, so both renderers and
44
+ * the completion layer need the same answer to "which axis does this name
45
+ * already decide?" — deriving it here keeps a hand-copied per-name mapping
46
+ * out of all three.
47
+ */
48
+ export declare function layoutWrapperAxis(name: string): LayoutAxis | undefined;
23
49
  /**
24
50
  * The two reserved layout attributes as `AttributeSchema`s, so a tool that
25
51
  * completes or documents attributes reads them exactly like a component's
package/dist/layout.js CHANGED
@@ -1,9 +1,53 @@
1
1
  /** The two attribute names docs/format.md reserves on every block directive. */
2
2
  export const LAYOUT_ATTRIBUTE_KEYS = ['width', 'align'];
3
- /** The closed `width=` preset vocabulary (docs/format.md), in the order the docs list them. */
4
- export const WIDTH_PRESETS = ['normal', 'narrow', 'wide', 'full'];
3
+ /** The axis a wrapper of the other axis takes as an attribute (docs/spec.md §3). */
4
+ export function otherLayoutAxis(axis) {
5
+ return axis === 'width' ? 'align' : 'width';
6
+ }
7
+ /**
8
+ * The closed `width=` preset vocabulary (docs/format.md), listed narrowest
9
+ * to widest so the list reads as the scale it is: `fit` hugs the block's
10
+ * own content, `normal` is the default column width, and `full` fills the
11
+ * column. Order is user-visible: a completion popup offers the values in
12
+ * this order.
13
+ */
14
+ export const WIDTH_PRESETS = [
15
+ 'fit',
16
+ 'narrow',
17
+ 'normal',
18
+ 'wide',
19
+ 'full',
20
+ ];
5
21
  /** The closed `align=` preset vocabulary (docs/format.md), in the order the docs list them. */
6
22
  export const ALIGN_PRESETS = ['left', 'center', 'right'];
23
+ /**
24
+ * Wrapper-name lookups as sets rather than `Array.includes` scans: a `Set`
25
+ * answers "is this a wrapper name" without ever resolving through a
26
+ * prototype chain, so a directive literally named `constructor` or
27
+ * `__proto__` misses cleanly. Built from the two preset lists above, so the
28
+ * wrapper vocabulary can never drift from the attribute vocabulary; there
29
+ * is no `normal` wrapper, since the default width needs no wrapper at all.
30
+ */
31
+ const ALIGN_WRAPPER_NAMES = new Set(ALIGN_PRESETS);
32
+ const WIDTH_WRAPPER_NAMES = new Set(WIDTH_PRESETS.filter((preset) => preset !== 'normal'));
33
+ /**
34
+ * The layout axis the wrapper container named `name` sets by its NAME
35
+ * (docs/format.md, "The same presets as wrappers"), or `undefined` when
36
+ * `name` is not one of the seven wrapper names.
37
+ *
38
+ * This is the one place the seven names are classified. A wrapper owns its
39
+ * own axis and takes the OTHER axis as an attribute, so both renderers and
40
+ * the completion layer need the same answer to "which axis does this name
41
+ * already decide?" — deriving it here keeps a hand-copied per-name mapping
42
+ * out of all three.
43
+ */
44
+ export function layoutWrapperAxis(name) {
45
+ if (ALIGN_WRAPPER_NAMES.has(name))
46
+ return 'align';
47
+ if (WIDTH_WRAPPER_NAMES.has(name))
48
+ return 'width';
49
+ return undefined;
50
+ }
7
51
  /**
8
52
  * The two reserved layout attributes as `AttributeSchema`s, so a tool that
9
53
  * completes or documents attributes reads them exactly like a component's
@@ -14,18 +58,21 @@ export const LAYOUT_ATTRIBUTES = {
14
58
  type: 'string',
15
59
  required: false,
16
60
  enum: WIDTH_PRESETS,
17
- description: 'Sets how wide the directive renders: `normal` (the default column ' +
18
- 'width), `narrow`, `wide`, or `full`. `normal` is the explicit ' +
19
- 'default and produces no class, the same as leaving `width` off ' +
20
- 'entirely. An invalid value is ignored as if absent; nothing errors.',
61
+ description: 'Sets how wide the directive renders, from narrowest to widest: ' +
62
+ '`fit` shrinks the block to its own content, then `narrow`, ' +
63
+ '`normal` at the default column width, `wide`, and `full`. ' +
64
+ '`normal` is the explicit default and produces no class, the same ' +
65
+ 'as leaving `width` off entirely. An invalid value is ignored as if ' +
66
+ 'absent; nothing errors.',
21
67
  },
22
68
  align: {
23
69
  type: 'string',
24
70
  required: false,
25
71
  enum: ALIGN_PRESETS,
26
- description: 'Sets how the directive is aligned within the column: `left`, ' +
27
- '`center`, or `right`. Only has a visible effect when the block is ' +
28
- 'narrower than the column; a full-width block has nothing to align. ' +
29
- 'An invalid value is ignored as if absent; nothing errors.',
72
+ description: 'Places the directive within the column: `left`, `center`, or ' +
73
+ "`right`, defaulting to `left`. It moves the block's box and never " +
74
+ 'its contents, so it only shows when the box is narrower than the ' +
75
+ 'column: pair it with a `width` such as `fit` or `narrow`. An ' +
76
+ 'invalid value is ignored as if absent; nothing errors.',
30
77
  },
31
78
  };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The `text` attribute (docs/format.md, "Aligning text inside a component";
3
+ * docs/spec.md §3): the standard set's one content-alignment attribute.
4
+ *
5
+ * `text` is deliberately NOT part of the layout system in `./layout.ts`.
6
+ * `width` and `align` are reserved on every directive and are intercepted
7
+ * by the renderer before a component ever sees them, because they mean the
8
+ * same thing on every block. `text` means "align the text inside THIS
9
+ * component", which only a component can act on, so it reaches the
10
+ * component like any other attribute of its own and appears in each of
11
+ * those components' contracts.
12
+ *
13
+ * This module carries values only: the closed value list and the schema
14
+ * four contracts share. It says nothing about markup or classes; each
15
+ * renderer's own `layout.ts` maps a value to a class.
16
+ */
17
+ import type { AttributeSchema } from './contracts.js';
18
+ /** The closed `text=` value vocabulary, in the order docs/format.md lists them. */
19
+ export declare const TEXT_ALIGN_PRESETS: readonly ["left", "center", "right"];
20
+ /** One of the closed `text=` values. */
21
+ export type TextAlignPreset = (typeof TEXT_ALIGN_PRESETS)[number];
22
+ /**
23
+ * The standard components that accept `text` (docs/spec.md §3). Listed here
24
+ * so `./contracts.ts` builds all four from one source, and so a test can
25
+ * assert the set both renderers honor is exactly this one.
26
+ */
27
+ export declare const TEXT_ALIGN_COMPONENTS: readonly ["row", "cell", "card", "callout"];
28
+ /**
29
+ * The shared `text` schema. Identical wording on all four components on
30
+ * purpose: the attribute means the same thing everywhere, and a reader who
31
+ * learned it on `callout` should not have to re-read it on `card`. The
32
+ * row-specific cascade is spelled out because it is the one place `text`
33
+ * reaches past the component it was written on.
34
+ */
35
+ export declare const TEXT_ALIGN_ATTRIBUTE: AttributeSchema;
@@ -0,0 +1,31 @@
1
+ /** The closed `text=` value vocabulary, in the order docs/format.md lists them. */
2
+ export const TEXT_ALIGN_PRESETS = ['left', 'center', 'right'];
3
+ /**
4
+ * The standard components that accept `text` (docs/spec.md §3). Listed here
5
+ * so `./contracts.ts` builds all four from one source, and so a test can
6
+ * assert the set both renderers honor is exactly this one.
7
+ */
8
+ export const TEXT_ALIGN_COMPONENTS = [
9
+ 'row',
10
+ 'cell',
11
+ 'card',
12
+ 'callout',
13
+ ];
14
+ /**
15
+ * The shared `text` schema. Identical wording on all four components on
16
+ * purpose: the attribute means the same thing everywhere, and a reader who
17
+ * learned it on `callout` should not have to re-read it on `card`. The
18
+ * row-specific cascade is spelled out because it is the one place `text`
19
+ * reaches past the component it was written on.
20
+ */
21
+ export const TEXT_ALIGN_ATTRIBUTE = {
22
+ type: 'string',
23
+ required: false,
24
+ enum: TEXT_ALIGN_PRESETS,
25
+ description: 'Aligns the text inside this component: `left`, `center`, or `right`. ' +
26
+ 'On a `row` it applies to every cell, a cell with its own `text` ' +
27
+ 'overrides it, and an alignment wrapper written inside a cell wins ' +
28
+ 'over both. Unlike the reserved `align`, which places the whole box ' +
29
+ 'within the column, `text` never moves the box. An invalid value is ' +
30
+ 'ignored as if absent; nothing errors.',
31
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markii/stdlib",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Neutral, framework-agnostic standard component contracts for Mark (.mk.md) — the kind/attribute-schema seam every renderer (e.g. @markii/react) implements against. No React, zero dependencies.",
5
5
  "keywords": [
6
6
  "markdown",