blocks) {
for (final block in blocks) {
switch (block) {
case HeadingBlock(:final inline):
addInline(inline);
case ParagraphBlock(:final inline):
addInline(inline);
case AddressBlock(:final inline):
addInline(inline);
case QuoteBlock(:final children):
addBlocks(children);
case PreBlock(:final text):
parts.add(text);
case ListBlock(:final items):
for (final item in items) {
addBlocks(item.blocks);
}
case DefinitionListBlock(:final items):
for (final item in items) {
addInline(item.term);
addInline(item.definition);
}
case TableBlock(:final rows):
for (final row in rows) {
for (final cell in row.cells) {
addInline(cell.inline);
}
}
case ImageBlock(:final alt):
parts.add(alt);
case RuleBlock():
break;
case FormBlock():
break;
}
}
}
addBlocks(blocks);
return parts.join(' ');
}
({MewsPage page, bool declaresMews}) convert(String source,
{String url = 'https://example.com/journal'}) =>
convertHtmlPage(url: url, source: source);
void main() {
test('converts a web page into the blocks Mews supports', () {
final page = convert(webPage).page;
final kinds = page.body.map((b) => b.runtimeType.toString()).toList();
expect(page.title, 'A Journal');
expect(page.lang, 'en');
expect(kinds, contains('HeadingBlock'));
expect(kinds, contains('ParagraphBlock'));
expect(kinds, contains('ListBlock'));
expect(kinds, contains('QuoteBlock'));
expect(kinds, contains('PreBlock'));
expect(kinds, contains('TableBlock'));
expect(kinds, contains('RuleBlock'));
});
test('a converted page is marked as a simplified view', () {
expect(convert(webPage).page.specVersion, htmlSpecVersion);
// The marker is what makes a page Mews, and this one never claimed it.
expect(convert(webPage).declaresMews, isFalse);
});
test('a page claiming the Mews marker is told apart', () {
const claimed = 'T'
''
'Never closed';
expect(convert(claimed).declaresMews, isTrue);
});
test('keeps text sitting directly inside an unknown wrapper', () {
final page = convert(webPage).page;
final text = page.body
.whereType()
.expand((b) => b.inline.whereType())
.map((t) => t.text)
.join(' ');
expect(text, contains('Loose text in a div.'));
});
test('closes tags the page left open', () {
final page = convert('One
Two
').page;
expect(page.body.whereType().length, 2);
expect(page.body.whereType().single.items.length, 2);
});
test('leaves out markup that is not content', () {
final all = textOf(convert(webPage).page.body);
expect(all, isNot(contains('ignore.me')));
expect(all, isNot(contains('color: red')));
expect(all, isNot(contains('Press me')));
expect(all, isNot(contains('circle')));
// A nav is a list of real links, so it is kept rather than stripped.
expect(all, contains('Home'));
// The table's cells come through.
expect(all, contains('3310'));
});
test('resolves a relative link against the page address', () {
final page = convert(webPage).page;
final links = page.body
.whereType()
.expand((b) => b.inline.whereType())
.map((l) => l.href);
expect(links, contains('https://example.com/about.html'));
});
test('an image on another site falls back to its alt text', () {
const source = '
'
'
';
final images = convert(source).page.body.whereType().toList();
expect(images.first.allowed, isFalse);
expect(images.first.alt, 'A chart');
expect(images.last.allowed, isTrue);
});
test('an attribute no XML name allows does not stop the page', () {
// The wrapper in webPage carries @click and v-bind:href.
expect(() => convert(webPage), returnsNormally);
expect(convert(webPage).page.body, isNotEmpty);
});
test('reads the charset the document declares, not the one assumed', () {
const html = 'T'
'Beschädigte Seite
';
final page = convertHtmlPage(
url: 'https://example.com/', bytes: utf8.encode(html))
.page;
final text = page.body
.whereType()
.expand((b) => b.inline.whereType())
.map((t) => t.text)
.join();
expect(text, contains('Beschädigte Seite'));
});
test('a nested table becomes a table of its own', () {
// Old web layout markup: a table inside a cell of another.
const source = ''
'| Outer head | Outer cell |
'
'| Holds one'
''
' | Beside it |
'
'
';
final tables = convert(source).page.body.whereType().toList();
expect(tables.length, 2);
String cells(TableBlock t) => t.rows
.map((r) =>
r.cells.map((c) => textOf([ParagraphBlock(c.inline)])).join('|'))
.join(' / ');
// The outer table keeps its own two rows; the inner row is not hoisted
// into it, and the inner cells do not run together in the cell's text.
expect(tables.first.rows.length, 2);
expect(cells(tables.first), contains('Outer head'));
expect(cells(tables.first), contains('Holds one'));
expect(cells(tables.first), contains('Beside it'));
expect(cells(tables.first), isNot(contains('Inner a')));
// The inner table stands on its own, with its content intact.
expect(tables.last.rows.length, 1);
expect(cells(tables.last), contains('Inner a'));
expect(cells(tables.last), contains('Inner b'));
});
test('leaves a form out but keeps what it wrapped', () {
const source = ''
''
''
'';
final page = convert(source).page;
final text = textOf(page.body);
// No form survives, so nothing in the view invites an answer the reader
// cannot send.
expect(page.body.whereType(), isEmpty);
expect(text, isNot(contains('Search')));
expect(text, isNot(contains('Titles')));
expect(text, isNot(contains('Draft')));
expect(text, isNot(contains('Go')));
// The form's wrapper goes, its content stays.
expect(page.body.whereType(), isNotEmpty);
expect(text, contains('Wrapped heading'));
expect(text, contains('Real content inside a form.'));
});
test('markup with nothing in it still yields a page', () {
final page = convert('').page;
expect(page.url, 'https://example.com/journal');
expect(page.body, isEmpty);
});
}