summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2017-02-19 11:08:06 +0000
committerGitHub <noreply@github.com>2017-02-19 11:08:06 +0000
commit2e0d6e07bb8d9ccbd53270a450bd9ee5145f845b (patch)
tree407ef0c07a0d6d567a16f6cc682915b2ee251289
parent15f4381cf54901895836551a30f66c0cee974a0c (diff)
parentf89115c9817b4cd4da91c7e3e9882b0eac4ae40d (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.js93
-rw-r--r--lib/docx/docx-reader.js4
-rw-r--r--test/docx/body-reader.tests.js253
-rw-r--r--test/docx/comments-reader.tests.js4
-rw-r--r--test/docx/document-matchers.js27
-rw-r--r--test/docx/notes-reader.tests.js4
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", {}, [