PDF UL OL with line numbers
Add UL LI PDF line numbers
This commit is contained in:
parent
2df284c82e
commit
5069371f5e
@ -1,6 +1,14 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { LineNumberingMode } from 'app/site/motions/models/view-motion';
|
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.
|
* Converts HTML strings to pdfmake compatible document definition.
|
||||||
*
|
*
|
||||||
@ -26,7 +34,7 @@ export class HtmlToPdfService {
|
|||||||
/**
|
/**
|
||||||
* Space between list elements
|
* Space between list elements
|
||||||
*/
|
*/
|
||||||
private LI_MARGIN_BOTTOM = 1.5;
|
private LI_MARGIN_BOTTOM = 8;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normal line height for paragraphs
|
* Normal line height for paragraphs
|
||||||
@ -110,7 +118,7 @@ export class HtmlToPdfService {
|
|||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const parsedHtml = parser.parseFromString(htmlText, 'text/html');
|
const parsedHtml = parser.parseFromString(htmlText, 'text/html');
|
||||||
// Since the spread operator did not work for HTMLCollection, use Array.from
|
// 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
|
// Parse the children of the current HTML element
|
||||||
for (const child of htmlArray) {
|
for (const child of htmlArray) {
|
||||||
@ -129,7 +137,7 @@ export class HtmlToPdfService {
|
|||||||
* @param styles holds the style attributes of HTML elements (`<div style="color: green">...`)
|
* @param styles holds the style attributes of HTML elements (`<div style="color: green">...`)
|
||||||
* @returns the doc def to the given element in consideration to the given paragraph and styles
|
* @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();
|
const nodeName = element.nodeName.toLowerCase();
|
||||||
let classes = [];
|
let classes = [];
|
||||||
let newParagraph: any;
|
let newParagraph: any;
|
||||||
@ -176,7 +184,8 @@ export class HtmlToPdfService {
|
|||||||
case 'div': {
|
case 'div': {
|
||||||
const children = this.parseChildren(element, styles);
|
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 = this.create('stack');
|
||||||
newParagraph.stack = children;
|
newParagraph.stack = children;
|
||||||
} else {
|
} else {
|
||||||
@ -184,7 +193,12 @@ export class HtmlToPdfService {
|
|||||||
newParagraph.text = children;
|
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;
|
newParagraph.lineHeight = this.LINE_HEIGHT;
|
||||||
|
|
||||||
const implicitStyles = this.computeStyle(this.elementStyles[nodeName]);
|
const implicitStyles = this.computeStyle(this.elementStyles[nodeName]);
|
||||||
@ -212,7 +226,7 @@ export class HtmlToPdfService {
|
|||||||
}
|
}
|
||||||
case 'span': {
|
case 'span': {
|
||||||
// Line numbering feature, will prevent compatibility to most other projects
|
// 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) {
|
if (this.lineNumberingMode === LineNumberingMode.Inside) {
|
||||||
// TODO: algorithm for "inline" line numbers is not yet implemented
|
// TODO: algorithm for "inline" line numbers is not yet implemented
|
||||||
} else if (this.lineNumberingMode === LineNumberingMode.Outside) {
|
} else if (this.lineNumberingMode === LineNumberingMode.Outside) {
|
||||||
@ -220,15 +234,7 @@ export class HtmlToPdfService {
|
|||||||
newParagraph = {
|
newParagraph = {
|
||||||
columns: [
|
columns: [
|
||||||
// the line number column
|
// the line number column
|
||||||
{
|
this.getLineNumberObject({ lineNumber: +currentLineNumber }),
|
||||||
width: 20,
|
|
||||||
text: currentLineNumber,
|
|
||||||
color: 'gray',
|
|
||||||
fontSize: 8,
|
|
||||||
margin: [0, 2, 0, 0],
|
|
||||||
lineHeight: this.LINE_HEIGHT
|
|
||||||
},
|
|
||||||
// target to push text into the line
|
|
||||||
{
|
{
|
||||||
text: []
|
text: []
|
||||||
}
|
}
|
||||||
@ -249,20 +255,70 @@ export class HtmlToPdfService {
|
|||||||
}
|
}
|
||||||
case 'br': {
|
case 'br': {
|
||||||
newParagraph = this.create('text');
|
newParagraph = this.create('text');
|
||||||
// Add a dummy line, if the next tag is a BR tag again. The line could
|
// yep thats all
|
||||||
// not be empty otherwise it will be removed and the empty line is not displayed
|
newParagraph.text = '\n';
|
||||||
if (element.nextSibling && element.nextSibling.nodeName === 'BR') {
|
|
||||||
newParagraph.text.push(this.create('text', ' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
newParagraph.lineHeight = this.LINE_HEIGHT;
|
newParagraph.lineHeight = this.LINE_HEIGHT;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ul':
|
case 'ul':
|
||||||
case 'ol': {
|
case 'ol': {
|
||||||
newParagraph = this.create(nodeName);
|
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);
|
const children = this.parseChildren(element, styles);
|
||||||
|
newParagraph = list;
|
||||||
newParagraph[nodeName] = children;
|
newParagraph[nodeName] = children;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@ -284,14 +340,15 @@ export class HtmlToPdfService {
|
|||||||
* @param styles the styles array, usually just to parse back into the `parseElement` function
|
* @param styles the styles array, usually just to parse back into the `parseElement` function
|
||||||
* @returns an array of parsed children
|
* @returns an array of parsed children
|
||||||
*/
|
*/
|
||||||
private parseChildren(element: any, styles?: string[]): any {
|
private parseChildren(element: Element, styles?: Array<string>): Element[] {
|
||||||
const childNodes = element.childNodes;
|
const childNodes = Array.from(element.childNodes) as Element[];
|
||||||
const paragraph = [];
|
const paragraph = [];
|
||||||
if (childNodes.length > 0) {
|
if (childNodes.length > 0) {
|
||||||
for (const child of childNodes) {
|
for (const child of childNodes) {
|
||||||
// skip empty child nodes
|
// skip empty child nodes
|
||||||
if (!(child.nodeName === '#text' && child.textContent.trim() === '')) {
|
if (!(child.nodeName === '#text' && child.textContent.trim() === '')) {
|
||||||
const parsedElement = this.parseElement(child, styles);
|
const parsedElement = this.parseElement(child, styles);
|
||||||
|
const firstChild = element.firstChild as Element;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
// add the line number column
|
// add the line number column
|
||||||
@ -304,9 +361,9 @@ export class HtmlToPdfService {
|
|||||||
} else if (
|
} else if (
|
||||||
// if the first child of the parsed element is line number
|
// if the first child of the parsed element is line number
|
||||||
this.lineNumberingMode === LineNumberingMode.Outside &&
|
this.lineNumberingMode === LineNumberingMode.Outside &&
|
||||||
element.firstChild &&
|
firstChild &&
|
||||||
element.firstChild.classList &&
|
firstChild.classList &&
|
||||||
element.firstChild.classList.contains('os-line-number')
|
firstChild.classList.contains('os-line-number')
|
||||||
) {
|
) {
|
||||||
const currentLine = paragraph.pop();
|
const currentLine = paragraph.pop();
|
||||||
// push the parsed element into the "text" array
|
// push the parsed element into the "text" array
|
||||||
@ -321,6 +378,135 @@ export class HtmlToPdfService {
|
|||||||
return paragraph;
|
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
|
* Extracts the style information from the given array
|
||||||
*
|
*
|
||||||
|
@ -315,7 +315,18 @@ export class MotionPdfService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// summary of change recommendations (for motion diff version only)
|
// 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) {
|
if (crMode === ChangeRecoMode.Diff && changeRecos.length > 0) {
|
||||||
const columnLineNumbers = [];
|
const columnLineNumbers = [];
|
||||||
const columnChangeType = [];
|
const columnChangeType = [];
|
||||||
|
Loading…
Reference in New Issue
Block a user