Skip to content

Commit

Permalink
Support for Quill attributes and custom blocks
Browse files Browse the repository at this point in the history
[Feat]: added support for subscript and superscript
[Feat]: added support for color and background-color
[Feat]: added support for custom blocks
[Feat]: added support for custom parsed `DOM Document` using `HtmlToDelta.convertDocument(DOMDocument)`
[Chore]: improved README
[Chore]: improved documentation about project
[Chore]: now `resolveCurrentElement` was moved to the interface to give the same functionality to all implementations
  • Loading branch information
CatHood0 committed Jul 7, 2024
1 parent fbe255e commit 95b481d
Show file tree
Hide file tree
Showing 12 changed files with 535 additions and 118 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 1.1.5

* Feat: added support for subscript and superscript
* Feat: added support for color and background-color
* Feat: added support for custom blocks
* Feat: added support for custom parsed `DOM Document` using `HtmlToDelta.convertDocument(DOMDocument)`
* Chore: improved README
* Chore: improved documentation about project
* Chore: now `resolveCurrentElement` was moved to the interface to give the same functionality to all implementations

## 1.1.0

* Added support for fully attributes (could be partially bugged)
Expand Down
140 changes: 122 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ This is a Dart package that converts HTML input into Quill Delta format, which i

**This package** supports the conversion of a wide range of HTML tags and attributes into their corresponding Delta operations, ensuring that your HTML content is accurately represented in the Quill editor.

## Supported tags
## Supported tags

```html
Text Formatting
<b>, <strong>: Bold text
<i>, <em>: Italic text
<u>, <ins>: Underlined text
<s>, <del>: Strikethrough text
<sup>: Superscript text
<sub>: Subscript text

Headings
<h1> to <h6>: Headings of various levels
Expand Down Expand Up @@ -39,32 +42,25 @@ This is a Dart package that converts HTML input into Quill Delta format, which i
<p style="text-align:left|center|right|justify">: Paragraph alignment

Text attributes
<span style="line-height: 1.0;font-size: 12;font-family: Times New Roman">: Span attributes
<p style="line-height: 1.0;font-size: 12;font-family: Times New Roman;color:#ffffff">: Inline attributes

Custom Blocks (alternative to this package to create html to `CustomBlockEmbed` for Quill Js)
```

## Not supported tags



```html
Text Formatting
<sup>: Superscript text
<sub>: Subscript text
Text colors
<span style="background-color: rgb(255,255,255);color: rgb(255,255,255)">: colors
Text indent
<p style="padding: 10px">: indented paragraph
Custom Blocks
Text indent
<p style="padding: 10px">: indented paragraph
```


Getting Started

Add it to your pubspec.yaml:

