aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2013-04-01 17:35:15 +0100
committerMichael Williamson <mike@zwobble.org>2013-04-01 17:35:15 +0100
commitbd8b2786247308c7bd82dc3cda627a23b4898958 (patch)
treee30fc62c22bbb0dc358155c5c0caebb126688091
parent25109ca7e318e188229cac92552ba0dcd7e897a1 (diff)
Implement reading of hyperlink elements
-rw-r--r--lib/docx-reader.js203
-rw-r--r--tests/docx-reader.tests.js30
-rw-r--r--tests/test-data/hyperlinks/word/_rels/document.xml.rels10
-rw-r--r--tests/test-data/hyperlinks/word/document.xml18
-rw-r--r--tests/testing.js9
5 files changed, 177 insertions, 93 deletions
diff --git a/lib/docx-reader.js b/lib/docx-reader.js
index 3e4054f..71707ff 100644
--- a/lib/docx-reader.js
+++ b/lib/docx-reader.js
@@ -1,5 +1,5 @@
exports.read = read;
-exports.readXmlElement = readXmlElement;
+exports.DocumentReader = DocumentReader;
var q = require("q");
var _ = require("underscore");
@@ -18,101 +18,144 @@ function read(docxFile) {
.then(function(value) {
return value.toString();
});
+ },
+ exists: function(path) {
+ return zipFile.names.indexOf(path) !== -1;
}
};
}
- return docxFile.read("word/document.xml")
- .then(xmlreader.read)
- .then(convertXmlToDocument);
-}
-
-function convertXmlToDocument(documentXml) {
- var body = documentXml.root.first("w:body");
-
- var result = readXmlElements(body.children)
- .map(function(children) {
- return new documents.Document(children);
- });
- result.document = result.value;
- return result;
+ return q.all([
+ readXmlFile(docxFile, "word/_rels/document.xml.rels"),
+ readXmlFile(docxFile, "word/document.xml")
+ ]).spread(function(relationshipsXml, documentXml) {
+ var relationships = relationshipsXml ? readRelationships(relationshipsXml) : {};
+ var reader = new DocumentReader(relationships);
+ return reader.convertXmlToDocument(documentXml);
+ });
}
-function readXmlElements(elements) {
- var results = elements.map(readXmlElement);
- var values = _.pluck(results, "value").filter(notNull);
- var messages = _.flatten(_.pluck(results, "messages"), true);
- return new Result(values, messages);
+function readXmlFile(docxFile, path) {
+ if (docxFile.exists(path)) {
+ return docxFile.read(path)
+ .then(xmlreader.read);
+ } else {
+ return null;
+ }
}
-function readXmlElement(element) {
- if (element.type === "element") {
- var handler = xmlElementReaders[element.name];
- if (handler) {
- return handler(element);
- } else if (!Object.prototype.hasOwnProperty.call(ignoreElements, element.name)) {
- return new Result(
- null,
- [warning("An unrecognised element was ignored: " + element.name)]
- );
+function readRelationships(relationshipsXml) {
+ var relationships = {};
+ relationshipsXml.root.children.forEach(function(child) {
+ if (child.name === "Relationship") {
+ relationships[child.attributes.Id] = {
+ target: child.attributes.Target
+ };
}
- }
- return new Result(null);
+ });
+ return relationships;
}
-var xmlElementReaders = {
- "w:p": function(element) {
- return readXmlElements(element.children)
- .map(function(children) {
- var properties = _.find(children, isParagraphProperties);
-
- return new documents.Paragraph(
- children.filter(negate(isParagraphProperties)),
- properties
- );
- });
- },
- "w:pPr": function(element) {
- var properties = {
- type: "paragraphProperties"
- };
-
- var styleElement = element.first("w:pStyle");
- if (styleElement) {
- properties.styleName = styleElement.attributes["w:val"];
- }
+function DocumentReader(relationships) {
+ function convertXmlToDocument(documentXml) {
+ var body = documentXml.root.first("w:body");
- return new Result(properties);
- },
- "w:r": function(element) {
- return readXmlElements(element.children)
+ var result = readXmlElements(body.children)
.map(function(children) {
- var properties = _.find(children, isRunProperties);
-
- return new documents.Run(
- children.filter(negate(isRunProperties)),
- properties
- );
+ return new documents.Document(children);
});
- },
- "w:rPr": function(element) {
- var properties = {
- type: "runProperties"
- };
-
- var styleElement = element.first("w:rStyle");
- if (styleElement) {
- properties.styleName = styleElement.attributes["w:val"];
+ result.document = result.value;
+ return result;
+ }
+
+ function readXmlElements(elements) {
+ var results = elements.map(readXmlElement);
+ var values = _.pluck(results, "value").filter(notNull);
+ var messages = _.flatten(_.pluck(results, "messages"), true);
+ return new Result(values, messages);
+ }
+
+ function readXmlElement(element) {
+ if (element.type === "element") {
+ var handler = xmlElementReaders[element.name];
+ if (handler) {
+ return handler(element);
+ } else if (!Object.prototype.hasOwnProperty.call(ignoreElements, element.name)) {
+ return new Result(
+ null,
+ [warning("An unrecognised element was ignored: " + element.name)]
+ );
+ }
}
- properties.isBold = !!element.first("w:b");
- properties.isItalic = !!element.first("w:i");
-
- return new Result(properties);
- },
- "w:t": function(element) {
- return new Result(new documents.Text(element.children[0].value));
+ return new Result(null);
}
-};
+
+ var xmlElementReaders = {
+ "w:p": function(element) {
+ return readXmlElements(element.children)
+ .map(function(children) {
+ var properties = _.find(children, isParagraphProperties);
+
+ return new documents.Paragraph(
+ children.filter(negate(isParagraphProperties)),
+ properties
+ );
+ });
+ },
+ "w:pPr": function(element) {
+ var properties = {
+ type: "paragraphProperties"
+ };
+
+ var styleElement = element.first("w:pStyle");
+ if (styleElement) {
+ properties.styleName = styleElement.attributes["w:val"];
+ }
+
+ return new Result(properties);
+ },
+ "w:r": function(element) {
+ return readXmlElements(element.children)
+ .map(function(children) {
+ var properties = _.find(children, isRunProperties);
+
+ return new documents.Run(
+ children.filter(negate(isRunProperties)),
+ properties
+ );
+ });
+ },
+ "w:rPr": function(element) {
+ var properties = {
+ type: "runProperties"
+ };
+
+ var styleElement = element.first("w:rStyle");
+ if (styleElement) {
+ properties.styleName = styleElement.attributes["w:val"];
+ }
+ properties.isBold = !!element.first("w:b");
+ properties.isItalic = !!element.first("w:i");
+
+ return new Result(properties);
+ },
+ "w:t": function(element) {
+ return new Result(new documents.Text(element.children[0].value));
+ },
+ "w:hyperlink": function(element) {
+ var relationshipId = element.attributes["r:id"];
+ var href = relationships[relationshipId].target;
+ return readXmlElements(element.children)
+ .map(function(children) {
+ return new documents.Hyperlink(children, {href: href});
+ });
+ }
+ };
+ return {
+ convertXmlToDocument: convertXmlToDocument,
+ readXmlElement: readXmlElement
+ };
+}
var ignoreElements = {
"w:bookmarkStart": true,
diff --git a/tests/docx-reader.tests.js b/tests/docx-reader.tests.js
index ef880f8..68efb0a 100644
--- a/tests/docx-reader.tests.js
+++ b/tests/docx-reader.tests.js
@@ -37,48 +37,56 @@ describe("docx-reader", function() {
});
});
- test("paragraph properties are not included as child of paragraph", function() {
+ test("hyperlink hrefs are read from relationships file", function() {
var docxFile = createFakeDocxFile({
- "word/document.xml": testData("paragraphStyles/word/document.xml")
+ "word/document.xml": testData("hyperlinks/word/document.xml"),
+ "word/_rels/document.xml.rels": testData("hyperlinks/word/_rels/document.xml.rels")
});
return docxReader.read(docxFile).then(function(result) {
var paragraph = result.document.children[0];
assert.equal(1, paragraph.children.length);
+ var hyperlink = paragraph.children[0];
+ assert.equal(hyperlink.href, "http://www.example.com");
+ assert.equal(hyperlink.children.length, 1);
});
});
});
+function readXmlElement(element) {
+ return new docxReader.DocumentReader({}).readXmlElement(element);
+}
+
describe("readElement", function() {
test("reads styles from run properties", function() {
var runStyleXml = new XmlElement("w:rStyle", {"w:val": "Emphasis"});
var runPropertiesXml = createRunPropertiesXml([runStyleXml]);
- var result = docxReader.readXmlElement(runPropertiesXml);
+ var result = readXmlElement(runPropertiesXml);
assert.equal(result.value.styleName, "Emphasis");
});
test("isBold is false if bold element is not present", function() {
var runPropertiesXml = createRunPropertiesXml([]);
- var result = docxReader.readXmlElement(runPropertiesXml);
+ var result = readXmlElement(runPropertiesXml);
assert.equal(result.value.isBold, false);
});
test("isBold is true if bold element is present", function() {
var boldXml = new XmlElement("w:b");
var runPropertiesXml = createRunPropertiesXml([boldXml]);
- var result = docxReader.readXmlElement(runPropertiesXml);
+ var result = readXmlElement(runPropertiesXml);
assert.equal(result.value.isBold, true);
});
test("isItalic is false if bold element is not present", function() {
var runPropertiesXml = createRunPropertiesXml([]);
- var result = docxReader.readXmlElement(runPropertiesXml);
+ var result = readXmlElement(runPropertiesXml);
assert.equal(result.value.isItalic, false);
});
test("isItalic is true if bold element is present", function() {
var italicXml = new XmlElement("w:i");
var runPropertiesXml = createRunPropertiesXml([italicXml]);
- var result = docxReader.readXmlElement(runPropertiesXml);
+ var result = readXmlElement(runPropertiesXml);
assert.equal(result.value.isItalic, true);
});
@@ -86,7 +94,7 @@ describe("readElement", function() {
var runStyleXml = new XmlElement("w:rStyle", {"w:val": "Emphasis"});
var runPropertiesXml = new XmlElement("w:rPr", {}, [runStyleXml]);
var runXml = new XmlElement("w:r", {}, [runPropertiesXml]);
- var result = docxReader.readXmlElement(runXml);
+ var result = readXmlElement(runXml);
assert.equal(result.value.properties.styleName, "Emphasis");
});
@@ -94,13 +102,13 @@ describe("readElement", function() {
var runStyleXml = new XmlElement("w:rStyle", {"w:val": "Emphasis"});
var runPropertiesXml = new XmlElement("w:rPr", {}, [runStyleXml]);
var runXml = new XmlElement("w:r", {}, [runPropertiesXml]);
- var result = docxReader.readXmlElement(runXml);
+ var result = readXmlElement(runXml);
assert.deepEqual(result.value.children, []);
});
test("emits warning on unrecognised element", function() {
var unrecognisedElement = new XmlElement("w:not-an-element");
- var result = docxReader.readXmlElement(unrecognisedElement);
+ var result = readXmlElement(unrecognisedElement);
assert.deepEqual(
result.messages,
[{
@@ -112,7 +120,7 @@ describe("readElement", function() {
test("w:bookmarkStart is ignored without warning", function() {
var ignoredElement = new XmlElement("w:bookmarkStart");
- var result = docxReader.readXmlElement(ignoredElement);
+ var result = readXmlElement(ignoredElement);
assert.deepEqual(result.messages, []);
assert.equal(null, result.value);
});
diff --git a/tests/test-data/hyperlinks/word/_rels/document.xml.rels b/tests/test-data/hyperlinks/word/_rels/document.xml.rels
new file mode 100644
index 0000000..baa4102
--- /dev/null
+++ b/tests/test-data/hyperlinks/word/_rels/document.xml.rels
@@ -0,0 +1,10 @@
+<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
+ <Relationship Id="rId8" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml" />
+ <Relationship Id="rId3" Type="http://schemas.microsoft.com/office/2007/relationships/stylesWithEffects" Target="stylesWithEffects.xml" />
+ <Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml" />
+ <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml" />
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml" />
+ <Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="http://www.example.com" TargetMode="External" />
+ <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml" />
+ <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml" />
+</Relationships> \ No newline at end of file
diff --git a/tests/test-data/hyperlinks/word/document.xml b/tests/test-data/hyperlinks/word/document.xml
new file mode 100644
index 0000000..f1ce575
--- /dev/null
+++ b/tests/test-data/hyperlinks/word/document.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" ?>
+<w:document mc:Ignorable="w14 wp14" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
+ <w:body>
+ <w:p w:rsidR="00E01ECE" w:rsidRDefault="000636A7">
+ <w:hyperlink r:id="rId6" w:history="1">
+ <w:r>
+ <w:t>coconuts</w:t>
+ </w:r>
+ </w:hyperlink>
+ </w:p>
+ <w:sectPr w:rsidR="00E01ECE">
+ <w:pgSz w:h="16838" w:w="11906"/>
+ <w:pgMar w:bottom="1440" w:footer="708" w:gutter="0" w:header="708" w:left="1440" w:right="1440" w:top="1440"/>
+ <w:cols w:space="708"/>
+ <w:docGrid w:linePitch="360"/>
+ </w:sectPr>
+ </w:body>
+</w:document>
diff --git a/tests/testing.js b/tests/testing.js
index f2cf55a..af8ec94 100644
--- a/tests/testing.js
+++ b/tests/testing.js
@@ -23,10 +23,15 @@ function testData(testDataPath) {
function createFakeDocxFile(files) {
function read(path) {
- return files[path];
+ return q.when(files[path]);
+ }
+
+ function exists(path) {
+ return !!files[path];
}
return {
- read: read
+ read: read,
+ exists: exists
};
}