diff --git a/packages/excerpter/README.md b/packages/excerpter/README.md index 47131da..35e79d4 100644 --- a/packages/excerpter/README.md +++ b/packages/excerpter/README.md @@ -138,6 +138,18 @@ they follow the Dart VM's supported syntax, and must be wrapped in forward slashes, such as `//`. If you're passing a normal string, the forward slashes are unnecessary. +### Diff parameters + +**Experimental:** Output might change in future updates. + +Inject instructions also support injecting the unified diff between two files. +This is supported through specifying a target with a `diff-with` argument, which +accepts a path and an optional region name just like the source file. + +You can also specify a `diff-u` argument to change +the surrounding shared context of the diff. +By default, a context of 3 lines is used. + ### Replacement syntax The `replace` argument accepts one or more semicolon separated diff --git a/packages/excerpter/lib/src/inject.dart b/packages/excerpter/lib/src/inject.dart index 5f1b918..1b0f94f 100644 --- a/packages/excerpter/lib/src/inject.dart +++ b/packages/excerpter/lib/src/inject.dart @@ -4,6 +4,8 @@ import 'dart:io'; import 'package:collection/collection.dart'; +import 'package:deviation/deviation.dart'; +import 'package:deviation/unified_diff.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; @@ -162,6 +164,26 @@ final class FileUpdater { reportError(e.error); } + Region? diffWithRegion; + String? diffWithRegionPath; + if (instruction.diffWith + case (path: final diffWithPath, region: final diffWithRegionName)) { + final combinedDiffWithPath = path.join( + baseSourcePath, + wholeFilePathBase, + diffWithPath, + ); + try { + diffWithRegion = await extractor.extractRegion( + combinedDiffWithPath, + diffWithRegionName, + ); + diffWithRegionPath = diffWithPath; + } on ExtractException catch (e) { + reportError(e.error); + } + } + var plaster = (instruction.plasterTemplate ?? wholeFilePlasterTemplate) ?.replaceAll(r'$defaultPlaster', defaultPlasterContent); @@ -173,6 +195,7 @@ final class FileUpdater { } var updatedLines = region.linesWithPlaster(plaster); + var updatedDiffLines = diffWithRegion?.linesWithPlaster(plaster); final transforms = [ ...instruction.transforms, @@ -182,9 +205,38 @@ final class FileUpdater { for (final transform in transforms) { updatedLines = transform.transform(updatedLines); + if (updatedDiffLines != null) { + updatedDiffLines = transform.transform(updatedDiffLines); + } } updatedLines = updatedLines.map((line) => line.trimRight()); + updatedDiffLines = updatedDiffLines?.map((line) => line.trimRight()); + + if (updatedDiffLines != null) { + final patch = _diffAlgorithm.compute( + updatedLines.toList(growable: false), + updatedDiffLines.toList(growable: false), + ); + + final UnifiedDiffHeader diffHeader; + if (diffWithRegionPath != null) { + diffHeader = UnifiedDiffHeader.custom( + sourceLineContent: instruction.targetPath, + targetLineContent: diffWithRegionPath, + ); + } else { + diffHeader = const UnifiedDiffHeader.simple(); + } + + final diff = UnifiedDiff.fromPatch( + patch, + header: diffHeader, + context: instruction.diffContext, + ); + + updatedLines = diff.toString().trimRight().split('\n'); + } // Remove all shared whitespace on the left. int? sharedLeftWhitespace; @@ -302,10 +354,16 @@ final class InjectionException implements Exception { String toString() => '$filePath:$lineNumber - $error'; } +const DiffAlgorithm _diffAlgorithm = DiffAlgorithm.myers(); + final RegExp _instructionPattern = RegExp( r'^\s*<\?code-excerpt\s+(?:"(?\S+)(?:\s\((?[^)]+)\))?\s*")?(?.*?)\?>$', ); +final RegExp _diffWithPattern = RegExp( + r'^(?\S+)(?:\s\((?[^)]+)\))?', +); + final RegExp _instructionStart = RegExp(r'^<\?code-excerpt'); final RegExp _codeBlockStart = @@ -394,6 +452,9 @@ final class _InjectInstruction extends _Instruction { final String targetPath; final String regionName; + final ({String path, String region})? diffWith; + final int diffContext; + final List transforms; final int? indentBy; @@ -405,6 +466,8 @@ final class _InjectInstruction extends _Instruction { required this.transforms, this.indentBy, this.plasterTemplate, + this.diffWith, + this.diffContext = 3, }); factory _InjectInstruction.fromArgs({ @@ -415,6 +478,8 @@ final class _InjectInstruction extends _Instruction { }) { String? indentByString; String? plasterTemplate; + String? diffWithString; + String? diffContextString; final transforms = []; @@ -436,6 +501,22 @@ final class _InjectInstruction extends _Instruction { ); } plasterTemplate = argValue; + case 'diff-with': + if (diffWithString != null) { + reportError( + 'The `diff-with` argument can only be ' + 'specified once per instruction.', + ); + } + diffWithString = argValue; + case 'diff-u': + if (diffContextString != null) { + reportError( + 'The `diff-u` argument can only be ' + 'specified once per instruction.', + ); + } + diffContextString = argValue; case 'skip': transforms.add(SkipTransform(int.parse(argValue))); case 'take': @@ -458,17 +539,45 @@ final class _InjectInstruction extends _Instruction { } } - final indentBy = indentByString == null ? null : int.parse(indentByString); - - if (indentBy != null && indentBy < 0) { + final indentBy = + indentByString == null ? null : int.tryParse(indentByString); + if (indentBy != null && indentBy < 1) { reportError( 'The `indent-by` argument must be positive.', ); } + final diffContext = + diffContextString == null ? null : int.tryParse(diffContextString); + if (diffContext != null && diffContext < 1) { + reportError( + 'The `diff-u` argument must be an integer greater than 1.', + ); + } + + ({String path, String region})? diffWith; + + if (diffWithString != null) { + final pathAndRegion = _diffWithPattern.firstMatch(diffWithString); + if (pathAndRegion == null) { + reportError('Invalid syntax for `diff-with` argument.'); + } + + diffWith = ( + path: pathAndRegion.namedGroup('path') ?? '', + region: pathAndRegion.namedGroup('region') ?? '', + ); + } else if (diffContext != null) { + reportError( + 'The `diff-u` argument must be specified with a `diff-with` argument.', + ); + } + return _InjectInstruction( targetPath: targetPath, regionName: regionName, + diffWith: diffWith, + diffContext: diffContext ?? 3, indentBy: indentBy, plasterTemplate: plasterTemplate, transforms: transforms, diff --git a/packages/excerpter/pubspec.yaml b/packages/excerpter/pubspec.yaml index efa483a..d0eade4 100644 --- a/packages/excerpter/pubspec.yaml +++ b/packages/excerpter/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: args: ^2.4.2 collection: ^1.18.0 + deviation: ^0.0.2 file: ^7.0.0 glob: ^2.1.2 meta: ^1.14.0 diff --git a/packages/excerpter/test/updater_test.dart b/packages/excerpter/test/updater_test.dart index 55ec780..f9e973c 100644 --- a/packages/excerpter/test/updater_test.dart +++ b/packages/excerpter/test/updater_test.dart @@ -50,8 +50,8 @@ void _defaultBehavior() { expect(results.errors, hasLength(0)); expect(results.excerptsNeedingUpdates, equals(0)); expect(results.excerptsVisited, greaterThan(0)); - expect(results.totalFilesToVisit, equals(4)); - expect(results.filesVisited, equals(4)); + expect(results.totalFilesToVisit, equals(5)); + expect(results.filesVisited, equals(5)); expect(results.madeUpdates, isFalse); }); diff --git a/packages/excerpter/test_data/example/first.dart b/packages/excerpter/test_data/example/first.dart new file mode 100644 index 0000000..596b815 --- /dev/null +++ b/packages/excerpter/test_data/example/first.dart @@ -0,0 +1,3 @@ +void main() { + print('Hello!'); +} diff --git a/packages/excerpter/test_data/example/second.dart b/packages/excerpter/test_data/example/second.dart new file mode 100644 index 0000000..57078fd --- /dev/null +++ b/packages/excerpter/test_data/example/second.dart @@ -0,0 +1,4 @@ +void main() { + print('Hello!'); + print('World!'); +} diff --git a/packages/excerpter/test_data/expected/diff.md b/packages/excerpter/test_data/expected/diff.md new file mode 100644 index 0000000..769f3d3 --- /dev/null +++ b/packages/excerpter/test_data/expected/diff.md @@ -0,0 +1,12 @@ +## Diff with + + +```diff2html +--- first.dart ++++ second.dart +@@ -1,3 +1,4 @@ + void main() { + print('Hello!'); ++ print('World!'); + } +``` diff --git a/packages/excerpter/test_data/src/diff.md b/packages/excerpter/test_data/src/diff.md new file mode 100644 index 0000000..c019094 --- /dev/null +++ b/packages/excerpter/test_data/src/diff.md @@ -0,0 +1,5 @@ +## Diff with + + +```diff2html +```