Rework docx parser, add comments to docx
This commit is contained in:
parent
3b2c7634f0
commit
f4c4f2553b
@ -26,6 +26,7 @@ Motions:
|
||||
- Bugfix: Added more distance in motion PDF for DEL-tags in new lines [#3211].
|
||||
- Added warning message if an edit dialog was already opened by another
|
||||
client [#3212].
|
||||
- Reworked DOCX export parser and added comments to DOCX [#3258].
|
||||
|
||||
Users:
|
||||
- User without permission to see users can now see agenda item speakers,
|
||||
|
356
openslides/core/static/js/core/docx.js
Normal file
356
openslides/core/static/js/core/docx.js
Normal file
@ -0,0 +1,356 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.core.docx', [])
|
||||
|
||||
.factory('Html2DocxConverter', [
|
||||
'$q',
|
||||
'ImageConverter',
|
||||
function ($q, ImageConverter) {
|
||||
var PAGEBREAK = '<w:p><w:r><w:br w:type="page" /></w:r></w:p>';
|
||||
|
||||
var createInstance = function () {
|
||||
var converter = {
|
||||
imageMap: {},
|
||||
documentImages: [],
|
||||
relationships: [],
|
||||
contentTypes: [],
|
||||
};
|
||||
|
||||
var html2docx = function (html) {
|
||||
var docx = '';
|
||||
var tagStack = [];
|
||||
|
||||
// With this variable, we keep track, if we are currently inside or outside of a paragraph.
|
||||
var inParagraph = true;
|
||||
// the text may not begin with a paragraph. If so, append one because word needs it.
|
||||
var skipFirstParagraphClosing = true;
|
||||
|
||||
var handleTag = function (tag) {
|
||||
if (tag.charAt(0) == "/") { // A closing tag
|
||||
// remove from stack
|
||||
tagStack.pop();
|
||||
|
||||
// Special: end paragraphs
|
||||
if (tag.startsWith('/p')) {
|
||||
docx += '</w:p>';
|
||||
inParagraph = false;
|
||||
}
|
||||
} else { // now all other tags
|
||||
var tagname = tag.split(' ')[0];
|
||||
handleNamedTag(tagname, tag);
|
||||
}
|
||||
return docx;
|
||||
};
|
||||
var handleNamedTag = function (tagname, fullTag) {
|
||||
var tag = {
|
||||
tag: tagname,
|
||||
attrs: {},
|
||||
};
|
||||
switch (tagname) {
|
||||
case 'p':
|
||||
if (inParagraph && !skipFirstParagraphClosing) {
|
||||
// End the paragrapth, if there is one
|
||||
docx += '</w:p>';
|
||||
}
|
||||
skipFirstParagraphClosing = false;
|
||||
docx += '<w:p>';
|
||||
inParagraph = true;
|
||||
break;
|
||||
case 'span':
|
||||
var styleRegex = /(?:\"|\;\s?)([a-zA-z\-]+)\:\s?([a-zA-Z0-9\-\#]+)/g, matchSpan;
|
||||
while ((matchSpan = styleRegex.exec(fullTag)) !== null) {
|
||||
switch (matchSpan[1]) {
|
||||
case 'color':
|
||||
tag.attrs.color = matchSpan[2].slice(1); // cut off the #
|
||||
break;
|
||||
case 'background-color':
|
||||
tag.attrs.backgroundColor = matchSpan[2].slice(1); // cut off the #
|
||||
break;
|
||||
case 'text-decoration':
|
||||
if (matchSpan[2] === 'underline') {
|
||||
tag.attrs.underline = true;
|
||||
} else if (matchSpan[2] === 'line-through') {
|
||||
tag.attrs.strike = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'a':
|
||||
var hrefRegex = /href="([^"]+)"/g;
|
||||
var href = hrefRegex.exec(fullTag)[1];
|
||||
tag.href = href;
|
||||
break;
|
||||
case 'img':
|
||||
imageTag(tag, fullTag);
|
||||
break;
|
||||
}
|
||||
if (tagname !== 'img' && tagname !== 'p') {
|
||||
tagStack.push(tag);
|
||||
}
|
||||
};
|
||||
var imageTag = function (tag, fullTag) {
|
||||
// images has to be placed instantly, so there is no use of 'tag'.
|
||||
var image = {};
|
||||
var attributeRegex = /(\w+)=\"([^\"]*)\"/g, attributeMatch;
|
||||
while ((attributeMatch = attributeRegex.exec(fullTag)) !== null) {
|
||||
image[attributeMatch[1]] = attributeMatch[2];
|
||||
}
|
||||
if (image.src && converter.imageMap[image.src]) {
|
||||
image.width = converter.imageMap[image.src].width;
|
||||
image.height = converter.imageMap[image.src].height;
|
||||
|
||||
var rrId = converter.relationships.length + 1;
|
||||
var imageId = converter.documentImages.length + 1;
|
||||
|
||||
// set name ('pic.jpg'), title, ext ('jpg'), mime ('image/jpeg')
|
||||
image.name = _.last(image.src.split('/'));
|
||||
|
||||
var tmp = image.name.split('.');
|
||||
image.ext = tmp.splice(-1);
|
||||
|
||||
// set name without extension as title if there isn't a title
|
||||
if (!image.title) {
|
||||
image.title = tmp.join('.');
|
||||
}
|
||||
|
||||
image.mime = 'image/' + image.ext;
|
||||
if (image.ext == 'jpe' || image.ext == 'jpg') {
|
||||
image.mime = 'image/jpeg';
|
||||
}
|
||||
|
||||
// x and y for the container and picture size in EMU (assuming 96dpi)!
|
||||
var x = image.width * 914400 / 96;
|
||||
var y = image.height * 914400 / 96;
|
||||
|
||||
// the image does not belong into a paragraph in ooxml
|
||||
if (inParagraph) {
|
||||
docx += '</w:p>';
|
||||
}
|
||||
docx += '<w:p><w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extend cx="' + x +'" cy="' + y + '"/><wp:effectExtent l="0" t="0" r="0" b="0"/>' +
|
||||
'<wp:docPr id="' + imageId + '" name="' + image.name + '" title="' + image.title + '" descr="' + image.title + '"/><wp:cNvGraphicFramePr>' +
|
||||
'<a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr>' +
|
||||
'<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">' +
|
||||
'<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="' + imageId + '" name="' +
|
||||
image.name + '" title="' + image.title + '" descr="' + image.title + '"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="rrId' + rrId + '"/><a:stretch>' +
|
||||
'<a:fillRect/></a:stretch></pic:blipFill><pic:spPr bwMode="auto"><a:xfrm><a:off x="0" y="0"/><a:ext cx="' + x + '" cy="' + y + '"/></a:xfrm>' +
|
||||
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r></w:p>';
|
||||
|
||||
// inParagraph stays untouched, the documents paragraph state is restored here
|
||||
if (inParagraph) {
|
||||
docx += '<w:p>';
|
||||
}
|
||||
|
||||
// entries in documentImages, relationships and contentTypes
|
||||
converter.documentImages.push({
|
||||
src: image.src,
|
||||
zipPath: 'word/media/' + image.name
|
||||
});
|
||||
converter.relationships.push({
|
||||
Id: 'rrId' + rrId,
|
||||
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
|
||||
Target: 'media/' + image.name
|
||||
});
|
||||
converter.contentTypes.push({
|
||||
PartName: '/word/media/' + image.name,
|
||||
ContentType: image.mime
|
||||
});
|
||||
}
|
||||
};
|
||||
var handleText = function (text) {
|
||||
// Start a new paragraph, if only loose text is there
|
||||
if (!inParagraph) {
|
||||
docx += '<w:p>';
|
||||
inParagraph = true;
|
||||
}
|
||||
var docxPart = '<w:r><w:rPr>';
|
||||
var hyperlink = false;
|
||||
tagStack.forEach(function (tag) {
|
||||
switch (tag.tag) {
|
||||
case 'b':
|
||||
case 'strong':
|
||||
docxPart += '<w:b/><w:bCs/>';
|
||||
break;
|
||||
case 'em':
|
||||
case 'i':
|
||||
docxPart += '<w:i/><w:iCs/>';
|
||||
break;
|
||||
case 'span':
|
||||
for (var key in tag.attrs) {
|
||||
switch (key) {
|
||||
case 'color':
|
||||
docxPart += '<w:color w:val="' + tag.attrs[key] + '"/>';
|
||||
break;
|
||||
case 'backgroundColor':
|
||||
docxPart += '<w:shd w:fill="' + tag.attrs[key] + '"/>';
|
||||
break;
|
||||
case 'underline':
|
||||
docxPart += '<w:u w:val="single"/>';
|
||||
break;
|
||||
case 'strike':
|
||||
docxPart += '<w:strike/>';
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'u':
|
||||
docxPart += '<w:u w:val="single"/>';
|
||||
break;
|
||||
case 'strike':
|
||||
docxPart += '<w:strike/>';
|
||||
break;
|
||||
case 'a':
|
||||
var id = converter.relationships.length + 1;
|
||||
docxPart = '<w:hyperlink r:id="rrId' + id + '">' + docxPart;
|
||||
docxPart += '<w:rStyle w:val="Internetlink"/>';
|
||||
converter.relationships.push({
|
||||
Id: 'rrId' + id,
|
||||
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
|
||||
Target: tag.href,
|
||||
TargetMode: 'External'
|
||||
});
|
||||
hyperlink = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
docxPart += '</w:rPr><w:t>' + text + '</w:t></w:r>';
|
||||
if (hyperlink) {
|
||||
docxPart += '</w:hyperlink>';
|
||||
}
|
||||
|
||||
// append to docx
|
||||
docx += docxPart;
|
||||
return docx;
|
||||
};
|
||||
|
||||
var replaceEntities = function () {
|
||||
// replacing of special symbols:
|
||||
docx = docx.replace(new RegExp('\ä\;', 'g'), 'ä');
|
||||
docx = docx.replace(new RegExp('\ü\;', 'g'), 'ü');
|
||||
docx = docx.replace(new RegExp('\ö\;', 'g'), 'ö');
|
||||
docx = docx.replace(new RegExp('\Ä\;', 'g'), 'Ä');
|
||||
docx = docx.replace(new RegExp('\Ü\;', 'g'), 'Ü');
|
||||
docx = docx.replace(new RegExp('\Ö\;', 'g'), 'Ö');
|
||||
docx = docx.replace(new RegExp('\ß\;', 'g'), 'ß');
|
||||
docx = docx.replace(new RegExp('\ \;', 'g'), ' ');
|
||||
docx = docx.replace(new RegExp('\§\;', 'g'), '§');
|
||||
|
||||
// remove all entities except gt, lt and amp
|
||||
var entityRegex = /\&(?!gt|lt|amp)\w+\;/g, matchEntry, indexes = [];
|
||||
while ((matchEntry = entityRegex.exec(docx)) !== null) {
|
||||
indexes.push({
|
||||
startId: matchEntry.index,
|
||||
stopId: matchEntry.index + matchEntry[0].length
|
||||
});
|
||||
}
|
||||
for (var i = indexes.length - 1; i>=0; i--) {
|
||||
docx = docx.substring(0, indexes[i].startId) + docx.substring(indexes[i].stopId, docx.length);
|
||||
}
|
||||
};
|
||||
|
||||
var parse = function () {
|
||||
if (html.substring(0,3) != '<p>') {
|
||||
docx += '<w:p>';
|
||||
skipFirstParagraphClosing = false;
|
||||
}
|
||||
html = html.split(/(<|>)/g);
|
||||
// remove whitespaces and > brackets. Leave < brackets in there to check, whether
|
||||
// the following string is a tag or text.
|
||||
html = _.filter(html, function (part) {
|
||||
var skippedCharsRegex = new RegExp('^([\s\n\r]|>)*$', 'gm');
|
||||
return !skippedCharsRegex.test(part);
|
||||
});
|
||||
|
||||
for (var i = 0; i < html.length; i++) {
|
||||
if (html[i] === '<') {
|
||||
i++;
|
||||
handleTag(html[i]);
|
||||
} else {
|
||||
handleText(html[i]);
|
||||
}
|
||||
}
|
||||
// for finishing close the last paragraph (if open)
|
||||
if (inParagraph) {
|
||||
docx += '</w:p>';
|
||||
}
|
||||
|
||||
replaceEntities();
|
||||
|
||||
return docx;
|
||||
};
|
||||
|
||||
return parse();
|
||||
};
|
||||
|
||||
// return a wrapper function for html2docx, that fetches all the images.
|
||||
converter.html2docx = function (html) {
|
||||
var imageSources = _.map($(html).find('img'), function (element) {
|
||||
return element.getAttribute('src');
|
||||
});
|
||||
// Don't get images multiple times; just if the converter has not seen them befor.
|
||||
imageSources = _.filter(imageSources, function (src) {
|
||||
return !converter.imageMap[src];
|
||||
});
|
||||
return $q(function (resolve) {
|
||||
ImageConverter.toBase64(imageSources).then(function (_imageMap) {
|
||||
_.forEach(_imageMap, function (value, key) {
|
||||
converter.imageMap[key] = value;
|
||||
});
|
||||
var docx = html2docx(html);
|
||||
resolve(docx);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
converter.updateZipFile = function (zip) {
|
||||
var updateRelationships = function (oldContent) {
|
||||
var content = oldContent.split('\n');
|
||||
_.forEach(converter.relationships, function (relationship) {
|
||||
content[1] += '<Relationship';
|
||||
_.forEach(relationship, function (value, key) {
|
||||
content[1] += ' ' + key + '="' + value + '"';
|
||||
});
|
||||
content[1] += '/>';
|
||||
});
|
||||
return content.join('\n');
|
||||
};
|
||||
var updateContentTypes = function (oldContent) {
|
||||
var content = oldContent.split('\n');
|
||||
_.forEach(converter.contentTypes, function (type) {
|
||||
content[1] += '<Override';
|
||||
_.forEach(type, function (value, key) {
|
||||
content[1] += ' ' + key + '="' + value + '"';
|
||||
});
|
||||
content[1] += '/>';
|
||||
});
|
||||
return content.join('\n');
|
||||
};
|
||||
// update relationships from 'relationships'
|
||||
var relationships = updateRelationships(zip.file('word/_rels/document.xml.rels').asText());
|
||||
zip.file('word/_rels/document.xml.rels', relationships);
|
||||
|
||||
// update content type from 'contentTypes'
|
||||
var contentTypes = updateContentTypes(zip.file('[Content_Types].xml').asText());
|
||||
zip.file('[Content_Types].xml', contentTypes);
|
||||
|
||||
converter.documentImages = _.uniqBy(converter.documentImages, 'src');
|
||||
_.forEach(converter.documentImages, function (image) {
|
||||
var dataUrl = converter.imageMap[image.src].data;
|
||||
var base64 = dataUrl.split(',')[1];
|
||||
zip.file(image.zipPath, base64, {base64: true});
|
||||
});
|
||||
return zip;
|
||||
};
|
||||
|
||||
return converter;
|
||||
};
|
||||
|
||||
return {
|
||||
createInstance: createInstance,
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
})();
|
@ -143,8 +143,8 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
'$q',
|
||||
'Config',
|
||||
'PDFLayout',
|
||||
'PdfImageConverter',
|
||||
function($q, Config, PDFLayout, PdfImageConverter) {
|
||||
'ImageConverter',
|
||||
function($q, Config, PDFLayout, ImageConverter) {
|
||||
/**
|
||||
* Provides the global document
|
||||
* @constructor
|
||||
@ -328,7 +328,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
logoHeaderUrl,
|
||||
logoFooterUrl
|
||||
];
|
||||
PdfImageConverter.toBase64(imageSources).then(function (_imageMap) {
|
||||
ImageConverter.toBase64(imageSources).then(function (_imageMap) {
|
||||
imageMap = _imageMap;
|
||||
resolve({
|
||||
getDocument: getDocument
|
||||
@ -948,7 +948,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
}
|
||||
])
|
||||
|
||||
.factory('PdfImageConverter', [
|
||||
.factory('ImageConverter', [
|
||||
'$q',
|
||||
'PDFLayout',
|
||||
function ($q, PDFLayout) {
|
||||
|
@ -2,24 +2,28 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.motions.docx', [])
|
||||
angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
|
||||
|
||||
.factory('MotionDocxExport', [
|
||||
'$http',
|
||||
'$q',
|
||||
'operator',
|
||||
'Config',
|
||||
'Category',
|
||||
'gettextCatalog',
|
||||
'FileSaver',
|
||||
'lineNumberingService',
|
||||
function ($http, $q, Config, Category, gettextCatalog, FileSaver, lineNumberingService) {
|
||||
'Html2DocxConverter',
|
||||
function ($http, $q, operator, Config, Category, gettextCatalog, FileSaver, lineNumberingService, Html2DocxConverter) {
|
||||
|
||||
var PAGEBREAK = '<w:p><w:r><w:br w:type="page" /></w:r></w:p>';
|
||||
var TAGS_NO_PARAM = ['b', 'strong', 'em', 'i'];
|
||||
/*var TAGS_NO_PARAM = ['b', 'strong', 'em', 'i'];
|
||||
|
||||
var images;
|
||||
var relationships;
|
||||
var contentTypes;
|
||||
var contentTypes; */
|
||||
|
||||
var converter;
|
||||
|
||||
var getData = function (motions, params) {
|
||||
var data = {};
|
||||
@ -48,11 +52,15 @@ angular.module('OpenSlidesApp.motions.docx', [])
|
||||
|
||||
// motions
|
||||
data.tableofcontents_translation = gettextCatalog.getString('Table of contents');
|
||||
data.motions = getMotionFullData(motions, params);
|
||||
data.motions_list = getMotionShortData(motions);
|
||||
data.no_motions = gettextCatalog.getString('No motions available.');
|
||||
|
||||
return data;
|
||||
return $q(function (resolve) {
|
||||
getMotionFullData(motions, params).then(function (motionData) {
|
||||
data.motions = motionData;
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var getCategoriesData = function (categories) {
|
||||
@ -74,289 +82,95 @@ angular.module('OpenSlidesApp.motions.docx', [])
|
||||
};
|
||||
|
||||
var getMotionFullData = function (motions, params) {
|
||||
// All translations
|
||||
var translation = gettextCatalog.getString('Motion'),
|
||||
sequential_translation = gettextCatalog.getString('Sequential number'),
|
||||
submitters_translation = gettextCatalog.getString('Submitters'),
|
||||
status_translation = gettextCatalog.getString('Status'),
|
||||
reason_translation = gettextCatalog.getString('Reason'),
|
||||
data = _.map(motions, function (motion) {
|
||||
comment_translation = gettextCatalog.getString('Comments');
|
||||
// promises for create the actual motion data
|
||||
var promises = _.map(motions, function (motion) {
|
||||
var text = motion.getTextByMode(params.changeRecommendationMode, null, null, false);
|
||||
var reason = params.includeReason ? motion.getReason() : '';
|
||||
return {
|
||||
var comments = params.includeComments ? getMotionComments(motion) : [];
|
||||
|
||||
// Data for one motions. Must include translations, ...
|
||||
var motionData = {
|
||||
// Translations
|
||||
motion_translation: translation,
|
||||
sequential_translation: sequential_translation,
|
||||
submitters_translation: submitters_translation,
|
||||
reason_translation: reason.length === 0 ? '' : reason_translation,
|
||||
status_translation: status_translation,
|
||||
comment_translation: comments.length === 0 ? '' : comment_translation,
|
||||
// Actual data
|
||||
id: motion.id,
|
||||
identifier: motion.identifier,
|
||||
title: motion.getTitle(),
|
||||
submitters_translation: submitters_translation,
|
||||
submitters: _.map(motion.submitters, function (submitter) {
|
||||
return submitter.get_full_name();
|
||||
}).join(', '),
|
||||
status_translation: status_translation,
|
||||
status: motion.getStateName(),
|
||||
// Miscellaneous stuff
|
||||
preamble: gettextCatalog.getString(Config.get('motions_preamble').value),
|
||||
text: html2docx(text),
|
||||
reason_translation: reason.length === 0 ? '' : reason_translation,
|
||||
reason: html2docx(reason),
|
||||
pagebreak: PAGEBREAK,
|
||||
};
|
||||
// converting html to docx is async, so text, reason and comments are inserted here.
|
||||
return $q(function (resolve) {
|
||||
var convertPromises = _.map(comments, function (comment) {
|
||||
return converter.html2docx(comment.comment).then(function (commentAsDocx) {
|
||||
comment.comment = commentAsDocx;
|
||||
});
|
||||
});
|
||||
convertPromises.push(converter.html2docx(text).then(function (textAsDocx) {
|
||||
motionData.text = textAsDocx;
|
||||
}));
|
||||
convertPromises.push(converter.html2docx(reason).then(function (reasonAsDocx) {
|
||||
motionData.reason = reasonAsDocx;
|
||||
}));
|
||||
$q.all(convertPromises).then(function () {
|
||||
motionData.comments = comments;
|
||||
resolve(motionData);
|
||||
});
|
||||
});
|
||||
});
|
||||
// resolve, if all motion data is fetched.
|
||||
return $q(function (resolve) {
|
||||
$q.all(promises).then(function (data) {
|
||||
if (data.length) {
|
||||
// clear pagebreak on last element
|
||||
data[data.length - 1].pagebreak = '';
|
||||
}
|
||||
return data;
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var html2docx = function (html) {
|
||||
var docx = '';
|
||||
var stack = [];
|
||||
|
||||
var isTag = false; // Even if html starts with '<p....' it is split to '', '<', ..., so always no tag at the beginning
|
||||
var hasParagraph = true;
|
||||
var skipFirstParagraphClosing = true;
|
||||
if (html.substring(0,3) != '<p>') {
|
||||
docx += '<w:p>';
|
||||
skipFirstParagraphClosing = false;
|
||||
}
|
||||
html = html.split(/(<|>)/g);
|
||||
|
||||
html.forEach(function (part) {
|
||||
if (part !== '' && part != '\n' && part != '<' && part != '>') {
|
||||
if (isTag) {
|
||||
if (part.startsWith('p')) { /** p **/
|
||||
// Special: begin new paragraph (only if its the first):
|
||||
if (hasParagraph && !skipFirstParagraphClosing) {
|
||||
// End, if there is one
|
||||
docx += '</w:p>';
|
||||
}
|
||||
skipFirstParagraphClosing = false;
|
||||
docx += '<w:p>';
|
||||
hasParagraph = true;
|
||||
} else if (part.startsWith('/p')) {
|
||||
// Special: end paragraph:
|
||||
docx += '</w:p>';
|
||||
hasParagraph = false;
|
||||
|
||||
} else if (part.charAt(0) == "/") {
|
||||
// remove from stack
|
||||
stack.pop();
|
||||
} else { // now all other tags
|
||||
var tag = {};
|
||||
if (_.indexOf(TAGS_NO_PARAM, part) > -1) { /** b, strong, em, i **/
|
||||
stack.push({tag: part});
|
||||
} else if (part.startsWith('span')) { /** span **/
|
||||
tag = {tag: 'span', attrs: {}};
|
||||
var rStyle = /(?:\"|\;\s?)([a-zA-z\-]+)\:\s?([a-zA-Z0-9\-\#]+)/g, matchSpan;
|
||||
while ((matchSpan = rStyle.exec(part)) !== null) {
|
||||
switch (matchSpan[1]) {
|
||||
case 'color':
|
||||
tag.attrs.color = matchSpan[2].slice(1); // cut off the #
|
||||
break;
|
||||
case 'background-color':
|
||||
tag.attrs.backgroundColor = matchSpan[2].slice(1); // cut off the #
|
||||
break;
|
||||
case 'text-decoration':
|
||||
if (matchSpan[2] === 'underline') {
|
||||
tag.attrs.underline = true;
|
||||
} else if (matchSpan[2] === 'line-through') {
|
||||
tag.attrs.strike = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
stack.push(tag);
|
||||
} else if (part.startsWith('a')) { /** a **/
|
||||
var rHref = /href="([^"]+)"/g;
|
||||
var href = rHref.exec(part)[1];
|
||||
tag = {tag: 'a', href: href};
|
||||
stack.push(tag);
|
||||
} else if (part.startsWith('img')) {
|
||||
// images has to be placed instantly, so there is no use of 'tag'.
|
||||
var img = {}, rImg = /(\w+)=\"([^\"]*)\"/g, matchImg;
|
||||
while ((matchImg = rImg.exec(part)) !== null) {
|
||||
img[matchImg[1]] = matchImg[2];
|
||||
}
|
||||
|
||||
// With and height and source have to be given!
|
||||
if (img.width && img.height && img.src) {
|
||||
var rrId = relationships.length + 1;
|
||||
var imgId = images.length + 1;
|
||||
|
||||
// set name ('pic.jpg'), title, ext ('jpg'), mime ('image/jpeg')
|
||||
img.name = img.src.split('/');
|
||||
img.name = _.last(img.name);
|
||||
|
||||
var tmp = img.name.split('.');
|
||||
// set name without extension as title if there isn't a title
|
||||
if (!img.title) {
|
||||
img.title = tmp[0];
|
||||
}
|
||||
img.ext = tmp[1];
|
||||
|
||||
img.mime = 'image/' + img.ext;
|
||||
if (img.ext == 'jpe' || img.ext == 'jpg') {
|
||||
img.mime = 'image/jpeg';
|
||||
}
|
||||
|
||||
// x and y for the container and picture size in EMU (assuming 96dpi)!
|
||||
var x = img.width * 914400 / 96;
|
||||
var y = img.height * 914400 / 96;
|
||||
|
||||
// Own paragraph for the image
|
||||
if (hasParagraph) {
|
||||
docx += '</w:p>';
|
||||
}
|
||||
docx += '<w:p><w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extend cx="' + x +'" cy="' + y + '"/><wp:effectExtent l="0" t="0" r="0" b="0"/>' +
|
||||
'<wp:docPr id="' + imgId + '" name="' + img.name + '" title="' + img.title + '" descr="' + img.title + '"/><wp:cNvGraphicFramePr>' +
|
||||
'<a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr>' +
|
||||
'<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">' +
|
||||
'<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="' + imgId + '" name="' +
|
||||
img.name + '" title="' + img.title + '" descr="' + img.title + '"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="rrId' + rrId + '"/><a:stretch>' +
|
||||
'<a:fillRect/></a:stretch></pic:blipFill><pic:spPr bwMode="auto"><a:xfrm><a:off x="0" y="0"/><a:ext cx="' + x + '" cy="' + y + '"/></a:xfrm>' +
|
||||
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r></w:p>';
|
||||
|
||||
// hasParagraph stays untouched, the documents paragraph state is restored here
|
||||
if (hasParagraph) {
|
||||
docx += '<w:p>';
|
||||
}
|
||||
|
||||
// entries in images, relationships and contentTypes
|
||||
images.push({
|
||||
url: img.src,
|
||||
zipPath: 'word/media/' + img.name
|
||||
});
|
||||
relationships.push({
|
||||
Id: 'rrId' + rrId,
|
||||
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
|
||||
Target: 'media/' + img.name
|
||||
});
|
||||
contentTypes.push({
|
||||
PartName: '/word/media/' + img.name,
|
||||
ContentType: img.mime
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { /** No tag **/
|
||||
if (!hasParagraph) {
|
||||
docx += '<w:p>';
|
||||
hasParagraph = true;
|
||||
}
|
||||
var docx_part = '<w:r><w:rPr>';
|
||||
var hyperlink = false;
|
||||
stack.forEach(function (tag) {
|
||||
switch (tag.tag) {
|
||||
case 'b': case 'strong':
|
||||
docx_part += '<w:b/><w:bCs/>';
|
||||
break;
|
||||
case 'em': case 'i':
|
||||
docx_part += '<w:i/><w:iCs/>';
|
||||
break;
|
||||
case 'span':
|
||||
for (var key in tag.attrs) {
|
||||
switch (key) {
|
||||
case 'color':
|
||||
docx_part += '<w:color w:val="' + tag.attrs[key] + '"/>';
|
||||
break;
|
||||
case 'backgroundColor':
|
||||
docx_part += '<w:shd w:fill="' + tag.attrs[key] + '"/>';
|
||||
break;
|
||||
case 'underline':
|
||||
docx_part += '<w:u w:val="single"/>';
|
||||
break;
|
||||
case 'strike':
|
||||
docx_part += '<w:strike/>';
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'a':
|
||||
var id = relationships.length + 1;
|
||||
docx_part = '<w:hyperlink r:id="rrId' + id + '">' + docx_part;
|
||||
docx_part += '<w:rStyle w:val="Internetlink"/>'; // necessary?
|
||||
relationships.push({
|
||||
Id: 'rrId' + id,
|
||||
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
|
||||
Target: tag.href,
|
||||
TargetMode: 'External'
|
||||
});
|
||||
hyperlink = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
docx_part += '</w:rPr><w:t>' + part + '</w:t></w:r>';
|
||||
if (hyperlink) {
|
||||
docx_part += '</w:hyperlink>';
|
||||
}
|
||||
|
||||
// append to docx
|
||||
docx += docx_part;
|
||||
}
|
||||
isTag = !isTag;
|
||||
}
|
||||
if (part === '' || part == '\n') {
|
||||
// just if two tags following eachother: <b><span> --> ...,'>', '', '<',...
|
||||
// or there is a line break between: <b>\n<span> --> ...,'>', '\n', '<',...
|
||||
isTag = !isTag;
|
||||
}
|
||||
});
|
||||
|
||||
// for finishing close the last paragraph (if open)
|
||||
if (hasParagraph) {
|
||||
docx += '</w:p>';
|
||||
}
|
||||
|
||||
// replacing of special symbols:
|
||||
docx = docx.replace(new RegExp('\ä\;', 'g'), 'ä');
|
||||
docx = docx.replace(new RegExp('\ü\;', 'g'), 'ü');
|
||||
docx = docx.replace(new RegExp('\ö\;', 'g'), 'ö');
|
||||
docx = docx.replace(new RegExp('\Ä\;', 'g'), 'Ä');
|
||||
docx = docx.replace(new RegExp('\Ü\;', 'g'), 'Ü');
|
||||
docx = docx.replace(new RegExp('\Ö\;', 'g'), 'Ö');
|
||||
docx = docx.replace(new RegExp('\ß\;', 'g'), 'ß');
|
||||
docx = docx.replace(new RegExp('\ \;', 'g'), ' ');
|
||||
docx = docx.replace(new RegExp('\§\;', 'g'), '§');
|
||||
|
||||
// remove all entities except gt, lt and amp
|
||||
var rEntity = /\&(?!gt|lt|amp)\w+\;/g, matchEntry, indexes = [];
|
||||
while ((matchEntry = rEntity.exec(docx)) !== null) {
|
||||
indexes.push({
|
||||
startId: matchEntry.index,
|
||||
stopId: matchEntry.index + matchEntry[0].length
|
||||
});
|
||||
}
|
||||
for (var i = indexes.length - 1; i>=0; i--) {
|
||||
docx = docx.substring(0, indexes[i].startId) + docx.substring(indexes[i].stopId, docx.length);
|
||||
}
|
||||
|
||||
return docx;
|
||||
var getMotionComments = function (motion) {
|
||||
var fields = Config.get('motions_comments').value;
|
||||
var canSeeComment = function (index) {
|
||||
return fields[index].public || operator.hasPerms('motions.can_manage');
|
||||
};
|
||||
var updateRelationships = function (oldContent) {
|
||||
var content = oldContent.split('\n');
|
||||
relationships.forEach(function (rel) {
|
||||
content[1] += '<Relationship';
|
||||
for (var key in rel) {
|
||||
content[1] += ' ' + key + '="' + rel[key] + '"';
|
||||
var comments = [];
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
if (motion.comments[i] && canSeeComment(i)) {
|
||||
var title = gettextCatalog.getString('Comment') + ' ' + fields[i].name;
|
||||
if (!fields[i].public) {
|
||||
title += ' (' + gettextCatalog.getString('internal') + ')';
|
||||
}
|
||||
content[1] += '/>';
|
||||
comments.push({
|
||||
title: title,
|
||||
comment: motion.comments[i],
|
||||
});
|
||||
return content.join('\n');
|
||||
};
|
||||
var updateContentTypes = function (oldContent) {
|
||||
var content = oldContent.split('\n');
|
||||
contentTypes.forEach(function (type) {
|
||||
content[1] += '<Override';
|
||||
for (var key in type) {
|
||||
content[1] += ' ' + key + '="' + type[key] + '"';
|
||||
}
|
||||
content[1] += '/>';
|
||||
});
|
||||
return content.join('\n');
|
||||
}
|
||||
return comments;
|
||||
};
|
||||
|
||||
return {
|
||||
export: function (motions, params) {
|
||||
converter = Html2DocxConverter.createInstance();
|
||||
if (!params) {
|
||||
params = {};
|
||||
}
|
||||
@ -364,40 +178,23 @@ angular.module('OpenSlidesApp.motions.docx', [])
|
||||
filename: 'motions-export.docx',
|
||||
changeRecommendationMode: Config.get('motions_recommendation_text_mode').value,
|
||||
includeReason: true,
|
||||
includeComments: false,
|
||||
});
|
||||
if (!_.includes(['original', 'changed', 'agreed'], params.changeRecommendationMode)) {
|
||||
params.changeRecommendationMode = 'original';
|
||||
}
|
||||
|
||||
images = [];
|
||||
relationships = [];
|
||||
contentTypes = [];
|
||||
$http.get('/motions/docxtemplate/').then(function (success) {
|
||||
var content = window.atob(success.data);
|
||||
var doc = new Docxgen(content);
|
||||
|
||||
doc.setData(getData(motions, params));
|
||||
getData(motions, params).then(function (data) {
|
||||
doc.setData(data);
|
||||
doc.render();
|
||||
|
||||
var zip = doc.getZip();
|
||||
zip = converter.updateZipFile(zip);
|
||||
|
||||
// update relationships from 'relationships'
|
||||
var rels = updateRelationships(zip.file('word/_rels/document.xml.rels').asText());
|
||||
zip.file('word/_rels/document.xml.rels', rels);
|
||||
|
||||
// update content type from 'contentTypes'
|
||||
var contentTypes = updateContentTypes(zip.file('[Content_Types].xml').asText());
|
||||
zip.file('[Content_Types].xml', contentTypes);
|
||||
|
||||
var imgPromises = [];
|
||||
images.forEach(function (img) {
|
||||
imgPromises.push(
|
||||
$http.get(img.url, {responseType: 'arraybuffer'}).then(function (resolvedImage) {
|
||||
zip.file(img.zipPath, resolvedImage.data);
|
||||
})
|
||||
);
|
||||
});
|
||||
// wait for all images to be resolved
|
||||
$q.all(imgPromises).then(function () {
|
||||
var out = zip.generate({type: 'blob'});
|
||||
FileSaver.saveAs(out, params.filename);
|
||||
});
|
||||
|
@ -10,12 +10,12 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
'gettextCatalog',
|
||||
'PDFLayout',
|
||||
'PdfMakeConverter',
|
||||
'PdfImageConverter',
|
||||
'ImageConverter',
|
||||
'HTMLValidizer',
|
||||
'Category',
|
||||
'Config',
|
||||
'Motion',
|
||||
function($q, operator, gettextCatalog, PDFLayout, PdfMakeConverter, PdfImageConverter, HTMLValidizer, Category, Config, Motion) {
|
||||
function($q, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter, HTMLValidizer, Category, Config, Motion) {
|
||||
/**
|
||||
* Provides the content as JS objects for Motions in pdfMake context
|
||||
* @constructor
|
||||
@ -354,7 +354,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
};
|
||||
|
||||
return $q(function (resolve) {
|
||||
PdfImageConverter.toBase64(getImageSources()).then(function (imageMap) {
|
||||
ImageConverter.toBase64(getImageSources()).then(function (imageMap) {
|
||||
converter = PdfMakeConverter.createInstance(imageMap);
|
||||
resolve({
|
||||
getContent: getContent,
|
||||
|
@ -733,7 +733,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
{name: gettextCatalog.getString('No'), value: false},
|
||||
],
|
||||
},
|
||||
hideExpression: "model.format !== 'pdf'",
|
||||
hideExpression: "model.format === 'csv'",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user