Skip to content

Commit

Permalink
fix: more support for different unit types for sizes and line-height
Browse files Browse the repository at this point in the history
  • Loading branch information
CatHood0 committed Jul 9, 2024
1 parent 79a1dc2 commit 27a58db
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 3 deletions.
69 changes: 69 additions & 0 deletions lib/parser/font_size_parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/// Converts various CSS units to pixels (px).
///
/// This function supports the following units:
/// - `px` (pixels)
/// - `cm` (centimeters)
/// - `mm` (millimeters)
/// - `in` (inches)
/// - `pt` (points)
/// - `pc` (picas)
/// - `em` (relative to the font-size of the element)
/// - `rem` (relative to the font-size of the root element)
///
/// For relative units (`em` and `rem`), you need to provide the font-size of the
/// relevant context. The default font-size is 16 pixels.
///
/// Example usage:
/// ```dart
/// print(convertToPx('2cm')); // 75.5905511812
/// print(convertToPx('10mm')); // 37.7952755906
/// print(convertToPx('1in')); // 96.0
/// print(convertToPx('12pt')); // 16.0
/// print(convertToPx('1pc')); // 16.0
/// print(convertToPx('2em', fontSize: 18.0)); // 36.0
/// print(convertToPx('2rem', rootFontSize: 20.0)); // 40.0
/// ```
///
/// [value] is the CSS value to be converted, e.g., '2cm', '10mm', etc.
/// [fontSize] is the font-size of the current element, used for `em` units.
/// [rootFontSize] is the font-size of the root element, used for `rem` units.
///
/// Returns the equivalent value in pixels.
const double cmSizeMultiplier = 37.7952755906;
const double mmSizeMultiplier = 3.7795275591;
const double inchSizeMultiplier = 96;
const double pointSizeMultiplier = 1.3333333333;
const double picasSizeMultiplier = 16;
double parseSizeToPx(
String value, {
double fontSizeEmMultiplier = 16.0,
double rootFontSizeRemMultiplier = 16.0,
}) {
// Extract the unit from the value string.
final unit = value.replaceAll(RegExp(r'[0-9.]'), '');

// Extract the numeric part of the value string.
final number = double.tryParse(value.replaceAll(RegExp(r'[a-z%]'), '')) ?? 0.0;

// Convert the numeric value to pixels based on the unit.
switch (unit) {
case 'px':
return number;
case 'cm':
return number * cmSizeMultiplier;
case 'mm':
return number * mmSizeMultiplier;
case 'in':
return number * inchSizeMultiplier;
case 'pt':
return number * pointSizeMultiplier;
case 'pc':
return number * picasSizeMultiplier;
case 'em':
return number * fontSizeEmMultiplier;
case 'rem':
return number * rootFontSizeRemMultiplier;
default:
throw UnsupportedError('Unit not supported: $unit');
}
}
40 changes: 37 additions & 3 deletions lib/parser/html_utils.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import 'package:dart_quill_delta/dart_quill_delta.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_quill_delta_from_html/parser/extensions/node_ext.dart';
import 'package:html/dom.dart' as dom;
import 'colors.dart';
import 'custom_html_part.dart';
import 'font_size_parser.dart';
import 'line_height_parser.dart';

