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

Conversation

cotw-fabier
Copy link

Thanks to your help in #369 I have been steadily working to implement Fleather into an app I am working on. One change which I think was worth pushing upstream to you all is the concept of building custom encoders. I originally wanted to do both encoders and decoders, but I started to run out of steam after finishing the encoders section.

I created a custom class [EncoderExtension] which encapsulates a function that both the HTML and Markdown encoder were updated to make use of. This allows you to convert custom [EmbeddableObject]s into basically any string you want using the fields in the EmbeddableObject.

I put together a pretty feature complete example which you can see in the parchment examples folder.

So my Youtube custom embed (in the example) which displays beautifully in the editor now will also be exportable into both markdown and html in whatever format you desire with whatever code you want to attach to it. Can use this for any custom embed block. This basically makes fleather infinitely extendable by the developers like myself with, hopefully, very little extra overhead.

I know this wasn't a requested feature, so I understand if it isn't something you all would like to make use of, But I feel like this is a fairly minimal expansion to Fleather which won't interrupt the current api in any problematic way so I thought I'd push it up and see if y'all would like to make use of it.

Feel free to adjust in any way you desire!

Example code from encoder_example.dart:

import 'package:parchment/codecs.dart';
import 'package:parchment/parchment.dart';
import 'package:parchment/src/codecs/codec_extensions.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();
}

@amantoux
Copy link
Member

Thank you for this PR @cotw-fabier
Will look into it shortly; sounds promising

@cotw-fabier
Copy link
Author

I'm a bit green when it comes to PRs so let me know if you need anything else from me. I'm just busy over here coding :). But I'm happy to help!

@amantoux
Copy link
Member

Can you please take care of greening the pipeline? Seems there are some issues with the analyzer

@cotw-fabier
Copy link
Author

Can you please take care of greening the pipeline? Seems there are some issues with the analyzer

My bad, Pushed updated version.

Copy link

codecov bot commented Jun 28, 2024

Codecov Report

Attention: Patch coverage is 50.00000% with 10 lines in your changes missing coverage. Please review.

Project coverage is 87.87%. Comparing base (1b4bbbf) to head (8dcdb5e).

Files Patch % Lines
packages/parchment/lib/src/codecs/markdown.dart 54.54% 5 Missing ⚠️
...ges/parchment/lib/src/codecs/codec_extensions.dart 0.00% 3 Missing ⚠️
packages/parchment/lib/src/codecs/html.dart 66.66% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #371      +/-   ##
==========================================
- Coverage   87.96%   87.87%   -0.10%     
==========================================
  Files          64       65       +1     
  Lines       10433    10446      +13     
==========================================
+ Hits         9177     9179       +2     
- Misses       1256     1267      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@amantoux
Copy link
Member

@cotw-fabier are you intending to continue working this PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants