(function () {
'use strict';
angular.module('OpenSlidesApp.motions.docx', [])
.factory('MotionDocxExport', [
'$http',
'$q',
'Config',
'gettextCatalog',
'FileSaver',
function ($http, $q, Config, gettextCatalog, FileSaver) {
var PAGEBREAK = '';
var TAGS_NO_PARAM = ['b', 'strong', 'em', 'i'];
var images;
var relationships;
var contentTypes;
// $scope.motionsFiltered, $scope.categories
var getData = function (motions, categories) {
var data = {};
// header
var headerline1 = [
Config.translate(Config.get('general_event_name').value),
Config.translate(Config.get('general_event_description').value)
].filter(Boolean).join(' – ');
var headerline2 = [
Config.get('general_event_location').value,
Config.get('general_event_date').value
].filter(Boolean).join(', ');
data.header = [headerline1, headerline2].join('\n');
// motion catalog title/preamble
data.title = Config.translate(Config.get('motions_export_title').value);
data.preamble = Config.get('motions_export_preamble').value;
// categories
data.has_categories = categories.length === 0 ? false : true;
data.categories_translation = gettextCatalog.getString('Categories');
data.categories = getCategoriesData(categories);
data.no_categories = gettextCatalog.getString('No categories available.');
data.pagebreak_main = categories.length === 0 ? '' : PAGEBREAK;
// motions
data.tableofcontents_translation = gettextCatalog.getString('Table of contents');
data.motions = getMotionFullData(motions);
data.motions_list = getMotionShortData(motions);
data.no_motions = gettextCatalog.getString('No motions available.');
return data;
};
var getCategoriesData = function (categories) {
return _.map(categories, function (category) {
return {
prefix: category.prefix,
name: category.name,
};
});
};
var getMotionShortData = function (motions) {
return _.map(motions, function (motion) {
return {
identifier: motion.identifier,
title: motion.getTitle(),
};
});
};
var getMotionFullData = function (motions) {
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) {
return {
motion_translation: translation,
sequential_translation: sequential_translation,
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(),
preamble: gettextCatalog.getString(Config.get('motions_preamble').value),
text: html2docx(motion.getText()),
reason_translation: motion.getReason().length === 0 ? '' : reason_translation,
reason: html2docx(motion.getReason()),
pagebreak: PAGEBREAK,
};
});
if (data.length) {
// clear pagebreak on last element
data[data.length - 1].pagebreak = '';
}
return data;
};
var html2docx = function (html) {
var docx = '';
var stack = [];
var isTag = false; // Even if html starts with '
') {
docx += '';
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 += '';
}
skipFirstParagraphClosing = false;
docx += '';
hasParagraph = true;
} else if (part.startsWith('/p')) {
// Special: end paragraph:
docx += '';
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 += '';
}
docx += '' +
'' +
'' +
'' +
'' +
'' +
'';
// hasParagraph stays untouched, the documents paragraph state is restored here
if (hasParagraph) {
docx += '';
}
// 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 += '';
hasParagraph = true;
}
var docx_part = '';
var hyperlink = false;
stack.forEach(function (tag) {
switch (tag.tag) {
case 'b': case 'strong':
docx_part += '';
break;
case 'em': case 'i':
docx_part += '';
break;
case 'span':
for (var key in tag.attrs) {
switch (key) {
case 'color':
docx_part += '';
break;
case 'backgroundColor':
docx_part += '';
break;
case 'underline':
docx_part += '';
break;
case 'strike':
docx_part += '';
break;
}
}
break;
case 'a':
var id = relationships.length + 1;
docx_part = '' + docx_part;
docx_part += ''; // 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 += '' + part + '';
if (hyperlink) {
docx_part += '';
}
// append to docx
docx += docx_part;
}
isTag = !isTag;
}
if (part === '' || part == '\n') {
// just if two tags following eachother: --> ...,'>', '', '<',...
// or there is a line break between: \n --> ...,'>', '\n', '<',...
isTag = !isTag;
}
});
// for finishing close the last paragraph (if open)
if (hasParagraph) {
docx += '';
}
// 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 updateRelationships = function (oldContent) {
var content = oldContent.split('\n');
relationships.forEach(function (rel) {
content[1] += '';
});
return content.join('\n');
};
var updateContentTypes = function (oldContent) {
var content = oldContent.split('\n');
contentTypes.forEach(function (type) {
content[1] += '';
});
return content.join('\n');
};
return {
export: function (motions, categories) {
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, categories));
doc.render();
var zip = doc.getZip();
// 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, 'motions-export.docx');
});
});
},
};
}
]);
}());