|
| 1 | +library contentful_rich_text; |
| 2 | + |
| 3 | +import 'package:contentful_rich_text/types/blocks.dart'; |
| 4 | +import 'package:contentful_rich_text/types/helpers.dart'; |
| 5 | +import 'package:contentful_rich_text/types/inlines.dart'; |
| 6 | +import 'package:contentful_rich_text/types/marks.dart'; |
| 7 | +import 'package:contentful_rich_text/types/types.dart'; |
| 8 | +import 'package:contentful_rich_text/widgets/heading.dart'; |
| 9 | +import 'package:contentful_rich_text/widgets/list_item.dart'; |
| 10 | +import 'package:contentful_rich_text/widgets/ordered_list.dart'; |
| 11 | +import 'package:contentful_rich_text/widgets/unordered_list.dart'; |
| 12 | +import 'package:flutter/material.dart'; |
| 13 | +import 'package:html_unescape/html_unescape_small.dart'; |
| 14 | + |
| 15 | +/// Contentful Rich Text widget |
| 16 | +class ContentFulRichText { |
| 17 | + RenderNode defaultNodeRenderers = RenderNode({ |
| 18 | + BLOCKS.PARAGRAPH.value: (node, next) => RichText( |
| 19 | + text: TextSpan(text: node.value, children: next(node.content)), |
| 20 | + ), |
| 21 | + BLOCKS.HEADING_1.value: (node, next) => Heading(level: BLOCKS.HEADING_1, text: node.value, content: node.content), |
| 22 | + BLOCKS.HEADING_2.value: (node, next) => Heading(level: BLOCKS.HEADING_2, text: node.value, content: node.content), |
| 23 | + BLOCKS.HEADING_3.value: (node, next) => Heading(level: BLOCKS.HEADING_3, text: node.value, content: node.content), |
| 24 | + BLOCKS.HEADING_4.value: (node, next) => Heading(level: BLOCKS.HEADING_4, text: node.value, content: node.content), |
| 25 | + BLOCKS.HEADING_5.value: (node, next) => Heading(level: BLOCKS.HEADING_5, text: node.value, content: node.content), |
| 26 | + BLOCKS.HEADING_6.value: (node, next) => Heading(level: BLOCKS.HEADING_6, text: node.value, content: node.content), |
| 27 | + BLOCKS.EMBEDDED_ENTRY.value: (node, next) => Container(), // TODO: implement |
| 28 | + BLOCKS.UL_LIST.value: (node, next) => UnorderedList(node.content), |
| 29 | + BLOCKS.OL_LIST.value: (node, next) => OrderedList(node.content), |
| 30 | + BLOCKS.LIST_ITEM.value: (node, next) => ListItem( |
| 31 | + text: node.value, |
| 32 | + // TODO: not sure we can use nodeType to determine the type of LIST_ITEM |
| 33 | + type: node.nodeType == BLOCKS.OL_LIST.value ? LI_TYPE.ORDERED : LI_TYPE.UNORDERED, |
| 34 | + ), |
| 35 | + BLOCKS.QUOTE.value: (node, next) => Container(), // TODO: implement |
| 36 | + BLOCKS.HR.value: (node, next) => Container(), // TODO: implement |
| 37 | + INLINES.ASSET_HYPERLINK.value: (node, next) => defaultInline(INLINES.ASSET_HYPERLINK, node as Inline), |
| 38 | + INLINES.ENTRY_HYPERLINK.value: (node, next) => defaultInline(INLINES.ENTRY_HYPERLINK, node as Inline), |
| 39 | + INLINES.EMBEDDED_ENTRY.value: (node, next) => defaultInline(INLINES.EMBEDDED_ENTRY, node as Inline), |
| 40 | + INLINES.HYPERLINK.value: (node, next) => Container(), // TODO: implement |
| 41 | + }); |
| 42 | + |
| 43 | + RenderMark defaultMarkRenderers = RenderMark({ |
| 44 | + MARKS.BOLD.value: (text) => TextSpan(text: text, style: TextStyle(fontWeight: FontWeight.bold)), |
| 45 | + MARKS.ITALIC.value: (text) => TextSpan(text: text, style: TextStyle(fontStyle: FontStyle.italic)), |
| 46 | + MARKS.UNDERLINE.value: (text) => TextSpan(text: text, style: TextStyle(decoration: TextDecoration.underline)), |
| 47 | + MARKS.CODE.value: (text) => TextSpan(text: text, style: TextStyle(decoration: TextDecoration.underline)), |
| 48 | + }); |
| 49 | + |
| 50 | + static Widget defaultInline(INLINES type, Inline node) => Container(); // TODO: implement |
| 51 | + |
| 52 | + Document richTextDocument; |
| 53 | + Options options; |
| 54 | + |
| 55 | + ContentFulRichText(this.richTextDocument, {this.options}); |
| 56 | + |
| 57 | + Widget get documentToWidgetTree { |
| 58 | + if (richTextDocument == null || richTextDocument.content == null) { |
| 59 | + return Container(); |
| 60 | + } |
| 61 | + |
| 62 | + Map<dynamic, Function> renderNode = Map.from(defaultNodeRenderers.renderNodes); |
| 63 | + renderNode.addAll(options?.renderNode?.renderNodes); |
| 64 | + Map<dynamic, Function> renderMark = Map.from(defaultMarkRenderers.renderMarks); |
| 65 | + renderMark.addAll(options?.renderMark?.renderMarks); |
| 66 | + |
| 67 | + return nodeListToWidget( |
| 68 | + richTextDocument.content, |
| 69 | + renderNode: renderNode, |
| 70 | + renderMark: renderMark, |
| 71 | + ); |
| 72 | + } |
| 73 | + |
| 74 | + Widget nodeListToWidget( |
| 75 | + List<TopLevelBlock> nodes, { |
| 76 | + Map<dynamic, Function> renderNode, |
| 77 | + Map<dynamic, Function> renderMark, |
| 78 | + }) { |
| 79 | + return Column( |
| 80 | + children: nodes.map<Widget>( |
| 81 | + (TopLevelBlock node) => nodeToWidget(node, renderNode: renderNode, renderMark: renderMark), |
| 82 | + ), |
| 83 | + ); |
| 84 | + } |
| 85 | + |
| 86 | + Widget nodeToWidget( |
| 87 | + TopLevelBlock node, { |
| 88 | + Map<dynamic, Function> renderNode, |
| 89 | + Map<dynamic, Function> renderMark, |
| 90 | + }) { |
| 91 | + if (Helpers.isText(node)) { |
| 92 | + TextNode textNode = node as TextNode; |
| 93 | + String nodeValue = HtmlUnescape().convert(textNode.value); |
| 94 | + if (textNode.marks.length > 0) { |
| 95 | + return RichText( |
| 96 | + text: TextSpan( |
| 97 | + children: textNode.marks.map<TextSpan>( |
| 98 | + (Mark mark) => renderMark[mark.type](textNode.value), |
| 99 | + ), |
| 100 | + ), |
| 101 | + ); |
| 102 | + } |
| 103 | + return Text(nodeValue); |
| 104 | + } else { |
| 105 | + Next nextNode = (nodes) => nodeListToWidget( |
| 106 | + nodes, |
| 107 | + renderNode: renderNode, |
| 108 | + renderMark: renderMark, |
| 109 | + ); |
| 110 | + if (node.nodeType == null || renderNode[node.nodeType] == null) { |
| 111 | + // TODO: Figure what to return when passed an unrecognized node. |
| 112 | + return Container(); |
| 113 | + } |
| 114 | + return renderNode[node.nodeType](node, nextNode); |
| 115 | + } |
| 116 | + } |
| 117 | +} |
0 commit comments