Skip to content

Commit 9d6e597

Browse files
committed
Fix issues with lists and cascading styles
1 parent 49fcc93 commit 9d6e597

File tree

4 files changed

+93
-42
lines changed

4 files changed

+93
-42
lines changed

example/lib/main.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ const htmlData = """
3333
These two lines should have an identical length:<br /><br />
3434
3535
The quick <b> brown </b><u><i> fox </i></u> jumped over the
36-
&nbsp;lazy
36+
lazy
3737
3838
3939
4040
41-
&nbsp;dog.<br />
41+
dog.<br />
4242
The quick brown fox jumped over the lazy dog.
4343
</p>
4444
<table>
@@ -66,11 +66,12 @@ const htmlData = """
6666
<li>With<br /><br />...</li>
6767
<li>a</li>
6868
<li>nested</li>
69-
<li>unordered</li>
69+
<li>unordered
7070
<ol>
7171
<li>With a nested</li>
7272
<li>ordered list.</li>
7373
</ol>
74+
</li>
7475
<li>list</li>
7576
</ul>
7677
</li>

lib/html_parser.dart

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import 'dart:collection';
12
import 'dart:math';
23

34
import 'package:flutter_html/flutter_html.dart';
45
import 'package:flutter_html/src/layout_element.dart';
6+
import 'package:flutter_html/src/utils.dart';
57
import 'package:flutter_html/style.dart';
68
import 'package:flutter/material.dart';
79
import 'package:csslib/visitor.dart' as css;
@@ -39,7 +41,8 @@ class HtmlParser extends StatelessWidget {
3941
StyledElement styledTree = applyCSS(lexedTree, sheet);
4042
StyledElement inlineStyledTree = applyInlineStyles(styledTree);
4143
StyledElement customStyledTree = _applyCustomStyles(inlineStyledTree);
42-
StyledElement cleanedTree = cleanTree(customStyledTree);
44+
StyledElement cascadedStyledTree = _cascadeStyles(customStyledTree);
45+
StyledElement cleanedTree = cleanTree(cascadedStyledTree);
4346
InlineSpan parsedTree = parseTree(
4447
RenderContext(style: Style.fromTextStyle(Theme.of(context).textTheme.body1)),
4548
cleanedTree,
@@ -153,6 +156,15 @@ class HtmlParser extends StatelessWidget {
153156
return tree;
154157
}
155158

159+
StyledElement _cascadeStyles(StyledElement tree) {
160+
tree.children?.forEach((child) {
161+
child.style = tree.style.copyOnlyInherited(child.style);
162+
_cascadeStyles(child);
163+
});
164+
165+
return tree;
166+
}
167+
156168
/// [cleanTree] optimizes the [StyledElement] tree so all [BlockElement]s are
157169
/// on the first level, redundant levels are collapsed, empty elements are
158170
/// removed, and specialty elements are processed.
@@ -177,13 +189,11 @@ class HtmlParser extends StatelessWidget {
177189
if (customRender?.containsKey(tree.name) ?? false) {
178190
return WidgetSpan(
179191
child: ContainerSpan(
180-
thisContext: context,
181192
newContext: newContext,
182193
style: tree.style,
183194
child: customRender[tree.name].call(
184195
newContext,
185196
ContainerSpan(
186-
thisContext: context,
187197
newContext: newContext,
188198
style: tree.style,
189199
children: tree.children?.map((tree) => parseTree(newContext, tree))?.toList() ?? [],
@@ -199,7 +209,6 @@ class HtmlParser extends StatelessWidget {
199209
return WidgetSpan(
200210
child: ContainerSpan(
201211
newContext: newContext,
202-
thisContext: context,
203212
style: tree.style,
204213
children: tree.children?.map((tree) => parseTree(newContext, tree))?.toList() ?? [],
205214
),
@@ -208,16 +217,16 @@ class HtmlParser extends StatelessWidget {
208217
return WidgetSpan(
209218
child: ContainerSpan(
210219
newContext: newContext,
211-
thisContext: context,
212220
style: tree.style,
213221
child: Stack(
214222
children: <Widget>[
215-
SizedBox(
216-
width: newContext.style.fontSize * 1.5, //TODO(Sub6Resources): this is a somewhat arbitrary constant.
217-
child: Text(newContext.style.markerContent ?? "", textAlign: TextAlign.center, style: newContext.style.generateTextStyle()),
223+
PositionedDirectional(
224+
width: 30, //TODO derive this from list padding.
225+
start: 0,
226+
child: Text('${newContext.style.markerContent}\t', textAlign: TextAlign.right, style: newContext.style.generateTextStyle()),
218227
),
219228
Padding(
220-
padding: const EdgeInsets.only(left: 30),
229+
padding: EdgeInsets.only(left: 30), //TODO derive this from list padding.
221230
child: RichText(
222231
text: TextSpan(
223232
children: tree.children?.map((tree) => parseTree(newContext, tree))?.toList() ?? [],
@@ -282,27 +291,27 @@ class HtmlParser extends StatelessWidget {
282291

283292
///TODO document
284293
static StyledElement _processInlineWhitespace(StyledElement tree) {
285-
final whitespaceParsingContext = WhitespaceParsingContext(false);
294+
final whitespaceParsingContext = Context(false);
286295
tree = _processInlineWhitespaceRecursive(tree, whitespaceParsingContext);
287296
return tree;
288297
}
289298

290299
///TODO document
291-
static StyledElement _processInlineWhitespaceRecursive(StyledElement tree, WhitespaceParsingContext wpc) {
300+
static StyledElement _processInlineWhitespaceRecursive(StyledElement tree, Context<bool> wpc) {
292301

293302
if(tree.style.display == Display.BLOCK) {
294-
wpc.inTrailingSpaceContext = false;
303+
wpc.data = false;
295304
}
296305

297306
if(tree is TextContentElement) {
298-
if(wpc.inTrailingSpaceContext && tree.text.startsWith(' ')) {
307+
if(wpc.data && tree.text.startsWith(' ')) {
299308
tree.text = tree.text.replaceFirst(' ', '');
300309
}
301310

302311
if(tree.text.endsWith(' ')) {
303-
wpc.inTrailingSpaceContext = true;
312+
wpc.data = true;
304313
} else {
305-
wpc.inTrailingSpaceContext = false;
314+
wpc.data = false;
306315
}
307316
}
308317

@@ -320,8 +329,8 @@ class HtmlParser extends StatelessWidget {
320329
/// (4) Replace any instances of two or more spaces with a single space.
321330
static String _removeUnnecessaryWhitespace(String text) {
322331
return text
323-
.replaceAll(RegExp("\ *(?=\n)"), "")
324-
.replaceAll(RegExp("(?:\n)\ *"), "")
332+
.replaceAll(RegExp("\ *(?=\n)"), "\n")
333+
.replaceAll(RegExp("(?:\n)\ *"), "\n")
325334
.replaceAll("\n", " ")
326335
.replaceAll("\t", " ")
327336
.replaceAll(RegExp(" {2,}"), " ");
@@ -330,20 +339,45 @@ class HtmlParser extends StatelessWidget {
330339
/// [processListCharacters] adds list characters to the front of all list items.
331340
/// TODO document better
332341
static StyledElement _processListCharacters(StyledElement tree) {
333-
if (tree.name == "ol" || tree.name == "ul") {
334-
for (int i = 0; i < tree.children?.length; i++) {
335-
if (tree.children[i].name == "li") {
336-
switch(tree.style.listStyleType) {
337-
case ListStyleType.DISC:
338-
tree.children[i].style.markerContent = '•';
339-
break;
340-
case ListStyleType.DECIMAL:
341-
tree.children[i].style.markerContent = '${i + 1}.';
342-
break;
343-
}}
342+
// if (tree.name == "ol" || tree.name == "ul") {
343+
// for (int i = 0; i < tree.children?.length; i++) {
344+
// if (tree.children[i].name == "li") {
345+
// switch(tree.style.listStyleType) {
346+
// case ListStyleType.DISC:
347+
// tree.children[i].style.markerContent = '•';
348+
// break;
349+
// case ListStyleType.DECIMAL:
350+
// tree.children[i].style.markerContent = '${i + 1}.';
351+
// break;
352+
// }}
353+
// }
354+
// }
355+
// tree.children?.forEach(_processListCharacters);
356+
final olStack = ListQueue<Context<int>>();
357+
tree = _processListCharactersRecursive(tree, olStack);
358+
return tree;
359+
}
360+
361+
static StyledElement _processListCharactersRecursive(StyledElement tree, ListQueue<Context<int>> olStack) {
362+
if(tree.name == 'ol') {
363+
olStack.add(Context(0));
364+
} else if (tree.style.display == Display.LIST_ITEM) {
365+
switch(tree.style.listStyleType) {
366+
case ListStyleType.DISC:
367+
tree.style.markerContent = '•';
368+
break;
369+
case ListStyleType.DECIMAL:
370+
olStack.last.data += 1;
371+
tree.style.markerContent = '${olStack.last.data}.';
344372
}
345373
}
346-
tree.children?.forEach(_processListCharacters);
374+
375+
tree.children?.forEach((e) => _processListCharactersRecursive(e, olStack));
376+
377+
if(tree.name == 'ol') {
378+
olStack.removeLast();
379+
}
380+
347381
return tree;
348382
}
349383

@@ -463,6 +497,12 @@ class HtmlParser extends StatelessWidget {
463497
toRemove.add(child);
464498
} else if (child is TextContentElement && (child.text.isEmpty)) {
465499
toRemove.add(child);
500+
} else if (child is TextContentElement &&
501+
child.style.whiteSpace != WhiteSpace.PRE &&
502+
tree.style.display == Display.BLOCK &&
503+
child.text.trim().isEmpty) {
504+
//TODO should this be moved to the whitespace functions?
505+
toRemove.add(child);
466506
} else {
467507
_removeEmptyElements(child);
468508
}
@@ -482,26 +522,17 @@ class RenderContext {
482522
});
483523
}
484524

485-
///TODO document
486-
class WhitespaceParsingContext {
487-
bool inTrailingSpaceContext;
488-
489-
WhitespaceParsingContext(this.inTrailingSpaceContext);
490-
}
491-
492525
///TODO document
493526
class ContainerSpan extends StatelessWidget {
494527
final Widget child;
495528
final List<InlineSpan> children;
496529
final Style style;
497-
final RenderContext thisContext;
498530
final RenderContext newContext;
499531

500532
ContainerSpan({
501533
this.child,
502534
this.children,
503535
this.style,
504-
this.thisContext,
505536
this.newContext,
506537
});
507538

@@ -520,7 +551,7 @@ class ContainerSpan extends StatelessWidget {
520551
child: child ??
521552
RichText(
522553
text: TextSpan(
523-
style: thisContext.style.merge(style).generateTextStyle(),
554+
style: newContext.style.generateTextStyle(),
524555
children: children,
525556
),
526557
),

lib/src/utils.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class Context<T> {
2+
T data;
3+
4+
Context(this.data);
5+
}

lib/style.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,20 @@ class Style {
133133
);
134134
}
135135

136+
Style copyOnlyInherited(Style child) {
137+
if (child == null) return this;
138+
139+
return child.copyWith(
140+
color: child.color ?? color,
141+
fontFamily: child.fontFamily ?? fontFamily,
142+
fontSize: child.fontSize ?? fontSize,
143+
fontStyle: child.fontStyle ?? fontStyle,
144+
fontWeight: child.fontWeight ?? fontWeight,
145+
listStyleType: child.listStyleType ?? listStyleType,
146+
whiteSpace: child.whiteSpace ?? whiteSpace,
147+
);
148+
}
149+
136150
Style copyWith({
137151
Color backgroundColor,
138152
Color color,

0 commit comments

Comments
 (0)