diff options
| author | Michael Williamson <mike@zwobble.org> | 2013-04-01 17:35:15 +0100 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2013-04-01 17:35:15 +0100 |
| commit | bd8b2786247308c7bd82dc3cda627a23b4898958 (patch) | |
| tree | e30fc62c22bbb0dc358155c5c0caebb126688091 | |
| parent | 25109ca7e318e188229cac92552ba0dcd7e897a1 (diff) | |
Implement reading of hyperlink elements
| -rw-r--r-- | lib/docx-reader.js | 203 | ||||
| -rw-r--r-- | tests/docx-reader.tests.js | 30 | ||||
| -rw-r--r-- | tests/test-data/hyperlinks/word/_rels/document.xml.rels | 10 | ||||
| -rw-r--r-- | tests/test-data/hyperlinks/word/document.xml | 18 | ||||
| -rw-r--r-- | tests/testing.js | 9 |
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 }; } |
