@art-md/constructs 0.0.1 → 0.0.3

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 (158) hide show
  1. package/package.json +2 -2
  2. package/src/factories/constructs/Document/factory/createDocument.test.ts +17 -0
  3. package/src/factories/constructs/Document/factory/createDocument.ts +11 -0
  4. package/src/factories/constructs/Document/factory/types.ts +5 -0
  5. package/src/factories/constructs/Document/index.ts +3 -0
  6. package/src/factories/constructs/FieldBlock/factory/createFieldBlock.test.ts +26 -0
  7. package/src/factories/constructs/FieldBlock/factory/createFieldBlock.ts +15 -0
  8. package/src/factories/constructs/FieldBlock/factory/types.ts +8 -0
  9. package/src/factories/constructs/FieldBlock/index.ts +4 -0
  10. package/src/factories/constructs/FieldBlock/types.ts +11 -0
  11. package/src/factories/constructs/FieldInline/factory/createFieldInline.test.ts +26 -0
  12. package/src/factories/constructs/FieldInline/factory/createFieldInline.ts +15 -0
  13. package/src/factories/constructs/FieldInline/factory/types.ts +8 -0
  14. package/src/factories/constructs/FieldInline/index.ts +4 -0
  15. package/src/factories/constructs/FieldInline/types.ts +11 -0
  16. package/src/factories/constructs/NaturalBlock/factory/createNaturalBlock.test.ts +36 -0
  17. package/src/factories/constructs/NaturalBlock/factory/createNaturalBlock.ts +25 -0
  18. package/src/factories/constructs/NaturalBlock/factory/types.ts +13 -0
  19. package/src/factories/constructs/NaturalBlock/index.ts +4 -0
  20. package/src/factories/constructs/NaturalBlock/types.ts +14 -0
  21. package/src/factories/constructs/NaturalExpression/factory/createNaturalExpression.test.ts +22 -0
  22. package/src/factories/constructs/NaturalExpression/factory/createNaturalExpression.ts +13 -0
  23. package/src/factories/constructs/NaturalExpression/factory/types.ts +8 -0
  24. package/src/factories/constructs/NaturalExpression/index.ts +4 -0
  25. package/src/factories/constructs/NaturalExpression/types.ts +9 -0
  26. package/src/factories/constructs/SectionBlock/factory/createSectionBlock.test.ts +30 -0
  27. package/src/factories/constructs/SectionBlock/factory/createSectionBlock.ts +15 -0
  28. package/src/factories/constructs/SectionBlock/factory/types.ts +11 -0
  29. package/src/factories/constructs/SectionBlock/index.ts +4 -0
  30. package/src/factories/constructs/SectionBlock/types.ts +12 -0
  31. package/src/factories/constructs/Tag/factory/createTag.test.ts +10 -0
  32. package/src/factories/constructs/Tag/factory/createTag.ts +7 -0
  33. package/src/factories/constructs/Tag/factory/types.ts +3 -0
  34. package/src/factories/constructs/Tag/index.ts +4 -0
  35. package/src/factories/constructs/Tag/types.ts +6 -0
  36. package/src/factories/index.ts +9 -0
  37. package/src/factories/public.ts +47 -0
  38. package/src/factories/types.ts +36 -0
  39. package/src/index.ts +3 -0
  40. package/src/parser/constructs/FieldBlock/createFieldBlockParser.test.ts +13 -0
  41. package/src/parser/constructs/FieldBlock/createFieldBlockParser.ts +9 -0
  42. package/src/parser/constructs/FieldBlock/index.ts +1 -0
  43. package/src/parser/constructs/FieldBlock/private/helpers/createFieldBlockFromNode.test.ts +63 -0
  44. package/src/parser/constructs/FieldBlock/private/helpers/createFieldBlockFromNode.ts +33 -0
  45. package/src/parser/constructs/FieldBlock/private/helpers/onBeforeConstruct.test.ts +30 -0
  46. package/src/parser/constructs/FieldBlock/private/helpers/onBeforeConstruct.ts +19 -0
  47. package/src/parser/constructs/FieldBlock/private/index.ts +2 -0
  48. package/src/parser/constructs/FieldBlock/private/integrator/createFieldBlockIntegrator.test.ts +23 -0
  49. package/src/parser/constructs/FieldBlock/private/integrator/createFieldBlockIntegrator.ts +13 -0
  50. package/src/parser/constructs/FieldBlock/private/processor/createFieldBlockProcessor.test.ts +63 -0
  51. package/src/parser/constructs/FieldBlock/private/processor/createFieldBlockProcessor.ts +21 -0
  52. package/src/parser/constructs/FieldInline/createFieldInlineParser.test.ts +12 -0
  53. package/src/parser/constructs/FieldInline/createFieldInlineParser.ts +8 -0
  54. package/src/parser/constructs/FieldInline/index.ts +1 -0
  55. package/src/parser/constructs/FieldInline/private/helpers/createFieldInlineFromNode.test.ts +100 -0
  56. package/src/parser/constructs/FieldInline/private/helpers/createFieldInlineFromNode.ts +51 -0
  57. package/src/parser/constructs/FieldInline/private/helpers/trimFieldEdges.test.ts +48 -0
  58. package/src/parser/constructs/FieldInline/private/helpers/trimFieldEdges.ts +18 -0
  59. package/src/parser/constructs/FieldInline/private/index.ts +1 -0
  60. package/src/parser/constructs/FieldInline/private/processor/createFieldInlineProcessor.test.ts +57 -0
  61. package/src/parser/constructs/FieldInline/private/processor/createFieldInlineProcessor.ts +21 -0
  62. package/src/parser/constructs/NaturalBlock/createNaturalBlockParser.test.ts +12 -0
  63. package/src/parser/constructs/NaturalBlock/createNaturalBlockParser.ts +8 -0
  64. package/src/parser/constructs/NaturalBlock/index.ts +1 -0
  65. package/src/parser/constructs/NaturalBlock/private/helpers/createNaturalBlockFromNode.test.ts +101 -0
  66. package/src/parser/constructs/NaturalBlock/private/helpers/createNaturalBlockFromNode.ts +51 -0
  67. package/src/parser/constructs/NaturalBlock/private/index.ts +1 -0
  68. package/src/parser/constructs/NaturalBlock/private/processor/createNaturalBlockProcessor.test.ts +24 -0
  69. package/src/parser/constructs/NaturalBlock/private/processor/createNaturalBlockProcessor.ts +10 -0
  70. package/src/parser/constructs/SectionBlock/createSectionBlockParser.test.ts +13 -0
  71. package/src/parser/constructs/SectionBlock/createSectionBlockParser.ts +9 -0
  72. package/src/parser/constructs/SectionBlock/index.ts +1 -0
  73. package/src/parser/constructs/SectionBlock/private/constants.test.ts +17 -0
  74. package/src/parser/constructs/SectionBlock/private/constants.ts +1 -0
  75. package/src/parser/constructs/SectionBlock/private/helpers/createSectionBlockFromNode.test.ts +58 -0
  76. package/src/parser/constructs/SectionBlock/private/helpers/createSectionBlockFromNode.ts +28 -0
  77. package/src/parser/constructs/SectionBlock/private/helpers/findParentSection.test.ts +44 -0
  78. package/src/parser/constructs/SectionBlock/private/helpers/findParentSection.ts +14 -0
  79. package/src/parser/constructs/SectionBlock/private/index.ts +2 -0
  80. package/src/parser/constructs/SectionBlock/private/integrator/createSectionBlockIntegrator.test.ts +60 -0
  81. package/src/parser/constructs/SectionBlock/private/integrator/createSectionBlockIntegrator.ts +31 -0
  82. package/src/parser/constructs/SectionBlock/private/processor/createSectionBlockProcessor.test.ts +34 -0
  83. package/src/parser/constructs/SectionBlock/private/processor/createSectionBlockProcessor.ts +15 -0
  84. package/src/parser/document/createArtDocumentFromNode.test.ts +23 -0
  85. package/src/parser/document/createArtDocumentFromNode.ts +10 -0
  86. package/src/parser/document/index.ts +1 -0
  87. package/src/parser/fields/constants.test.ts +23 -0
  88. package/src/parser/fields/constants.ts +1 -0
  89. package/src/parser/fields/index.ts +3 -0
  90. package/src/parser/fields/isFieldStrong.test.ts +27 -0
  91. package/src/parser/fields/isFieldStrong.ts +9 -0
  92. package/src/parser/fields/stripStrong.test.ts +38 -0
  93. package/src/parser/fields/stripStrong.ts +12 -0
  94. package/src/parser/index.ts +8 -0
  95. package/src/parser/mdast/index.ts +1 -0
  96. package/src/parser/mdast/rawSlice.test.ts +43 -0
  97. package/src/parser/mdast/rawSlice.ts +8 -0
  98. package/src/parser/naturalExpression/createNaturalExpressionFromNode.test.ts +40 -0
  99. package/src/parser/naturalExpression/createNaturalExpressionFromNode.ts +25 -0
  100. package/src/parser/naturalExpression/index.ts +1 -0
  101. package/src/parser/public.ts +16 -0
  102. package/src/parser/tags/extractTags.test.ts +85 -0
  103. package/src/parser/tags/extractTags.ts +23 -0
  104. package/src/parser/tags/index.ts +1 -0
  105. package/src/parser/types.ts +21 -0
  106. package/src/serializer/constructs/Document/createDocumentToMdast.test.ts +22 -0
  107. package/src/serializer/constructs/Document/createDocumentToMdast.ts +15 -0
  108. package/src/serializer/constructs/Document/index.ts +1 -0
  109. package/src/serializer/constructs/FieldBlock/createFieldBlockToMdast.test.ts +36 -0
  110. package/src/serializer/constructs/FieldBlock/createFieldBlockToMdast.ts +26 -0
  111. package/src/serializer/constructs/FieldBlock/index.ts +1 -0
  112. package/src/serializer/constructs/FieldInline/createFieldInlineToMdast.test.ts +38 -0
  113. package/src/serializer/constructs/FieldInline/createFieldInlineToMdast.ts +27 -0
  114. package/src/serializer/constructs/FieldInline/index.ts +1 -0
  115. package/src/serializer/constructs/NaturalBlock/createNaturalBlockToMdast.test.ts +122 -0
  116. package/src/serializer/constructs/NaturalBlock/createNaturalBlockToMdast.ts +30 -0
  117. package/src/serializer/constructs/NaturalBlock/index.ts +1 -0
  118. package/src/serializer/constructs/NaturalExpression/createNaturalExpressionToMdast.test.ts +29 -0
  119. package/src/serializer/constructs/NaturalExpression/createNaturalExpressionToMdast.ts +15 -0
  120. package/src/serializer/constructs/NaturalExpression/index.ts +1 -0
  121. package/src/serializer/constructs/SectionBlock/createSectionBlockToMdast.test.ts +83 -0
  122. package/src/serializer/constructs/SectionBlock/createSectionBlockToMdast.ts +29 -0
  123. package/src/serializer/constructs/SectionBlock/index.ts +1 -0
  124. package/src/serializer/index.ts +8 -0
  125. package/src/serializer/public.ts +18 -0
  126. package/src/serializer/tags/index.ts +2 -0
  127. package/src/serializer/tags/tagToMdast.test.ts +10 -0
  128. package/src/serializer/tags/tagToMdast.ts +7 -0
  129. package/src/serializer/tags/tagsToMdast.test.ts +19 -0
  130. package/src/serializer/tags/tagsToMdast.ts +10 -0
  131. package/src/serializer/types.ts +10 -0
  132. package/src/test/helpers/constructs/FieldBlock/createFieldBlockFromNodeMock.ts +17 -0
  133. package/src/test/helpers/constructs/FieldBlock/createFieldBlockMock.ts +17 -0
  134. package/src/test/helpers/constructs/FieldBlock/makeFieldBlockFixture.ts +16 -0
  135. package/src/test/helpers/constructs/FieldBlock/makeFieldBlockMock.ts +16 -0
  136. package/src/test/helpers/constructs/FieldInline/createFieldInlineFromNodeMock.ts +17 -0
  137. package/src/test/helpers/constructs/FieldInline/createFieldInlineMock.ts +17 -0
  138. package/src/test/helpers/constructs/FieldInline/makeFieldInlineFixture.ts +16 -0
  139. package/src/test/helpers/constructs/FieldInline/makeFieldInlineMock.ts +16 -0
  140. package/src/test/helpers/constructs/NaturalBlock/createNaturalBlockFromNodeMock.ts +17 -0
  141. package/src/test/helpers/constructs/NaturalBlock/createNaturalBlockMock.ts +17 -0
  142. package/src/test/helpers/constructs/NaturalBlock/makeNaturalBlockFixture.ts +22 -0
  143. package/src/test/helpers/constructs/NaturalBlock/makeNaturalBlockMock.ts +22 -0
  144. package/src/test/helpers/constructs/NaturalExpression/makeNaturalExpressionFixture.ts +18 -0
  145. package/src/test/helpers/constructs/NaturalExpression/makeNaturalExpressionMock.ts +18 -0
  146. package/src/test/helpers/constructs/SectionBlock/createSectionBlockFromNodeMock.ts +17 -0
  147. package/src/test/helpers/constructs/SectionBlock/createSectionBlockMock.ts +17 -0
  148. package/src/test/helpers/constructs/SectionBlock/makeSectionBlockFixture.ts +20 -0
  149. package/src/test/helpers/constructs/Tag/makeTagFixture.ts +10 -0
  150. package/src/test/helpers/constructs/index.ts +14 -0
  151. package/src/test/helpers/index.ts +2 -0
  152. package/src/test/helpers/parsers/fields/stripStrongMock.ts +11 -0
  153. package/src/test/helpers/parsers/index.ts +5 -0
  154. package/src/test/helpers/parsers/mdast/rawSliceMock.ts +9 -0
  155. package/src/test/helpers/parsers/natural-expression/createNaturalExpressionFromNodeMock.ts +16 -0
  156. package/src/test/helpers/parsers/tags/extractTagsMock.ts +12 -0
  157. package/src/test/helpers/parsers/tags/tagToMdastMock.ts +9 -0
  158. package/dist/esm/index.js +0 -3135