```yaml
dependencies:
flutter_quill_delta_from_html: ^1.1.0
flutter_quill_delta_from_html: ^1.1.5
```
Then, import the package and use it in your Flutter application:
Expand All @@ -75,13 +71,121 @@ import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart
void main() {
String htmlContent = "<p>Hello, <b>world</b>!</p>";
var delta = HtmlToDelta().convert(htmlContent);
print(delta); // [ { "insert": "hello, " }, { "insert": "world", "attributes": {"bold": true} }, { "insert": "!" }, { "insert": "\n" } ]
/*
{ "insert": "hello, " },
{ "insert": "world", "attributes": {"bold": true} },
{ "insert": "!" },
{ "insert": "\n" }
*/
}
```

_For now the API is experimental and just to be sure, it's better use other already
tested alternatives, such as the original implementation of `flutter_quill`, where it also
allows obtaining `Delta` from a `HTML` input_
## Creating your own CustomHtmlPart (alternative to create `CustomBlockEmbeds` from custom html)

First you need to define your own `CustomHtmlPart`

```dart
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart';
import 'package:html/dom.dart' as dom;
/// Custom block handler for <pullquote> elements.
class PullquoteBlock extends CustomBlock {
@override
bool matches(dom.Element element) {
return element.localName == 'pullquote';
}
@override
List<Operation> convert(dom.Element element, {Map<String, dynamic>? currentAttributes}) {
final Delta delta = Delta();
final Map<String, dynamic> attributes = currentAttributes != null ? Map.from(currentAttributes) : {};
// Extract custom attributes from the <pullquote> element
final author = element.attributes['data-author'];
final style = element.attributes['data-style'];
// Apply custom attributes to the Delta operations
if (author != null) {
delta.insert('Pullquote: "${element.text}" by $author', attributes);
} else {
delta.insert('Pullquote: "${element.text}"', attributes);
}
if (style != null && style.toLowerCase() == 'italic') {
attributes['italic'] = true;
}
delta.insert('\n', attributes);
return delta.toList();
}
}
```

After, put your `PullquoteBlock` to `HtmlToDelta` using the param `customBlocks`

```dart
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart';
void main() {
// Example HTML snippet
final htmlText = '''
<html>
<body>
<p>Regular paragraph before the custom block</p>
<pullquote data-author="John Doe" data-style="italic">This is a custom pullquote</pullquote>
<p>Regular paragraph after the custom block</p>
</body>
</html>
''';
// Registering the custom block
final customBlocks = [PullquoteBlock()];
// Initialize HtmlToDelta with the HTML text and custom blocks
final converter = HtmlToDelta(customBlocks: customBlocks);
// Convert HTML to Delta operations
final delta = converter.convert(htmlText);
/*
This should be resulting delta
{"insert": "Regular paragraph before the custom block\n"},
{"insert": "Pullquote: \"This is a custom pullquote\" by John Doe", "attributes": {"italic": true}},
{"insert": "\n"},
{"insert": "Regular paragraph after the custom block\n"}
*/
}
```

## HtmlOperations

The `HtmlOperations` class is designed to streamline the conversion process from `HTML` to `Delta` operations, accommodating a wide range of `HTML` structures and attributes commonly used in web content.

To utilize `HtmlOperations`, extend this class and implement the methods necessary to handle specific `HTML` elements. Each method corresponds to a different `HTML` tag or element type and converts it into Delta operations suitable for use with `QuillJS`.

```dart
abstract class HtmlOperations {
const HtmlOperations();
//You don't need to override this method
//as it simply calls the other methods
//to detect the type of HTML tag
List<Operation> resolveCurrentElement(dom.Element element);
List<Operation> brToOp(dom.Element element);
List<Operation> headerToOp(dom.Element element);
List<Operation> listToOp(dom.Element element);
List<Operation> paragraphToOp(dom.Element element);
List<Operation> linkToOp(dom.Element element);
List<Operation> spanToOp(dom.Element element);
List<Operation> imgToOp(dom.Element element);
List<Operation> videoToOp(dom.Element element);
List<Operation> codeblockToOp(dom.Element element);
List<Operation> blockquoteToOp(dom.Element element);
bool isInline(String tagName);
void processNode(dom.Node node, Map<String, dynamic> attributes, Delta delta);
List<Operation> inlineToOp(dom.Element element);
}
```

## Contributions

Expand Down
3 changes: 3 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
analyzer:
errors:
avoid_function_literals_in_foreach_calls: ignore
include: package:flutter_lints/flutter.yaml

# Additional information about this file can be found at
Expand Down
3 changes: 2 additions & 1 deletion lib/flutter_quill_delta_from_html.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
library flutter_quill_delta_from_html;

export 'package:flutter_quill_delta_from_html/parser/html_to_delta.dart';
export 'package:flutter_quill_delta_from_html/parser/colors.dart';
export 'package:flutter_quill_delta_from_html/parser/custom_html_part.dart';
export 'package:flutter_quill_delta_from_html/parser/html_utils.dart';
export 'package:flutter_quill_delta_from_html/parser/html_to_operation.dart';
export 'package:flutter_quill_delta_from_html/parser/extensions/node_ext.dart';

110 changes: 110 additions & 0 deletions lib/parser/colors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
///validate the string color to avoid unsupported colors
String validateAndGetColor(String colorString) {
if (colorString.startsWith('#')) return colorString;
return colorToHex(colorString);
}

///Decide the color format type and parse to hex
String colorToHex(String color) {
// Detectar el tipo de color y llamar a la función correspondiente
if (color.startsWith('rgb(')) {
return rgbToHex(color);
} else if (color.startsWith('rgba(')) {
return rgbaToHex(color);
} else if (color.startsWith('hsl(')) {
return hslToHex(color);
} else if (color.startsWith('hsla(')) {
return hslaToHex(color);
} else {
throw ArgumentError('color format not supported: $color');
}
}

///Parse RGB to valid hex string
String rgbToHex(String rgb) {
rgb = rgb.replaceAll('rgb(', '').replaceAll(')', '');
List<String> rgbValues = rgb.split(',');
int r = int.parse(rgbValues[0].trim());
int g = int.parse(rgbValues[1].trim());
int b = int.parse(rgbValues[2].trim());
return _toHex(r, g, b, 255);
}

///Parse RGBA to valid hex string
String rgbaToHex(String rgba) {
rgba = rgba.replaceAll('rgba(', '').replaceAll(')', '');
List<String> rgbaValues = rgba.split(',');
int r = int.parse(rgbaValues[0].trim());
int g = int.parse(rgbaValues[1].trim());
int b = int.parse(rgbaValues[2].trim());
double a = double.parse(rgbaValues[3].trim());
int alpha = (a * 255).round();
return _toHex(r, g, b, alpha);
}

///Parse hsl to valid hex string
String hslToHex(String hsl) {
hsl = hsl.replaceAll('hsl(', '').replaceAll(')', '');
List<String> hslValues = hsl.split(',');
double h = double.parse(hslValues[0].trim());
double s = double.parse(hslValues[1].replaceAll('%', '').trim()) / 100;
double l = double.parse(hslValues[2].replaceAll('%', '').trim()) / 100;
List<int> rgb = _hslToRgb(h, s, l);
return _toHex(rgb[0], rgb[1], rgb[2], 255);
}

///Parse hsla to valid hex string
String hslaToHex(String hsla) {
hsla = hsla.replaceAll('hsla(', '').replaceAll(')', '');
List<String> hslaValues = hsla.split(',');
double h = double.parse(hslaValues[0].trim());
double s = double.parse(hslaValues[1].replaceAll('%', '').trim()) / 100;
double l = double.parse(hslaValues[2].replaceAll('%', '').trim()) / 100;
double a = double.parse(hslaValues[3].trim());
int alpha = (a * 255).round();
List<int> rgb = _hslToRgb(h, s, l);
return _toHex(rgb[0], rgb[1], rgb[2], alpha);
}

///Ensure parse hsl to rgb string to make more simple convertion to hex
List<int> _hslToRgb(double h, double s, double l) {
double c = (1 - (2 * l - 1).abs()) * s;
double x = c * (1 - ((h / 60) % 2 - 1).abs());
double m = l - c / 2;
double r = 0, g = 0, b = 0;

if (h >= 0 && h < 60) {
r = c;
g = x;
} else if (h >= 60 && h < 120) {
r = x;
g = c;
} else if (h >= 120 && h < 180) {
g = c;
b = x;
} else if (h >= 180 && h < 240) {
g = x;
b = c;
} else if (h >= 240 && h < 300) {
r = x;
b = c;
} else if (h >= 300 && h < 360) {
r = c;
b = x;
}

int red = ((r + m) * 255).round();
int green = ((g + m) * 255).round();
int blue = ((b + m) * 255).round();

return [red, green, blue];
}

///Conver RGBA values to hex
String _toHex(int r, int g, int b, int a) {
String hexR = r.toRadixString(16).padLeft(2, '0');
String hexG = g.toRadixString(16).padLeft(2, '0');
String hexB = b.toRadixString(16).padLeft(2, '0');
String hexA = a.toRadixString(16).padLeft(2, '0');
return '#$hexR$hexG$hexB$hexA';
}
12 changes: 12 additions & 0 deletions lib/parser/custom_html_part.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:flutter_quill/quill_delta.dart';
import 'package:html/dom.dart' as dom;

/// Interface for defining a custom block handler.
abstract class CustomHtmlPart {
/// Determines if this custom block handler matches the given HTML element.
bool matches(dom.Element element);

/// Converts the HTML element into Delta operations.
List<Operation> convert(dom.Element element,
{Map<String, dynamic>? currentAttributes});
}
42 changes: 40 additions & 2 deletions lib/parser/extensions/node_ext.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
import 'package:html/dom.dart';

///DOM Node extension to make more easy call certain operations or validations
extension NodeExt on Element {
///Ensure to detect italic html tags
bool get isItalic => localName == 'em' || localName == 'i';

///Ensure to detect bold html tags
bool get isStrong => localName == 'strong' || localName == 'b';

///Ensure to detect underline html tags
bool get isUnderline => localName == 'ins' || localName == 'u';

///Ensure to detect strikethrough html tags
bool get isStrike => localName == 's' || localName == 'del';

///Ensure to detect p html tags
bool get isParagraph => localName == 'p';

///Ensure to detect sub html tags
bool get isSubscript => localName == 'sub';

///Ensure to detect sup html tags
bool get isSuperscript => localName == 'sup';

///Ensure to detect br html tags
bool get isBreakLine => localName == 'br';

///Ensure to detect span html tags
bool get isSpan => localName == 'span';
bool get isHeader => localName != null && localName!.contains(RegExp('h[1-6]'));

///Ensure to detect h(1-6) html tags
bool get isHeader =>
localName != null && localName!.contains(RegExp('h[1-6]'));

///Ensure to detect img html tags
bool get isImg => localName == 'img';

///Ensure to detect li,ul,ol,<input type=checkbox> html tags
bool get isList =>
localName == 'li' || localName == 'ul' || localName == 'ol' || querySelector('input[type="checkbox"]') != null;
localName == 'li' ||
localName == 'ul' ||
localName == 'ol' ||
querySelector('input[type="checkbox"]') != null;

///Ensure to detect video html tags
bool get isVideo => localName == 'video' || localName == 'iframe';

///Ensure to detect a html tags
bool get isLink => localName == 'a';

///Ensure to detect blockquote html tags
bool get isBlockquote => localName == 'blockquote';

///Ensure to detect pre,code html tags
bool get isCodeBlock => localName == 'pre' || localName == 'code';
}
Loading

0 comments on commit 95b481d

Please sign in to comment.