Skip to content
Closed
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
34 changes: 16 additions & 18 deletions packages/builder/lib/src/assets/asset_generation_pipeline.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import 'dart:async';

import 'package:path/path.dart' as path;
import 'package:superdeck_builder/superdeck_builder.dart';
import 'package:superdeck_core/asset_cache_store_io.dart';
import 'package:superdeck_core/superdeck_core.dart';

import '../parsers/fenced_code_parser.dart';
import '../markdown_utils.dart';
import 'asset_generator.dart';

/// Result of asset generation pipeline processing on slide content.
class AssetGenerationResult {
/// The updated slide content with asset references replaced.
Expand All @@ -28,7 +31,7 @@ class AssetGenerationPipeline {
final List<AssetGenerator> _generators;
final DeckService _store;
final AssetCacheStore _cache;
final Logger _logger = Logger('AssetGenerationPipeline');
final _logger = Logger('AssetGenerationPipeline');

AssetGenerationPipeline({
required List<AssetGenerator> generators,
Expand Down Expand Up @@ -87,10 +90,9 @@ class AssetGenerationPipeline {
);
}

/// Finds the appropriate generator for the given content type using pattern matching.
/// Finds the first generator that can process [contentType].
AssetGenerator? _findGenerator(String contentType) {
for (final generator in _generators) {
// Use generator's canProcess method which might use pattern matching internally
if (generator.canProcess(contentType)) {
return generator;
}
Expand Down Expand Up @@ -123,13 +125,8 @@ class AssetGenerationPipeline {
final generatedAsset = generator.createAssetReference(codeBlock.content);

final assetPath = _store.getGeneratedAssetPath(generatedAsset);
final cachedUri = await _cache.resolve(generatedAsset.fileName);
if (cachedUri != null) {
_validateCachedAssetPath(
cacheUri: cachedUri,
expectedPath: assetPath,
assetKey: generatedAsset.fileName,
);
var resolvedUri = await _cache.resolve(generatedAsset.fileName);
if (resolvedUri != null) {
_logger.info(
'${generator.type} asset already exists for slide $slideIndex',
);
Expand All @@ -140,20 +137,21 @@ class AssetGenerationPipeline {
codeBlock.content,
assetPath,
);
final writtenUri = await _cache.write(generatedAsset.fileName, assetData);
if (writtenUri == null) {
resolvedUri = await _cache.write(generatedAsset.fileName, assetData);
if (resolvedUri == null) {
throw StateError(
'Failed to write ${generator.type} asset to cache for key '
'"${generatedAsset.fileName}".',
);
}
_validateCachedAssetPath(
cacheUri: writtenUri,
expectedPath: assetPath,
assetKey: generatedAsset.fileName,
);
}

_validateCachedAssetPath(
cacheUri: resolvedUri,
expectedPath: assetPath,
assetKey: generatedAsset.fileName,
);

// Create replacement syntax with relative path from project directory
final projectDir = _store.configuration.superdeckDir.parent.path;
final relativePath = path.relative(assetPath, from: projectDir);
Expand Down
126 changes: 85 additions & 41 deletions packages/builder/lib/src/parsers/fenced_code_parser.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import 'package:superdeck_core/superdeck_core.dart';

final _codeFencePattern = RegExp(
r'```(?<backtickInfo>[^`]*)[\s\S]*?```',
multiLine: true,
);

// ```<language> {<key1>: <value1>, <key2>: <value2>, ...}
// <code content>
// ```
Expand Down Expand Up @@ -37,50 +32,99 @@ class FencedCodeParser {
const FencedCodeParser();

List<ParsedFencedCode> parse(String text) {
final matches = _codeFencePattern.allMatches(text);
List<ParsedFencedCode> parsedBlocks = [];

for (final match in matches) {
final backtickInfo = match.namedGroup('backtickInfo');
final parsedBlocks = <ParsedFencedCode>[];

String? activeFence;
String? openingLine;
int? startIndex;
final blockContent = <String>[];

var offset = 0;
while (offset < text.length) {
final newlineIndex = text.indexOf('\n', offset);
final lineEnd = newlineIndex == -1 ? text.length : newlineIndex;
final line = text.substring(offset, lineEnd);

final fence = parseCodeFenceLine(line);
if (activeFence == null) {
if (fence != null) {
activeFence = fence.marker;
openingLine = fence.rest;
startIndex = offset;
blockContent.clear();
}
} else if (fence != null &&
canCloseCodeFence(
marker: activeFence,
minLength: activeFence.length,
line: line,
)) {
final endIndex = lineEnd;
final header = _parseFenceHeader(openingLine!);
final optionsMap = _parseFenceOptions(
options: header.options,
startIndex: startIndex!,
endIndex: endIndex,
language: header.language,
);

parsedBlocks.add(
ParsedFencedCode(
options: optionsMap,
language: header.language,
content: blockContent.join('\n').trim(),
startIndex: startIndex,
endIndex: endIndex,
),
);

activeFence = null;
openingLine = null;
startIndex = null;
} else {
blockContent.add(line);
}

final lines = backtickInfo?.split('\n');
final firstLine = lines?.first ?? '';
final rest = lines?.sublist(1).join('\n') ?? '';
if (newlineIndex == -1) {
break;
}
offset = lineEnd + 1;
}

final language = firstLine.split(' ')[0];
final options = firstLine.replaceFirst(language, '').trim();
return parsedBlocks;
}

final content = rest;
({String language, String options}) _parseFenceHeader(String openingLine) {
final firstLine = openingLine.trim();
final spaceIndex = firstLine.indexOf(' ');

final startIndex = match.start;
final endIndex = match.end;
if (spaceIndex == -1) {
return (language: firstLine, options: '');
}

final Map<String, Object?> optionsMap;
if (options.isNotEmpty) {
try {
optionsMap = convertYamlToMap(options, strict: true);
} catch (e) {
throw Exception(
'Failed to parse options for code block at position $startIndex-$endIndex. '
'Language: $language. Options: "$options". Error: $e',
);
}
} else {
optionsMap = {};
}
return (
language: firstLine.substring(0, spaceIndex),
options: firstLine.substring(spaceIndex + 1).trim(),
);
}

parsedBlocks.add(
ParsedFencedCode(
options: optionsMap,
language: language,
content: content.trim(),
startIndex: startIndex,
endIndex: endIndex,
),
Map<String, Object?> _parseFenceOptions({
required String options,
required int startIndex,
required int endIndex,
required String language,
}) {
if (options.isEmpty) return {};

try {
return convertYamlToMap(options, strict: true);
} catch (e) {
throw Exception(
'Failed to parse options for code block at position '
'$startIndex-$endIndex. Language: $language. Options: "$options". '
'Error: $e',
);
}

return parsedBlocks;
}
}

Expand Down
49 changes: 37 additions & 12 deletions packages/builder/lib/src/parsers/front_matter_parser.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:superdeck_core/superdeck_core.dart';
import 'package:yaml/yaml.dart';

typedef ExtractedFrontmatter = ({
Map<String, Object?> frontmatter,
Expand Down Expand Up @@ -61,23 +61,48 @@ class FrontmatterParser {

ExtractedFrontmatter parse(String content) {
final result = parseFrontMatter(content);
return (
frontmatter: _parseFrontmatterYaml(result.yaml),
contents: result.markdown,
);
}

final yamlString = result.yaml;
final markdownContent = result.markdown;
Map<String, Object?> yamlMap = {};
Map<String, Object?> _parseFrontmatterYaml(String yamlString) {
if (yamlString.isEmpty) return {};

if (yamlString.isNotEmpty) {
try {
yamlMap = convertYamlToMap(yamlString);
} catch (e) {
try {
final yamlDoc = loadYaml(yamlString);
if (yamlDoc == null) {
return {};
}
if (yamlDoc is! YamlMap) {
throw FormatException(
'Invalid YAML frontmatter in slide. '
'Check for syntax errors in your slide configuration. '
'Error: $e',
'Frontmatter must be a YAML map. Received: ${yamlDoc.runtimeType}',
);
}
return _toPlainMap(yamlDoc);
} catch (e) {
throw FormatException(
'Invalid YAML frontmatter in slide. '
'Check for syntax errors in your slide configuration. '
'Error: $e',
);
}
}

return (frontmatter: yamlMap, contents: markdownContent);
Map<String, Object?> _toPlainMap(YamlMap yamlMap) {
return Map<String, Object?>.fromEntries(
yamlMap.entries.map(
(entry) => MapEntry(entry.key.toString(), _toPlainValue(entry.value)),
),
);
}

Object? _toPlainValue(Object? value) {
if (value is YamlMap) return _toPlainMap(value);
if (value is YamlList) {
return value.map(_toPlainValue).toList();
}
return value;
}
}
Loading
Loading