diff options
| author | Michael Williamson <mike@zwobble.org> | 2015-07-03 21:31:00 +0100 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2015-07-03 21:31:00 +0100 |
| commit | 792aa9fb8cab1b68c585f09336758e2c76e3e8d5 (patch) | |
| tree | e492c0a1165fc08a70198d8927346a8ec6bbe15d /lib | |
| parent | 419eb705048dae39807ffeac7bce8d52563d7013 (diff) | |
Extract reading of document body to separate module
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/docx/body-reader.js | 342 | ||||
| -rw-r--r-- | lib/docx/document-xml-reader.js | 338 | ||||
| -rw-r--r-- | lib/docx/docx-reader.js | 22 |
3 files changed, 362 insertions, 340 deletions
diff --git a/lib/docx/body-reader.js b/lib/docx/body-reader.js new file mode 100644 index 0000000..e6d8857 --- /dev/null +++ b/lib/docx/body-reader.js @@ -0,0 +1,342 @@ +exports.BodyReader = BodyReader; + +var _ = require("underscore"); + +var documents = require("../documents"); +var Result = require("../results").Result; +var warning = require("../results").warning; + + +function BodyReader(options) { + var relationships = options.relationships; + var contentTypes = options.contentTypes; + var docxFile = options.docxFile; + var numbering = options.numbering; + var styles = options.styles; + + function readXmlElements(elements) { + var results = elements.map(readXmlElement); + return combineResults(results); + } + + 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)) { + var message = warning("An unrecognised element was ignored: " + element.name); + return emptyResultWithMessages([message]); + } + } + return emptyResult(); + } + + function readRunProperties(element) { + var properties = { + type: "runProperties" + }; + + readRunStyle(properties, element); + + var verticalAlignmentElement = element.first("w:vertAlign"); + if (verticalAlignmentElement) { + properties.verticalAlignment = verticalAlignmentElement.attributes["w:val"]; + } + + properties.isBold = !!element.first("w:b"); + properties.isUnderline = !!element.first("w:u"); + properties.isItalic = !!element.first("w:i"); + properties.isStrikethrough = !!element.first("w:strike"); + + return elementResult(properties); + } + + function readRunStyle(properties, element) { + var styleElement = element.first("w:rStyle"); + if (styleElement) { + var styleId = styleElement.attributes["w:val"]; + properties.styleId = styleId; + if (styleId) { + var style = styles.findCharacterStyleById(styleId); + properties.styleName = style.name; + } + } + } + + function noteReferenceReader(noteType) { + return function(element) { + var noteId = element.attributes["w:id"]; + return elementResult(new documents.NoteReference({ + noteType: noteType, + noteId: noteId + })); + }; + } + + function readChildElements(element) { + return readXmlElements(element.children); + } + + 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 + ); + }) + .insertExtra(); + }, + "w:pPr": function(element) { + var properties = { + type: "paragraphProperties" + }; + + var styleElement = element.first("w:pStyle"); + if (styleElement) { + var styleId = styleElement.attributes["w:val"]; + properties.styleId = styleId; + if (styleId) { + var style = styles.findParagraphStyleById(styleId); + properties.styleName = style.name; + } + } + var alignElement = element.first("w:jc"); + if (alignElement) { + properties.alignment = alignElement.attributes["w:val"]; + } + var numberingPropertiesElement = element.first("w:numPr"); + if (numberingPropertiesElement) { + var levelElement = numberingPropertiesElement.first("w:ilvl"); + var level = levelElement.attributes["w:val"]; + var numId = numberingPropertiesElement.first("w:numId").attributes["w:val"]; + properties.numbering = numbering.findLevel(numId, level); + } + + return elementResult(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": readRunProperties, + "w:t": function(element) { + return elementResult(new documents.Text(element.text())); + }, + "w:tab": function(element) { + return elementResult(new documents.Tab()); + }, + "w:hyperlink": function(element) { + var relationshipId = element.attributes["r:id"]; + var anchor = element.attributes["w:anchor"]; + return readXmlElements(element.children).map(function(children) { + if (relationshipId) { + var href = relationships[relationshipId].target; + return new documents.Hyperlink(children, {href: href}); + } else if (anchor) { + return new documents.Hyperlink(children, {anchor: anchor}); + } else { + return children; + } + }); + }, + "w:tbl": function(element) { + return readXmlElements(element.children).map(documents.Table); + }, + "w:tr": function(element) { + return readXmlElements(element.children).map(documents.TableRow); + }, + "w:tc": function(element) { + return readXmlElements(element.children).map(documents.TableCell); + }, + "w:footnoteReference": noteReferenceReader("footnote"), + "w:endnoteReference": noteReferenceReader("endnote"), + "w:br": function(element) { + var breakType = element.attributes["w:type"]; + if (breakType) { + return emptyResultWithMessages([warning("Unsupported break type: " + breakType)]); + } else { + return elementResult(new documents.LineBreak()); + } + }, + "w:bookmarkStart": function(element){ + var name = element.attributes["w:name"]; + if (name === "_GoBack") { + return emptyResult(); + } else { + return elementResult(new documents.BookmarkStart({name: name})); + } + }, + + "mc:AlternateContent": function(element) { + return readChildElements(element.first("mc:Fallback")); + }, + + "w:ins": readChildElements, + "w:smartTag": readChildElements, + "w:drawing": readChildElements, + "w:pict": function(element) { + return readChildElements(element).toExtra(); + }, + "v:shape": readChildElements, + "v:textbox": readChildElements, + "w:txbxContent": readChildElements, + "wp:inline": readDrawingElement, + "wp:anchor": readDrawingElement + }; + return { + readXmlElement: readXmlElement, + readXmlElements: readXmlElements + }; + + function readDrawingElement(element) { + var blips = element + .getElementsByTagName("a:graphic") + .getElementsByTagName("a:graphicData") + .getElementsByTagName("pic:pic") + .getElementsByTagName("pic:blipFill") + .getElementsByTagName("a:blip"); + + return combineResults(blips.map(readBlip.bind(null, element))); + } + + function readBlip(element, blip) { + var relationshipId = blip.attributes["r:embed"]; + var imagePath = joinZipPath("word", relationships[relationshipId].target); + var readImage = docxFile.read.bind(docxFile, imagePath); + var altText = element.first("wp:docPr").attributes.descr; + var contentType = contentTypes.findContentType(imagePath); + + var image = documents.Image({ + readImage: readImage, + altText: altText, + contentType: contentType + }); + var warnings = supportedImageTypes[contentType] ? + [] : warning("Image of type " + contentType + " is unlikely to display in web browsers"); + return elementResultWithMessages(image, warnings); + } +} + +var supportedImageTypes = { + "image/png": true, + "image/gif": true, + "image/jpeg": true, + "image/svg+xml": true, + "image/tiff": true +}; + +var ignoreElements = { + "w:bookmarkEnd": true, + "w:sectPr": true, + "w:proofErr": true, + "w:lastRenderedPageBreak": true, + "w:commentRangeStart": true, + "w:commentRangeEnd": true, + "w:commentReference": true, + "w:del": true, + "w:footnoteRef": true, + "w:endnoteRef": true, + "w:tblPr": true, + "w:tblGrid": true, + "w:tcPr": true +}; + +function isParagraphProperties(element) { + return element.type === "paragraphProperties"; +} + +function isRunProperties(element) { + return element.type === "runProperties"; +} + +function negate(predicate) { + return function(value) { + return !predicate(value); + }; +} + +function joinZipPath(first, second) { + // In general, we should check first and second for trailing and leading slashes, + // but in our specific case this seems to be sufficient + return first + "/" + second; +} + + + +function emptyResultWithMessages(messages) { + return new ReadResult(null, null, messages); +} + +function emptyResult() { + return new ReadResult(null); +} + +function elementResult(element) { + return new ReadResult(element); +} + +function elementResultWithMessages(element, messages) { + return new ReadResult(element, null, messages); +} + +function ReadResult(element, extra, messages) { + this.value = element || []; + this.extra = extra; + this._result = new Result({ + element: this.value, + extra: extra + }, messages); + this.messages = this._result.messages; +} + +ReadResult.prototype.toExtra = function() { + return new ReadResult(null, joinElements(this.extra, this.value), this.messages); +}; + +ReadResult.prototype.insertExtra = function() { + var extra = this.extra; + if (extra && extra.length) { + return new ReadResult(joinElements(this.value, extra), null, this.messages); + } else { + return this; + } +}; + +ReadResult.prototype.map = function(func) { + var result = this._result.map(function(value) { + return func(value.element); + }); + return new ReadResult(result.value, this.extra, result.messages); +}; + +ReadResult.prototype.flatMap = function(func) { + var result = this._result.flatMap(function(value) { + var funcResult = func(value.element); + return funcResult._result || elementResultWithMessages(funcResult.value, funcResult.messages)._result; + }); + return new ReadResult(result.value.element, joinElements(this.extra, result.value.extra), result.messages); +}; + +function combineResults(results) { + var result = Result.combine(_.pluck(results, "_result")); + return new ReadResult( + _.flatten(_.pluck(result.value, "element")), + _.filter(_.flatten(_.pluck(result.value, "extra")), function(x) { return x; }), + result.messages + ); +} + +function joinElements(first, second) { + return _.flatten([first, second]); +} diff --git a/lib/docx/document-xml-reader.js b/lib/docx/document-xml-reader.js index 1d36d91..fdf451f 100644 --- a/lib/docx/document-xml-reader.js +++ b/lib/docx/document-xml-reader.js @@ -1,25 +1,18 @@ exports.DocumentXmlReader = DocumentXmlReader; -var _ = require("underscore"); - var documents = require("../documents"); var Result = require("../results").Result; -var warning = require("../results").warning; function DocumentXmlReader(options) { - var relationships = options.relationships; - var contentTypes = options.contentTypes; - var docxFile = options.docxFile; - var numbering = options.numbering; - var styles = options.styles; + var bodyReader = options.bodyReader; var rawNotes = (options.footnotes || []).concat(options.endnotes || []); function convertXmlToDocument(documentXml) { var body = documentXml.root.first("w:body"); - var result = readXmlElements(body.children) + var result = bodyReader.readXmlElements(body.children) .flatMap(function(children) { return readNotes().map(function(notes) { return new documents.Document(children, {notes: notes}); @@ -29,8 +22,8 @@ function DocumentXmlReader(options) { } function readNotes() { - return combineResults(rawNotes.map(function(rawNote) { - return readXmlElements(rawNote.body).map(function(body) { + return Result.combine(rawNotes.map(function(rawNote) { + return bodyReader.readXmlElements(rawNote.body).map(function(body) { return new documents.Note({ noteType: rawNote.noteType, noteId: rawNote.id, @@ -41,329 +34,8 @@ function DocumentXmlReader(options) { return new documents.Notes(notes); }); } - - function readXmlElements(elements) { - var results = elements.map(readXmlElement); - return combineResults(results); - } - - 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)) { - var message = warning("An unrecognised element was ignored: " + element.name); - return emptyResultWithMessages([message]); - } - } - return emptyResult(); - } - - function readRunProperties(element) { - var properties = { - type: "runProperties" - }; - - readRunStyle(properties, element); - - var verticalAlignmentElement = element.first("w:vertAlign"); - if (verticalAlignmentElement) { - properties.verticalAlignment = verticalAlignmentElement.attributes["w:val"]; - } - - properties.isBold = !!element.first("w:b"); - properties.isUnderline = !!element.first("w:u"); - properties.isItalic = !!element.first("w:i"); - properties.isStrikethrough = !!element.first("w:strike"); - - return elementResult(properties); - } - - function readRunStyle(properties, element) { - var styleElement = element.first("w:rStyle"); - if (styleElement) { - var styleId = styleElement.attributes["w:val"]; - properties.styleId = styleId; - if (styleId) { - var style = styles.findCharacterStyleById(styleId); - properties.styleName = style.name; - } - } - } - - function noteReferenceReader(noteType) { - return function(element) { - var noteId = element.attributes["w:id"]; - return elementResult(new documents.NoteReference({ - noteType: noteType, - noteId: noteId - })); - }; - } - function readChildElements(element) { - return readXmlElements(element.children); - } - - 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 - ); - }) - .insertExtra(); - }, - "w:pPr": function(element) { - var properties = { - type: "paragraphProperties" - }; - - var styleElement = element.first("w:pStyle"); - if (styleElement) { - var styleId = styleElement.attributes["w:val"]; - properties.styleId = styleId; - if (styleId) { - var style = styles.findParagraphStyleById(styleId); - properties.styleName = style.name; - } - } - var alignElement = element.first("w:jc"); - if (alignElement) { - properties.alignment = alignElement.attributes["w:val"]; - } - var numberingPropertiesElement = element.first("w:numPr"); - if (numberingPropertiesElement) { - var levelElement = numberingPropertiesElement.first("w:ilvl"); - var level = levelElement.attributes["w:val"]; - var numId = numberingPropertiesElement.first("w:numId").attributes["w:val"]; - properties.numbering = numbering.findLevel(numId, level); - } - - return elementResult(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": readRunProperties, - "w:t": function(element) { - return elementResult(new documents.Text(element.text())); - }, - "w:tab": function(element) { - return elementResult(new documents.Tab()); - }, - "w:hyperlink": function(element) { - var relationshipId = element.attributes["r:id"]; - var anchor = element.attributes["w:anchor"]; - return readXmlElements(element.children).map(function(children) { - if (relationshipId) { - var href = relationships[relationshipId].target; - return new documents.Hyperlink(children, {href: href}); - } else if (anchor) { - return new documents.Hyperlink(children, {anchor: anchor}); - } else { - return children; - } - }); - }, - "w:tbl": function(element) { - return readXmlElements(element.children).map(documents.Table); - }, - "w:tr": function(element) { - return readXmlElements(element.children).map(documents.TableRow); - }, - "w:tc": function(element) { - return readXmlElements(element.children).map(documents.TableCell); - }, - "w:footnoteReference": noteReferenceReader("footnote"), - "w:endnoteReference": noteReferenceReader("endnote"), - "w:br": function(element) { - var breakType = element.attributes["w:type"]; - if (breakType) { - return emptyResultWithMessages([warning("Unsupported break type: " + breakType)]); - } else { - return elementResult(new documents.LineBreak()); - } - }, - "w:bookmarkStart": function(element){ - var name = element.attributes["w:name"]; - if (name === "_GoBack") { - return emptyResult(); - } else { - return elementResult(new documents.BookmarkStart({name: name})); - } - }, - - "mc:AlternateContent": function(element) { - return readChildElements(element.first("mc:Fallback")); - }, - - "w:ins": readChildElements, - "w:smartTag": readChildElements, - "w:drawing": readChildElements, - "w:pict": function(element) { - return readChildElements(element).toExtra(); - }, - "v:shape": readChildElements, - "v:textbox": readChildElements, - "w:txbxContent": readChildElements, - "wp:inline": readDrawingElement, - "wp:anchor": readDrawingElement - }; return { - convertXmlToDocument: convertXmlToDocument, - readXmlElement: readXmlElement + convertXmlToDocument: convertXmlToDocument }; - - function readDrawingElement(element) { - var blips = element - .getElementsByTagName("a:graphic") - .getElementsByTagName("a:graphicData") - .getElementsByTagName("pic:pic") - .getElementsByTagName("pic:blipFill") - .getElementsByTagName("a:blip"); - - return combineResults(blips.map(readBlip.bind(null, element))); - } - - function readBlip(element, blip) { - var relationshipId = blip.attributes["r:embed"]; - var imagePath = joinZipPath("word", relationships[relationshipId].target); - var readImage = docxFile.read.bind(docxFile, imagePath); - var altText = element.first("wp:docPr").attributes.descr; - var contentType = contentTypes.findContentType(imagePath); - - var image = documents.Image({ - readImage: readImage, - altText: altText, - contentType: contentType - }); - var warnings = supportedImageTypes[contentType] ? - [] : warning("Image of type " + contentType + " is unlikely to display in web browsers"); - return elementResultWithMessages(image, warnings); - } -} - -var supportedImageTypes = { - "image/png": true, - "image/gif": true, - "image/jpeg": true, - "image/svg+xml": true, - "image/tiff": true -}; - -var ignoreElements = { - "w:bookmarkEnd": true, - "w:sectPr": true, - "w:proofErr": true, - "w:lastRenderedPageBreak": true, - "w:commentRangeStart": true, - "w:commentRangeEnd": true, - "w:commentReference": true, - "w:del": true, - "w:footnoteRef": true, - "w:endnoteRef": true, - "w:tblPr": true, - "w:tblGrid": true, - "w:tcPr": true -}; - -function isParagraphProperties(element) { - return element.type === "paragraphProperties"; -} - -function isRunProperties(element) { - return element.type === "runProperties"; -} - -function negate(predicate) { - return function(value) { - return !predicate(value); - }; -} - -function joinZipPath(first, second) { - // In general, we should check first and second for trailing and leading slashes, - // but in our specific case this seems to be sufficient - return first + "/" + second; -} - - - -function emptyResultWithMessages(messages) { - return new ReadResult(null, null, messages); -} - -function emptyResult() { - return new ReadResult(null); -} - -function elementResult(element) { - return new ReadResult(element); -} - -function elementResultWithMessages(element, messages) { - return new ReadResult(element, null, messages); -} - -function ReadResult(element, extra, messages) { - this.value = element || []; - this.extra = extra; - this._result = new Result({ - element: this.value, - extra: extra - }, messages); - this.messages = this._result.messages; -} - -ReadResult.prototype.toExtra = function() { - return new ReadResult(null, joinElements(this.extra, this.value), this.messages); -}; - -ReadResult.prototype.insertExtra = function() { - var extra = this.extra; - if (extra && extra.length) { - return new ReadResult(joinElements(this.value, extra), null, this.messages); - } else { - return this; - } -}; - -ReadResult.prototype.map = function(func) { - var result = this._result.map(function(value) { - return func(value.element); - }); - return new ReadResult(result.value, this.extra, result.messages); -}; - -ReadResult.prototype.flatMap = function(func) { - var result = this._result.flatMap(function(value) { - return func(value.element)._result; - }); - return new ReadResult(result.value.element, joinElements(this.extra, result.value.extra), result.messages); -}; - -function combineResults(results) { - var result = Result.combine(_.pluck(results, "_result")); - return new ReadResult( - _.flatten(_.pluck(result.value, "element")), - _.filter(_.flatten(_.pluck(result.value, "extra")), function(x) { return x; }), - result.messages - ); -} - -function joinElements(first, second) { - return _.flatten([first, second]); } diff --git a/lib/docx/docx-reader.js b/lib/docx/docx-reader.js index 23f821c..554e329 100644 --- a/lib/docx/docx-reader.js +++ b/lib/docx/docx-reader.js @@ -4,6 +4,7 @@ var promises = require("../promises"); var unzip = require("../unzip"); var readXmlFromZipFile = require("./office-xml-reader").readXmlFromZipFile; +var BodyReader = require("./body-reader").BodyReader; var DocumentXmlReader = require("./document-xml-reader").DocumentXmlReader; var relationshipsReader = require("./relationships-reader"); var contentTypesReader = require("./content-types-reader"); @@ -19,17 +20,24 @@ function read(options) { readRelationshipsFromZipFile(docxFile), readContentTypesFromZipFile(docxFile), readNumberingFromZipFile(docxFile), - readStylesFromZipFile(docxFile), - readFootnotesFromZipFile(docxFile), - readEndnotesFromZipFile(docxFile), - readXmlFromZipFile(docxFile, "word/document.xml"), - ]).spread(function(relationships, contentTypes, numbering, styles, footnotes, endnotes, documentXml) { - var reader = new DocumentXmlReader({ + readStylesFromZipFile(docxFile) + ]).spread(function(relationships, contentTypes, numbering, styles) { + var bodyReader = new BodyReader({ relationships: relationships, contentTypes: contentTypes, docxFile: docxFile, numbering: numbering, - styles: styles, + styles: styles + }); + return promises.all([ + bodyReader, + readFootnotesFromZipFile(docxFile), + readEndnotesFromZipFile(docxFile), + readXmlFromZipFile(docxFile, "word/document.xml") + ]); + }).spread(function(bodyReader, footnotes, endnotes, documentXml) { + var reader = new DocumentXmlReader({ + bodyReader: bodyReader, footnotes: footnotes, endnotes: endnotes }); |
