diff options
| author | Michael Williamson <mike@zwobble.org> | 2017-02-19 11:08:06 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2017-02-19 11:08:06 +0000 |
| commit | 2e0d6e07bb8d9ccbd53270a450bd9ee5145f845b (patch) | |
| tree | 407ef0c07a0d6d567a16f6cc682915b2ee251289 | |
| parent | 15f4381cf54901895836551a30f66c0cee974a0c (diff) | |
| parent | f89115c9817b4cd4da91c7e3e9882b0eac4ae40d (diff) | |
Merge pull request #110 from jdlehman/instrText-hyperlinks
Enable hyperlinks that are not defined with w:hyperlink
| -rw-r--r-- | lib/docx/body-reader.js | 93 | ||||
| -rw-r--r-- | lib/docx/docx-reader.js | 4 | ||||
| -rw-r--r-- | test/docx/body-reader.tests.js | 253 | ||||
| -rw-r--r-- | test/docx/comments-reader.tests.js | 4 | ||||
| -rw-r--r-- | test/docx/document-matchers.js | 27 | ||||
| -rw-r--r-- | test/docx/notes-reader.tests.js | 4 |
6 files changed, 347 insertions, 38 deletions
diff --git a/lib/docx/body-reader.js b/lib/docx/body-reader.js index 529d6f2..a19ecdb 100644 --- a/lib/docx/body-reader.js +++ b/lib/docx/body-reader.js @@ -1,4 +1,5 @@ -exports.BodyReader = BodyReader; +exports.createBodyReader = createBodyReader; +exports._readNumberingProperties = readNumberingProperties; var _ = require("underscore"); @@ -7,8 +8,20 @@ var Result = require("../results").Result; var warning = require("../results").warning; var uriToZipEntryName = require("./uris").uriToZipEntryName; +function createBodyReader(options) { + return { + readXmlElement: function(element) { + return new BodyReader(options).readXmlElement(element); + }, + readXmlElements: function(elements) { + return new BodyReader(options).readXmlElements(elements); + } + }; +} function BodyReader(options) { + var complexFields = []; + var currentInstrText = []; var relationships = options.relationships; var contentTypes = options.contentTypes; var docxFile = options.docxFile; @@ -125,7 +138,7 @@ function BodyReader(options) { styleId: style.styleId, styleName: style.name, alignment: element.firstOrEmpty("w:jc").attributes["w:val"], - numbering: readNumberingProperties(element.firstOrEmpty("w:numPr")) + numbering: readNumberingProperties(element.firstOrEmpty("w:numPr"), numbering) }; }); }, @@ -133,14 +146,37 @@ function BodyReader(options) { return readXmlElements(element.children) .map(function(children) { var properties = _.find(children, isRunProperties); - - return new documents.Run( - children.filter(negate(isRunProperties)), - properties - ); + children = children.filter(negate(isRunProperties)); + + var scopedHyperlink = scopedComplexFieldOfType(complexFields, "hyperlink"); + if (scopedHyperlink !== undefined) { + children = [new documents.Hyperlink(children, {href: scopedHyperlink.href})]; + } + + return new documents.Run(children, properties); }); }, "w:rPr": readRunProperties, + "w:fldChar": function(element) { + var type = element.attributes["w:fldCharType"]; + if (type === "begin") { + currentInstrText = []; + } else if (type === "end") { + complexFields.pop(); + } else if (type === "separate") { + var href = parseHyperlinkFieldCode(currentInstrText.join('')); + if (href !== null) { + complexFields.push({type: "hyperlink", href: href}); + } else { + complexFields.push(null); + } + } + return emptyResult(); + }, + "w:instrText": function(element) { + currentInstrText.push(element.text()); + return emptyResult(); + }, "w:t": function(element) { return elementResult(new documents.Text(element.text())); }, @@ -210,21 +246,12 @@ function BodyReader(options) { "wp:anchor": readDrawingElement, "v:imagedata": readImageData }; + return { readXmlElement: readXmlElement, - readXmlElements: readXmlElements, - _readNumberingProperties: readNumberingProperties + readXmlElements: readXmlElements }; - function readNumberingProperties(element) { - var level = element.firstOrEmpty("w:ilvl").attributes["w:val"]; - var numId = element.firstOrEmpty("w:numId").attributes["w:val"]; - if (level === undefined || numId === undefined) { - return null; - } else { - return numbering.findLevel(numId, level); - } - } function readTable(element) { return readXmlElements(element.children) @@ -379,6 +406,17 @@ function BodyReader(options) { type + " style with ID " + styleId + " was referenced but not defined in the document"); } } + + +function readNumberingProperties(element, numbering) { + var level = element.firstOrEmpty("w:ilvl").attributes["w:val"]; + var numId = element.firstOrEmpty("w:numId").attributes["w:val"]; + if (level === undefined || numId === undefined) { + return null; + } else { + return numbering.findLevel(numId, level); + } +} var supportedImageTypes = { "image/png": true, @@ -491,3 +529,22 @@ function joinElements(first, second) { function identity(value) { return value; } + +function parseHyperlinkFieldCode(code) { + var result = /\s*HYPERLINK "(.*)"/.exec(code); + if (result) { + return result[1]; + } else { + return null; + } +} + +function complexFieldsOfType(complexFields, type) { + return complexFields.filter(function(complexField) { + return complexField.type === type; + }); +} + +function scopedComplexFieldOfType(complexFields, type) { + return _.last(complexFieldsOfType(complexFields, type)); +} diff --git a/lib/docx/docx-reader.js b/lib/docx/docx-reader.js index 0c7d57f..5aa95ca 100644 --- a/lib/docx/docx-reader.js +++ b/lib/docx/docx-reader.js @@ -7,7 +7,7 @@ var documents = require("../documents"); var Result = require("../results").Result; var readXmlFromZipFile = require("./office-xml-reader").readXmlFromZipFile; -var BodyReader = require("./body-reader").BodyReader; +var createBodyReader = require("./body-reader").createBodyReader; var DocumentXmlReader = require("./document-xml-reader").DocumentXmlReader; var relationshipsReader = require("./relationships-reader"); var contentTypesReader = require("./content-types-reader"); @@ -95,7 +95,7 @@ function readXmlFileWithBody(name, options, func) { }); return readRelationshipsFromZipFile(options.docxFile).then(function(relationships) { - var bodyReader = new BodyReader({ + var bodyReader = new createBodyReader({ relationships: relationships, contentTypes: options.contentTypes, docxFile: options.docxFile, diff --git a/test/docx/body-reader.tests.js b/test/docx/body-reader.tests.js index f605c2d..a670202 100644 --- a/test/docx/body-reader.tests.js +++ b/test/docx/body-reader.tests.js @@ -3,6 +3,7 @@ var path = require("path"); var _ = require("underscore"); var hamjest = require("hamjest"); +var assertThat = hamjest.assertThat; var promiseThat = hamjest.promiseThat; var allOf = hamjest.allOf; var contains = hamjest.contains; @@ -10,7 +11,14 @@ var hasProperties = hamjest.hasProperties; var willBe = hamjest.willBe; var FeatureMatcher = hamjest.FeatureMatcher; -var BodyReader = require("../../lib/docx/body-reader").BodyReader; +var documentMatchers = require("./document-matchers"); +var isEmptyRun = documentMatchers.isEmptyRun; +var isHyperlink = documentMatchers.isHyperlink; +var isRun = documentMatchers.isRun; +var isText = documentMatchers.isText; + +var createBodyReader = require("../../lib/docx/body-reader").createBodyReader; +var _readNumberingProperties = require("../../lib/docx/body-reader")._readNumberingProperties; var documents = require("../../lib/documents"); var xml = require("../../lib/xml"); var XmlElement = xml.Element; @@ -25,7 +33,7 @@ var createFakeDocxFile = testing.createFakeDocxFile; function readXmlElement(element, options) { options = Object.create(options || {}); options.styles = options.styles || new Styles({}, {}); - return new BodyReader(options).readXmlElement(element); + return createBodyReader(options).readXmlElement(element); } function readXmlElementValue(element, options) { @@ -106,10 +114,7 @@ test("numbering properties are converted to numbering at specified level", funct var numbering = new Numbering({"42": {"1": {isOrdered: true, level: "1"}}}); - var reader = new BodyReader({ - numbering: numbering - }); - var numberingLevel = reader._readNumberingProperties(numberingPropertiesXml); + var numberingLevel = _readNumberingProperties(numberingPropertiesXml, numbering); assert.deepEqual(numberingLevel, {level: "1", isOrdered: true}); }); @@ -120,10 +125,7 @@ test("numbering properties are ignored if w:ilvl is missing", function() { var numbering = new Numbering({"42": {"1": {isOrdered: true, level: "1"}}}); - var reader = new BodyReader({ - numbering: numbering - }); - var numberingLevel = reader._readNumberingProperties(numberingPropertiesXml); + var numberingLevel = _readNumberingProperties(numberingPropertiesXml, numbering); assert.equal(numberingLevel, null); }); @@ -134,13 +136,231 @@ test("numbering properties are ignored if w:numId is missing", function() { var numbering = new Numbering({"42": {"1": {isOrdered: true, level: "1"}}}); - var reader = new BodyReader({ - numbering: numbering - }); - var numberingLevel = reader._readNumberingProperties(numberingPropertiesXml); + var numberingLevel = _readNumberingProperties(numberingPropertiesXml, numbering); assert.equal(numberingLevel, null); }); +test("complex fields", (function() { + var uri = "http://example.com"; + var beginXml = new XmlElement("w:r", {}, [ + new XmlElement("w:fldChar", {"w:fldCharType": "begin"}) + ]); + var endXml = new XmlElement("w:r", {}, [ + new XmlElement("w:fldChar", {"w:fldCharType": "end"}) + ]); + var separateXml = new XmlElement("w:r", {}, [ + new XmlElement("w:fldChar", {"w:fldCharType": "separate"}) + ]); + var hyperlinkInstrText = new XmlElement("w:instrText", {}, [ + xml.text(' HYPERLINK "' + uri + '"') + ]); + var hyperlinkRunXml = runOfText("this is a hyperlink"); + + return { + "stores instrText returns empty result": function() { + var instrText = readXmlElementValue(hyperlinkInstrText); + assert.deepEqual(instrText, []); + }, + + "runs in a complex field for hyperlinks are read as hyperlinks": function() { + var hyperlinkRunXml = runOfText("this is a hyperlink"); + var paragraphXml = new XmlElement("w:p", {}, [ + beginXml, + hyperlinkInstrText, + separateXml, + hyperlinkRunXml, + endXml + ]); + var paragraph = readXmlElementValue(paragraphXml); + + assertThat(paragraph.children, contains( + isEmptyRun, + isRun({ + children: contains( + isHyperlink({ + href: uri, + children: [] + }) + ) + }), + isRun({ + children: contains( + isHyperlink({ + href: uri, + children: contains( + isText("this is a hyperlink") + ) + }) + ) + }), + isEmptyRun + )); + }, + + "runs after a complex field for hyperlinks are not read as hyperlinks": function() { + var afterEndXml = runOfText("this will not be a hyperlink"); + var paragraphXml = new XmlElement("w:p", {}, [ + beginXml, + hyperlinkInstrText, + separateXml, + endXml, + afterEndXml + ]); + var paragraph = readXmlElementValue(paragraphXml); + + assertThat(paragraph.children, contains( + isEmptyRun, + isRun({ + children: contains( + isHyperlink({ + href: uri, + children: [] + }) + ) + }), + isEmptyRun, + isRun({ + children: contains( + isText("this will not be a hyperlink") + ) + }) + )); + }, + + "can handle split instrText elements": function() { + var hyperlinkInstrTextPart1 = new XmlElement("w:instrText", {}, [ + xml.text(" HYPE") + ]); + var hyperlinkInstrTextPart2 = new XmlElement("w:instrText", {}, [ + xml.text('RLINK "' + uri + '"') + ]); + var paragraphXml = new XmlElement("w:p", {}, [ + beginXml, + hyperlinkInstrTextPart1, + hyperlinkInstrTextPart2, + separateXml, + hyperlinkRunXml, + endXml + ]); + var paragraph = readXmlElementValue(paragraphXml); + + assertThat(paragraph.children, contains( + isEmptyRun, + isRun({ + children: contains( + isHyperlink({ + href: uri, + children: [] + }) + ) + }), + isRun({ + children: contains( + isHyperlink({ + href: uri, + children: contains( + isText("this is a hyperlink") + ) + }) + ) + }), + isEmptyRun + )); + }, + + "hyperlink with a nested complex field uses the outer hyperlink": function() { + var authorInstrText = new XmlElement("w:instrText", {}, [ + xml.text(' AUTHOR "John Doe"') + ]); + var paragraphXml = new XmlElement("w:p", {}, [ + beginXml, + beginXml, + authorInstrText, + endXml, + hyperlinkInstrText, + separateXml, + hyperlinkRunXml, + endXml + ]); + var paragraph = readXmlElementValue(paragraphXml); + + assertThat(paragraph.children, contains( + isEmptyRun, + isEmptyRun, + isEmptyRun, + isRun({ + children: contains( + isHyperlink({ + href: uri, + children: [] + }) + ) + }), + isRun({ + children: contains( + isHyperlink({ + href: uri, + children: contains( + isText("this is a hyperlink") + ) + }) + ) + }), + isEmptyRun + )); + }, + + "complex field nested within a hyperlink complex field is wrapped with the hyperlink": function() { + var authorInstrText = new XmlElement("w:instrText", {}, [ + xml.text(' AUTHOR "John Doe"') + ]); + var paragraphXml = new XmlElement("w:p", {}, [ + beginXml, + hyperlinkInstrText, + separateXml, + hyperlinkRunXml, + beginXml, + authorInstrText, + endXml, + endXml + ]); + var paragraph = readXmlElementValue(paragraphXml); + + assertThat(paragraph.children, contains( + isEmptyRun, + isRun({ + children: contains( + isHyperlink({ + href: uri, + children: [] + }) + ) + }), + isRun({ + children: contains( + isHyperlink({ + href: uri, + children: contains( + isText("this is a hyperlink") + ) + }) + ) + }), + isRun({ + children: contains( + isHyperlink({ + href: uri, + children: [] + }) + ) + }), + isEmptyRun, + isEmptyRun + )); + } + }; +})()); + test("run has no style if it has no properties", function() { var runXml = runWithProperties([]); var run = readXmlElementValue(runXml); @@ -861,6 +1081,11 @@ function createLinkedBlip(relationshipId) { return new XmlElement("a:blip", {"r:link": relationshipId}); } +function runOfText(text) { + var textXml = new XmlElement("w:t", {}, [xml.text(text)]); + return new XmlElement("w:r", {}, [textXml]); +} + function assertImageBuffer(element, expectedImageBuffer) { return element.read() .then(function(readValue) { diff --git a/test/docx/comments-reader.tests.js b/test/docx/comments-reader.tests.js index 4bb0e9b..0b3c0ce 100644 --- a/test/docx/comments-reader.tests.js +++ b/test/docx/comments-reader.tests.js @@ -1,14 +1,14 @@ var assert = require("assert"); var createCommentsReader = require("../../lib/docx/comments-reader").createCommentsReader; -var BodyReader = require("../../lib/docx/body-reader").BodyReader; +var createBodyReader = require("../../lib/docx/body-reader").createBodyReader; var documents = require("../../lib/documents"); var xml = require("../../lib/xml"); var test = require("../test")(module); function readComment(element) { - var bodyReader = new BodyReader({}); + var bodyReader = createBodyReader({}); var commentsReader = createCommentsReader(bodyReader); var comments = commentsReader(element); assert.equal(comments.value.length, 1); diff --git a/test/docx/document-matchers.js b/test/docx/document-matchers.js new file mode 100644 index 0000000..b750bed --- /dev/null +++ b/test/docx/document-matchers.js @@ -0,0 +1,27 @@ +var hamjest = require("hamjest"); +var _ = require("underscore"); + +var documents = require("../../lib/documents"); + + +exports.isEmptyRun = isRun({children: []}); +exports.isRun = isRun; +exports.isText = isText; +exports.isHyperlink = isHyperlink; + + +function isRun(properties) { + return isDocumentElement(documents.types.run, properties); +} + +function isText(text) { + return isDocumentElement(documents.types.text, {value: text}); +} + +function isHyperlink(properties) { + return isDocumentElement(documents.types.hyperlink, properties); +} + +function isDocumentElement(type, properties) { + return hamjest.hasProperties(_.extend({type: hamjest.equalTo(type)}, properties)); +} diff --git a/test/docx/notes-reader.tests.js b/test/docx/notes-reader.tests.js index 1d4cf9c..71bb794 100644 --- a/test/docx/notes-reader.tests.js +++ b/test/docx/notes-reader.tests.js @@ -1,14 +1,14 @@ var assert = require("assert"); var createFootnotesReader = require("../../lib/docx/notes-reader").createFootnotesReader; -var BodyReader = require("../../lib/docx/body-reader").BodyReader; +var createBodyReader = require("../../lib/docx/body-reader").createBodyReader; var documents = require("../../lib/documents"); var XmlElement = require("../../lib/xml").Element; var test = require("../test")(module); test('ID and body of footnote are read', function() { - var bodyReader = new BodyReader({}); + var bodyReader = new createBodyReader({}); var footnoteBody = [new XmlElement("w:p", {}, [])]; var footnotes = createFootnotesReader(bodyReader)( new XmlElement("w:footnotes", {}, [ |
