diff --git a/packages/parchment/example/encoder_example.dart b/packages/parchment/example/encoder_example.dart new file mode 100644 index 00000000..b71c1968 --- /dev/null +++ b/packages/parchment/example/encoder_example.dart @@ -0,0 +1,117 @@ +import 'package:parchment/codecs.dart'; +import 'package:parchment/parchment.dart'; +import 'package:parchment_delta/parchment_delta.dart'; + +void main() { + // We're going to start by creating a new blank document + final doc = ParchmentDocument(); + + // Since this is an example of building a custom embed. We're going to define a custom embed object. + // "Youtube" refers to the name of the embed object + // "inline" will communicate if this embed is inline with other content, or if it lives by itself on its own line. + // Embeds take up one character but are encoded as a simple object with Map data. + // You can see the data as the next argument in the constructor. + // Data can have literally any data you want. + final url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; + final thumbUrl = 'https://img.youtube.com/vi/dQw4w9WgXcQ/0.jpg'; + + // We're going to do both an inline and a block embed. They are essentially the same except the inline property. + // Inline Block Embed + final youtubeInlineEmbedDelta = { + '_type': 'youtube', + '_inline': true, + 'url': url, + 'title': 'Read the Url Before Clicking', + 'language': 'en', + 'thumbUrl': thumbUrl + }; + + // Block Embed + final youtubeBlockEmbedDelta = { + '_type': 'youtube', + '_inline': false, + 'url': url, + 'title': 'Read the Url Before Clicking', + 'language': 'en', + 'thumbUrl': thumbUrl + }; + + // Lets create new Delta to insert content into our document. + final newDelta = Delta() + ..insert( + 'Lets add in some examples of custom embed blocks which we\'ll implement custom encoders to encode the result.') + ..insert('\n') + ..insert('Lets Start with a simple inline block: ') + ..insert(youtubeInlineEmbedDelta) + ..insert('\n') + ..insert('Now lets add a block embed: \n') + ..insert(youtubeBlockEmbedDelta); + + // Since we know our changes are progormatically generated they don't need to be run through Heuristics and Autoformatting. + // So we are going to use the compose command which bypasses any of Fleather's additional logic to keep content consistent and clean. + // This is useful for programatically generated content but you should use another command if the content includes any user input so it properly formats. + // Using ChangeSource.local because these changes originated programmatically on our machine. + doc.compose(newDelta, ChangeSource.local); + + // This is where some of the magic happens. Lets define a custom encoder so we can format our youtube embed for export from fleather. + // If you are just saving to the database then using jsonEncode(doc) would be enough and no additional work needed. + // But if you want to make use of fleather's excellent HTML and Markdown encoders then we need to take an additional step. + + // Lets start with markdown since it is simpler. + final markdownYouTubeEncoder = EncodeExtension( + codecType: CodecExtensionType + .markdown, // We use this so we can pass all encoders to the converter and the converter can smart select the correct encoders it would like to use. + blockType: + 'youtube', // We're matching against the type of embed. "youtube" was defined above as the first param of our EmbeddableObject. + encode: (EmbeddableObject embeddable) { + return "[![${embeddable.data['title']}](${embeddable.data['thumbUrl']})](${embeddable.data['url']})"; + }); // This function takes in an embeddable object and returns a string which you can use with markdown. + + // A few important things to note about the encode function. + // 1.) The encode function left out the language. We can store information in the embed object which we don't want to display. + // 2.) You have access to all the fields of the embed by using embeddable.data['field_name'] + + // Lets trying making an encoder for HTML now. + final htmlYouTubeEncoder = EncodeExtension( + codecType: CodecExtensionType.html, // We change to HTML here + blockType: + 'youtube', // Still matching against Youtube since we're encoding the same type of block embed. + encode: (EmbeddableObject embeddable) { + return """
+ + ${embeddable.data['title']} + ${embeddable.data['title']} + +
+ + """; + }); + + // For the HTML output we set the content to display as inline-block. This is because the encoder runs both as a block and inline elemnt. + // Fleather will still wrap block embeds in

tags, so displaying as inline-block should work for both. + + // Now that we have two encoders for our HTML block, Markdown and HTML, lets try to export out document has HTML and Markdown. + final encoderList = [markdownYouTubeEncoder, htmlYouTubeEncoder]; + + // Lets encode our document to HTML and Markdown + // Notice how we can just pass our list to the codec without any additional work. So define all your encoders and just pass them along when encoding. + final htmlOutput = ParchmentHtmlCodec(extensions: encoderList).encode(doc); + final markdownOutput = + ParchmentMarkdownCodec(extensions: encoderList).encode(doc); + + // Lets print out our results. + print('HTML Output:'); + print(htmlOutput); + print('\n\n'); + print('Markdown Output:'); + print(markdownOutput); + + // Congrats! You can now make all manner of awesome custom embeds and work with them like any other text. + // Using fleather's fabulous embed rendering engine in the editor you can call functions, update widgets + // and do all sorts of logic within your embed functions. Then when you're done, call these export functions + // with your custom encoders and you're good to go! + + // Dispose resources allocated by this document, e.g. closes "changes" stream. + // After document is closed it cannot be modified. + doc.close(); +} diff --git a/packages/parchment/lib/codecs.dart b/packages/parchment/lib/codecs.dart index 41be299d..8d8a1510 100644 --- a/packages/parchment/lib/codecs.dart +++ b/packages/parchment/lib/codecs.dart @@ -7,6 +7,9 @@ import 'src/codecs/html.dart'; export 'src/codecs/markdown.dart'; export 'src/codecs/html.dart'; +// Extensions for Markdown and HTML codecs +export 'src/codecs/codec_extensions.dart'; + /// Markdown codec for Parchment documents. const parchmentMarkdown = ParchmentMarkdownCodec(); diff --git a/packages/parchment/lib/src/codecs/codec_extensions.dart b/packages/parchment/lib/src/codecs/codec_extensions.dart new file mode 100644 index 00000000..f8d10d8f --- /dev/null +++ b/packages/parchment/lib/src/codecs/codec_extensions.dart @@ -0,0 +1,54 @@ +// This document contains functions for extending the default Encoders and Decoders which come with Fleather +// This allows you to write custom extensions which are called when running the encode and decode functions. +// By including a type you allow the extension to be scoped to availble options (Markdown, HTML). +// So you can just build a big pile of extensions if you want and just add them in to every instance of the encoder/decoder. + +// Custom Encoder and Decoder functions run BEFORE the default encoder and decoder functions. +// This means you can override normal behavior of the default embed encoder if desired (really just for HR and image tags at this point). + +import 'package:parchment/src/document/embeds.dart'; + +// Simple enum to allow us to write one encode class to encapsulate both Markdown and HTML encode extensions +enum CodecExtensionType { + markdown, + html, +} + +// This class is exported for the end-user developer to define custom encoders +// This allows Parchment encoder function to take in a list of EncodeExtensions +// Which will run before the default encoders so developers can override default behavior +// or define their own custom encoders. +// This is built specifically for block embeds. +class EncodeExtension { + // Specify Markdown or HTML + // More verbose to write extensions for each type + // But probably more clear. + final CodecExtensionType codecType; + + // Which embeddable Block Type are we matching against? + final String blockType; + + // This function will run if we find an embeddable block of matching blockType. + // Should output a string with the encoded block in the format the encoder perfers. + // For example, a block which outputs an image might parse and output the following string (taken from the default encode function): + // ''); + // Markdown might look like this: + // '![${embeddable.data['alt']}](${embeddable.data['source']})' + // Function takes in an EmbeddableObject and returns a string. + final String Function(EmbeddableObject embeddable) encode; + + // Constructor + EncodeExtension({ + required this.codecType, + required this.blockType, + required this.encode, + }); + + // Simple bool to see if this node can be encoded. String match on node type + bool canEncode(CodecExtensionType type, String node) { + return codecType == type && node == blockType; + } +} + +// TODO: Implement DecodeExtension class +// Might need to make more specalized decode classes for markdown and HTML. diff --git a/packages/parchment/lib/src/codecs/html.dart b/packages/parchment/lib/src/codecs/html.dart index 7320c762..519e7604 100644 --- a/packages/parchment/lib/src/codecs/html.dart +++ b/packages/parchment/lib/src/codecs/html.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:html/dom.dart' as html; import 'package:html/parser.dart'; +import 'package:parchment/codecs.dart'; import 'package:parchment_delta/parchment_delta.dart'; import '../document.dart'; @@ -56,7 +57,10 @@ const _indentWidthInPx = 32; ///
/// *NB2: a single line of text with only inline attributes will not be surrounded with `

`* class ParchmentHtmlCodec extends Codec { - const ParchmentHtmlCodec(); + // We're adding in custom extensions here. This can be null to not break current implementations. + // It will evaluate to a const empty list if null in encoder. + final List? extensions; + const ParchmentHtmlCodec({this.extensions}); @override Converter get decoder => @@ -64,7 +68,7 @@ class ParchmentHtmlCodec extends Codec { @override Converter get encoder => - const _ParchmentHtmlEncoder(); + _ParchmentHtmlEncoder(extensions: extensions); } // Mutable record for the state of the encoder @@ -101,7 +105,12 @@ class _EncoderState { // These can be code or lists. // These behave almost as line tags except there can be nested blocks class _ParchmentHtmlEncoder extends Converter { - const _ParchmentHtmlEncoder(); + // Insert custom extensions if needed + final List? extensions; + + const _ParchmentHtmlEncoder({ + this.extensions = const [], + }); static const _htmlElementEscape = HtmlEscape(HtmlEscapeMode.element); static final _brPrEolRegex = RegExp(r'

$'); @@ -483,6 +492,15 @@ class _ParchmentHtmlEncoder extends Converter { if (op.data is Map) { final data = op.data as Map; final embeddable = EmbeddableObject.fromJson(data); + + // We're going to loop through our custom encoder extensions here to see if we can encode this block. + for (final EncodeExtension extension in extensions ?? []) { + if (extension.canEncode(CodecExtensionType.html, embeddable.type)) { + buffer.write(extension.encode(embeddable)); + return; + } + } + if (embeddable is BlockEmbed) { if (embeddable.type == 'hr') { buffer.write('
'); diff --git a/packages/parchment/lib/src/codecs/markdown.dart b/packages/parchment/lib/src/codecs/markdown.dart index 2abe2a31..d1e0a63c 100644 --- a/packages/parchment/lib/src/codecs/markdown.dart +++ b/packages/parchment/lib/src/codecs/markdown.dart @@ -7,9 +7,14 @@ import '../document.dart'; import '../document/block.dart'; import '../document/leaf.dart'; import '../document/line.dart'; +import './codec_extensions.dart'; class ParchmentMarkdownCodec extends Codec { - const ParchmentMarkdownCodec(); + // We're adding in custom extensions here. This can be null to not break current implementations. + // It will evaluate to a const empty list if null in encoder. + final List? extensions; + + const ParchmentMarkdownCodec({this.extensions}); @override Converter get decoder => @@ -17,7 +22,7 @@ class ParchmentMarkdownCodec extends Codec { @override Converter get encoder => - _ParchmentMarkdownEncoder(); + _ParchmentMarkdownEncoder(extensions: extensions); } class _ParchmentMarkdownDecoder extends Converter { @@ -349,6 +354,13 @@ class _ParchmentMarkdownDecoder extends Converter { } class _ParchmentMarkdownEncoder extends Converter { + // Insert custom extensions if needed + final List? extensions; + + const _ParchmentMarkdownEncoder({ + this.extensions = const [], + }); + static final simpleBlocks = { ParchmentAttribute.bq: '> ', ParchmentAttribute.ul: '* ', @@ -398,8 +410,6 @@ class _ParchmentMarkdownEncoder extends Converter { ParchmentAttribute? currentBlockAttribute; void handleLine(LineNode node) { - if (node.hasBlockEmbed) return; - for (final attr in node.style.lineAttributes) { if (attr.key == ParchmentAttribute.block.key) { if (currentBlockAttribute != attr) { @@ -414,8 +424,23 @@ class _ParchmentMarkdownEncoder extends Converter { } for (final textNode in node.children) { - handleText(lineBuffer, textNode as TextNode, currentInlineStyle); - currentInlineStyle = textNode.style; + if (textNode is TextNode) { + handleText(lineBuffer, textNode, currentInlineStyle); + currentInlineStyle = textNode.style; + } else if (textNode is EmbedNode) { + // Import custom extensions for block and inline embeds. + // If there is an extension which matches the extension type and the EmbedBlock type + // then we will run the encode function and write the output to the buffer. + // Otherwise we'll drop it silently. + for (final EncodeExtension extension in extensions ?? []) { + if (extension.canEncode( + CodecExtensionType.markdown, textNode.value.type)) { + // Pass the embeddable object to the extension encode function + // Return a string which writes to the encode buffer. + lineBuffer.write(extension.encode(textNode.value)); + } + } + } } handleText(lineBuffer, TextNode(), currentInlineStyle);