Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/parchment/example/image_demo.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:parchment/codecs.dart';
import 'package:parchment/parchment.dart';

void main() {
print('=== Image Support in Parchment Markdown Codec ===\n');

// Test 1: Markdown to Parchment (Decoding)
print('1. Decoding Markdown with image to Parchment:');
final markdown = '![Alt text](https://example.com/image.jpg)';
print('Input: $markdown');

final document = parchmentMarkdown.decode(markdown);
final delta = document.toDelta();

final embed = delta.elementAt(0).data as BlockEmbed;
print('Output: Image embed with source: ${embed.data['source']}');
print('');

// Test 2: Parchment to Markdown (Encoding)
print('2. Encoding Parchment with image to Markdown:');
final imageEmbed = BlockEmbed.image('https://example.com/my-image.png');
final deltaWithImage = Delta()
..insert(imageEmbed)
..insert('\n');
final documentWithImage = ParchmentDocument.fromDelta(deltaWithImage);

final encodedMarkdown = parchmentMarkdown.encode(documentWithImage);
print('Output: $encodedMarkdown');

// Test 3: Round-trip conversion
print('3. Round-trip conversion:');
final originalMarkdown = '![Test image](https://example.com/test.jpg)';
final roundTripDocument = parchmentMarkdown.decode(originalMarkdown);
final backToMarkdown = parchmentMarkdown.encode(roundTripDocument);

print('Original: $originalMarkdown');
print('Round-trip: ${backToMarkdown.trim()}');
print(
'Success: ${backToMarkdown.contains('![](https://example.com/test.jpg)')}');
}
37 changes: 36 additions & 1 deletion packages/parchment/lib/src/codecs/markdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:parchment_delta/parchment_delta.dart';
import '../document.dart';
import '../document/attributes.dart';
import '../document/block.dart';
import '../document/embeds.dart';
import '../document/leaf.dart';
import '../document/line.dart';

Expand Down Expand Up @@ -38,6 +39,7 @@ class _ParchmentMarkdownDecoder extends Converter<String, ParchmentDocument> {
);

static final _linkRegExp = RegExp(r'\[(.+?)\]\(([^)]+)\)');
static final _imageRegExp = RegExp(r'!\[([^\]]*)\]\(([^)]+)\)');
static final _ulRegExp = RegExp(r'^( *)[-*+] +(.*)');
static final _olRegExp = RegExp(r'^( *)\d+[.)] +(.*)');
static final _clRegExp = RegExp(r'^( *)- +\[( |x|X)\] +(.*)');
Expand All @@ -64,6 +66,9 @@ class _ParchmentMarkdownDecoder extends Converter<String, ParchmentDocument> {
return;
}

if (_handleImage(line, delta, style)) {
return;
}
if (_handleBlockQuote(line, delta, style)) {
return;
}
Expand All @@ -86,6 +91,21 @@ class _ParchmentMarkdownDecoder extends Converter<String, ParchmentDocument> {
}
}

bool _handleImage(String line, Delta delta, [ParchmentStyle? style]) {
final match = _imageRegExp.matchAsPrefix(line);
if (match != null) {
final imageUrl = match.group(2);
if (imageUrl != null) {
// Create an image block embed
final imageEmbed = BlockEmbed.image(imageUrl);
delta.insert(imageEmbed);
delta.insert('\n');
return true;
}
}
return false;
}

// Markdown supports headings and blocks within blocks (except for within code)
// but not blocks within headers, or ul within
bool _handleBlock(String line, Delta delta, [ParchmentStyle? style]) {
Expand Down Expand Up @@ -409,7 +429,22 @@ class _ParchmentMarkdownEncoder extends Converter<ParchmentDocument, String> {
ParchmentAttribute? currentBlockAttribute;

void handleLine(LineNode node) {
if (node.hasBlockEmbed) return;
if (node.hasBlockEmbed) {
// Handle image embeds specifically
if (node.children.length == 1 &&
node.children.single is EmbedNode &&
(node.children.single as EmbedNode).value.type == 'image') {
final embedNode = node.children.single as EmbedNode;
final imageEmbed = embedNode.value;
final source = imageEmbed.data['source'] as String?;
if (source != null) {
lineBuffer.write('![]($source)');
buffer.write(lineBuffer);
lineBuffer.clear();
}
}
return;
}

for (final attr in node.style.lineAttributes) {
if (attr.key == ParchmentAttribute.block.key) {
Expand Down
108 changes: 108 additions & 0 deletions packages/parchment/test/codecs/markdown_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,72 @@ void main() {
expect(delta, exp);
});

test('image embed decode', () {
final markdown = '![alt text](https://example.com/image.jpg)';
final document = parchmentMarkdown.decode(markdown);
final delta = document.toDelta();

expect(delta.length, 2);
final embed = delta.elementAt(0).data;
expect(embed, isA<BlockEmbed>());
expect((embed as BlockEmbed).type, 'image');
expect(embed.data['source'], 'https://example.com/image.jpg');
expect(delta.elementAt(1).data, '\n');
});

test('image embed decode without alt text', () {
final markdown = '![](https://example.com/image.png)';
final document = parchmentMarkdown.decode(markdown);
final delta = document.toDelta();

expect(delta.length, 2);
final embed = delta.elementAt(0).data;
expect(embed, isA<BlockEmbed>());
expect((embed as BlockEmbed).type, 'image');
expect(embed.data['source'], 'https://example.com/image.png');
});

test('multiple images', () {
final markdown = '''![First](https://example.com/image1.jpg)
![Second](https://example.com/image2.png)''';
final document = parchmentMarkdown.decode(markdown);
final delta = document.toDelta();

expect(delta.length, 4);

// First image
final firstEmbed = delta.elementAt(0).data;
expect(firstEmbed, isA<BlockEmbed>());
expect((firstEmbed as BlockEmbed).type, 'image');
expect(firstEmbed.data['source'], 'https://example.com/image1.jpg');
expect(delta.elementAt(1).data, '\n');

// Second image
final secondEmbed = delta.elementAt(2).data;
expect(secondEmbed, isA<BlockEmbed>());
expect((secondEmbed as BlockEmbed).type, 'image');
expect(secondEmbed.data['source'], 'https://example.com/image2.png');
expect(delta.elementAt(3).data, '\n');
});

test('image with text around', () {
final markdown = '''Some text before
![Image](https://example.com/image.jpg)
Some text after''';
final document = parchmentMarkdown.decode(markdown);
final delta = document.toDelta();

expect(delta.length, 3);
expect(delta.elementAt(0).data, 'Some text before\n');

final embed = delta.elementAt(1).data;
expect(embed, isA<BlockEmbed>());
expect((embed as BlockEmbed).type, 'image');
expect(embed.data['source'], 'https://example.com/image.jpg');

expect(delta.elementAt(2).data, '\nSome text after\n');
});

test('multiple styles', () {
final delta = parchmentMarkdown.decode(markdown);
final andBack = parchmentMarkdown.encode(delta);
Expand All @@ -477,6 +543,48 @@ void main() {
});

group('ParchmentMarkdownCodec.encode', () {
test('image embed encode', () {
final imageEmbed = BlockEmbed.image('https://example.com/image.jpg');
final delta = Delta()
..insert(imageEmbed)
..insert('\n');
final document = ParchmentDocument.fromDelta(delta);

final markdown = parchmentMarkdown.encode(document);
expect(markdown, '![](https://example.com/image.jpg)\n\n');
});

test('multiple images encode', () {
final delta = Delta()
..insert(BlockEmbed.image('https://example.com/image1.jpg'))
..insert('\n')
..insert(BlockEmbed.image('https://example.com/image2.png'))
..insert('\n');
final document = ParchmentDocument.fromDelta(delta);

final markdown = parchmentMarkdown.encode(document);
expect(markdown,
'![](https://example.com/image1.jpg)\n\n![](https://example.com/image2.png)\n\n');
});

test('image round trip conversion', () {
final originalMarkdown = '![](https://example.com/image.jpg)';
final document = parchmentMarkdown.decode(originalMarkdown);
final backToMarkdown = parchmentMarkdown.encode(document);

// The output will have extra formatting but should contain the image
expect(backToMarkdown, contains('![](https://example.com/image.jpg)'));

// Round trip back to ensure data integrity
final roundTripDocument = parchmentMarkdown.decode(backToMarkdown);
final roundTripDelta = roundTripDocument.toDelta();

final embed = roundTripDelta.elementAt(0).data;
expect(embed, isA<BlockEmbed>());
expect((embed as BlockEmbed).type, 'image');
expect(embed.data['source'], 'https://example.com/image.jpg');
});

test('split adjacent paragraphs', () {
final delta = Delta()..insert('First line\nSecond line\n');
final result =
Expand Down