Skip to content

Commit 9ed2c44

Browse files
committed
Extract parsing linked fields into parseTextEntry
So the linking logic in the contentModel/index is thinner — more abstract. An unfortunate result of extraction is that now two places are digging into entry fields in the same way. Feels like a tooth cavity. But it is necessary to dig the array field items one by one to be able to preserve non-linked items in them.
1 parent e8ef914 commit 9ed2c44

3 files changed

Lines changed: 235 additions & 14 deletions

File tree

src/compiler/contentModel/index.js

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,6 @@ const models = {
1414
Asset: require('./asset')
1515
}
1616

17-
const LINKED_FIELD_SYNTAX = /^\+[^ ]+$/
18-
19-
const parseLink = (value) => {
20-
return value.replace(/^\+/g, '').split('/').filter(Boolean)
21-
}
22-
2317
const findLinkedNode = (allNodes, linkPath) => {
2418
const leafSlug = linkPath.pop()
2519
const leafRe = new RegExp(`^${leafSlug}$`, 'i')
@@ -51,17 +45,15 @@ const findLinkedNode = (allNodes, linkPath) => {
5145

5246
const linkNodes = (nodes) => {
5347
nodes.forEach(node => {
54-
const fields = Object.keys(node)
5548
Object.keys(node).forEach(key => {
5649
const value = node[key]
5750
if (Array.isArray(value)) {
5851
for (let i = 0; i < value.length; i++) {
5952
let valueItem = value[i]
60-
if (!LINKED_FIELD_SYNTAX.test(valueItem)) {
53+
if (!valueItem.linkPath) {
6154
break
6255
}
63-
const link = parseLink(valueItem)
64-
const linkedNode = findLinkedNode(nodes, link)
56+
const linkedNode = findLinkedNode(nodes, valueItem.linkPath)
6557
if (linkedNode) {
6658
node[key][i] = Object.assign({}, linkedNode)
6759
linkBack(node, linkedNode, key)
@@ -71,11 +63,10 @@ const linkNodes = (nodes) => {
7163
}
7264
}
7365
} else {
74-
if (!LINKED_FIELD_SYNTAX.test(value)) {
66+
if (!value?.linkPath) {
7567
return
7668
}
77-
const link = parseLink(value)
78-
const linkedNode = findLinkedNode(nodes, link)
69+
const linkedNode = findLinkedNode(nodes, value.linkPath)
7970
if (linkedNode) {
8071
node[key] = Object.assign({}, linkedNode)
8172
linkBack(node, linkedNode, key)

src/lib/parseTextEntry.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,38 @@ const frontMatter = require('front-matter')
33
const slug = require('slug')
44
const { removeExtension, Markdown } = require('./contentModelHelpers')
55

6+
const LINKED_FIELD_SYNTAX = /^\+[^ ]+$/
7+
8+
const parseLinkValue = (value) => {
9+
return value.replace(/^\+/g, '').split('/').filter(Boolean)
10+
}
11+
12+
const parseLinkedFields = (fields) => {
13+
Object.keys(fields).forEach(key => {
14+
const value = fields[key]
15+
if (Array.isArray(value)) {
16+
for (let i = 0; i < value.length; i++) {
17+
if (!LINKED_FIELD_SYNTAX.test(value[i])) {
18+
continue
19+
}
20+
fields[key][i] = {
21+
key,
22+
linkPath: parseLinkValue(value[i])
23+
}
24+
}
25+
} else {
26+
if (!LINKED_FIELD_SYNTAX.test(value)) {
27+
return
28+
}
29+
fields[key] = {
30+
key,
31+
linkPath: parseLinkValue(value)
32+
}
33+
}
34+
})
35+
return fields
36+
}
37+
638
const parseContent = (node, content) => {
739
if (node.extension.match(/(html|htm|hbs|handlebars)/i)) {
840
return content
@@ -46,6 +78,7 @@ const parseTextEntry = (fsNode, indexNode, isFlatData) => {
4678
return {
4779
..._.omit(fsNode, 'children'),
4880
...attributes,
81+
...parseLinkedFields(_.cloneDeep(attributes)),
4982
__originalAttributes__: attributes,
5083
hasIndex,
5184
title: attributes.title || entryName,
@@ -56,6 +89,8 @@ const parseTextEntry = (fsNode, indexNode, isFlatData) => {
5689
}
5790

5891
module.exports = {
92+
parseLinkValue,
93+
parseLinkedFields,
5994
parseContent,
6095
normalizeEntryName,
6196
parseFlatData,

src/lib/parseTextEntry.spec.js

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,206 @@
11
const test = require('tape')
22
const {
3-
parseFlatData,
3+
parseLinkValue,
4+
parseLinkedFields,
45
parseContent,
56
normalizeEntryName,
7+
parseFlatData,
68
parseTextEntry
79
} = require('./parseTextEntry')
810

11+
test('parseLinkValue removes leading plus and splits by slash', (t) => {
12+
const value = '+folder/subfolder/item'
13+
14+
const result = parseLinkValue(value)
15+
16+
t.deepEqual(
17+
result,
18+
['folder', 'subfolder', 'item'],
19+
'removes plus and splits by slash'
20+
)
21+
22+
t.end()
23+
})
24+
25+
test('parseLinkValue handles single segment path', (t) => {
26+
const value = '+folder'
27+
28+
const result = parseLinkValue(value)
29+
30+
t.deepEqual(
31+
result,
32+
['folder'],
33+
'returns array with single segment'
34+
)
35+
36+
t.end()
37+
})
38+
39+
test('parseLinkValue filters out empty segments', (t) => {
40+
const value = '+folder//item'
41+
42+
const result = parseLinkValue(value)
43+
44+
t.deepEqual(
45+
result,
46+
['folder', 'item'],
47+
'removes empty segments from consecutive slashes'
48+
)
49+
50+
t.end()
51+
})
52+
53+
test('parseLinkValue preserves case sensitivity', (t) => {
54+
const value = '+PEOPLE/users/ADMINS/john/PROFILE'
55+
56+
const result = parseLinkValue(value)
57+
58+
t.deepEqual(
59+
result,
60+
['PEOPLE', 'users', 'ADMINS', 'john', 'PROFILE'],
61+
'preserves mixed case with uppercase and lowercase segments'
62+
)
63+
64+
t.end()
65+
})
66+
67+
test('parseLinkValue ignores trailing slash', (t) => {
68+
const value = '+folder/subfolder/'
69+
70+
const result = parseLinkValue(value)
71+
72+
t.deepEqual(
73+
result,
74+
['folder', 'subfolder'],
75+
'filters out empty segment from trailing slash'
76+
)
77+
78+
t.end()
79+
})
80+
81+
test('parseLinkedFields converts single string linked field', (t) => {
82+
const fields = { author: '+people/users/admins/john' }
83+
84+
const result = parseLinkedFields(fields)
85+
86+
t.deepEqual(
87+
result.author,
88+
{ key: 'author', linkPath: ['people', 'users', 'admins', 'john'] },
89+
'converts linked field with multiple path segments'
90+
)
91+
92+
t.end()
93+
})
94+
95+
test('parseLinkedFields converts array of linked fields', (t) => {
96+
const fields = { tags: ['+categories/tech/frontend/react', '+categories/js/backend/node'] }
97+
98+
const result = parseLinkedFields(fields)
99+
100+
t.deepEqual(
101+
result.tags[0],
102+
{ key: 'tags', linkPath: ['categories', 'tech', 'frontend', 'react'] },
103+
'converts first array element with deep path'
104+
)
105+
106+
t.deepEqual(
107+
result.tags[1],
108+
{ key: 'tags', linkPath: ['categories', 'js', 'backend', 'node'] },
109+
'converts second array element with deep path'
110+
)
111+
112+
t.end()
113+
})
114+
115+
test('parseLinkedFields preserves non-linked string fields', (t) => {
116+
const fields = { title: 'My Title', description: 'Plain text' }
117+
118+
const result = parseLinkedFields(fields)
119+
120+
t.equal(
121+
result.title,
122+
'My Title',
123+
'preserves non-linked string field'
124+
)
125+
126+
t.equal(
127+
result.description,
128+
'Plain text',
129+
'preserves other non-linked fields'
130+
)
131+
132+
t.end()
133+
})
134+
135+
test('parseLinkedFields converts only the linked items in mixed arrays', (t) => {
136+
const fields = { mixed: ['+category/tech', 'plain-text', '+category/other'] }
137+
138+
const result = parseLinkedFields(fields)
139+
140+
t.deepEqual(
141+
result.mixed[0],
142+
{ key: 'mixed', linkPath: ['category', 'tech'] },
143+
'converts first linked field'
144+
)
145+
146+
t.equal(
147+
result.mixed[1],
148+
'plain-text',
149+
'preserves non-linked item'
150+
)
151+
152+
t.deepEqual(
153+
result.mixed[2],
154+
{ key: 'mixed', linkPath: ['category', 'other'] },
155+
'converts linked field that comes after non-linked item'
156+
)
157+
158+
t.end()
159+
})
160+
161+
test('parseLinkedFields handles mix of linked and non-linked fields', (t) => {
162+
const fields = {
163+
author: '+users/john',
164+
title: 'Article Title',
165+
tags: ['+tags/featured', '+tags/new'],
166+
description: 'An article'
167+
}
168+
169+
const result = parseLinkedFields(fields)
170+
171+
t.deepEqual(
172+
result.author,
173+
{ key: 'author', linkPath: ['users', 'john'] },
174+
'converts linked field'
175+
)
176+
177+
t.equal(
178+
result.title,
179+
'Article Title',
180+
'preserves non-linked string'
181+
)
182+
183+
t.deepEqual(
184+
result.tags[0],
185+
{ key: 'tags', linkPath: ['tags', 'featured'] },
186+
'converts first array element'
187+
)
188+
189+
t.deepEqual(
190+
result.tags[1],
191+
{ key: 'tags', linkPath: ['tags', 'new'] },
192+
'converts second array element'
193+
)
194+
195+
t.equal(
196+
result.description,
197+
'An article',
198+
'preserves non-linked fields among linked ones'
199+
)
200+
201+
t.end()
202+
})
203+
9204
test('parseContent returns content as-is for HTML files', (t) => {
10205
const node = { extension: '.html' }
11206
const content = '<div>HTML content</div>'

0 commit comments

Comments
 (0)