@@ -0,0 +1,101 @@
1
+ import { makeParserVisitContextMock } from '@art-md/primitives/src/test/helpers';
2
+ import type { Node } from 'mdast';
3
+ import { fromMarkdown } from 'mdast-util-from-markdown';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+
6
+ import { createNaturalBlock } from '../../../../../factories';
7
+
8
+ import { createNaturalBlockFromNode } from './createNaturalBlockFromNode';
9
+
10
+ vi.mock('../../../../../factories/constructs/NaturalBlock/factory/createNaturalBlock', async () => {
11
+ const { createNaturalBlockMock } = await import('../../../../../test/helpers');
12
+ return createNaturalBlockMock();
13
+ });
14
+
15
+ describe('createNaturalBlockFromNode', () => {
16
+ it('WHEN creating a NaturalBlock from a paragraph', () => {
17
+ const markdown = 'hello';
18
+ const tree = fromMarkdown(markdown);
19
+ const node = tree.children[0] as Node;
20
+ const context = makeParserVisitContextMock({ markdown });
21
+
22
+ const result = createNaturalBlockFromNode(node, context);
23
+
24
+ expect(createNaturalBlock).toHaveBeenCalledWith(
25
+ expect.objectContaining({
26
+ value: 'hello',
27
+ type: 'paragraph',
28
+ children: [
29
+ expect.objectContaining({
30
+ construct: 'NaturalExpression',
31
+ type: 'text',
32
+ value: 'hello',
33
+ }),
34
+ ],
35
+ tags: [],
36
+ }),
37
+ );
38
+ expect(result).toBe(vi.mocked(createNaturalBlock).mock.results[0]?.value);
39
+ });
40
+
41
+ it('WHEN trailing tags follow the last text child passes them to createNaturalBlock', () => {
42
+ const markdown = 'hello (#test)';
43
+ const tree = fromMarkdown(markdown);
44
+ const node = tree.children[0] as Node;
45
+ const context = makeParserVisitContextMock({ markdown });
46
+
47
+ createNaturalBlockFromNode(node, context);
48
+
49
+ expect(createNaturalBlock).toHaveBeenCalledWith(
50
+ expect.objectContaining({
51
+ children: [expect.objectContaining({ value: 'hello' })],
52
+ tags: [{ construct: 'Tag', name: 'test' }],
53
+ }),
54
+ );
55
+ });
56
+
57
+ it('WHEN a child is not phrasing recurses into a nested NaturalBlock', () => {
58
+ const markdown = '> hello';
59
+ const tree = fromMarkdown(markdown);
60
+ const node = tree.children[0] as Node;
61
+ const context = makeParserVisitContextMock({ markdown });
62
+
63
+ createNaturalBlockFromNode(node, context);
64
+
65
+ expect(createNaturalBlock).toHaveBeenCalledWith(
66
+ expect.objectContaining({
67
+ type: 'blockquote',
68
+ children: [expect.objectContaining({ construct: 'NaturalBlock' })],
69
+ }),
70
+ );
71
+ });
72
+
73
+ it('FOR nodes without children creates a NaturalBlock without children', () => {
74
+ const markdown = '---';
75
+ const tree = fromMarkdown(markdown);
76
+ const node = tree.children[0] as Node;
77
+ const context = makeParserVisitContextMock({ markdown });
78
+
79
+ createNaturalBlockFromNode(node, context);
80
+
81
+ expect(createNaturalBlock).toHaveBeenCalledWith(
82
+ expect.objectContaining({ type: 'thematicBreak', children: [] }),
83
+ );
84
+ });
85
+
86
+ it('WHEN the node has extra mdast fields passes them as attributes', () => {
87
+ const markdown = '- one\n- two';
88
+ const tree = fromMarkdown(markdown);
89
+ const node = tree.children[0] as Node;
90
+ const context = makeParserVisitContextMock({ markdown });
91
+
92
+ createNaturalBlockFromNode(node, context);
93
+
94
+ expect(createNaturalBlock).toHaveBeenCalledWith(
95
+ expect.objectContaining({
96
+ type: 'list',
97
+ attributes: expect.objectContaining({ ordered: false }),
98
+ }),
99
+ );
100
+ });
101
+ });
@@ -0,0 +1,51 @@
1
+ import type { MdastNode, ParserVisitContext } from '@art-md/primitives';
2
+ import { nodePosition } from '@art-md/primitives';
3
+ import type { Node } from 'mdast';
4
+ import { phrasing } from 'mdast-util-phrasing';
5
+
6
+ import {
7
+ type NaturalBlock,
8
+ type NaturalExpression,
9
+ type Tag,
10
+ createNaturalBlock,
11
+ } from '../../../../../factories';
12
+ import { rawSlice } from '../../../../mdast';
13
+ import { createNaturalExpressionFromNode } from '../../../../naturalExpression';
14
+ import { extractTags } from '../../../../tags';
15
+
16
+ export function createNaturalBlockFromNode(node: Node, context: ParserVisitContext): NaturalBlock {
17
+ let children: (NaturalBlock | NaturalExpression)[] = [];
18
+ if (Array.isArray((node as MdastNode).children)) {
19
+ const nodeChildren = (node as MdastNode).children ?? [];
20
+ const phrasingContainer =
21
+ node.type === 'paragraph' || node.type === 'heading' || node.type === 'tableCell';
22
+ children = nodeChildren.map(child =>
23
+ phrasingContainer || phrasing(child)
24
+ ? createNaturalExpressionFromNode(child)
25
+ : createNaturalBlockFromNode(child as Node, context),
26
+ );
27
+ }
28
+
29
+ let tags: Tag[] = [];
30
+ if (node.type === 'paragraph') {
31
+ const last = children[children.length - 1];
32
+ if (last?.type === 'text' && typeof last.value === 'string') {
33
+ const { tags: extractedTags, stripped } = extractTags(last.value);
34
+ if (extractedTags.length) {
35
+ tags = extractedTags;
36
+ last.value = stripped;
37
+ }
38
+ }
39
+ }
40
+
41
+ const { type, ...attributes } = node as MdastNode;
42
+ const block = createNaturalBlock({
43
+ value: rawSlice(node, context),
44
+ children,
45
+ type,
46
+ attributes,
47
+ tags,
48
+ });
49
+ block.position = nodePosition(node);
50
+ return block;
51
+ }
@@ -0,0 +1 @@
1
+ export * from './processor/createNaturalBlockProcessor';
@@ -0,0 +1,24 @@
1
+ import { makeParserVisitContextMock } from '@art-md/primitives/src/test/helpers';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { createNaturalBlockFromNodeMock } from '../../../../../test/helpers';
5
+ import { createNaturalBlockFromNode } from '../helpers/createNaturalBlockFromNode';
6
+
7
+ import { createNaturalBlockProcessor } from './createNaturalBlockProcessor';
8
+
9
+ vi.mock('../helpers/createNaturalBlockFromNode', async () => {
10
+ return createNaturalBlockFromNodeMock();
11
+ });
12
+
13
+ describe('createNaturalBlockProcessor', () => {
14
+ it('WHEN capturing a node returns the createNaturalBlockFromNode result', () => {
15
+ const processor = createNaturalBlockProcessor();
16
+ const context = makeParserVisitContextMock();
17
+ const node = { type: 'paragraph', children: [] };
18
+
19
+ const result = processor.captureNode(context, node);
20
+
21
+ expect(createNaturalBlockFromNode).toHaveBeenCalledWith(node, context);
22
+ expect(result).toBe(vi.mocked(createNaturalBlockFromNode).mock.results[0]?.value);
23
+ });
24
+ });
@@ -0,0 +1,10 @@
1
+ import type { ConstructProcessor } from '../../../../types';
2
+ import { createNaturalBlockFromNode } from '../helpers/createNaturalBlockFromNode';
3
+
4
+ export function createNaturalBlockProcessor(): ConstructProcessor {
5
+ return {
6
+ captureNode(context, node) {
7
+ return createNaturalBlockFromNode(node, context);
8
+ },
9
+ };
10
+ }
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createSectionBlockParser } from './createSectionBlockParser';
4
+
5
+ describe('createSectionBlockParser', () => {
6
+ it('WHEN called returns a parser with SectionBlock name, processor, integrator and factory', () => {
7
+ const parser = createSectionBlockParser();
8
+
9
+ expect(parser.name).toBe('SectionBlock');
10
+ expect(parser.processor).toBeDefined();
11
+ expect(parser.integrator).toBeDefined();
12
+ });
13
+ });
@@ -0,0 +1,9 @@
1
+ import type { ConstructParserFactory } from '../../types';
2
+
3
+ import { createSectionBlockIntegrator, createSectionBlockProcessor } from './private';
4
+
5
+ export const createSectionBlockParser: ConstructParserFactory = () => ({
6
+ name: 'SectionBlock',
7
+ processor: createSectionBlockProcessor(),
8
+ integrator: createSectionBlockIntegrator(),
9
+ });
@@ -0,0 +1 @@
1
+ export { createSectionBlockParser } from './createSectionBlockParser';
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { KIND_PATTERN } from './constants';
4
+
5
+ describe('KIND_PATTERN', () => {
6
+ it('WHEN matching a kind pattern', () => {
7
+ const result = 'Module: Hello'.match(KIND_PATTERN);
8
+
9
+ expect(result).toBeTruthy();
10
+ expect(result?.[1]).toBe('Module');
11
+ expect(result?.[2]).toBe('Hello');
12
+ });
13
+
14
+ it('WHEN match text without colon does not', () => {
15
+ expect('Hello World'.match(KIND_PATTERN)).toBeFalsy();
16
+ });
17
+ });
@@ -0,0 +1 @@
1
+ export const KIND_PATTERN = /^([\w-]+(?: [\w-]+)*):\s*(.+)$/;
@@ -0,0 +1,58 @@
1
+ import { makeParserVisitContextMock } from '@art-md/primitives/src/test/helpers';
2
+ import type { Heading } from 'mdast';
3
+ import { fromMarkdown } from 'mdast-util-from-markdown';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+
6
+ import { createSectionBlock } from '../../../../../factories';
7
+
8
+ import { createSectionBlockFromNode } from './createSectionBlockFromNode';
9
+
10
+ vi.mock('../../../../../factories/constructs/SectionBlock/factory/createSectionBlock', async () => {
11
+ const { createSectionBlockMock } = await import('../../../../../test/helpers');
12
+ return createSectionBlockMock();
13
+ });
14
+
15
+ describe('createSectionBlockFromNode', () => {
16
+ it('WHEN creating a SectionBlock from a heading', () => {
17
+ const markdown = '# Hello World';
18
+ const tree = fromMarkdown(markdown);
19
+ const heading = tree.children[0] as Heading;
20
+ const context = makeParserVisitContextMock({ markdown });
21
+
22
+ const result = createSectionBlockFromNode(heading, context);
23
+
24
+ expect(createSectionBlock).toHaveBeenCalledWith(
25
+ expect.objectContaining({ name: 'Hello World', depth: 1, children: [], tags: [] }),
26
+ );
27
+ expect(result).toBe(vi.mocked(createSectionBlock).mock.results[0]?.value);
28
+ });
29
+
30
+ it('WHEN the heading declares a kind extracts it', () => {
31
+ const markdown = '# Module: Hello';
32
+ const tree = fromMarkdown(markdown);
33
+ const heading = tree.children[0] as Heading;
34
+ const context = makeParserVisitContextMock({ markdown });
35
+
36
+ createSectionBlockFromNode(heading, context);
37
+
38
+ expect(createSectionBlock).toHaveBeenCalledWith(
39
+ expect.objectContaining({ kind: 'Module', name: 'Hello' }),
40
+ );
41
+ });
42
+
43
+ it('WHEN the heading has trailing tags passes them and strips the name', () => {
44
+ const markdown = '# Hello (#tag)';
45
+ const tree = fromMarkdown(markdown);
46
+ const heading = tree.children[0] as Heading;
47
+ const context = makeParserVisitContextMock({ markdown });
48
+
49
+ createSectionBlockFromNode(heading, context);
50
+
51
+ expect(createSectionBlock).toHaveBeenCalledWith(
52
+ expect.objectContaining({
53
+ name: 'Hello',
54
+ tags: [{ construct: 'Tag', name: 'tag' }],
55
+ }),
56
+ );
57
+ });
58
+ });
@@ -0,0 +1,28 @@
1
+ import { nodePosition } from '@art-md/primitives';
2
+ import type { ParserVisitContext } from '@art-md/primitives';
3
+ import type { Heading } from 'mdast';
4
+
5
+ import { type SectionBlock, createSectionBlock } from '../../../../../factories';
6
+ import { rawSlice } from '../../../../mdast';
7
+ import { extractTags } from '../../../../tags';
8
+ import { KIND_PATTERN } from '../constants';
9
+
10
+ export function createSectionBlockFromNode(
11
+ node: Heading,
12
+ context: ParserVisitContext,
13
+ ): SectionBlock {
14
+ const text = rawSlice(node, context)
15
+ .replace(/^[ \t]*#+[ \t]*/, '')
16
+ .trim();
17
+ const { tags, stripped } = extractTags(text);
18
+ const kindMatch = stripped.match(KIND_PATTERN);
19
+ const section = createSectionBlock({
20
+ name: kindMatch?.[2]?.trim() ?? stripped,
21
+ kind: kindMatch?.[1],
22
+ depth: node.depth,
23
+ children: [],
24
+ tags,
25
+ });
26
+ section.position = nodePosition(node);
27
+ return section;
28
+ }
@@ -0,0 +1,44 @@
1
+ import type { ParserVisitContext } from '@art-md/primitives';
2
+ import { makeDocumentMock, makeParserVisitContextMock } from '@art-md/primitives/src/test/helpers';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { makeFieldBlockFixture, makeSectionBlockFixture } from '../../../../../test/helpers';
6
+
7
+ import { findParentSection } from './findParentSection';
8
+
9
+ function makeContext(construct: ParserVisitContext['construct'], parent?: ParserVisitContext) {
10
+ return {
11
+ ...makeParserVisitContextMock(),
12
+ construct,
13
+ parent: () => parent,
14
+ };
15
+ }
16
+
17
+ describe('findParentSection', () => {
18
+ it('WHEN found in context chain returns the section block', () => {
19
+ const section = makeSectionBlockFixture();
20
+ const context = makeContext(section);
21
+
22
+ const result = findParentSection(context);
23
+
24
+ expect(result).toBe(section);
25
+ });
26
+
27
+ it('WHEN no section block is found returns undefined', () => {
28
+ const context = makeContext(makeDocumentMock());
29
+
30
+ const result = findParentSection(context);
31
+
32
+ expect(result).toBeUndefined();
33
+ });
34
+
35
+ it('WHEN walking up the parent chain', () => {
36
+ const section = makeSectionBlockFixture();
37
+ const parent = makeContext(section);
38
+ const context = makeContext(makeFieldBlockFixture(), parent);
39
+
40
+ const result = findParentSection(context);
41
+
42
+ expect(result).toBe(section);
43
+ });
44
+ });
@@ -0,0 +1,14 @@
1
+ import type { ParserVisitContext } from '@art-md/primitives';
2
+
3
+ import type { SectionBlock } from '../../../../../factories';
4
+
5
+ export function findParentSection(context: ParserVisitContext): SectionBlock | undefined {
6
+ let current: ParserVisitContext | undefined = context;
7
+ while (current) {
8
+ if (current.construct?.construct === 'SectionBlock') {
9
+ return current.construct as SectionBlock;
10
+ }
11
+ current = current.parent();
12
+ }
13
+ return undefined;
14
+ }
@@ -0,0 +1,2 @@
1
+ export * from './integrator/createSectionBlockIntegrator';
2
+ export * from './processor/createSectionBlockProcessor';
@@ -0,0 +1,60 @@
1
+ import { createParserVisitContext } from '@art-md/primitives';
2
+ import { makeDocumentMock, makeParserVisitContextMock } from '@art-md/primitives/src/test/helpers';
3
+ import type { Heading } from 'mdast';
4
+ import { fromMarkdown } from 'mdast-util-from-markdown';
5
+ import { describe, expect, it, vi } from 'vitest';
6
+
7
+ import { makeSectionBlockFixture } from '../../../../../test/helpers';
8
+
9
+ import { createSectionBlockIntegrator } from './createSectionBlockIntegrator';
10
+
11
+ function makeHeading(markdown: string): Heading {
12
+ return fromMarkdown(markdown).children[0] as Heading;
13
+ }
14
+
15
+ describe('createSectionBlockIntegrator', () => {
16
+ it('WHEN integrating captures the construct and returns a child context', () => {
17
+ const integrator = createSectionBlockIntegrator();
18
+ const context = makeParserVisitContextMock();
19
+ const nextContext = makeParserVisitContextMock();
20
+ vi.mocked(context.childContext).mockReturnValue(nextContext);
21
+ const section = makeSectionBlockFixture();
22
+
23
+ const result = integrator.integrate(context, makeHeading('# Hello'), section);
24
+
25
+ expect(context.captureChildConstruct).toHaveBeenCalledWith(section);
26
+ expect(context.childContext).toHaveBeenCalledWith(section);
27
+ expect(result).toBe(nextContext);
28
+ });
29
+
30
+ it('WHEN the nearest section is at or below the heading depth pops to its parent', () => {
31
+ const integrator = createSectionBlockIntegrator();
32
+ const documentContext = createParserVisitContext(makeDocumentMock(), {
33
+ markdown: '',
34
+ tree: fromMarkdown(''),
35
+ });
36
+ const capture = vi.spyOn(documentContext, 'captureChildConstruct');
37
+ const sectionContext = documentContext.childContext(makeSectionBlockFixture({ depth: 2 }));
38
+ const context = sectionContext.childContext(makeSectionBlockFixture({ depth: 2 }));
39
+ const section = makeSectionBlockFixture();
40
+
41
+ integrator.integrate(context, makeHeading('# Hello'), section);
42
+
43
+ expect(capture).toHaveBeenCalledWith(section);
44
+ });
45
+
46
+ it('WHEN the nearest section is above the heading depth stops at it', () => {
47
+ const integrator = createSectionBlockIntegrator();
48
+ const documentContext = createParserVisitContext(makeDocumentMock(), {
49
+ markdown: '',
50
+ tree: fromMarkdown(''),
51
+ });
52
+ const context = documentContext.childContext(makeSectionBlockFixture({ depth: 1 }));
53
+ const capture = vi.spyOn(context, 'captureChildConstruct');
54
+ const section = makeSectionBlockFixture();
55
+
56
+ integrator.integrate(context, makeHeading('## Hello'), section);
57
+
58
+ expect(capture).toHaveBeenCalledWith(section);
59
+ });
60
+ });
@@ -0,0 +1,31 @@
1
+ import { sectionDepth } from '@art-md/primitives';
2
+ import type { Heading } from 'mdast';
3
+
4
+ import type { SectionBlock } from '../../../../../factories';
5
+ import type { ConstructIntegrator } from '../../../../types';
6
+ import { findParentSection } from '../helpers/findParentSection';
7
+
8
+ export function createSectionBlockIntegrator(): ConstructIntegrator {
9
+ return {
10
+ integrate(context, node, construct) {
11
+ const section = construct as SectionBlock;
12
+ let currentContext = context;
13
+
14
+ const heading = node as Heading;
15
+ while (currentContext.construct.construct === 'SectionBlock') {
16
+ const parentSection = findParentSection(currentContext);
17
+ if (parentSection && sectionDepth(parentSection) >= heading.depth) {
18
+ const parentContext = currentContext.parent();
19
+ if (parentContext) {
20
+ currentContext = parentContext;
21
+ }
22
+ } else {
23
+ break;
24
+ }
25
+ }
26
+
27
+ currentContext.captureChildConstruct(section);
28
+ return currentContext.childContext(section);
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,34 @@
1
+ import { makeParserVisitContextMock } from '@art-md/primitives/src/test/helpers';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { createSectionBlockFromNodeMock } from '../../../../../test/helpers';
5
+ import { createSectionBlockFromNode } from '../helpers/createSectionBlockFromNode';
6
+
7
+ import { createSectionBlockProcessor } from './createSectionBlockProcessor';
8
+
9
+ vi.mock('../helpers/createSectionBlockFromNode', async () => {
10
+ return createSectionBlockFromNodeMock();
11
+ });
12
+
13
+ describe('createSectionBlockProcessor', () => {
14
+ it('FOR non-heading nodes returns null', () => {
15
+ const processor = createSectionBlockProcessor();
16
+ const context = makeParserVisitContextMock();
17
+ const node = { type: 'paragraph', children: [] };
18
+
19
+ const result = processor.captureNode(context, node);
20
+
21
+ expect(result).toBeNull();
22
+ });
23
+
24
+ it('WHEN capturing a heading returns the createSectionBlockFromNode result', () => {
25
+ const processor = createSectionBlockProcessor();
26
+ const context = makeParserVisitContextMock();
27
+ const node = { type: 'heading', children: [] };
28
+
29
+ const result = processor.captureNode(context, node);
30
+
31
+ expect(createSectionBlockFromNode).toHaveBeenCalledWith(node, context);
32
+ expect(result).toBe(vi.mocked(createSectionBlockFromNode).mock.results[0]?.value);
33
+ });
34
+ });
@@ -0,0 +1,15 @@
1
+ import type { Heading } from 'mdast';
2
+
3
+ import type { ConstructProcessor } from '../../../../types';
4
+ import { createSectionBlockFromNode } from '../helpers/createSectionBlockFromNode';
5
+
6
+ export function createSectionBlockProcessor(): ConstructProcessor {
7
+ return {
8
+ captureNode(context, node) {
9
+ if (node.type !== 'heading') {
10
+ return null;
11
+ }
12
+ return createSectionBlockFromNode(node as Heading, context);
13
+ },
14
+ };
15
+ }
@@ -0,0 +1,23 @@
1
+ import { nodePositionMock } from '@art-md/primitives/src/test/helpers';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { createArtDocumentFromNode } from './createArtDocumentFromNode';
5
+
6
+ vi.mock('@art-md/primitives', async () => {
7
+ const actual = await import('@art-md/primitives');
8
+ return {
9
+ ...actual,
10
+ ...nodePositionMock(),
11
+ };
12
+ });
13
+
14
+ describe('createArtDocument', () => {
15
+ it('WHEN creating an ArtDocument from a root node', async () => {
16
+ const root = { type: 'root' };
17
+
18
+ const result = createArtDocumentFromNode(root);
19
+
20
+ expect(result.construct).toBe('Document');
21
+ expect(result.children).toEqual([]);
22
+ });
23
+ });
@@ -0,0 +1,10 @@
1
+ import { type ArtDocument, nodePosition } from '@art-md/primitives';
2
+ import type { Node } from 'unist';
3
+
4
+ import { createDocument } from '../../factories';
5
+
6
+ export function createArtDocumentFromNode(root: Node): ArtDocument {
7
+ const document = createDocument({});
8
+ document.position = nodePosition(root);
9
+ return document;
10
+ }
@@ -0,0 +1 @@
1
+ export * from './createArtDocumentFromNode';
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { FIELD_TEXT_PATTERN } from './constants';
4
+
5
+ describe('FIELD_TEXT_PATTERN', () => {
6
+ it('WHEN matching a field text pattern', () => {
7
+ const resultPurpose = 'Purpose: '.match(FIELD_TEXT_PATTERN);
8
+
9
+ const resultMyField = 'My Field: '.match(FIELD_TEXT_PATTERN);
10
+
11
+ expect(resultPurpose).toBeTruthy();
12
+ expect(resultMyField).toBeTruthy();
13
+ });
14
+
15
+ it('WHEN match non-field text does not', () => {
16
+ const resultHello = 'hello'.match(FIELD_TEXT_PATTERN);
17
+
18
+ const resultNoColon = 'no colon'.match(FIELD_TEXT_PATTERN);
19
+
20
+ expect(resultHello).toBeFalsy();
21
+ expect(resultNoColon).toBeFalsy();
22
+ });
23
+ });
@@ -0,0 +1 @@
1
+ export const FIELD_TEXT_PATTERN = /^[A-Za-z][A-Za-z ]*:(?:\s|$)/;
@@ -0,0 +1,3 @@
1
+ export { FIELD_TEXT_PATTERN } from './constants';
2
+ export { isFieldStrong } from './isFieldStrong';
3
+ export { stripStrong } from './stripStrong';
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { stripStrongMock } from '../../test/helpers';
4
+
5
+ import { isFieldStrong } from './isFieldStrong';
6
+
7
+ vi.mock('./stripStrong', () => {
8
+ return stripStrongMock('Purpose:');
9
+ });
10
+
11
+ describe('isFieldStrong', () => {
12
+ it('FOR a strong node matching field pattern returns true', () => {
13
+ const node = { type: 'strong', children: [{ type: 'text', value: 'Purpose:' }] };
14
+
15
+ const result = isFieldStrong(node, { markdown: '**Purpose:**' } as never);
16
+
17
+ expect(result).toBe(true);
18
+ });
19
+
20
+ it('FOR non-strong node returns false', () => {
21
+ const node = { type: 'text', value: 'Purpose:' };
22
+
23
+ const result = isFieldStrong(node, { markdown: 'Purpose:' } as never);
24
+
25
+ expect(result).toBe(false);
26
+ });
27
+ });
@@ -0,0 +1,9 @@
1
+ import type { ParserVisitContext } from '@art-md/primitives';
2
+ import type { Node, Strong } from 'mdast';
3
+
4
+ import { FIELD_TEXT_PATTERN } from './constants';
5
+ import { stripStrong } from './stripStrong';
6
+
7
+ export function isFieldStrong(node: Node, context: ParserVisitContext): node is Strong {
8
+ return node.type === 'strong' && FIELD_TEXT_PATTERN.test(stripStrong(node as Strong, context));
9
+ }