Skip to content
Snippets Groups Projects
Commit 9a270960 authored by Marius Dumitru Florea's avatar Marius Dumitru Florea
Browse files

XWIKI-20599: Failed to export the Dashboard page to PDF with Table of Contents

parent 9b8963ca
No related branches found
No related tags found
No related merge requests found
<?xml version="1.1" encoding="UTF-8"?>
<!--
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
-->
<xwikidoc version="1.5" reference="PDFExportIT.InvalidTOCAnchors" locale="">
<web>PDFExportIT</web>
<name>InvalidTOCAnchors</name>
<language/>
<defaultLanguage>en</defaultLanguage>
<translation>0</translation>
<creator>xwiki:XWiki.Admin</creator>
<parent>WebHome</parent>
<author>xwiki:XWiki.Admin</author>
<contentAuthor>xwiki:XWiki.Admin</contentAuthor>
<version>1.1</version>
<title/>
<comment/>
<minorEdit>false</minorEdit>
<syntaxId>xwiki/2.1</syntaxId>
<hidden>false</hidden>
<content>(% id="" %)
= WithoutID =
(% id="1 2 3" %)
= DigitsAndSpace =
(% id="¬`!£$%^&amp;*()_-+={}[]:;@'~#&lt;,&gt;.?/|\" %)
= Symbols =
= Valid =</content>
</xwikidoc>
......@@ -393,6 +393,44 @@ void updatePDFExportConfigurationWithValidation(TestUtils setup, TestConfigurati
}
}
@Test
@Order(7)
void invalidTOCAnchors(TestUtils setup, TestConfiguration testConfiguration) throws Exception
{
ViewPage viewPage = setup.gotoPage(new LocalDocumentReference("PDFExportIT", "InvalidTOCAnchors"));
PDFExportOptionsModal exportOptions = PDFExportOptionsModal.open(viewPage);
try (PDFDocument pdf = export(exportOptions, testConfiguration)) {
// We should have 3 pages: cover page, table of contents and one page for the content.
assertEquals(3, pdf.getNumberOfPages());
//
// Verify the table of contents page.
//
String tocPageText = pdf.getTextFromPage(1);
// The document title is not included when a single page is exported.
assertTrue(tocPageText.contains("Table of Contents\nWithoutID\nDigitsAndSpace\nSymbols\nValid"),
"Unexpected table of contents: " + tocPageText);
// The table of contents should have internal links (anchors) to each section, provided the sections have a
// valid id (otherwise the section title is displayed but without a link).
Map<String, String> tocPageLinks = pdf.getLinksFromPage(1);
assertEquals(Collections.singletonList("HValid"),
tocPageLinks.values().stream().collect(Collectors.toList()));
//
// Verify the content page.
//
String contentPageText = pdf.getTextFromPage(2);
// The document title is not included when a single page is exported.
assertTrue(
contentPageText.startsWith("InvalidTOCAnchors\n3 / 3\nWithoutID\nDigitsAndSpace\nSymbols\nValid"),
"Unexpected content: " + contentPageText);
}
}
private URL getHostURL(TestConfiguration testConfiguration) throws Exception
{
return new URL(String.format("http://%s:%d", testConfiguration.getServletEngine().getIP(),
......
......@@ -182,6 +182,7 @@
<name>code</name>
<number>2</number>
<prettyName>Code</prettyName>
<restricted>0</restricted>
<rows>20</rows>
<size>50</size>
<unmodifiable>0</unmodifiable>
......@@ -273,17 +274,69 @@
};
// Remove the Table of Contents if empty.
const removeEmptyToC = function() {
const removeEmptyTableOfContents = function() {
const toc = $('.pdf-toc');
if (!toc.find('li').length) {
toc.remove();
}
};
/**
* The anchors from the PDF table of contents must target existing sections within the PDF content, otherwise Paged.js
* cannot compute the print page number where those sections appear, in order to show this to the right of the anchor.
* Anchors that don't targer an existing section or that use an invalid section id are especially problematic because
* they break Paged.js (e.g. "Failed to execute 'querySelector' on 'Element': '#...' is not a valid selector"). We
* mark the invalid table of contents anchors in order to skip them when determining the page number, otherwise we
* would get 0 which is misleading. Of course, the code that generates those invalid anchors should to be fixed also.
*/
const validateTableOfContentsAnchors = function() {
$('.pdf-toc ul a[href]').each(function() {
const target = $(this).attr('href');
try {
// We replicate the behavior of Paged.js which performs CSS escaping before calling querySelector. The problem
// is that it does CSS escaping using an internal querySelectorEscape function that we can't access. Instead of
// duplicating the code we rely on the standard CSS#escape function which should lead to similar results.
if (document.querySelector(querySelectorEscape(target))) {
// Valid anchor. Don't touch it.
return;
} else {
console.warn('Missing section expected by the PDF table of contents: ' + target);
}
} catch (e) {
console.warn('Invalid anchor in PDF table of contents: ' + target);
}
$(this).removeAttr('href').attr('data-invalid-href', target);
});
};
const querySelectorEscape = function(value) {
// querySelectorEscape from Paged.js doesn't escape # (Hash) and . (Dot, when the string doesn't start with #) so we
// need to do the same. Replace # (Hash) and . (Dot) with _ (Underscore) in order to not escape them.
const escapeIndex = [];
const doesNotStartWithHash = value.charAt(0) !== '#';
let escapedValue = [...value].map(char =&gt; {
if (char === '_' || char === '#' || (doesNotStartWithHash &amp;&amp; char === '.')) {
escapeIndex.push(char);
return '_';
}
return char;
}).join('');
escapedValue = CSS.escape(escapedValue);
// Restore the # (Hash) and . (Dot) that were previously replaced with _ (Underscore).
let escapeCount = 0;
return [...escapedValue].map(char =&gt; {
if (char === '_') {
return escapeIndex[escapeCount++];
}
return char;
}).join('');
};
// Adjust the exported content before performing the print layout.
pageReady.afterPageReady(() =&gt; {
fixInternalAnchors($('#xwikicontent, .pdf-toc'));
removeEmptyToC();
removeEmptyTableOfContents();
validateTableOfContentsAnchors();
});
// Trigger the print preview after the page is ready. Note that by returning a promise we're making the next page
......@@ -363,6 +416,7 @@
<name>code</name>
<number>2</number>
<prettyName>Code</prettyName>
<restricted>0</restricted>
<rows>20</rows>
<size>50</size>
<unmodifiable>0</unmodifiable>
......@@ -547,7 +601,7 @@
}
/* Page number */
.pdf-toc ul a::after {
.pdf-toc ul a[href]::after {
content: target-counter(attr(href), page);
/* For the fake dotted leader. */
position: absolute;
......@@ -639,7 +693,7 @@
<separators>|, </separators>
<size>5</size>
<unmodifiable>0</unmodifiable>
<values>action=Action|doc.reference=Document|icon.theme=Icon theme|locale=Language|rendering.defaultsyntax=Default syntax|rendering.restricted=Restricted|rendering.targetsyntax=Target syntax|request.base=Request base URL|request.cookies|request.parameters=Request parameters|request.url=Request URL|request.wiki=Request wiki|user=User|wiki=Wiki</values>
<values>action=Action|doc.reference=Document|icon.theme=Icon theme|locale=Language|rendering.defaultsyntax=Default syntax|rendering.restricted=Restricted|rendering.targetsyntax=Target syntax|request.base=Request base URL|request.cookies|request.headers|request.parameters=Request parameters|request.remoteAddr|request.url=Request URL|request.wiki=Request wiki|user=User|wiki=Wiki</values>
<classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
</async_context>
<async_enabled>
......@@ -659,6 +713,7 @@
<name>content</name>
<number>1</number>
<prettyName>Executed Content</prettyName>
<restricted>0</restricted>
<rows>25</rows>
<size>120</size>
<unmodifiable>0</unmodifiable>
......@@ -689,6 +744,7 @@
<name>parameters</name>
<number>7</number>
<prettyName>Extension Parameters</prettyName>
<restricted>0</restricted>
<rows>10</rows>
<size>40</size>
<unmodifiable>0</unmodifiable>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment