diff --git a/README.md b/README.md index b504558..6eb097e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # Docx Editor Library -IN WIP \ No newline at end of file +IN WIP + +0.0.1 diff --git a/bin/main.dart b/bin/main.dart deleted file mode 100644 index 75a2699..0000000 --- a/bin/main.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:docx/docx.dart'; -import 'dart:io'; -import 'package:docx/replace.dart'; - -void main(List args) { - DocxEditor docx = DocxEditor(File('test/assets/docx_test.docx'), { - 'Title': ReplaceContent(value: 'NewTitle'), - 'A': ReplaceContent(value: 'AModified'), - 'ImageCat': ReplaceContent(img: DocxImage(file: File('test/assets/cat.png'), sizeX: 200, sizeY: 400)), - 'ImageDog': ReplaceContent(img: DocxImage(file: File('test/assets/dog.png'), sizeX: 400, sizeY: 300)), - 'Footer': ReplaceContent(img: DocxImage(file: File('test/assets/dog.jpg'), sizeX: 400, sizeY: 300)) - }); -} diff --git a/lib/docx.dart b/lib/docx.dart index 8811d54..6935221 100644 --- a/lib/docx.dart +++ b/lib/docx.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'dart:convert'; import 'dart:io'; @@ -8,13 +9,19 @@ import 'package:docx/replace.dart'; class DocxEditor { - File fileDocx; + File fileInDocx; + String? pathOutDocx; - /// [fileDocx] The Docx file... - /// [replaceMap] All text and image to change - DocxEditor(this.fileDocx, Map replaceMap) { - if (!fileDocx.existsSync()) - throw(PathNotFoundException(fileDocx.path, OSError('File not found', 404))); + /// [fileInDocx] The Docx file (required) + /// [pathOutDocx] The path where new file is save (optional) + /// [replaceMap] All text and image to change (required) + DocxEditor({ + required this.fileInDocx, + this.pathOutDocx, + required Map replaceMap + }) { + if (!fileInDocx.existsSync()) + throw(PathNotFoundException(fileInDocx.path, OSError('File not found', 404))); List documentsXml = editById(replaceMap); save(documentsXml, replaceMap); @@ -22,67 +29,74 @@ class DocxEditor { /// This function search ID in [replaceMap] and replace by his value or by a image List editById(Map replaceMap) { - Uint8List content = fileDocx.readAsBytesSync(); + Uint8List content = fileInDocx.readAsBytesSync(); - Archive archive = ZipDecoder().decodeBytes(content); - ArchiveFile archiveFile = archive.firstWhere((f) => f.name == 'word/document.xml'); - XmlDocument documentXml = XmlDocument.parse(utf8.decode(archiveFile.content as List)); + Archive archive = ZipDecoder().decodeBytes(content); + ArchiveFile archiveFile = archive.firstWhere((f) => f.name == 'word/document.xml'); + XmlDocument documentXml = XmlDocument.parse(utf8.decode(archiveFile.content as List)); - XmlDocument documentXmlRels = addImageinArchive(archive, replaceMap); + XmlDocument documentXmlRels = addImageinArchive(archive, replaceMap); - print(documentXml.toXmlString(pretty: true, indent: ' ')); + //print(documentXml.toXmlString(pretty: true, indent: ' ')); - // TODO: Rework this function, doesn't work with ambiguous ID to search, or multiple Id in one paragraphe, separator like '_' or '-' or space - // TODO: Essayer de parcourir par rapport au paragraphe () et non les runs () - for (XmlElement node in documentXml.findAllElements('w:r')) { - String ndText = node.innerText.toLowerCase(); - //Iterable replacesId = replaceMap.keys.where((rm) => rm.toLowerCase() == ndText); - for (String key in replaceMap.keys) { - Iterable replacesId = RegExp(key.toLowerCase()).allMatches(ndText).map((m) => m.group(0)!); - XmlNode? parent; - for (int i = 0; i < replacesId.length; i++) { - ReplaceContent replaceCnt = replaceMap[key]!; - if (replaceCnt.img != null) - parent = putImage(node, replaceCnt, parent); - else { /// Replace Text by other text - int start = node.innerXml.indexOf(''); - int end = node.innerXml.indexOf(''); - node.innerXml = node.innerXml.replaceRange(start + 5, end, replaceCnt.value!); + // sort descending orderz + final keys = replaceMap.keys.toList()..sort((a, b) => b.length.compareTo(a.length)); - } - } - } + final pattern = RegExp( + keys.map((key) => '(?<=^|\\s)${RegExp.escape(key)}(?=\\s|\$)').join('|'), + ); + + // replace image + for (XmlElement node in documentXml.findAllElements('w:t')) { + String ndText = node.innerText.toLowerCase(); + if (ndText.isEmpty) + continue; + XmlElement? parent; + for (String key in replaceMap.keys) { + ReplaceContent replaceCnt = replaceMap[key]!; + if (replaceCnt.img == null) + continue; + for (RegExpMatch regMatch in RegExp('(?<=^|\\s)${key.toLowerCase()}(?=\\s|\$)').allMatches(ndText)) + parent = putImage(node.parentElement, replaceCnt, parent); } + } + + // Only replace text! + for (XmlElement paragraph in documentXml.findAllElements('w:p')) { + List texts = paragraph.findAllElements('w:t').toList(); + if (texts.isEmpty) + continue; + + String original = texts.map((t) { + return t.innerText; + }).join(); + + String replaced = original.replaceAllMapped(pattern, (match) { + ReplaceContent rc = replaceMap[match[0]]!; + return rc.img == null ? rc.value! : ''; + }); + + if (original == replaced) + continue; + + final firstRun = paragraph.findElements('w:r').first; + final newRun = firstRun.copy(); + newRun.findAllElements('w:t').first.innerText = replaced; // Update with new text + + // replace the first w:r with the new + firstRun.replace(newRun); + } return [documentXml, documentXmlRels]; } - /// Add all image in [replaceMap] to Archive in 'word/_rels/document.xml.rels' - XmlDocument addImageinArchive(Archive archive, Map replaceMap) { - - ArchiveFile archiveFile = archive.firstWhere((f) => f.name == 'word/_rels/document.xml.rels'); - XmlDocument documentXmlRels = XmlDocument.parse(utf8.decode(archiveFile.content as List)); - String documentXmlStr = documentXmlRels.toXmlString(); - RegExp exp = RegExp(r'Id="rId([0-9]+)"'); - Iterable matches = exp.allMatches(documentXmlStr); - int maxId = matches.isEmpty ? 1 : int.parse(matches.last.group(1)!); - - for (ReplaceContent replaceCnt in replaceMap.values.where((rm) => rm.img != null)) { - replaceCnt.img!.id = ++maxId; - String text = '\n'; - int lastIndex = documentXmlStr.lastIndexOf('/>') + 3; - documentXmlStr = documentXmlStr.replaceRange(lastIndex, lastIndex, text); - } - documentXmlRels = XmlDocument.parse(documentXmlStr); - return documentXmlRels; - } - + // TODO: Find a solution to put multiple image in only one /// Replaces the text ([node]) with an image ([replaceContent]) - XmlNode? putImage(XmlElement node, ReplaceContent replaceContent, XmlNode? newParent) { + XmlElement? putImage(XmlElement? node, ReplaceContent replaceContent, XmlElement? newParent) { - if (node.parent == null && newParent != null) + if (node!.parentElement == null && newParent != null) node.attachParent(newParent); - XmlNode? parent = node.parent; + XmlElement? parent = node.parentElement; String nodeStr = node.toXmlString(); node.replace(XmlDocumentFragment.parse(""" $nodeStr @@ -124,12 +138,32 @@ class DocxEditor { return parent; } - /// Recreate a Archive to add modification and save it on [fileDocx] + /// Add all image in [replaceMap] to Archive in 'word/_rels/document.xml.rels' + XmlDocument addImageinArchive(Archive archive, Map replaceMap) { + + ArchiveFile archiveFile = archive.firstWhere((f) => f.name == 'word/_rels/document.xml.rels'); + XmlDocument documentXmlRels = XmlDocument.parse(utf8.decode(archiveFile.content as List)); + String documentXmlStr = documentXmlRels.toXmlString(); + RegExp exp = RegExp(r'Id="rId([0-9]+)"'); + Iterable matches = exp.allMatches(documentXmlStr); + int maxId = matches.isEmpty ? 1 : int.parse(matches.last.group(1)!); + + for (ReplaceContent replaceCnt in replaceMap.values.where((rm) => rm.img != null)) { + replaceCnt.img!.id = ++maxId; + String text = '\n'; + int lastIndex = documentXmlStr.lastIndexOf('/>') + 3; + documentXmlStr = documentXmlStr.replaceRange(lastIndex, lastIndex, text); + } + documentXmlRels = XmlDocument.parse(documentXmlStr); + return documentXmlRels; + } + + /// Recreate a Archive to add modification and save it on [fileInDocx] /// [documentsXml] is list -> ['word/document.xml', 'word/_rels/document.xml.rels'] /// [replaceMap] for upload file image in docx file - void save(List documentsXml, Map replaceMap) { + File save(List documentsXml, Map replaceMap) { try { - final originalBytes = fileDocx.readAsBytesSync(); + final originalBytes = fileInDocx.readAsBytesSync(); final archive = ZipDecoder().decodeBytes(originalBytes); final Uint8List uListDocumentXml = utf8.encode(documentsXml[0].toXmlString()); @@ -167,9 +201,13 @@ class DocxEditor { } final zipData = ZipEncoder().encode(newArchive); - fileDocx.writeAsBytesSync(zipData); + + File newFile = pathOutDocx == null ? fileInDocx : File(pathOutDocx!); + newFile.writeAsBytesSync(zipData); + return newFile; + } catch (e) { throw StateError('DOCX BUILDER ERROR: $e'); } } -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index 7649126..766cdc1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ environment: dependencies: xml: ^6.6.0 archive: ^4.0.7 + xml2json: ^6.2.7 # path: ^1.8.0 dev_dependencies: diff --git a/test/assets/docx_test.docx b/test/assets/docx_test.docx deleted file mode 100644 index 1479137..0000000 Binary files a/test/assets/docx_test.docx and /dev/null differ diff --git a/test/assets/docx_test_copy.docx b/test/assets/docx_test_copy.docx index c676b5d..204434a 100644 Binary files a/test/assets/docx_test_copy.docx and b/test/assets/docx_test_copy.docx differ diff --git a/test/docx_test.dart b/test/docx_test.dart index 6b53026..23241de 100644 --- a/test/docx_test.dart +++ b/test/docx_test.dart @@ -6,12 +6,17 @@ import 'dart:io'; void main() { test('Try to generate a file without error', () { - DocxEditor docx = DocxEditor(File('test/assets/docx_test.docx'), { + DocxEditor docx = DocxEditor( + fileInDocx: File('test/assets/docx_test_copy.docx'), + pathOutDocx: 'test/assets/docx_test.docx', + replaceMap: { 'Title': ReplaceContent(value: 'NewTitle'), 'A': ReplaceContent(value: 'AModified'), - 'Image-Chat': ReplaceContent(img: DocxImage(file: File('test/assets/cat.png'), sizeX: 200, sizeY: 400)), - 'Image-Dog': ReplaceContent(img: DocxImage(file: File('test/assets/dog.png'), sizeX: 400, sizeY: 300)), + 'Image-Dog': ReplaceContent(value: 'NotImage-Dog'), + 'ImageCat': ReplaceContent(img: DocxImage(file: File('test/assets/cat.png'), sizeX: 200, sizeY: 400)), + 'Image-Cat': ReplaceContent(img: DocxImage(file: File('test/assets/cat.png'), sizeX: 200, sizeY: 400)), 'Footer': ReplaceContent(img: DocxImage(file: File('test/assets/dog.jpg'), sizeX: 400, sizeY: 300)) - }); + } + ); }); }