From 97a807ddec856fd21f7817a8afa25ad60c40c778 Mon Sep 17 00:00:00 2001 From: AliceArmstrong <22509772+AliceArmstrong@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:22:35 +0000 Subject: [PATCH 1/2] Implement Question and Answer nodes --- SPEC.md | 51 ++++++ content-tree.d.ts | 86 +++++++++- content_tree.go | 279 +++++++++++++++++++++++++++++++ schemas/body-tree.schema.json | 149 +++++++++++++++++ schemas/content-tree.schema.json | 157 +++++++++++++++++ schemas/transit-tree.schema.json | 149 +++++++++++++++++ 6 files changed, 867 insertions(+), 4 deletions(-) diff --git a/SPEC.md b/SPEC.md index ba8c0c1..f437812 100644 --- a/SPEC.md +++ b/SPEC.md @@ -63,6 +63,19 @@ type VideoSource = AVSource & { `VideoSource` extends AVSource to add in the properties relevant just to videos +### `Byline` + +```ts +interface Byline extends Node { + type: "byline" + title?: string + external displayName: string + external headshotUrl: string +} +``` + +`Byline` defines a reusable visual representation of an author + ## Core Nodes ### `Node` @@ -342,6 +355,9 @@ type StoryBlock = | Definition | InfoBox | InfoPair + | QuestionAndAnswer + | Question + | Answer ``` `StoryBlock` nodes are things that can be inserted into an article body. @@ -375,6 +391,41 @@ interface ImageSet extends Node { } ``` +### `QuestionAndAnswer` + +```ts +interface QuestionAndAnswer extends Parent { + type: "question-and-answer" + children: [Question, Answer, ...Answer[]] +} +``` + +`QuestionAndAnswer` defines a container grouping a `Question` followed by one or more `Answer` nodes + +### `Question` + +```ts +interface Question extends Parent { + type: "question" + displayName?: string + children: (Paragraph | Exclude)[] +} +``` + +`Question` defines the question copy with an optional `displayName` string, disallowing inline `Link` and `FindOutMoreLink` nodes + +### `Answer` + +```ts +interface Answer extends Parent { + type: "answer" + author: Byline + children: (Paragraph | Phrasing)[] +} +``` + +`Answer` defines an authored reply that requires an `Author` + #### Image types ##### `ImageSetPicture` diff --git a/content-tree.d.ts b/content-tree.d.ts index 23a1ef6..ad9a69a 100644 --- a/content-tree.d.ts +++ b/content-tree.d.ts @@ -12,6 +12,12 @@ export declare namespace ContentTree { pixelWidth?: number; videoCodec?: string; }; + interface Byline extends Node { + type: "byline"; + title?: string; + displayName: string; + headshotUrl: string; + } interface Node { type: string; data?: any; @@ -88,7 +94,7 @@ export declare namespace ContentTree { type: "blockquote"; children: (Paragraph | Phrasing)[]; } - type StoryBlock = ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | Pullquote | ScrollyBlock | ClipSet | Table | Recommended | RecommendedList | Tweet | Video | YoutubeVideo | Timeline | ImagePair | InNumbers | Definition | InfoBox | InfoPair; + type StoryBlock = ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | Pullquote | ScrollyBlock | ClipSet | Table | Recommended | RecommendedList | Tweet | Video | YoutubeVideo | Timeline | ImagePair | InNumbers | Definition | InfoBox | InfoPair | QuestionAndAnswer | Question | Answer; interface Pullquote extends Node { type: "pullquote"; text: string; @@ -100,6 +106,20 @@ export declare namespace ContentTree { picture: ImageSetPicture; fragmentIdentifier?: string; } + interface QuestionAndAnswer extends Parent { + type: "question-and-answer"; + children: [Question, Answer, ...Answer[]]; + } + interface Question extends Parent { + type: "question"; + displayName?: string; + children: (Paragraph | Exclude)[]; + } + interface Answer extends Parent { + type: "answer"; + author: Byline; + children: (Paragraph | Phrasing)[]; + } type ImageSetPicture = { layoutWidth: string; imageType: "image" | "graphic"; @@ -431,6 +451,12 @@ export declare namespace ContentTree { pixelWidth?: number; videoCodec?: string; }; + interface Byline extends Node { + type: "byline"; + title?: string; + displayName: string; + headshotUrl: string; + } interface Node { type: string; data?: any; @@ -507,7 +533,7 @@ export declare namespace ContentTree { type: "blockquote"; children: (Paragraph | Phrasing)[]; } - type StoryBlock = ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | Pullquote | ScrollyBlock | ClipSet | Table | Recommended | RecommendedList | Tweet | Video | YoutubeVideo | Timeline | ImagePair | InNumbers | Definition | InfoBox | InfoPair; + type StoryBlock = ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | Pullquote | ScrollyBlock | ClipSet | Table | Recommended | RecommendedList | Tweet | Video | YoutubeVideo | Timeline | ImagePair | InNumbers | Definition | InfoBox | InfoPair | QuestionAndAnswer | Question | Answer; interface Pullquote extends Node { type: "pullquote"; text: string; @@ -519,6 +545,20 @@ export declare namespace ContentTree { picture: ImageSetPicture; fragmentIdentifier?: string; } + interface QuestionAndAnswer extends Parent { + type: "question-and-answer"; + children: [Question, Answer, ...Answer[]]; + } + interface Question extends Parent { + type: "question"; + displayName?: string; + children: (Paragraph | Exclude)[]; + } + interface Answer extends Parent { + type: "answer"; + author: Byline; + children: (Paragraph | Phrasing)[]; + } type ImageSetPicture = { layoutWidth: string; imageType: "image" | "graphic"; @@ -851,6 +891,10 @@ export declare namespace ContentTree { pixelWidth?: number; videoCodec?: string; }; + interface Byline extends Node { + type: "byline"; + title?: string; + } interface Node { type: string; data?: any; @@ -927,7 +971,7 @@ export declare namespace ContentTree { type: "blockquote"; children: (Paragraph | Phrasing)[]; } - type StoryBlock = ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | Pullquote | ScrollyBlock | ClipSet | Table | Recommended | RecommendedList | Tweet | Video | YoutubeVideo | Timeline | ImagePair | InNumbers | Definition | InfoBox | InfoPair; + type StoryBlock = ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | Pullquote | ScrollyBlock | ClipSet | Table | Recommended | RecommendedList | Tweet | Video | YoutubeVideo | Timeline | ImagePair | InNumbers | Definition | InfoBox | InfoPair | QuestionAndAnswer | Question | Answer; interface Pullquote extends Node { type: "pullquote"; text: string; @@ -938,6 +982,20 @@ export declare namespace ContentTree { id: string; fragmentIdentifier?: string; } + interface QuestionAndAnswer extends Parent { + type: "question-and-answer"; + children: [Question, Answer, ...Answer[]]; + } + interface Question extends Parent { + type: "question"; + displayName?: string; + children: (Paragraph | Exclude)[]; + } + interface Answer extends Parent { + type: "answer"; + author: Byline; + children: (Paragraph | Phrasing)[]; + } type ImageSetPicture = { layoutWidth: string; imageType: "image" | "graphic"; @@ -1244,6 +1302,12 @@ export declare namespace ContentTree { pixelWidth?: number; videoCodec?: string; }; + interface Byline extends Node { + type: "byline"; + title?: string; + displayName?: string; + headshotUrl?: string; + } interface Node { type: string; data?: any; @@ -1320,7 +1384,7 @@ export declare namespace ContentTree { type: "blockquote"; children: (Paragraph | Phrasing)[]; } - type StoryBlock = ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | Pullquote | ScrollyBlock | ClipSet | Table | Recommended | RecommendedList | Tweet | Video | YoutubeVideo | Timeline | ImagePair | InNumbers | Definition | InfoBox | InfoPair; + type StoryBlock = ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | Pullquote | ScrollyBlock | ClipSet | Table | Recommended | RecommendedList | Tweet | Video | YoutubeVideo | Timeline | ImagePair | InNumbers | Definition | InfoBox | InfoPair | QuestionAndAnswer | Question | Answer; interface Pullquote extends Node { type: "pullquote"; text: string; @@ -1332,6 +1396,20 @@ export declare namespace ContentTree { picture?: ImageSetPicture; fragmentIdentifier?: string; } + interface QuestionAndAnswer extends Parent { + type: "question-and-answer"; + children: [Question, Answer, ...Answer[]]; + } + interface Question extends Parent { + type: "question"; + displayName?: string; + children: (Paragraph | Exclude)[]; + } + interface Answer extends Parent { + type: "answer"; + author: Byline; + children: (Paragraph | Phrasing)[]; + } type ImageSetPicture = { layoutWidth: string; imageType: "image" | "graphic"; diff --git a/content_tree.go b/content_tree.go index fc883b1..c28a408 100644 --- a/content_tree.go +++ b/content_tree.go @@ -80,6 +80,7 @@ const ( ScrollyCopyChildType = "scrolly-copy-child" ScrollySectionChildType = "scrolly-section-child" TableChildType = "table-child" + QuestionChildType = "question-child" TimelineType = "timeline" TimelineEventType = "timeline-event" @@ -94,6 +95,9 @@ const ( InNumbersType = "in-numbers" ImagePairType = "image-pair" + QuestionAndAnswerType = "question-and-answer" + QuestionType = "question" + AnswerType = "answer" ) var ( @@ -142,6 +146,11 @@ type ColumnSettingsItems struct { Sortable bool `json:"sortable,omitempty"` } +type Byline struct { + Title string `json:"title,omitempty"` +} +} + type BigNumber struct { Type string `json:"type"` Description string `json:"description"` @@ -433,6 +442,9 @@ type BodyBlock struct { *ImagePair *InfoBox *InfoPair + *QuestionAndAnswer + *Question + *Answer } func (n *BodyBlock) GetType() string { @@ -515,6 +527,15 @@ func (n *BodyBlock) GetEmbedded() Node { if n.InfoPair != nil { return n.InfoPair } + if n.QuestionAndAnswer != nil { + return n.QuestionAndAnswer + } + if n.Question != nil { + return n.Question + } + if n.Answer != nil { + return n.Answer + } return nil } @@ -756,6 +777,24 @@ func (n *BodyBlock) UnmarshalJSON(data []byte) error { return err } n.InfoPair = &v + case QuestionAndAnswerType: + var v QuestionAndAnswer + if err := json.Unmarshal(data, &v); err != nil { + return err + } + n.QuestionAndAnswer = &v + case QuestionType: + var v Question + if err := json.Unmarshal(data, &v); err != nil { + return err + } + n.Question = &v + case AnswerType: + var v Answer + if err := json.Unmarshal(data, &v); err != nil { + return err + } + n.Answer = &v default: return fmt.Errorf("failed to unmarshal BodyBlock from %s: %w", data, ErrUnmarshalInvalidNode) } @@ -814,6 +853,12 @@ func (n *BodyBlock) MarshalJSON() ([]byte, error) { return json.Marshal(n.InfoBox) case n.InfoPair != nil: return json.Marshal(n.InfoPair) + case n.QuestionAndAnswer != nil: + return json.Marshal(n.QuestionAndAnswer) + case n.Question != nil: + return json.Marshal(n.Question) + case n.Answer != nil: + return json.Marshal(n.Answer) default: return []byte(`{}`), nil } @@ -872,6 +917,12 @@ func makeBodyBlock(n Node) (*BodyBlock, error) { return &BodyBlock{InfoBox: n.(*InfoBox)}, nil case InfoPairType: return &BodyBlock{InfoPair: n.(*InfoPair)}, nil + case QuestionAndAnswerType: + return &BodyBlock{QuestionAndAnswer: n.(*QuestionAndAnswer)}, nil + case QuestionType: + return &BodyBlock{Question: n.(*Question)}, nil + case AnswerType: + return &BodyBlock{Answer: n.(*Answer)}, nil default: return nil, ErrInvalidChildType } @@ -3333,3 +3384,231 @@ func (n *InfoPair) AppendChild(child Node) error { n.Children = append(n.Children, child.(*Card)) return nil } + +type QuestionAndAnswer struct { + Type string `json:"type"` + Children []*BodyBlock `json:"children"` +} + +func (n *QuestionAndAnswer) GetType() string { return n.Type } +func (n *QuestionAndAnswer) GetEmbedded() Node { return nil } +func (n *QuestionAndAnswer) GetChildren() []Node { + result := make([]Node, len(n.Children)) + for i, v := range n.Children { result[i] = v } + return result +} +func (n *QuestionAndAnswer) AppendChild(child Node) error { + switch child.GetType() { + case QuestionType: + if len(n.Children) != 0 { return ErrInvalidChildType } + bb, err := makeBodyBlock(child) + if err != nil { return err } + n.Children = append(n.Children, bb) + return nil + case AnswerType: + if len(n.Children) == 0 { return ErrInvalidChildType } + bb, err := makeBodyBlock(child) + if err != nil { return err } + n.Children = append(n.Children, bb) + return nil + default: + return ErrInvalidChildType + } +} + +func (n *QuestionAndAnswer) UnmarshalJSON(data []byte) error { + // Temporary struct to parse children as raw messages first + var raw struct { + Type string `json:"type"` + Children []json.RawMessage `json:"children"` + } + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if raw.Type != QuestionAndAnswerType { + return fmt.Errorf("failed to unmarshal QuestionAndAnswer from %s: %w", data, ErrUnmarshalInvalidNode) + } + + // Must have at least two children: one Question and one Answer + if len(raw.Children) < 2 { + return fmt.Errorf("invalid QuestionAndAnswer, expected at least 2 children, got %d", len(raw.Children)) + } + + children := make([]*BodyBlock, 0, len(raw.Children)) + for i, rc := range raw.Children { + // Determine the type of the child + var tn typedNode + if err := json.Unmarshal(rc, &tn); err != nil { return err } + var bb BodyBlock + switch tn.Type { + case QuestionType: + // first child must be Question + if i != 0 { + return fmt.Errorf("invalid QuestionAndAnswer: Question found at position %d", i) + } + var v Question + if err := json.Unmarshal(rc, &v); err != nil { return err } + bb = BodyBlock{Question: &v} + case AnswerType: + // Answers allowed only after first child + if i == 0 { + return fmt.Errorf("invalid QuestionAndAnswer: Answer found at position 0") + } + var v Answer + if err := json.Unmarshal(rc, &v); err != nil { return err } + bb = BodyBlock{Answer: &v} + default: + return fmt.Errorf("invalid child type %s in QuestionAndAnswer", tn.Type) + } + children = append(children, &bb) + } + + n.Type = raw.Type + n.Children = children + return nil +} + +type Question struct { + Type string `json:"type"` + DisplayName string `json:"displayName,omitempty"` + Children []*QuestionChild `json:"children"` +} + +func (n *Question) GetType() string { return n.Type } +func (n *Question) GetEmbedded() Node { return nil } +func (n *Question) GetChildren() []Node { + result := make([]Node, len(n.Children)) + for i, v := range n.Children { result[i] = v } + return result +} +func (n *Question) AppendChild(child Node) error { + c, err := makeQuestionChild(child) + if err != nil { return err } + n.Children = append(n.Children, c) + return nil +} + +type Answer struct { + Type string `json:"type"` + Author Byline `json:"author"` + Children []*ListItemChild `json:"children"` +} + +func (n *Answer) GetType() string { return n.Type } +func (n *Answer) GetEmbedded() Node { return nil } +func (n *Answer) GetChildren() []Node { + result := make([]Node, len(n.Children)) + for i, v := range n.Children { result[i] = v } + return result +} +func (n *Answer) AppendChild(child Node) error { + c, err := makeListItemChild(child) + if err != nil { return err } + n.Children = append(n.Children, c) + return nil +} + +type QuestionChild struct { + *Paragraph + *Text + *Break + *Strong + *Emphasis + *Strikethrough +} + +func (n *QuestionChild) GetType() string { return QuestionChildType } + +func (n *QuestionChild) GetEmbedded() Node { + if n.Paragraph != nil { return n.Paragraph } + if n.Text != nil { return n.Text } + if n.Break != nil { return n.Break } + if n.Strong != nil { return n.Strong } + if n.Emphasis != nil { return n.Emphasis } + if n.Strikethrough != nil { return n.Strikethrough } + return nil +} + +func (n *QuestionChild) GetChildren() []Node { + if n.Paragraph != nil { return n.Paragraph.GetChildren() } + if n.Text != nil { return n.Text.GetChildren() } + if n.Break != nil { return n.Break.GetChildren() } + if n.Strong != nil { return n.Strong.GetChildren() } + if n.Emphasis != nil { return n.Emphasis.GetChildren() } + if n.Strikethrough != nil { return n.Strikethrough.GetChildren() } + return nil +} + +func (n *QuestionChild) AppendChild(_ Node) error { return ErrCannotHaveChildren } + +func (n *QuestionChild) UnmarshalJSON(data []byte) error { + var tn typedNode + if err := json.Unmarshal(data, &tn); err != nil { return err } + switch tn.Type { + case ParagraphType: + var v Paragraph + if err := json.Unmarshal(data, &v); err != nil { return err } + n.Paragraph = &v + case TextType: + var v Text + if err := json.Unmarshal(data, &v); err != nil { return err } + n.Text = &v + case BreakType: + var v Break + if err := json.Unmarshal(data, &v); err != nil { return err } + n.Break = &v + case StrongType: + var v Strong + if err := json.Unmarshal(data, &v); err != nil { return err } + n.Strong = &v + case EmphasisType: + var v Emphasis + if err := json.Unmarshal(data, &v); err != nil { return err } + n.Emphasis = &v + case StrikethroughType: + var v Strikethrough + if err := json.Unmarshal(data, &v); err != nil { return err } + n.Strikethrough = &v + default: + return fmt.Errorf("failed to unmarshal QuestionChild from %s: %w", data, ErrUnmarshalInvalidNode) + } + return nil +} + +func (n *QuestionChild) MarshalJSON() ([]byte, error) { + switch { + case n.Paragraph != nil: + return json.Marshal(n.Paragraph) + case n.Text != nil: + return json.Marshal(n.Text) + case n.Break != nil: + return json.Marshal(n.Break) + case n.Strong != nil: + return json.Marshal(n.Strong) + case n.Emphasis != nil: + return json.Marshal(n.Emphasis) + case n.Strikethrough != nil: + return json.Marshal(n.Strikethrough) + default: + return []byte(`{}`), nil + } +} + +func makeQuestionChild(n Node) (*QuestionChild, error) { + switch n.GetType() { + case ParagraphType: + return &QuestionChild{Paragraph: n.(*Paragraph)}, nil + case TextType: + return &QuestionChild{Text: n.(*Text)}, nil + case BreakType: + return &QuestionChild{Break: n.(*Break)}, nil + case StrongType: + return &QuestionChild{Strong: n.(*Strong)}, nil + case EmphasisType: + return &QuestionChild{Emphasis: n.(*Emphasis)}, nil + case StrikethroughType: + return &QuestionChild{Strikethrough: n.(*Strikethrough)}, nil + default: + return nil, ErrInvalidChildType + } +} diff --git a/schemas/body-tree.schema.json b/schemas/body-tree.schema.json index 41d0904..8dbfe69 100644 --- a/schemas/body-tree.schema.json +++ b/schemas/body-tree.schema.json @@ -2,6 +2,56 @@ "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "definitions": { + "ContentTree.transit.Answer": { + "additionalProperties": false, + "properties": { + "author": { + "$ref": "#/definitions/ContentTree.transit.Byline" + }, + "children": { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/ContentTree.transit.Paragraph" + }, + { + "$ref": "#/definitions/ContentTree.transit.Text" + }, + { + "$ref": "#/definitions/ContentTree.transit.Break" + }, + { + "$ref": "#/definitions/ContentTree.transit.Strong" + }, + { + "$ref": "#/definitions/ContentTree.transit.Emphasis" + }, + { + "$ref": "#/definitions/ContentTree.transit.Strikethrough" + }, + { + "$ref": "#/definitions/ContentTree.transit.Link" + }, + { + "$ref": "#/definitions/ContentTree.transit.FindOutMoreLink" + } + ] + }, + "type": "array" + }, + "data": {}, + "type": { + "const": "answer", + "type": "string" + } + }, + "required": [ + "author", + "children", + "type" + ], + "type": "object" + }, "ContentTree.transit.BigNumber": { "additionalProperties": false, "properties": { @@ -149,6 +199,15 @@ }, { "$ref": "#/definitions/ContentTree.transit.InfoPair" + }, + { + "$ref": "#/definitions/ContentTree.transit.QuestionAndAnswer" + }, + { + "$ref": "#/definitions/ContentTree.transit.Question" + }, + { + "$ref": "#/definitions/ContentTree.transit.Answer" } ] }, @@ -166,6 +225,23 @@ ], "type": "object" }, + "ContentTree.transit.Byline": { + "additionalProperties": false, + "properties": { + "data": {}, + "title": { + "type": "string" + }, + "type": { + "const": "byline", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, "ContentTree.transit.Card": { "additionalProperties": false, "description": "A card describes a subject with images and text", @@ -907,6 +983,79 @@ ], "type": "object" }, + "ContentTree.transit.Question": { + "additionalProperties": false, + "properties": { + "children": { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/ContentTree.transit.Paragraph" + }, + { + "$ref": "#/definitions/ContentTree.transit.Text" + }, + { + "$ref": "#/definitions/ContentTree.transit.Break" + }, + { + "$ref": "#/definitions/ContentTree.transit.Strong" + }, + { + "$ref": "#/definitions/ContentTree.transit.Emphasis" + }, + { + "$ref": "#/definitions/ContentTree.transit.Strikethrough" + } + ] + }, + "type": "array" + }, + "data": {}, + "displayName": { + "type": "string" + }, + "type": { + "const": "question", + "type": "string" + } + }, + "required": [ + "children", + "type" + ], + "type": "object" + }, + "ContentTree.transit.QuestionAndAnswer": { + "additionalProperties": false, + "properties": { + "children": { + "additionalItems": { + "$ref": "#/definitions/ContentTree.transit.Answer" + }, + "items": [ + { + "$ref": "#/definitions/ContentTree.transit.Question" + }, + { + "$ref": "#/definitions/ContentTree.transit.Answer" + } + ], + "minItems": 2, + "type": "array" + }, + "data": {}, + "type": { + "const": "question-and-answer", + "type": "string" + } + }, + "required": [ + "children", + "type" + ], + "type": "object" + }, "ContentTree.transit.Recommended": { "additionalProperties": false, "properties": { diff --git a/schemas/content-tree.schema.json b/schemas/content-tree.schema.json index c8a2adc..8e8b340 100644 --- a/schemas/content-tree.schema.json +++ b/schemas/content-tree.schema.json @@ -2,6 +2,56 @@ "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "definitions": { + "ContentTree.full.Answer": { + "additionalProperties": false, + "properties": { + "author": { + "$ref": "#/definitions/ContentTree.full.Byline" + }, + "children": { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/ContentTree.full.Paragraph" + }, + { + "$ref": "#/definitions/ContentTree.full.Text" + }, + { + "$ref": "#/definitions/ContentTree.full.Break" + }, + { + "$ref": "#/definitions/ContentTree.full.Strong" + }, + { + "$ref": "#/definitions/ContentTree.full.Emphasis" + }, + { + "$ref": "#/definitions/ContentTree.full.Strikethrough" + }, + { + "$ref": "#/definitions/ContentTree.full.Link" + }, + { + "$ref": "#/definitions/ContentTree.full.FindOutMoreLink" + } + ] + }, + "type": "array" + }, + "data": {}, + "type": { + "const": "answer", + "type": "string" + } + }, + "required": [ + "author", + "children", + "type" + ], + "type": "object" + }, "ContentTree.full.AssetFormat": { "enum": [ "desktop", @@ -186,6 +236,15 @@ }, { "$ref": "#/definitions/ContentTree.full.InfoPair" + }, + { + "$ref": "#/definitions/ContentTree.full.QuestionAndAnswer" + }, + { + "$ref": "#/definitions/ContentTree.full.Question" + }, + { + "$ref": "#/definitions/ContentTree.full.Answer" } ] }, @@ -203,6 +262,31 @@ ], "type": "object" }, + "ContentTree.full.Byline": { + "additionalProperties": false, + "properties": { + "data": {}, + "displayName": { + "type": "string" + }, + "headshotUrl": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "const": "byline", + "type": "string" + } + }, + "required": [ + "displayName", + "headshotUrl", + "type" + ], + "type": "object" + }, "ContentTree.full.Card": { "additionalProperties": false, "description": "A card describes a subject with images and text", @@ -1391,6 +1475,79 @@ ], "type": "object" }, + "ContentTree.full.Question": { + "additionalProperties": false, + "properties": { + "children": { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/ContentTree.full.Paragraph" + }, + { + "$ref": "#/definitions/ContentTree.full.Text" + }, + { + "$ref": "#/definitions/ContentTree.full.Break" + }, + { + "$ref": "#/definitions/ContentTree.full.Strong" + }, + { + "$ref": "#/definitions/ContentTree.full.Emphasis" + }, + { + "$ref": "#/definitions/ContentTree.full.Strikethrough" + } + ] + }, + "type": "array" + }, + "data": {}, + "displayName": { + "type": "string" + }, + "type": { + "const": "question", + "type": "string" + } + }, + "required": [ + "children", + "type" + ], + "type": "object" + }, + "ContentTree.full.QuestionAndAnswer": { + "additionalProperties": false, + "properties": { + "children": { + "additionalItems": { + "$ref": "#/definitions/ContentTree.full.Answer" + }, + "items": [ + { + "$ref": "#/definitions/ContentTree.full.Question" + }, + { + "$ref": "#/definitions/ContentTree.full.Answer" + } + ], + "minItems": 2, + "type": "array" + }, + "data": {}, + "type": { + "const": "question-and-answer", + "type": "string" + } + }, + "required": [ + "children", + "type" + ], + "type": "object" + }, "ContentTree.full.Recommended": { "additionalProperties": false, "properties": { diff --git a/schemas/transit-tree.schema.json b/schemas/transit-tree.schema.json index 52035ec..8991ec9 100644 --- a/schemas/transit-tree.schema.json +++ b/schemas/transit-tree.schema.json @@ -2,6 +2,56 @@ "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "definitions": { + "ContentTree.transit.Answer": { + "additionalProperties": false, + "properties": { + "author": { + "$ref": "#/definitions/ContentTree.transit.Byline" + }, + "children": { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/ContentTree.transit.Paragraph" + }, + { + "$ref": "#/definitions/ContentTree.transit.Text" + }, + { + "$ref": "#/definitions/ContentTree.transit.Break" + }, + { + "$ref": "#/definitions/ContentTree.transit.Strong" + }, + { + "$ref": "#/definitions/ContentTree.transit.Emphasis" + }, + { + "$ref": "#/definitions/ContentTree.transit.Strikethrough" + }, + { + "$ref": "#/definitions/ContentTree.transit.Link" + }, + { + "$ref": "#/definitions/ContentTree.transit.FindOutMoreLink" + } + ] + }, + "type": "array" + }, + "data": {}, + "type": { + "const": "answer", + "type": "string" + } + }, + "required": [ + "author", + "children", + "type" + ], + "type": "object" + }, "ContentTree.transit.BigNumber": { "additionalProperties": false, "properties": { @@ -174,6 +224,15 @@ }, { "$ref": "#/definitions/ContentTree.transit.InfoPair" + }, + { + "$ref": "#/definitions/ContentTree.transit.QuestionAndAnswer" + }, + { + "$ref": "#/definitions/ContentTree.transit.Question" + }, + { + "$ref": "#/definitions/ContentTree.transit.Answer" } ] }, @@ -191,6 +250,23 @@ ], "type": "object" }, + "ContentTree.transit.Byline": { + "additionalProperties": false, + "properties": { + "data": {}, + "title": { + "type": "string" + }, + "type": { + "const": "byline", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, "ContentTree.transit.Card": { "additionalProperties": false, "description": "A card describes a subject with images and text", @@ -932,6 +1008,79 @@ ], "type": "object" }, + "ContentTree.transit.Question": { + "additionalProperties": false, + "properties": { + "children": { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/ContentTree.transit.Paragraph" + }, + { + "$ref": "#/definitions/ContentTree.transit.Text" + }, + { + "$ref": "#/definitions/ContentTree.transit.Break" + }, + { + "$ref": "#/definitions/ContentTree.transit.Strong" + }, + { + "$ref": "#/definitions/ContentTree.transit.Emphasis" + }, + { + "$ref": "#/definitions/ContentTree.transit.Strikethrough" + } + ] + }, + "type": "array" + }, + "data": {}, + "displayName": { + "type": "string" + }, + "type": { + "const": "question", + "type": "string" + } + }, + "required": [ + "children", + "type" + ], + "type": "object" + }, + "ContentTree.transit.QuestionAndAnswer": { + "additionalProperties": false, + "properties": { + "children": { + "additionalItems": { + "$ref": "#/definitions/ContentTree.transit.Answer" + }, + "items": [ + { + "$ref": "#/definitions/ContentTree.transit.Question" + }, + { + "$ref": "#/definitions/ContentTree.transit.Answer" + } + ], + "minItems": 2, + "type": "array" + }, + "data": {}, + "type": { + "const": "question-and-answer", + "type": "string" + } + }, + "required": [ + "children", + "type" + ], + "type": "object" + }, "ContentTree.transit.Recommended": { "additionalProperties": false, "properties": { From c3974a0d9a96493a0cbc2f4e16acf365692c90e6 Mon Sep 17 00:00:00 2001 From: AliceArmstrong <22509772+AliceArmstrong@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:41:58 +0000 Subject: [PATCH 2/2] fix: rebase me --- SPEC.md | 35 +++---- content-tree.d.ts | 36 ++++--- content_tree.go | 161 ++++++++++++++++++++++--------- schemas/body-tree.schema.json | 44 +++++---- schemas/content-tree.schema.json | 60 ++++++------ schemas/transit-tree.schema.json | 44 +++++---- 6 files changed, 231 insertions(+), 149 deletions(-) diff --git a/SPEC.md b/SPEC.md index f437812..5d71ef3 100644 --- a/SPEC.md +++ b/SPEC.md @@ -63,18 +63,19 @@ type VideoSource = AVSource & { `VideoSource` extends AVSource to add in the properties relevant just to videos -### `Byline` +### `QuestionAndAnswerByline` ```ts -interface Byline extends Node { - type: "byline" - title?: string - external displayName: string - external headshotUrl: string +interface QuestionAndAnswerByline extends Node { + type: "question-and-answer-byline" + conceptId: string + title?: string + external displayName: string + external headshotUrl: string } ``` -`Byline` defines a reusable visual representation of an author +`QuestionAndAnswerByline` is a byline used for Q&A answers - it carries a `conceptId` to resolve external fields in cp-content-pipeline. ## Core Nodes @@ -395,8 +396,8 @@ interface ImageSet extends Node { ```ts interface QuestionAndAnswer extends Parent { - type: "question-and-answer" - children: [Question, Answer, ...Answer[]] + type: "question-and-answer" + children: [Question, Answer, ...Answer[]] } ``` @@ -406,25 +407,25 @@ interface QuestionAndAnswer extends Parent { ```ts interface Question extends Parent { - type: "question" - displayName?: string - children: (Paragraph | Exclude)[] + type: "question" + displayName?: string + children: (Paragraph | (Text | Break | Strong | Emphasis | Strikethrough))[] } ``` -`Question` defines the question copy with an optional `displayName` string, disallowing inline `Link` and `FindOutMoreLink` nodes +`Question` defines a Q&A question. Disallows `Link` and `FindOutMoreLink` nodes by explicitly picking the allowed phrasing node types. ### `Answer` ```ts interface Answer extends Parent { - type: "answer" - author: Byline - children: (Paragraph | Phrasing)[] + type: "answer" + byline: QuestionAndAnswerByline + children: (Paragraph | Phrasing)[] } ``` -`Answer` defines an authored reply that requires an `Author` +`Answer` defines an answer to a Q&A Question. Uses `QuestionAndAnswerByline` to display the author. #### Image types diff --git a/content-tree.d.ts b/content-tree.d.ts index ad9a69a..79ad2a9 100644 --- a/content-tree.d.ts +++ b/content-tree.d.ts @@ -12,8 +12,9 @@ export declare namespace ContentTree { pixelWidth?: number; videoCodec?: string; }; - interface Byline extends Node { - type: "byline"; + interface QuestionAndAnswerByline extends Node { + type: "question-and-answer-byline"; + conceptId: string; title?: string; displayName: string; headshotUrl: string; @@ -113,11 +114,11 @@ export declare namespace ContentTree { interface Question extends Parent { type: "question"; displayName?: string; - children: (Paragraph | Exclude)[]; + children: (Paragraph | (Text | Break | Strong | Emphasis | Strikethrough))[]; } interface Answer extends Parent { type: "answer"; - author: Byline; + byline: QuestionAndAnswerByline; children: (Paragraph | Phrasing)[]; } type ImageSetPicture = { @@ -451,8 +452,9 @@ export declare namespace ContentTree { pixelWidth?: number; videoCodec?: string; }; - interface Byline extends Node { - type: "byline"; + interface QuestionAndAnswerByline extends Node { + type: "question-and-answer-byline"; + conceptId: string; title?: string; displayName: string; headshotUrl: string; @@ -552,11 +554,11 @@ export declare namespace ContentTree { interface Question extends Parent { type: "question"; displayName?: string; - children: (Paragraph | Exclude)[]; + children: (Paragraph | (Text | Break | Strong | Emphasis | Strikethrough))[]; } interface Answer extends Parent { type: "answer"; - author: Byline; + byline: QuestionAndAnswerByline; children: (Paragraph | Phrasing)[]; } type ImageSetPicture = { @@ -891,8 +893,9 @@ export declare namespace ContentTree { pixelWidth?: number; videoCodec?: string; }; - interface Byline extends Node { - type: "byline"; + interface QuestionAndAnswerByline extends Node { + type: "question-and-answer-byline"; + conceptId: string; title?: string; } interface Node { @@ -989,11 +992,11 @@ export declare namespace ContentTree { interface Question extends Parent { type: "question"; displayName?: string; - children: (Paragraph | Exclude)[]; + children: (Paragraph | (Text | Break | Strong | Emphasis | Strikethrough))[]; } interface Answer extends Parent { type: "answer"; - author: Byline; + byline: QuestionAndAnswerByline; children: (Paragraph | Phrasing)[]; } type ImageSetPicture = { @@ -1302,8 +1305,9 @@ export declare namespace ContentTree { pixelWidth?: number; videoCodec?: string; }; - interface Byline extends Node { - type: "byline"; + interface QuestionAndAnswerByline extends Node { + type: "question-and-answer-byline"; + conceptId: string; title?: string; displayName?: string; headshotUrl?: string; @@ -1403,11 +1407,11 @@ export declare namespace ContentTree { interface Question extends Parent { type: "question"; displayName?: string; - children: (Paragraph | Exclude)[]; + children: (Paragraph | (Text | Break | Strong | Emphasis | Strikethrough))[]; } interface Answer extends Parent { type: "answer"; - author: Byline; + byline: QuestionAndAnswerByline; children: (Paragraph | Phrasing)[]; } type ImageSetPicture = { diff --git a/content_tree.go b/content_tree.go index c28a408..92c3129 100644 --- a/content_tree.go +++ b/content_tree.go @@ -94,10 +94,10 @@ const ( DefinitionType = "definition" InNumbersType = "in-numbers" - ImagePairType = "image-pair" + ImagePairType = "image-pair" QuestionAndAnswerType = "question-and-answer" - QuestionType = "question" - AnswerType = "answer" + QuestionType = "question" + AnswerType = "answer" ) var ( @@ -146,9 +146,12 @@ type ColumnSettingsItems struct { Sortable bool `json:"sortable,omitempty"` } -type Byline struct { - Title string `json:"title,omitempty"` -} +type QuestionAndAnswerByline struct { + Type string `json:"type"` + ConceptId string `json:"conceptId"` + Title string `json:"title,omitempty"` + DisplayName string `json:"displayName"` + HeadshotUrl string `json:"headshotUrl"` } type BigNumber struct { @@ -3390,25 +3393,35 @@ type QuestionAndAnswer struct { Children []*BodyBlock `json:"children"` } -func (n *QuestionAndAnswer) GetType() string { return n.Type } +func (n *QuestionAndAnswer) GetType() string { return n.Type } func (n *QuestionAndAnswer) GetEmbedded() Node { return nil } func (n *QuestionAndAnswer) GetChildren() []Node { result := make([]Node, len(n.Children)) - for i, v := range n.Children { result[i] = v } + for i, v := range n.Children { + result[i] = v + } return result } func (n *QuestionAndAnswer) AppendChild(child Node) error { switch child.GetType() { case QuestionType: - if len(n.Children) != 0 { return ErrInvalidChildType } + if len(n.Children) != 0 { + return ErrInvalidChildType + } bb, err := makeBodyBlock(child) - if err != nil { return err } + if err != nil { + return err + } n.Children = append(n.Children, bb) return nil case AnswerType: - if len(n.Children) == 0 { return ErrInvalidChildType } + if len(n.Children) == 0 { + return ErrInvalidChildType + } bb, err := makeBodyBlock(child) - if err != nil { return err } + if err != nil { + return err + } n.Children = append(n.Children, bb) return nil default: @@ -3438,7 +3451,9 @@ func (n *QuestionAndAnswer) UnmarshalJSON(data []byte) error { for i, rc := range raw.Children { // Determine the type of the child var tn typedNode - if err := json.Unmarshal(rc, &tn); err != nil { return err } + if err := json.Unmarshal(rc, &tn); err != nil { + return err + } var bb BodyBlock switch tn.Type { case QuestionType: @@ -3447,7 +3462,9 @@ func (n *QuestionAndAnswer) UnmarshalJSON(data []byte) error { return fmt.Errorf("invalid QuestionAndAnswer: Question found at position %d", i) } var v Question - if err := json.Unmarshal(rc, &v); err != nil { return err } + if err := json.Unmarshal(rc, &v); err != nil { + return err + } bb = BodyBlock{Question: &v} case AnswerType: // Answers allowed only after first child @@ -3455,7 +3472,9 @@ func (n *QuestionAndAnswer) UnmarshalJSON(data []byte) error { return fmt.Errorf("invalid QuestionAndAnswer: Answer found at position 0") } var v Answer - if err := json.Unmarshal(rc, &v); err != nil { return err } + if err := json.Unmarshal(rc, &v); err != nil { + return err + } bb = BodyBlock{Answer: &v} default: return fmt.Errorf("invalid child type %s in QuestionAndAnswer", tn.Type) @@ -3469,42 +3488,50 @@ func (n *QuestionAndAnswer) UnmarshalJSON(data []byte) error { } type Question struct { - Type string `json:"type"` - DisplayName string `json:"displayName,omitempty"` - Children []*QuestionChild `json:"children"` + Type string `json:"type"` + DisplayName string `json:"displayName,omitempty"` + Children []*QuestionChild `json:"children"` } -func (n *Question) GetType() string { return n.Type } +func (n *Question) GetType() string { return n.Type } func (n *Question) GetEmbedded() Node { return nil } func (n *Question) GetChildren() []Node { result := make([]Node, len(n.Children)) - for i, v := range n.Children { result[i] = v } + for i, v := range n.Children { + result[i] = v + } return result } func (n *Question) AppendChild(child Node) error { c, err := makeQuestionChild(child) - if err != nil { return err } + if err != nil { + return err + } n.Children = append(n.Children, c) return nil } type Answer struct { - Type string `json:"type"` - Author Byline `json:"author"` - Children []*ListItemChild `json:"children"` + Type string `json:"type"` + Byline QuestionAndAnswerByline `json:"byline"` + Children []*Phrasing `json:"children"` } -func (n *Answer) GetType() string { return n.Type } +func (n *Answer) GetType() string { return n.Type } func (n *Answer) GetEmbedded() Node { return nil } func (n *Answer) GetChildren() []Node { result := make([]Node, len(n.Children)) - for i, v := range n.Children { result[i] = v } + for i, v := range n.Children { + result[i] = v + } return result } func (n *Answer) AppendChild(child Node) error { - c, err := makeListItemChild(child) - if err != nil { return err } - n.Children = append(n.Children, c) + p, err := makePhrasing(child) + if err != nil { + return err + } + n.Children = append(n.Children, p) return nil } @@ -3520,22 +3547,46 @@ type QuestionChild struct { func (n *QuestionChild) GetType() string { return QuestionChildType } func (n *QuestionChild) GetEmbedded() Node { - if n.Paragraph != nil { return n.Paragraph } - if n.Text != nil { return n.Text } - if n.Break != nil { return n.Break } - if n.Strong != nil { return n.Strong } - if n.Emphasis != nil { return n.Emphasis } - if n.Strikethrough != nil { return n.Strikethrough } + if n.Paragraph != nil { + return n.Paragraph + } + if n.Text != nil { + return n.Text + } + if n.Break != nil { + return n.Break + } + if n.Strong != nil { + return n.Strong + } + if n.Emphasis != nil { + return n.Emphasis + } + if n.Strikethrough != nil { + return n.Strikethrough + } return nil } func (n *QuestionChild) GetChildren() []Node { - if n.Paragraph != nil { return n.Paragraph.GetChildren() } - if n.Text != nil { return n.Text.GetChildren() } - if n.Break != nil { return n.Break.GetChildren() } - if n.Strong != nil { return n.Strong.GetChildren() } - if n.Emphasis != nil { return n.Emphasis.GetChildren() } - if n.Strikethrough != nil { return n.Strikethrough.GetChildren() } + if n.Paragraph != nil { + return n.Paragraph.GetChildren() + } + if n.Text != nil { + return n.Text.GetChildren() + } + if n.Break != nil { + return n.Break.GetChildren() + } + if n.Strong != nil { + return n.Strong.GetChildren() + } + if n.Emphasis != nil { + return n.Emphasis.GetChildren() + } + if n.Strikethrough != nil { + return n.Strikethrough.GetChildren() + } return nil } @@ -3543,31 +3594,45 @@ func (n *QuestionChild) AppendChild(_ Node) error { return ErrCannotHaveChildren func (n *QuestionChild) UnmarshalJSON(data []byte) error { var tn typedNode - if err := json.Unmarshal(data, &tn); err != nil { return err } + if err := json.Unmarshal(data, &tn); err != nil { + return err + } switch tn.Type { case ParagraphType: var v Paragraph - if err := json.Unmarshal(data, &v); err != nil { return err } + if err := json.Unmarshal(data, &v); err != nil { + return err + } n.Paragraph = &v case TextType: var v Text - if err := json.Unmarshal(data, &v); err != nil { return err } + if err := json.Unmarshal(data, &v); err != nil { + return err + } n.Text = &v case BreakType: var v Break - if err := json.Unmarshal(data, &v); err != nil { return err } + if err := json.Unmarshal(data, &v); err != nil { + return err + } n.Break = &v case StrongType: var v Strong - if err := json.Unmarshal(data, &v); err != nil { return err } + if err := json.Unmarshal(data, &v); err != nil { + return err + } n.Strong = &v case EmphasisType: var v Emphasis - if err := json.Unmarshal(data, &v); err != nil { return err } + if err := json.Unmarshal(data, &v); err != nil { + return err + } n.Emphasis = &v case StrikethroughType: var v Strikethrough - if err := json.Unmarshal(data, &v); err != nil { return err } + if err := json.Unmarshal(data, &v); err != nil { + return err + } n.Strikethrough = &v default: return fmt.Errorf("failed to unmarshal QuestionChild from %s: %w", data, ErrUnmarshalInvalidNode) diff --git a/schemas/body-tree.schema.json b/schemas/body-tree.schema.json index 8dbfe69..bcb4d2c 100644 --- a/schemas/body-tree.schema.json +++ b/schemas/body-tree.schema.json @@ -5,8 +5,8 @@ "ContentTree.transit.Answer": { "additionalProperties": false, "properties": { - "author": { - "$ref": "#/definitions/ContentTree.transit.Byline" + "byline": { + "$ref": "#/definitions/ContentTree.transit.QuestionAndAnswerByline" }, "children": { "items": { @@ -46,7 +46,7 @@ } }, "required": [ - "author", + "byline", "children", "type" ], @@ -225,23 +225,6 @@ ], "type": "object" }, - "ContentTree.transit.Byline": { - "additionalProperties": false, - "properties": { - "data": {}, - "title": { - "type": "string" - }, - "type": { - "const": "byline", - "type": "string" - } - }, - "required": [ - "type" - ], - "type": "object" - }, "ContentTree.transit.Card": { "additionalProperties": false, "description": "A card describes a subject with images and text", @@ -1056,6 +1039,27 @@ ], "type": "object" }, + "ContentTree.transit.QuestionAndAnswerByline": { + "additionalProperties": false, + "properties": { + "conceptId": { + "type": "string" + }, + "data": {}, + "title": { + "type": "string" + }, + "type": { + "const": "question-and-answer-byline", + "type": "string" + } + }, + "required": [ + "conceptId", + "type" + ], + "type": "object" + }, "ContentTree.transit.Recommended": { "additionalProperties": false, "properties": { diff --git a/schemas/content-tree.schema.json b/schemas/content-tree.schema.json index 8e8b340..8811ef8 100644 --- a/schemas/content-tree.schema.json +++ b/schemas/content-tree.schema.json @@ -5,8 +5,8 @@ "ContentTree.full.Answer": { "additionalProperties": false, "properties": { - "author": { - "$ref": "#/definitions/ContentTree.full.Byline" + "byline": { + "$ref": "#/definitions/ContentTree.full.QuestionAndAnswerByline" }, "children": { "items": { @@ -46,7 +46,7 @@ } }, "required": [ - "author", + "byline", "children", "type" ], @@ -262,31 +262,6 @@ ], "type": "object" }, - "ContentTree.full.Byline": { - "additionalProperties": false, - "properties": { - "data": {}, - "displayName": { - "type": "string" - }, - "headshotUrl": { - "type": "string" - }, - "title": { - "type": "string" - }, - "type": { - "const": "byline", - "type": "string" - } - }, - "required": [ - "displayName", - "headshotUrl", - "type" - ], - "type": "object" - }, "ContentTree.full.Card": { "additionalProperties": false, "description": "A card describes a subject with images and text", @@ -1548,6 +1523,35 @@ ], "type": "object" }, + "ContentTree.full.QuestionAndAnswerByline": { + "additionalProperties": false, + "properties": { + "conceptId": { + "type": "string" + }, + "data": {}, + "displayName": { + "type": "string" + }, + "headshotUrl": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "const": "question-and-answer-byline", + "type": "string" + } + }, + "required": [ + "conceptId", + "displayName", + "headshotUrl", + "type" + ], + "type": "object" + }, "ContentTree.full.Recommended": { "additionalProperties": false, "properties": { diff --git a/schemas/transit-tree.schema.json b/schemas/transit-tree.schema.json index 8991ec9..9d24f63 100644 --- a/schemas/transit-tree.schema.json +++ b/schemas/transit-tree.schema.json @@ -5,8 +5,8 @@ "ContentTree.transit.Answer": { "additionalProperties": false, "properties": { - "author": { - "$ref": "#/definitions/ContentTree.transit.Byline" + "byline": { + "$ref": "#/definitions/ContentTree.transit.QuestionAndAnswerByline" }, "children": { "items": { @@ -46,7 +46,7 @@ } }, "required": [ - "author", + "byline", "children", "type" ], @@ -250,23 +250,6 @@ ], "type": "object" }, - "ContentTree.transit.Byline": { - "additionalProperties": false, - "properties": { - "data": {}, - "title": { - "type": "string" - }, - "type": { - "const": "byline", - "type": "string" - } - }, - "required": [ - "type" - ], - "type": "object" - }, "ContentTree.transit.Card": { "additionalProperties": false, "description": "A card describes a subject with images and text", @@ -1081,6 +1064,27 @@ ], "type": "object" }, + "ContentTree.transit.QuestionAndAnswerByline": { + "additionalProperties": false, + "properties": { + "conceptId": { + "type": "string" + }, + "data": {}, + "title": { + "type": "string" + }, + "type": { + "const": "question-and-answer-byline", + "type": "string" + } + }, + "required": [ + "conceptId", + "type" + ], + "type": "object" + }, "ContentTree.transit.Recommended": { "additionalProperties": false, "properties": {