Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parchment custom encoders #371

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions packages/parchment/example/encoder_example.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic> 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 """<div style="display: inline-block; text-align: center;">
<a href="${embeddable.data['url']}" target="_blank" style="text-decoration: none; color: black;">
<img src="${embeddable.data['thumbrUrl']}" alt="${embeddable.data['title']}" style="width: 200px; height: auto; display: block; margin-bottom: 8px;">
<span style="display: block; font-size: 16px; font-weight: bold;">${embeddable.data['title']}</span>
</a>
</div>

""";
});

// 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 <p></p> 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();
}
3 changes: 3 additions & 0 deletions packages/parchment/lib/codecs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
54 changes: 54 additions & 0 deletions packages/parchment/lib/src/codecs/codec_extensions.dart
Original file line number Diff line number Diff line change
@@ -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):
// '<img src="${embeddable.data['source']}" style="max-width: 100%; object-fit: contain;">');
// 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({

Check warning on line 41 in packages/parchment/lib/src/codecs/codec_extensions.dart

View check run for this annotation

Codecov / codecov/patch

packages/parchment/lib/src/codecs/codec_extensions.dart#L41

Added line #L41 was not covered by tests
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;

Check warning on line 49 in packages/parchment/lib/src/codecs/codec_extensions.dart

View check run for this annotation

Codecov / codecov/patch

packages/parchment/lib/src/codecs/codec_extensions.dart#L48-L49

Added lines #L48 - L49 were not covered by tests
}
}

// TODO: Implement DecodeExtension class
// Might need to make more specalized decode classes for markdown and HTML.
24 changes: 21 additions & 3 deletions packages/parchment/lib/src/codecs/html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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';
Expand Down Expand Up @@ -56,15 +57,18 @@
/// <br>
/// *NB2: a single line of text with only inline attributes will not be surrounded with `<p>`*
class ParchmentHtmlCodec extends Codec<ParchmentDocument, String> {
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<EncodeExtension>? extensions;
const ParchmentHtmlCodec({this.extensions});

@override
Converter<String, ParchmentDocument> get decoder =>
const _ParchmentHtmlDecoder();

@override
Converter<ParchmentDocument, String> get encoder =>
const _ParchmentHtmlEncoder();
_ParchmentHtmlEncoder(extensions: extensions);
}

// Mutable record for the state of the encoder
Expand Down Expand Up @@ -101,7 +105,12 @@
// These can be code or lists.
// These behave almost as line tags except there can be nested blocks
class _ParchmentHtmlEncoder extends Converter<ParchmentDocument, String> {
const _ParchmentHtmlEncoder();
// Insert custom extensions if needed
final List<EncodeExtension>? extensions;

const _ParchmentHtmlEncoder({
this.extensions = const [],
});

static const _htmlElementEscape = HtmlEscape(HtmlEscapeMode.element);
static final _brPrEolRegex = RegExp(r'<br></p>$');
Expand Down Expand Up @@ -483,6 +492,15 @@
if (op.data is Map<String, dynamic>) {
final data = op.data as Map<String, dynamic>;
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));

Check warning on line 499 in packages/parchment/lib/src/codecs/html.dart

View check run for this annotation

Codecov / codecov/patch

packages/parchment/lib/src/codecs/html.dart#L498-L499

Added lines #L498 - L499 were not covered by tests
return;
}
}

if (embeddable is BlockEmbed) {
if (embeddable.type == 'hr') {
buffer.write('<hr>');
Expand Down
37 changes: 31 additions & 6 deletions packages/parchment/lib/src/codecs/markdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@
import '../document/block.dart';
import '../document/leaf.dart';
import '../document/line.dart';
import './codec_extensions.dart';

class ParchmentMarkdownCodec extends Codec<ParchmentDocument, String> {
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<EncodeExtension>? extensions;

const ParchmentMarkdownCodec({this.extensions});

@override
Converter<String, ParchmentDocument> get decoder =>
_ParchmentMarkdownDecoder();

@override
Converter<ParchmentDocument, String> get encoder =>
_ParchmentMarkdownEncoder();
_ParchmentMarkdownEncoder(extensions: extensions);
}

class _ParchmentMarkdownDecoder extends Converter<String, ParchmentDocument> {
Expand Down Expand Up @@ -349,6 +354,13 @@
}

class _ParchmentMarkdownEncoder extends Converter<ParchmentDocument, String> {
// Insert custom extensions if needed
final List<EncodeExtension>? extensions;

const _ParchmentMarkdownEncoder({
this.extensions = const [],
});

static final simpleBlocks = <ParchmentAttribute, String>{
ParchmentAttribute.bq: '> ',
ParchmentAttribute.ul: '* ',
Expand Down Expand Up @@ -398,8 +410,6 @@
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) {
Expand All @@ -414,8 +424,23 @@
}

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) {

Check warning on line 430 in packages/parchment/lib/src/codecs/markdown.dart

View check run for this annotation

Codecov / codecov/patch

packages/parchment/lib/src/codecs/markdown.dart#L430

Added line #L430 was not covered by tests
// 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)) {

Check warning on line 437 in packages/parchment/lib/src/codecs/markdown.dart

View check run for this annotation

Codecov / codecov/patch

packages/parchment/lib/src/codecs/markdown.dart#L435-L437

Added lines #L435 - L437 were not covered by tests
// Pass the embeddable object to the extension encode function
// Return a string which writes to the encode buffer.
lineBuffer.write(extension.encode(textNode.value));

Check warning on line 440 in packages/parchment/lib/src/codecs/markdown.dart

View check run for this annotation

Codecov / codecov/patch

packages/parchment/lib/src/codecs/markdown.dart#L440

Added line #L440 was not covered by tests
}
}
}
}

handleText(lineBuffer, TextNode(), currentInlineStyle);
Expand Down
Loading