@financial-times/cp-content-pipeline-schema 2.3.0 → 2.3.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/lib/model/Concept.test.js +0 -9
  3. package/lib/model/Concept.test.js.map +1 -1
  4. package/lib/model/RichText.d.ts +1 -1
  5. package/lib/model/RichText.js +20 -34
  6. package/lib/model/RichText.js.map +1 -1
  7. package/lib/model/RichText.test.js +0 -9
  8. package/lib/model/RichText.test.js.map +1 -1
  9. package/lib/resolvers/content-tree/bodyXMLToTree.d.ts +4 -3
  10. package/lib/resolvers/content-tree/bodyXMLToTree.js +2 -2
  11. package/lib/resolvers/content-tree/bodyXMLToTree.js.map +1 -1
  12. package/lib/resolvers/content-tree/bodyXMLToTree.test.js +20 -32
  13. package/lib/resolvers/content-tree/bodyXMLToTree.test.js.map +1 -1
  14. package/lib/resolvers/content-tree/nodePredicates.d.ts +6 -13
  15. package/lib/resolvers/content-tree/nodePredicates.js +33 -33
  16. package/lib/resolvers/content-tree/nodePredicates.js.map +1 -1
  17. package/lib/resolvers/content-tree/references/Reference.d.ts +1 -1
  18. package/lib/resolvers/content-tree/tagMappings.d.ts +2 -1
  19. package/lib/resolvers/content-tree/tagMappings.js +60 -60
  20. package/lib/resolvers/content-tree/tagMappings.js.map +1 -1
  21. package/package.json +1 -2
  22. package/src/model/Concept.test.ts +0 -9
  23. package/src/model/RichText.test.ts +0 -10
  24. package/src/model/RichText.ts +27 -39
  25. package/src/resolvers/content-tree/bodyXMLToTree.test.ts +22 -32
  26. package/src/resolvers/content-tree/bodyXMLToTree.ts +11 -6
  27. package/src/resolvers/content-tree/nodePredicates.ts +39 -45
  28. package/src/resolvers/content-tree/tagMappings.ts +102 -60
  29. package/tsconfig.tsbuildinfo +1 -1
  30. package/__mocks__/worker_threads.ts +0 -3
  31. package/lib/resolvers/content-tree/bodyXMLToTreeWorker.d.ts +0 -9
  32. package/lib/resolvers/content-tree/bodyXMLToTreeWorker.js +0 -18
  33. package/lib/resolvers/content-tree/bodyXMLToTreeWorker.js.map +0 -1
  34. package/src/resolvers/content-tree/bodyXMLToTreeWorker.ts +0 -31
@@ -1,8 +1,9 @@
1
- import type { ContentTree } from '@financial-times/content-tree'
2
1
  import * as cheerio from 'cheerio'
3
- import { isTag, isText } from 'domhandler'
4
2
 
3
+ import type { ContentTree } from '@financial-times/content-tree'
5
4
  import { AnyNode } from './Workarounds'
5
+ import { QueryContext } from '../..'
6
+ import { isTag, isText } from 'domhandler'
6
7
 
