diff --git a/client/src/app/core/ui-services/html-to-pdf.service.ts b/client/src/app/core/ui-services/html-to-pdf.service.ts
index 01a2b1d30..0ef316e35 100644
--- a/client/src/app/core/ui-services/html-to-pdf.service.ts
+++ b/client/src/app/core/ui-services/html-to-pdf.service.ts
@@ -1,6 +1,14 @@
import { Injectable } from '@angular/core';
import { LineNumberingMode } from 'app/site/motions/models/view-motion';
+/**
+ * Shape of line number objects
+ */
+interface LineNumberObject {
+ lineNumber: number;
+ marginBottom?: number;
+}
+
/**
* Converts HTML strings to pdfmake compatible document definition.
*
@@ -26,7 +34,7 @@ export class HtmlToPdfService {
/**
* Space between list elements
*/
- private LI_MARGIN_BOTTOM = 1.5;
+ private LI_MARGIN_BOTTOM = 8;
/**
* Normal line height for paragraphs
@@ -110,7 +118,7 @@ export class HtmlToPdfService {
const parser = new DOMParser();
const parsedHtml = parser.parseFromString(htmlText, 'text/html');
// Since the spread operator did not work for HTMLCollection, use Array.from
- const htmlArray = Array.from(parsedHtml.body.childNodes);
+ const htmlArray = Array.from(parsedHtml.body.childNodes) as Element[];
// Parse the children of the current HTML element
for (const child of htmlArray) {
@@ -129,7 +137,7 @@ export class HtmlToPdfService {
* @param styles holds the style attributes of HTML elements (`
...`)
* @returns the doc def to the given element in consideration to the given paragraph and styles
*/
- public parseElement(element: any, styles?: string[]): any {
+ public parseElement(element: Element, styles?: string[]): any {
const nodeName = element.nodeName.toLowerCase();
let classes = [];
let newParagraph: any;
@@ -176,7 +184,8 @@ export class HtmlToPdfService {
case 'div': {
const children = this.parseChildren(element, styles);
- if (this.lineNumberingMode === LineNumberingMode.Outside) {
+ // this introduces a bug with rendering sub-lists in PDF
+ if (this.lineNumberingMode === LineNumberingMode.Outside && !this.isInsideAList(element)) {
newParagraph = this.create('stack');
newParagraph.stack = children;
} else {
@@ -184,7 +193,12 @@ export class HtmlToPdfService {
newParagraph.text = children;
}
- newParagraph.margin = [0, this.getMarginBottom(nodeName)];
+ if (classes.includes('os-split-before')) {
+ newParagraph.listType = 'none';
+ } else {
+ newParagraph.marginBottom = this.getMarginBottom(nodeName);
+ }
+
newParagraph.lineHeight = this.LINE_HEIGHT;
const implicitStyles = this.computeStyle(this.elementStyles[nodeName]);
@@ -212,7 +226,7 @@ export class HtmlToPdfService {
}
case 'span': {
// Line numbering feature, will prevent compatibility to most other projects
- if (element.getAttribute('data-line-number')) {
+ if (element.getAttribute('data-line-number') && !this.isInsideAList(element)) {
if (this.lineNumberingMode === LineNumberingMode.Inside) {
// TODO: algorithm for "inline" line numbers is not yet implemented
} else if (this.lineNumberingMode === LineNumberingMode.Outside) {
@@ -220,15 +234,7 @@ export class HtmlToPdfService {
newParagraph = {
columns: [
// the line number column
- {
- width: 20,
- text: currentLineNumber,
- color: 'gray',
- fontSize: 8,
- margin: [0, 2, 0, 0],
- lineHeight: this.LINE_HEIGHT
- },
- // target to push text into the line
+ this.getLineNumberObject({ lineNumber: +currentLineNumber }),
{
text: []
}
@@ -249,20 +255,70 @@ export class HtmlToPdfService {
}
case 'br': {
newParagraph = this.create('text');
- // Add a dummy line, if the next tag is a BR tag again. The line could
- // not be empty otherwise it will be removed and the empty line is not displayed
- if (element.nextSibling && element.nextSibling.nodeName === 'BR') {
- newParagraph.text.push(this.create('text', ' '));
- }
-
+ // yep thats all
+ newParagraph.text = '\n';
newParagraph.lineHeight = this.LINE_HEIGHT;
break;
}
case 'ul':
case 'ol': {
- newParagraph = this.create(nodeName);
- const children = this.parseChildren(element, styles);
- newParagraph[nodeName] = children;
+ const list = this.create(nodeName);
+
+ // keep the numbers of the ol list
+ if (nodeName === 'ol') {
+ const start = element.getAttribute('start');
+ if (start) {
+ list.start = +start;
+ }
+ }
+
+ // in case of line numbers and only of the list is not nested in another list.
+ if (this.lineNumberingMode === LineNumberingMode.Outside && !this.isInsideAList(element)) {
+ const lines = this.extractLineNumbers(element);
+
+ const cleanedChildDom = this.cleanLineNumbers(element);
+ const cleanedChildren = this.parseChildren(cleanedChildDom, styles);
+
+ if (lines.length > 0) {
+ const listCol = {
+ columns: [
+ {
+ width: 20,
+ stack: []
+ }
+ ],
+ margin: [0, 0, 0, 0]
+ };
+
+ // For correction factor for "change reco" elements in lists, cause
+ // they open a new OL-List and have additional distance
+ if (
+ element.classList.contains('os-split-before') &&
+ element.classList.contains('os-split-after')
+ ) {
+ listCol.margin = [0, -this.LI_MARGIN_BOTTOM, 0, -this.LI_MARGIN_BOTTOM];
+ } else if (!element.classList.contains('os-split-before')) {
+ listCol.margin = [0, 5, 0, 0];
+ }
+
+ for (const line of lines) {
+ listCol.columns[0].stack.push(this.getLineNumberObject(line));
+ }
+
+ list[nodeName] = cleanedChildren;
+ listCol.columns.push(list);
+ newParagraph = listCol;
+ } else {
+ // that is usually the case for "inserted" lists during change recomendations
+ list.margin = [20, 0, 0, 0];
+ newParagraph = list;
+ newParagraph[nodeName] = cleanedChildren;
+ }
+ } else {
+ const children = this.parseChildren(element, styles);
+ newParagraph = list;
+ newParagraph[nodeName] = children;
+ }
break;
}
default: {
@@ -284,14 +340,15 @@ export class HtmlToPdfService {
* @param styles the styles array, usually just to parse back into the `parseElement` function
* @returns an array of parsed children
*/
- private parseChildren(element: any, styles?: string[]): any {
- const childNodes = element.childNodes;
+ private parseChildren(element: Element, styles?: Array): Element[] {
+ const childNodes = Array.from(element.childNodes) as Element[];
const paragraph = [];
if (childNodes.length > 0) {
for (const child of childNodes) {
// skip empty child nodes
if (!(child.nodeName === '#text' && child.textContent.trim() === '')) {
const parsedElement = this.parseElement(child, styles);
+ const firstChild = element.firstChild as Element;
if (
// add the line number column
@@ -304,9 +361,9 @@ export class HtmlToPdfService {
} else if (
// if the first child of the parsed element is line number
this.lineNumberingMode === LineNumberingMode.Outside &&
- element.firstChild &&
- element.firstChild.classList &&
- element.firstChild.classList.contains('os-line-number')
+ firstChild &&
+ firstChild.classList &&
+ firstChild.classList.contains('os-line-number')
) {
const currentLine = paragraph.pop();
// push the parsed element into the "text" array
@@ -321,6 +378,135 @@ export class HtmlToPdfService {
return paragraph;
}
+ /**
+ * Helper function to make a line-number object
+ *
+ * @param line and object in the shape: { lineNumber: X }
+ * @returns line number as pdfmake-object
+ */
+ private getLineNumberObject(line: LineNumberObject): object {
+ return {
+ width: 20,
+ text: [
+ {
+ // Add a blank with the normal font size here, so in rare cases the text
+ // is rendered on the next page and the line number on the previous page.
+ text: ' ',
+ fontSize: 10,
+ decoration: ''
+ },
+ {
+ text: line.lineNumber,
+ color: 'gray',
+ fontSize: 8
+ }
+ ],
+ marginBottom: line.marginBottom,
+ lineHeight: this.LINE_HEIGHT
+ };
+ }
+
+ /**
+ * Cleans the elements children from line-number spans
+ *
+ * @param element a html dom tree
+ * @returns a DOM element without line number spans
+ */
+ private cleanLineNumbers(element: Element): Element {
+ const elementCopy = element.cloneNode(true) as Element;
+ const children = elementCopy.childNodes;
+
+ // using for-of did not work as expected
+ for (let i = 0; i < children.length; i++) {
+ if (this.getLineNumber(children[i] as Element)) {
+ children[i].remove();
+ }
+
+ if (children[i].childNodes.length > 0) {
+ const cleanChildren = this.cleanLineNumbers(children[i] as Element);
+ elementCopy.replaceChild(cleanChildren, children[i]);
+ }
+ }
+
+ return elementCopy;
+ }
+
+ /**
+ * Helper function to extract line numbers from child elements
+ *
+ * TODO: Cleanup
+ *
+ * @param element element to check for containing line numbers (usually a list)
+ * @returns a list with the line numbers
+ */
+ private extractLineNumbers(element: Element): LineNumberObject[] {
+ let foundLineNumbers = [];
+ const lineNumber = this.getLineNumber(element);
+ if (lineNumber) {
+ foundLineNumbers.push({ lineNumber: lineNumber });
+ } else if (element.nodeName === 'BR') {
+ // Check if there is a new line, but it does not get a line number.
+ // If so, insert a dummy line, so the line numbers stays aligned with
+ // the text.
+ if (!this.getLineNumber(element.nextSibling as Element)) {
+ foundLineNumbers.push({ lineNumber: '' });
+ }
+ } else {
+ const children = Array.from(element.childNodes) as Element[];
+ let childrenLength = children.length;
+ let childrenLineNumbers = [];
+ for (let i = 0; i < children.length; i++) {
+ childrenLineNumbers = childrenLineNumbers.concat(this.extractLineNumbers(children[i]));
+ if (children.length < childrenLength) {
+ i -= childrenLength - children.length;
+ childrenLength = children.length;
+ }
+ }
+
+ // If this is an list item, add some space to the lineNumbers:
+ if (childrenLineNumbers.length && element.nodeName === 'LI') {
+ childrenLineNumbers[childrenLineNumbers.length - 1].marginBottom = this.LI_MARGIN_BOTTOM;
+ }
+
+ foundLineNumbers = foundLineNumbers.concat(childrenLineNumbers);
+ }
+ return foundLineNumbers;
+ }
+
+ /**
+ * Recursive helper function to determine if the element is inside a list
+ *
+ * @param element the current html node
+ * @returns wheater the element is inside a list or not
+ */
+ private isInsideAList(element: Element): boolean {
+ let parent = element.parentNode;
+ while (parent !== null) {
+ if (parent.nodeName === 'UL' || parent.nodeName === 'OL') {
+ return true;
+ }
+ parent = parent.parentNode;
+ }
+ return false;
+ }
+
+ /**
+ * Helper function to safer extract a line number from an element
+ *
+ * @param element
+ * @returns the line number of the element
+ */
+ private getLineNumber(element: Element): number {
+ if (
+ element &&
+ element.nodeName === 'SPAN' &&
+ element.getAttribute('class') &&
+ element.getAttribute('class').indexOf('os-line-number') > -1
+ ) {
+ return +element.getAttribute('data-line-number');
+ }
+ }
+
/**
* Extracts the style information from the given array
*
diff --git a/client/src/app/site/motions/services/motion-pdf.service.ts b/client/src/app/site/motions/services/motion-pdf.service.ts
index 8ced48730..d2ec53f74 100644
--- a/client/src/app/site/motions/services/motion-pdf.service.ts
+++ b/client/src/app/site/motions/services/motion-pdf.service.ts
@@ -315,7 +315,18 @@ export class MotionPdfService {
}
// summary of change recommendations (for motion diff version only)
- const changeRecos = this.changeRecoRepo.getChangeRecoOfMotion(motion.id);
+ const changeRecos = this.changeRecoRepo
+ .getChangeRecoOfMotion(motion.id)
+ .sort((a: ViewUnifiedChange, b: ViewUnifiedChange) => {
+ if (a.getLineFrom() < b.getLineFrom()) {
+ return -1;
+ } else if (a.getLineFrom() > b.getLineFrom()) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+
if (crMode === ChangeRecoMode.Diff && changeRecos.length > 0) {
const columnLineNumbers = [];
const columnChangeType = [];