///verify if the tag is from a inline html tag attribute
bool isInline(String tag) {
Expand All @@ -15,6 +18,7 @@ Map<String, dynamic> parseStyleAttribute(String style) {
if (style.isEmpty) return attributes;

final styles = style.split(';');
double? fontSize ;
for (var style in styles) {
final parts = style.split(':');
if (parts.length == 2) {
Expand All @@ -34,14 +38,41 @@ Map<String, dynamic> parseStyleAttribute(String style) {
attributes['background'] = color;
break;
case 'font-size':
attributes['size'] = value.replaceAll('px', '').replaceAll('em', '').replaceAll('vm', '');
String? sizeToPass;
//Those are the default values used by [vsc_quill_delta_to_html]
if (value == '0.75em') {
fontSize = 10;
sizeToPass = 'small';
} else if (value == '1.5em') {
fontSize = 18;
sizeToPass = 'large';
} else if (value == '2.5em') {
fontSize = 22;
sizeToPass = 'huge';
} else {
try {
final size = parseSizeToPx(value);
if (size <= 10) {
fontSize = 10;
sizeToPass = 'small';
} else {
fontSize = size.floorToDouble();
sizeToPass = '${size.floor()}';
}
} on UnsupportedError catch (e) {
debugPrint(e.message);
debugPrintStack(stackTrace: e.stackTrace);
break;
}
}
attributes['size'] = sizeToPass;
break;
case 'font-family':
attributes['font'] = value;
break;
case 'line-height':
attributes['line-height'] =
double.parse(value.replaceAll('px', '').replaceAll('em', '').replaceAll('vm', ''));
final lineHeight = parseLineHeight(value, fontSize: fontSize ?? 16.0);
attributes['line-height'] = lineHeight;
break;
default:
break;
Expand Down Expand Up @@ -109,20 +140,23 @@ void processNode(
newAttributes.addAll({...spanAttributes});
}
}

///the current node is <a>
if (node.isLink) {
final String? src = node.attributes['href'];
if (src != null) {
newAttributes['link'] = src;
}
}

///the current node is <br>
if (node.isBreakLine) {
newAttributes.remove('align');
newAttributes.remove('direction');
delta.insert('\n', newAttributes);
}
}

///Store on the nodes into the current one
for (final child in node.nodes) {
processNode(child, newAttributes, delta, addSpanAttrs: addSpanAttrs);
Expand Down
62 changes: 62 additions & 0 deletions lib/parser/line_height_parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/// Parses a CSS `line-height` value to a pixel value based on the specified [fontSize] and optional [rootFontSize].
///
/// Supports unitless values, percentages, pixels (`px`), ems (`em`), rems (`rem`), and the keyword `normal`.
/// Adjusts the parsed value to fit within the supported range of line heights: 1.0, 1.15, 1.5, and 2.0.
///
/// Parameters:
/// - [lineHeight]: The CSS `line-height` value to parse.
/// - [fontSize]: The font size to use for unit conversions. Defaults to 16.
/// - [rootFontSize]: The root font size, used for `rem` conversions. Defaults to [fontSize].
///
/// Returns:
/// The parsed `line-height` value as a double in pixels.
///
/// Examples:
/// ```dart
/// print(parseLineHeight('0.8')); // 1.0
/// print(parseLineHeight('1.1')); // 1.0
/// print(parseLineHeight('1.2')); // 1.15
/// print(parseLineHeight('1.3')); // 1.5
/// print(parseLineHeight('1.7')); // 1.5
/// print(parseLineHeight('2.2')); // 2.0
///
/// // Example with different units
/// print(parseLineHeight('120%')); // 19.2 (16 * 1.2)
/// print(parseLineHeight('1.5em')); // 24.0 (16 * 1.5)
/// print(parseLineHeight('1.2rem')); // 19.2 (16 * 1.2)
/// ```
const normalLineHeightMultiplier = 1.2;
double parseLineHeight(String lineHeight, {double fontSize = 16, double rootFontSize = 16}) {
// Convert line-height values
double parsedValue;
if (lineHeight.endsWith('px')) {
parsedValue = double.parse(lineHeight.replaceAll('px', ''));
} else if (lineHeight.endsWith('%')) {
parsedValue = fontSize * (double.parse(lineHeight.replaceAll('%', '')) / 100);
} else if (lineHeight.endsWith('rem')) {
parsedValue = rootFontSize * double.parse(lineHeight.replaceAll('rem', ''));
} else if (lineHeight.endsWith('em')) {
parsedValue = fontSize * double.parse(lineHeight.replaceAll('em', ''));
} else if (lineHeight == 'normal') {
parsedValue = fontSize * normalLineHeightMultiplier;
} else {
parsedValue = fontSize * double.parse(lineHeight);
}

// Apply additional constraints
if (parsedValue < 1.0) {
parsedValue = 1.0;
} else if (parsedValue > 1.0 && parsedValue < 1.15) {
parsedValue = 1.0;
} else if (parsedValue > 1.15 && parsedValue < 1.25) {
parsedValue = 1.15;
} else if (parsedValue > 1.25 && parsedValue < 1.5) {
parsedValue = 1.5;
} else if (parsedValue > 1.5 && parsedValue < 2.0) {
parsedValue = 1.5;
} else if (parsedValue > 2.0) {
parsedValue = 2.0;
}

return parsedValue;
}
33 changes: 33 additions & 0 deletions test/flutter_quill_delta_from_html_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,39 @@ void main() {
expect(delta, expectedDelta);
});

test('Paragraph with different font-size unit type', () {
const htmlSmall = '<p style="font-size: 0.75em">This is a paragraph example</p>';
const htmlHuge = '<p style="font-size: 2.5em">This is a paragraph example 2</p>';
const htmlLarge = '<p style="font-size: 1.5em">This is a paragraph example 3</p>';
const htmlCustomSize = '<p style="font-size: 12pt">This is a paragraph example 4</p>';
final converter = HtmlToDelta();
final deltaSmall = converter.convert(htmlSmall);
final deltaLarge = converter.convert(htmlLarge);
final deltaHuge = converter.convert(htmlHuge);
final deltaCustom = converter.convert(htmlCustomSize);

final expectedDeltaSmall = Delta()
..insert('This is a paragraph example', {"size": "small"})
..insert('\n');

final expectedDeltaHuge = Delta()
..insert('This is a paragraph example 2', {"size": "huge"})
..insert('\n');

final expectedDeltaLarge = Delta()
..insert('This is a paragraph example 3', {"size": "large"})
..insert('\n');

final expectedDeltaCustom = Delta()
..insert('This is a paragraph example 4', {"size": "15"})
..insert('\n');

expect(deltaSmall, expectedDeltaSmall);
expect(deltaLarge, expectedDeltaLarge);
expect(deltaHuge, expectedDeltaHuge);
expect(deltaCustom, expectedDeltaCustom);
});

test('Paragraph to RTL', () {
const html = '<p dir="rtl">This is a RTL paragraph example</p>';
final converter = HtmlToDelta();
Expand Down

0 comments on commit 27a58db

Please sign in to comment.