7
8
  function isNode(node: ContentTree.Node | undefined): node is ContentTree.Node {
8
9
  return Boolean(node && 'type' in node)
@@ -10,7 +11,8 @@ function isNode(node: ContentTree.Node | undefined): node is ContentTree.Node {
10
11
 
11
12
  type ContentTreeTransform = (
12
13
  $el: cheerio.Cheerio<any>,
13
- traverse: () => AnyNode[]
14
+ traverse: () => AnyNode[],
15
+ context?: QueryContext
14
16
  ) => AnyNode | AnyNode[]
15
17
 
16
18
  export type TagMappings = Record<string, ContentTreeTransform>
@@ -21,7 +23,8 @@ export type TagMappings = Record<string, ContentTreeTransform>
21
23
  */
22
24
  export default function bodyXMLToTree(
23
25
  xml: string,
24
- tagMappings: TagMappings
26
+ tagMappings: TagMappings,
27
+ context?: QueryContext
25
28
  ): ContentTree.Body {
26
29
  const $ = cheerio.load(xml)
27
30
 
@@ -41,8 +44,10 @@ export default function bodyXMLToTree(
41
44
 
42
45
  if (matchedSelector) {
43
46
  const contentTreeTransform = tagMappings[matchedSelector]
44
- return contentTreeTransform($(node), () =>
45
- flattenAndTraverseChildren(node.children)
47
+ return contentTreeTransform(
48
+ $(node),
49
+ () => flattenAndTraverseChildren(node.children),
50
+ context
46
51
  )
47
52
  }
48
53
 
@@ -1,6 +1,6 @@
1
- import { parentPort } from 'worker_threads'
2
-
3
- import type { AnyNode } from './Workarounds'
1
+ import { OperationalError } from '@dotcom-reliability-kit/errors'
2
+ import { AnyNode } from './Workarounds'
3
+ import { QueryContext } from '../..'
4
4
 
5
5
  type ValuesOfTuple<Tuple extends readonly string[]> = Tuple[number]
6
6
 
@@ -19,39 +19,24 @@ export const phrasingTypes = [
19
19
  'link',
20
20
  ] as const
21
21
 
22
- export interface PredicateError {
23
- event: string
24
- code: string
25
- message: string
26
- expected: AnyNode['type'] | readonly AnyNode['type'][]
27
- actual: AnyNode['type'][]
28
- error: Error
29
- }
30
-
31
- // reliability-kit's logger creates a worker thread under the hood but we can't
32
- // access this thread directly from these worker threads. Instead let's send
33
- // all our recoverable errors to the piscina pool managing us.
34
- const postLoggerError = (error: Omit<PredicateError, 'error'>) => {
35
- // stacks will only copy over if they're in an Error object
36
- const errorWithStack = { ...error, error: new Error() }
37
- parentPort?.postMessage(errorWithStack)
38
- }
39
-
40
22
  export const findChildOftype = <NodeType extends AnyNode>(
41
23
  type: NodeType['type'],
42
24
  nodes: AnyNode[],
43
- parentType: AnyNode['type']
25
+ parentType: AnyNode['type'],
26
+ context?: QueryContext
44
27
  ): NodeType | undefined => {
45
28
  const predicate = (node: AnyNode): node is NodeType => node.type === type
46
29
  const child = nodes.find(predicate)
47
30
 
48
31
  if (!child) {
49
- postLoggerError({
32
+ context?.logger.error({
50
33
  event: 'RECOVERABLE_ERROR',
51
- code: 'BODY_XML_UNEXPECTED_STRUCTURE',
52
- message: `Didn't find expected child type in ${parentType}`,
53
- expected: type,
54
- actual: nodes.map((node) => node.type),
34
+ error: new OperationalError({
35
+ code: 'BODY_XML_UNEXPECTED_STRUCTURE',
36
+ message: `Didn't find expected child type in ${parentType}`,
37
+ expected: type,
38
+ actual: nodes.map((node) => node.type),
39
+ }),
55
40
  })
56
41
  }
57
42
 
@@ -61,18 +46,21 @@ export const findChildOftype = <NodeType extends AnyNode>(
61
46
  export const everyChildIsType = <NodeType extends AnyNode>(
62
47
  type: NodeType['type'],
63
48
  nodes: AnyNode[],
64
- parentType: AnyNode['type']
49
+ parentType: AnyNode['type'],
50
+ context?: QueryContext
65
51
  ): NodeType[] => {
66
52
  const predicate = (node: AnyNode): node is NodeType => node.type === type
67
53
  const allChildrenAreType = nodes.every(predicate)
68
54
 
69
55
  if (!allChildrenAreType) {
70
- postLoggerError({
56
+ context?.logger.error({
71
57
  event: 'RECOVERABLE_ERROR',
72
- code: 'BODY_XML_UNEXPECTED_STRUCTURE',
73
- message: `Unexpected children types for ${parentType}`,
74
- expected: type,
75
- actual: nodes.map((node) => node.type),
58
+ error: new OperationalError({
59
+ code: 'BODY_XML_UNEXPECTED_STRUCTURE',
60
+ message: `Unexpected children types for ${parentType}`,
61
+ expected: type,
62
+ actual: nodes.map((node) => node.type),
63
+ }),
76
64
  })
77
65
  }
78
66
 
@@ -82,19 +70,22 @@ export const everyChildIsType = <NodeType extends AnyNode>(
82
70
  export const childrenOfTypes = <Types extends readonly AnyNode['type'][]>(
83
71
  types: Types,
84
72
  nodes: AnyNode[],
85
- parentType: AnyNode['type']
73
+ parentType: AnyNode['type'],
74
+ context?: QueryContext
86
75
  ): NodeOfType<ValuesOfTuple<Types>>[] => {
87
76
  const predicate = (node: AnyNode): node is NodeOfType<ValuesOfTuple<Types>> =>
88
77
  types.includes(node.type)
89
78
  const allChildrenAreType = nodes.every(predicate)
90
79
 
91
80
  if (!allChildrenAreType) {
92
- postLoggerError({
81
+ context?.logger.error({
93
82
  event: 'RECOVERABLE_ERROR',
94
- code: 'BODY_XML_UNEXPECTED_STRUCTURE',
95
- message: `Unexpected ordered children types for ${parentType}`,
96
- expected: types,
97
- actual: nodes.map((node) => node.type),
83
+ error: new OperationalError({
84
+ code: 'BODY_XML_UNEXPECTED_STRUCTURE',
85
+ message: `Unexpected ordered children types for ${parentType}`,
86
+ expected: types,
87
+ actual: nodes.map((node) => node.type),
88
+ }),
98
89
  })
99
90
  }
100
91
 
@@ -119,18 +110,21 @@ export const childrenOfOrderedTypes = <
119
110
  >(
120
111
  types: Types,
121
112
  nodes: AnyNode[],
122
- parentType: AnyNode['type']
113
+ parentType: AnyNode['type'],
114
+ context?: QueryContext
123
115
  ): NodesOfTypes<Types> => {
124
116
  if (nodesAreOrderedTypes(types, nodes)) {
125
117
  return nodes
126
118
  }
127
119
 
128
- postLoggerError({
120
+ context?.logger.error({
129
121
  event: 'RECOVERABLE_ERROR',
130
- code: 'BODY_XML_UNEXPECTED_STRUCTURE',
131
- message: `Unexpected children types for ${parentType}`,
132
- expected: types,
133
- actual: nodes.map((node) => node.type),
122
+ error: new OperationalError({
123
+ code: 'BODY_XML_UNEXPECTED_STRUCTURE',
124
+ message: `Unexpected children types for ${parentType}`,
125
+ expected: types,
126
+ actual: nodes.map((node) => node.type),
127
+ }),
134
128
  })
135
129
 
136
130
  return nodes as unknown as NodesOfTypes<Types>
@@ -19,6 +19,7 @@ import {
19
19
 
20
20
  import * as scrollytelling from '@financial-times/n-scrollytelling-image/server'
21
21
  import { LiteralToPrimitiveDeep } from 'type-fest'
22
+ import { CapiResponse } from '../../model/CapiResponse'
22
23
 
23
24
  const tableResponsiveStyleMap: Record<string, Table['responsiveStyle']> = {
24
25
  stacked: 'flat',
@@ -81,10 +82,10 @@ const validScrollytellingOption = <
81
82
  : false
82
83
  }
83
84
 
84
- const articleTagMappings = (topperHasImage: boolean): TagMappings => ({
85
+ const articleTagMappings = (capiData?: CapiResponse): TagMappings => ({
85
86
  'body > ft-content[type="http://www.ft.com/ontology/content/ImageSet"]:first-child':
86
87
  ($el) => ({
87
- type: topperHasImage ? 'image-set' : 'main-image',
88
+ type: capiData?.topperHasImage() ? 'image-set' : 'main-image',
88
89
  id: $el.attr('url') || '',
89
90
  }),
90
91
  'body > ft-content[type="http://www.ft.com/ontology/content/ImageSet"]:not(:first-child),:not(body) > ft-content[type="http://www.ft.com/ontology/content/ImageSet"]':
@@ -103,55 +104,60 @@ const liveBlogPostTagMappings: TagMappings = {
103
104
 
104
105
  const commonTagMappings: TagMappings = {
105
106
  body: ($el, traverse) => ({ type: 'body', version: 1, children: traverse() }),
106
- 'a:not([data-asset-type])': ($el, traverse) => ({
107
+ 'a:not([data-asset-type])': ($el, traverse, context) => ({
107
108
  type: 'link',
108
109
  url: $el.attr('href') || '',
109
110
  title: '',
110
- children: childrenOfTypes(phrasingTypes, traverse(), 'link'),
111
+ children: childrenOfTypes(phrasingTypes, traverse(), 'link', context),
111
112
  }),
112
- strong: ($el, traverse) => ({
113
+ strong: ($el, traverse, context) => ({
113
114
  type: 'strong',
114
- children: childrenOfTypes(phrasingTypes, traverse(), 'strong'),
115
+ children: childrenOfTypes(phrasingTypes, traverse(), 'strong', context),
115
116
  }),
116
- em: ($el, traverse) => ({
117
+ em: ($el, traverse, context) => ({
117
118
  type: 'emphasis',
118
- children: childrenOfTypes(phrasingTypes, traverse(), 'emphasis'),
119
+ children: childrenOfTypes(phrasingTypes, traverse(), 'emphasis', context),
119
120
  }),
120
- blockquote: ($el, traverse) => ({
121
+ blockquote: ($el, traverse, context) => ({
121
122
  type: 'blockquote',
122
- children: childrenOfTypes(phrasingTypes, traverse(), 'blockquote'),
123
+ children: childrenOfTypes(phrasingTypes, traverse(), 'blockquote', context),
123
124
  }),
124
125
  // strip any (redundant) line breaks in between two paragraphs
125
126
  'p + br': ($el) => ($el.next()[0]?.tagName === 'p' ? [] : { type: 'break' }),
126
127
  br: () => ({ type: 'break' }),
127
128
  hr: () => ({ type: 'thematic-break' }),
128
- s: ($el, traverse) => ({
129
+ s: ($el, traverse, context) => ({
129
130
  type: 'strikethrough',
130
- children: childrenOfTypes(phrasingTypes, traverse(), 'strikethrough'),
131
+ children: childrenOfTypes(
132
+ phrasingTypes,
133
+ traverse(),
134
+ 'strikethrough',
135
+ context
136
+ ),
131
137
  }),
132
- ul: ($el, traverse) => ({
138
+ ul: ($el, traverse, context) => ({
133
139
  type: 'list',
134
- children: everyChildIsType('list-item', traverse(), 'list'),
140
+ children: everyChildIsType('list-item', traverse(), 'list', context),
135
141
  ordered: false,
136
142
  }),
137
- ol: ($el, traverse) => ({
143
+ ol: ($el, traverse, context) => ({
138
144
  type: 'list',
139
- children: everyChildIsType('list-item', traverse(), 'list'),
145
+ children: everyChildIsType('list-item', traverse(), 'list', context),
140
146
  ordered: true,
141
147
  }),
142
- li: ($el, traverse) => ({
148
+ li: ($el, traverse, context) => ({
143
149
  type: 'list-item',
144
- children: childrenOfTypes(phrasingTypes, traverse(), 'list-item'),
150
+ children: childrenOfTypes(phrasingTypes, traverse(), 'list-item', context),
145
151
  }),
146
152
  'a[data-asset-type="tweet"]': ($el) => ({
147
153
  type: 'tweet',
148
154
  id: $el.attr('href') || '',
149
155
  }),
150
156
 
151
- 'h4,h5,h6': ($el, traverse) => ({
157
+ 'h4,h5,h6': ($el, traverse, context) => ({
152
158
  type: 'heading',
153
159
  level: 'label',
154
- children: everyChildIsType('text', traverse(), 'heading'),
160
+ children: everyChildIsType('text', traverse(), 'heading', context),
155
161
  }),
156
162
  'pull-quote': ($el) => ({
157
163
  type: 'pullquote',
@@ -230,7 +236,7 @@ const commonTagMappings: TagMappings = {
230
236
  description: $description.text(),
231
237
  }
232
238
  },
233
- '.n-content-layout': ($el, traverse) => {
239
+ '.n-content-layout': ($el, traverse, context) => {
234
240
  //TODO: this is a bit gross??
235
241
  const isValidWidth = (
236
242
  str: string
@@ -254,14 +260,16 @@ const commonTagMappings: TagMappings = {
254
260
  ...everyChildIsType<ContentTree.LayoutSlot>(
255
261
  'layout-slot',
256
262
  otherChildren,
257
- 'layout'
263
+ 'layout',
264
+ context
258
265
  ),
259
266
  ]
260
267
  } else {
261
268
  return everyChildIsType<ContentTree.LayoutSlot>(
262
269
  'layout-slot',
263
270
  children,
264
- 'layout'
271
+ 'layout',
272
+ context
265
273
  )
266
274
  }
267
275
  }
@@ -273,7 +281,7 @@ const commonTagMappings: TagMappings = {
273
281
  children: getChildren(traverse()),
274
282
  }
275
283
  },
276
- '.n-content-layout__slot': ($el, traverse) => ({
284
+ '.n-content-layout__slot': ($el, traverse, context) => ({
277
285
  type: 'layout-slot',
278
286
  children: childrenOfTypes(
279
287
  ['heading', 'paragraph', 'layout-image'],
@@ -283,7 +291,8 @@ const commonTagMappings: TagMappings = {
283
291
  .flatMap((node) =>
284
292
  node.type === 'layout-slot' ? node.children : [node]
285
293
  ),
286
- 'layout-slot'
294
+ 'layout-slot',
295
+ context
287
296
  ),
288
297
  }),
289
298
  '.n-content-layout img': ($el) => ({
@@ -294,7 +303,7 @@ const commonTagMappings: TagMappings = {
294
303
  credit: $el.attr('data-copyright') || '',
295
304
  }),
296
305
 
297
- table: ($el, traverse): Table | ContentTree.Layout => {
306
+ table: ($el, traverse, context): Table | ContentTree.Layout => {
298
307
  const layoutSmallscreen = $el.attr('data-table-layout-smallscreen')
299
308
  const responsiveStyle: Table['responsiveStyle'] =
300
309
  layoutSmallscreen && layoutSmallscreen in tableResponsiveStyleMap
@@ -309,12 +318,14 @@ const commonTagMappings: TagMappings = {
309
318
  const caption = findChildOftype<TableCaption>(
310
319
  'table-caption',
311
320
  children,
312
- 'table'
321
+ 'table',
322
+ context
313
323
  )
314
324
  const footer = findChildOftype<TableFooter>(
315
325
  'table-footer',
316
326
  children,
317
- 'table'
327
+ 'table',
328
+ context
318
329
  )
319
330
 
320
331
  // HACK:KB:20230523 transform single-cell tables into infoboxes, for legacy articles
@@ -328,7 +339,8 @@ const commonTagMappings: TagMappings = {
328
339
  children: childrenOfTypes(
329
340
  ['paragraph', 'heading'],
330
341
  bodies[0].children[0].children[0].children,
331
- 'layout-slot'
342
+ 'layout-slot',
343
+ context
332
344
  ),
333
345
  }
334
346
 
@@ -337,7 +349,12 @@ const commonTagMappings: TagMappings = {
337
349
  {
338
350
  type: 'heading',
339
351
  level: 'subheading',
340
- children: everyChildIsType('text', caption.children, 'heading'),
352
+ children: everyChildIsType(
353
+ 'text',
354
+ caption.children,
355
+ 'heading',
356
+ context
357
+ ),
341
358
  },
342
359
  slot,
343
360
  ]
@@ -417,34 +434,44 @@ const commonTagMappings: TagMappings = {
417
434
  // HACK needs to come before tr. TODO sort by selector specificity
418
435
  'tfoot > tr': ($el, traverse) => traverse(),
419
436
  // HACK needs to come before td. TODO sort by selector specificity
420
- 'tfoot > tr > td': ($el, traverse) => ({
437
+ 'tfoot > tr > td': ($el, traverse, context) => ({
421
438
  type: 'table-footer',
422
- children: childrenOfTypes(phrasingTypes, traverse(), 'table-footer'),
439
+ children: childrenOfTypes(
440
+ phrasingTypes,
441
+ traverse(),
442
+ 'table-footer',
443
+ context
444
+ ),
423
445
  }),
424
- tr: ($el, traverse) => ({
446
+ tr: ($el, traverse, context) => ({
425
447
  type: 'table-row',
426
- children: everyChildIsType('table-cell', traverse(), 'table-row'),
448
+ children: everyChildIsType('table-cell', traverse(), 'table-row', context),
427
449
  }),
428
- td: ($el, traverse) => ({
450
+ td: ($el, traverse, context) => ({
429
451
  type: 'table-cell',
430
- children: childrenOfTypes(phrasingTypes, traverse(), 'table-cell'),
452
+ children: childrenOfTypes(phrasingTypes, traverse(), 'table-cell', context),
431
453
  }),
432
- th: ($el, traverse) => ({
454
+ th: ($el, traverse, context) => ({
433
455
  type: 'table-cell',
434
456
  heading: true,
435
- children: childrenOfTypes(phrasingTypes, traverse(), 'table-cell'),
457
+ children: childrenOfTypes(phrasingTypes, traverse(), 'table-cell', context),
436
458
  }),
437
- thead: ($el, traverse) => ({
459
+ thead: ($el, traverse, context) => ({
438
460
  type: 'table-body',
439
- children: everyChildIsType('table-row', traverse(), 'table-body'),
461
+ children: everyChildIsType('table-row', traverse(), 'table-body', context),
440
462
  }),
441
- tbody: ($el, traverse) => ({
463
+ tbody: ($el, traverse, context) => ({
442
464
  type: 'table-body',
443
- children: everyChildIsType('table-row', traverse(), 'table-body'),
465
+ children: everyChildIsType('table-row', traverse(), 'table-body', context),
444
466
  }),
445
- caption: ($el, traverse) => ({
467
+ caption: ($el, traverse, context) => ({
446
468
  type: 'table-caption',
447
- children: childrenOfTypes(phrasingTypes, traverse(), 'table-caption'),
469
+ children: childrenOfTypes(
470
+ phrasingTypes,
471
+ traverse(),
472
+ 'table-caption',
473
+ context
474
+ ),
448
475
  }),
449
476
 
450
477
  // HACK needs to come after LayoutImage. TODO sort by selector specificity
@@ -468,14 +495,15 @@ const commonTagMappings: TagMappings = {
468
495
  format: 'standard',
469
496
  }),
470
497
 
471
- 'scrollable-block': ($el, traverse) => {
498
+ 'scrollable-block': ($el, traverse, context) => {
472
499
  return {
473
500
  type: 'scrolly-block',
474
501
  theme: $el.attr('theme') === '2' ? 'serif' : 'sans',
475
502
  children: everyChildIsType(
476
503
  'scrolly-section',
477
504
  traverse(),
478
- 'scrolly-block'
505
+ 'scrolly-block',
506
+ context
479
507
  ),
480
508
  }
481
509
  },
@@ -486,7 +514,7 @@ const commonTagMappings: TagMappings = {
486
514
  id: $el.attr('url') ?? '',
487
515
  }),
488
516
 
489
- 'scrollable-section': ($el, traverse) => {
517
+ 'scrollable-section': ($el, traverse, context) => {
490
518
  const [firstChild, ...restChildren] = traverse()
491
519
 
492
520
  const display = $el.attr('theme-display')
@@ -510,29 +538,33 @@ const commonTagMappings: TagMappings = {
510
538
  ...everyChildIsType<ContentTree.ScrollyImage>(
511
539
  'scrolly-image',
512
540
  [firstChild],
513
- 'scrolly-section'
541
+ 'scrolly-section',
542
+ context
514
543
  ),
515
544
  ...everyChildIsType<ContentTree.ScrollyCopy>(
516
545
  'scrolly-copy',
517
546
  restChildren,
518
- 'scrolly-section'
547
+ 'scrolly-section',
548
+ context
519
549
  ),
520
550
  ] as [ContentTree.ScrollyImage, ...ContentTree.ScrollyCopy[]],
521
551
  }
522
552
  },
523
553
 
524
- 'scrollable-text': ($el, traverse) => ({
554
+ 'scrollable-text': ($el, traverse, context) => ({
525
555
  type: 'scrolly-copy',
526
556
  children: childrenOfTypes(
527
557
  ['scrolly-heading', 'paragraph'],
528
558
  traverse(),
529
- 'scrolly-copy'
559
+ 'scrolly-copy',
560
+ context
530
561
  ),
531
562
  }),
532
563
 
533
564
  'scrollable-text > p, scrollable-text > h1, scrollable-text > h2': (
534
565
  $el,
535
- traverse
566
+ traverse,
567
+ context
536
568
  ) => {
537
569
  const textStyle = $el.attr('theme-style')
538
570
 
@@ -540,31 +572,41 @@ const commonTagMappings: TagMappings = {
540
572
  return {
541
573
  type: 'scrolly-heading',
542
574
  level: scrollytellingOptionsToContentTree.textStyle[textStyle],
543
- children: everyChildIsType('text', traverse(), 'scrolly-heading'),
575
+ children: everyChildIsType(
576
+ 'text',
577
+ traverse(),
578
+ 'scrolly-heading',
579
+ context
580
+ ),
544
581
  }
545
582
  }
546
583
 
547
584
  return {
548
585
  type: 'paragraph',
549
- children: childrenOfTypes(phrasingTypes, traverse(), 'paragraph'),
586
+ children: childrenOfTypes(
587
+ phrasingTypes,
588
+ traverse(),
589
+ 'paragraph',
590
+ context
591
+ ),
550
592
  }
551
593
  },
552
594
 
553
595
  // HACK:KB:20230703 needs to come after `scrollable-text > X`. todo sort by specificity
554
- p: ($el, traverse) => ({
596
+ p: ($el, traverse, context) => ({
555
597
  type: 'paragraph',
556
- children: childrenOfTypes(phrasingTypes, traverse(), 'paragraph'),
598
+ children: childrenOfTypes(phrasingTypes, traverse(), 'paragraph', context),
557
599
  }),
558
600
 
559
- h1: ($el, traverse) => ({
601
+ h1: ($el, traverse, context) => ({
560
602
  type: 'heading',
561
603
  level: 'chapter',
562
- children: everyChildIsType('text', traverse(), 'heading'),
604
+ children: everyChildIsType('text', traverse(), 'heading', context),
563
605
  }),
564
- 'h2,h3': ($el, traverse) => ({
606
+ 'h2,h3': ($el, traverse, context) => ({
565
607
  type: 'heading',
566
608
  level: 'subheading',
567
- children: everyChildIsType('text', traverse(), 'heading'),
609
+ children: everyChildIsType('text', traverse(), 'heading', context),
568
610
  }),
569
611
  'ft-content[type="http://www.ft.com/ontology/content/ClipSet"]': ($el) => ({
570
612
  type: 'clip-set',