diff --git a/DokuPDF.class.php b/DokuPDF.class.php index 41a4f34d..093d99b5 100644 --- a/DokuPDF.class.php +++ b/DokuPDF.class.php @@ -26,7 +26,7 @@ class DokuPDF extends Mpdf * @throws MpdfException * @throws Exception */ - public function __construct($pagesize = 'A4', $orientation = 'portrait', $fontsize = 11, $docLang = 'en') + public function __construct($pagesize = 'A4', $orientation = 'portrait', $fontsize = 11, $docLang = 'en', $tpl = 'default') { global $conf; global $lang; @@ -52,6 +52,23 @@ public function __construct($pagesize = 'A4', $orientation = 'portrait', $fontsi $mode = 'UTF-8-s'; } + // Get the default font data + $defaultFontConfig = (new \Mpdf\Config\FontVariables())->getDefaults(); + $fontData = $defaultFontConfig['fontdata']; + + // Get the default font dirs + $defaultConfig = (new \Mpdf\Config\ConfigVariables())->getDefaults(); + $fontDirs = $defaultConfig['fontDir']; + + // Append the custom path to it + $tplfonts = DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl . '/fonts'; + if (file_exists($tplfonts . '.php')) { + $fontDirs[] = $tplfonts; + + $fontDefs = array($fontData, include($tplfonts . '.php')); + $fontData = array_reduce($fontDefs, 'array_merge', array()); + } + parent::__construct([ 'mode' => $mode, 'format' => $format, @@ -59,6 +76,8 @@ public function __construct($pagesize = 'A4', $orientation = 'portrait', $fontsi 'ImageProcessorClass' => DokuImageProcessorDecorator::class, 'tempDir' => _MPDF_TEMP_PATH, //$conf['tmpdir'] . '/tmp/dwpdf' 'SHYlang' => $docLang, + 'fontDir' => $fontDirs, + 'fontdata' => $fontData ]); $this->autoScriptToLang = true; diff --git a/action.php b/action.php index 1b665733..7f432626 100644 --- a/action.php +++ b/action.php @@ -41,8 +41,25 @@ class action_plugin_dw2pdf extends ActionPlugin */ public function __construct() { + global $JSINFO; + require_once __DIR__ . '/vendor/autoload.php'; + $JSINFO['plugins']['dw2pdf']['showexporttemplate'] = $this->getConf('showexporttemplate'); + + if($this->getConf('showexporttemplate')) { + $templates = [$this->getExportConfig('template')]; + $dir = scandir(DOKU_PLUGIN . 'dw2pdf' . DIRECTORY_SEPARATOR . 'tpl'); + foreach ($dir as $key => $value) + { + if (is_dir(DOKU_PLUGIN . 'dw2pdf' . DIRECTORY_SEPARATOR . 'tpl' . DIRECTORY_SEPARATOR . $value) && !in_array($value,array(".","..",$this->getExportConfig('template')))) + { + $templates[] = $value; + } + } + $JSINFO['plugins']['dw2pdf']['templates'] = json_encode($templates); + } + $this->tpl = $this->getExportConfig('template'); } @@ -435,7 +452,8 @@ protected function generatePDF($cachefile, $event) $this->getExportConfig('pagesize'), $this->getExportConfig('orientation'), $this->getExportConfig('font-size'), - $this->getDocumentLanguage($this->list[0]) //use language of first page + $this->getDocumentLanguage($this->list[0]), + $this->getExportConfig('template') //use language of first page ); // let mpdf fix local links @@ -479,6 +497,7 @@ protected function generatePDF($cachefile, $event) $styles = '@page { size:auto; ' . $template['page'] . '}'; $styles .= '@page :first {' . $template['first'] . '}'; + $styles .= '@page last-page :first {' . $template['last'] . '}'; $styles .= '@page landscape-page { size:landscape }'; $styles .= 'div.dw2pdf-landscape { page:landscape-page }'; @@ -621,13 +640,14 @@ protected function loadTemplate() 'html' => '', 'page' => '', 'first' => '', + 'last' => '', 'cite' => '', ]; // prepare header/footer elements $html = ''; foreach (['header', 'footer'] as $section) { - foreach (['', '_odd', '_even', '_first'] as $order) { + foreach (['', '_odd', '_even', '_first', '_last'] as $order) { $file = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/' . $section . $order . '.html'; if (file_exists($file)) { $html .= '' . DOKU_LF; @@ -637,6 +657,8 @@ protected function loadTemplate() // register the needed pseudo CSS if ($order == '_first') { $output['first'] .= $section . ': html_' . $section . $order . ';' . DOKU_LF; + } elseif ($order == '_last') { + $output['last'] .= $section . ': html_' . $section . $order . ';' . DOKU_LF; } elseif ($order == '_even') { $output['page'] .= 'even-' . $section . '-name: html_' . $section . $order . ';' . DOKU_LF; } elseif ($order == '_odd') { @@ -655,7 +677,6 @@ protected function loadTemplate() '@TITLE@' => hsc($this->title), '@WIKI@' => $conf['title'], '@WIKIURL@' => DOKU_URL, - '@DATE@' => dformat(time()), '@USERNAME@' => $INFO['userinfo']['name'] ?? '', '@BASE@' => DOKU_BASE, '@INC@' => DOKU_INC, @@ -677,10 +698,10 @@ protected function loadTemplate() $output['cover'] .= ''; } - // cover page + // back page $backfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/back.html'; if (file_exists($backfile)) { - $output['back'] = ''; + $output['back'] = ''; $output['back'] .= file_get_contents($backfile); $output['back'] = str_replace(array_keys($replace), array_values($replace), $output['back']); $output['back'] = $this->pageDependReplacements($output['back'], $ID); @@ -728,6 +749,15 @@ protected function pageDependReplacements($raw, $id) } $replace['@PAGEURL@'] = wl($id, $params, true, "&"); $replace['@QRCODE@'] = $qr_code; + $replace['@OLDREVISIONS@'] = $this->changesToHTML($id); + $replace['@DATE@'] = dformat(time()); + + // @DATE([, ])@ + $raw = preg_replace_callback( + '/@DATE\((.*?)(?:,\s*(.*?))?\)@/', + [$this, 'replaceDate'], + $raw + ); $content = $raw; @@ -741,10 +771,11 @@ protected function pageDependReplacements($raw, $id) // plugins may post-process HTML, e.g to clean up unused replacements $event->advise_after(); - // @DATE([, ])@ + // @OLDREVISIONS([, ])@ + // /@OLDREVISIONS\\(([\\"\'])(.*?[^\\\\])\\1(?:,\\s*(.*?))?\\)@/ $content = preg_replace_callback( - '/@DATE\((.*?)(?:,\s*(.*?))?\)@/', - [$this, 'replaceDate'], + '/@OLDREVISIONS\\(([\\"\'])(.*?[^\\\\])\\1(?:,\\s*(.*?))?\\)@/', + fn($matches) => $this->changesToHTML($id, $matches), $content ); @@ -762,11 +793,171 @@ protected function pageDependReplacements($raw, $id) public function replaceDate($match) { global $conf; + if ($match[1] == '@DATE@') { + $match[1] = time(); + } //no 2nd argument for default date format - if ($match[2] == null) { + if (!isset($match[2])) { $match[2] = $conf['dformat']; } - return strftime($match[2], strtotime($match[1])); + return dformat($match[1], $match[2]); + } + + /** + * Load page changelog to Array + * + * @param string page changelog file location + * @return array + */ + public function changesToArray($f_changes) { + GLOBAL $auth; + + $a_changes = []; + if (file_exists($f_changes)) { + $lines = explode(PHP_EOL, io_readFile($f_changes, false)); + for($l = 0; $l < count($lines)-1; $l++) { // Remove last empty line from file + $a_changes[$l] = explode("\t", $lines[$l]); + $a_keys = ['date', 'ip', 'type', 'id', 'user', 'sum', 'extra', 'sizechange']; + if(count($a_changes[$l]) < 8) { + array_splice($a_keys, 7, 1); // Remove missing 'extra' key on revisions created on older DokuWiki releases + } + $a_changes[$l] = array_combine( + $a_keys, + $a_changes[$l] + ); + + // Username + if (!empty($a_changes[$l]['user'])) { + $userinfo = $auth->getUserData($a_changes[$l]['user'], true); + if (!empty($userinfo)) { + $a_changes[$l]['user'] = $userinfo['name']; // Real Name + } + } else { + // Set "ip" as "user" for Anonymous edits + $a_changes[$l]['user'] = $a_changes[$l]['ip']; + } + } + } + return array_reverse($a_changes); // Latest revision history first + } + + /** + * Convert page changelog from Array to HTML + * + * @param int page id + * @param array @OLDREVISIONS@ preg_match array (html, ) + * @return string + */ + public function changesToHTML($id, $matches = [null, null]) { + global $lang; + + $changes[] = metaFN($id, '.changes'); + $f_changes = $changes[0]; + $a_changes = $this->changesToArray($f_changes); + + + $html = $matches[2] ?? null; + $first = $matches[3] ?? null; + + // Return last X revisions + if ($first == null) { + $first = count($a_changes); + } + + $changes_html = ''; + // Render as Table by default + if($html == null) { + $changes_html .= ''; + $changes_html .= ''; + $changes_html .= ''; + $changes_html .= ''; + $changes_html .= ''; + $changes_html .= ''; + $changes_html .= ''; + $changes_html .= ''; + $changes_html .= ''; + $last_date = ''; + $last_author = ''; + for($l = 0; $l < $first; ++$l) { + // Summary contains text + if (!empty($a_changes[$l]['sum'])) { + // Wrap Date between + $a_date = explode(" ", dformat($a_changes[$l]['date'])); + $a_date_span = []; + for ($i = 0; $i < count($a_date); $i++) { + $a_date_span[] = '' . $a_date[$i] . ''; + } + + // Wrap User between + $a_user = explode(" ", $a_changes[$l]['user']); + $a_user_span = []; + for ($i = 0; $i < count($a_user); $i++) { + $a_user_span[] = '' . $a_user[$i] . ''; + } + + $changes_html .= ''; + $changes_html .= ''; + $changes_html .= ''; // Date + /* $changes_html .= ''; // Source IP + $changes_html .= ''; // Operation Type (C - Create, E - Edit) + $changes_html .= ''; // Namespace */ + $changes_html .= ''; // Author + $changes_html .= ''; // Summary + /* $changes_html .= ''; // Extra (Flags) + $changes_html .= ''; // Bytes changed */ + $changes_html . ''; + $changes_html .= ($l == 0 ? '' : ''); + + $last_author = $a_changes[$l]['user']; + $last_date = dformat($a_changes[$l]['date'], '%Y/%m/%d'); + } + $l++; + } + $changes_html .= ''; + $changes_html .= '
' . $lang['media_sort_date'] . '' . $lang['user'] . '' . $lang['summary'] . '
' . implode(" ", $a_date_span) . '' . $a_change['ip'] . '' . $a_change['type'] . '' . $a_change['id'] . '' . implode(" ", $a_user_span) . '' . trim($a_changes[$l]['sum']) . '' . $a_change['extra'] . '' . $a_change['sizechange'] . '
'; + } else { + // Reverse array for negative first numbers. + // e.g. -1 returns the revision when the page was first created + if($first < 0) { + $a_changes = array_reverse($a_changes); + $first = abs($first); // Convert number to positive + } + + for($l = 0; $l < $first; ++$l) { + $variables = array( + "REVDATE" => $a_changes[$l]['date'], + "REVIP" => $a_changes[$l]['ip'], + "REVTYPE" => $a_changes[$l]['type'], + "REVID" => $a_changes[$l]['id'], + "REVUSER" => $a_changes[$l]['user'], + "REVSUM" => $a_changes[$l]['sum'], + "REVEXTRA" => $a_changes[$l]['extra'], + "REVSIZECHANGE" => $a_changes[$l]['sizechange'] + ); + + $changes_string = $html; + foreach($variables as $key => $value){ + $changes_string = str_replace('@'.strtoupper($key).'@', $value, $changes_string); + } + + // @REVDATE()@ + $changes_string = preg_replace_callback( + '/@REVDATE\((.*?)\)@/', + fn($datematches) => dformat($a_changes[$l]['date'], $datematches[1]), + $changes_string + ); + + // @REVNAME()@ + $changes_string = preg_replace_callback( + '/@REVUSER\((.*?)\)@/', + fn($namematches) => ($namematches[1] >= 0 ? explode(" ", $a_changes[$l]['user'])[$namematches[1]] ?? '' : array_reverse(explode(" ", $a_changes[$l]['user']))[abs($namematches[1]+1)] ?? ''), + $changes_string + ); + + $changes_html .= $changes_string; + } + } + return $changes_html; } /** diff --git a/conf/default.php b/conf/default.php index 77aae8d1..5df388cd 100644 --- a/conf/default.php +++ b/conf/default.php @@ -14,3 +14,4 @@ $conf['usestyles'] = 'wrap,'; $conf['qrcodescale'] = '1'; $conf['showexportbutton'] = 1; +$conf['showexporttemplate'] = 0; diff --git a/conf/metadata.php b/conf/metadata.php index 321242f0..de44052d 100644 --- a/conf/metadata.php +++ b/conf/metadata.php @@ -14,3 +14,4 @@ $meta['usestyles'] = array('string'); $meta['qrcodescale'] = array('string', '_pattern' => '/^(|\d+(\.\d+)?)$/'); $meta['showexportbutton'] = array('onoff'); +$meta['showexporttemplate'] = array('onoff'); diff --git a/lang/en/lang.php b/lang/en/lang.php index 9d24c289..0023fd25 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -9,3 +9,7 @@ $lang['export_ns'] = 'Export namespace "%s:" to file %s.pdf'; $lang['forbidden'] = "You have no access to these pages: %s.

Use option 'Skip Forbidden Pages' to create your book with the available pages."; $lang['missingbookcreator'] = 'The Bookcreator Plugin is not installed or is disabled'; +$lang['js']['export_pdf_modal'] = "Export to PDF"; +$lang['js']['template'] = 'Which template should be used for formatting the PDFs?'; +$lang['js']['cancel'] = 'Cancel'; +$lang['js']['export'] = 'Export'; \ No newline at end of file diff --git a/lang/en/settings.php b/lang/en/settings.php index facc067c..e003490b 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -24,3 +24,4 @@ $lang['usestyles'] = 'You can give a comma separated list of plugins of which the style.css or screen.css should be used for PDF generation. By default only print.css and pdf.css are used.'; $lang['qrcodescale'] = 'Size scaling of the embedded QR code. Empty or 0 to disable.'; $lang['showexportbutton'] = 'Show PDF export button (only when supported by your template)'; +$lang['showexporttemplate'] = 'Show PDF export template selection'; diff --git a/lang/pt-br/lang.php b/lang/pt-br/lang.php index 241d8fb1..0e537ad0 100644 --- a/lang/pt-br/lang.php +++ b/lang/pt-br/lang.php @@ -5,6 +5,7 @@ * * @author Daniel Dias Rodrigues * @author Schopf + * @author Eduardo Mozart de Oliveira */ $lang['export_pdf_button'] = 'Exportar para PDF'; $lang['needtitle'] = 'Digite um título.'; @@ -15,3 +16,7 @@ $lang['export_ns'] = 'Exportar domínio "%s:" para o arquivo %s.pdf'; $lang['forbidden'] = 'Você não tem acesso a essas páginas: %s.

Use a opção \'Ignorar Páginas Proibidas\' para criar seu livro com as páginas disponíveis.'; $lang['missingbookcreator'] = 'O plugin Bookcreator não está instalado ou está desativado'; +$lang['js']['export_pdf_modal'] = 'Exportar para PDF'; +$lang['js']['template'] = 'Qual modelo deve ser usado para formatar os PDFs?'; +$lang['js']['cancel'] = 'Cancelar'; +$lang['js']['export'] = 'Exportar'; diff --git a/lang/pt-br/settings.php b/lang/pt-br/settings.php index 204f63ef..5dc5e092 100644 --- a/lang/pt-br/settings.php +++ b/lang/pt-br/settings.php @@ -7,7 +7,7 @@ * @author Schopf * @author Juliano Marconi Lanigra */ -$lang['pagesize'] = 'O formato de página como suportado pelo mPDF. Normalmente A4 ou carta.'; +$lang['pagesize'] = 'O formato de página como suportado pelo mPDF. Normalmente A4 ou letter.'; $lang['orientation'] = 'A orientação da página.'; $lang['orientation_o_portrait'] = 'Retrato'; $lang['orientation_o_landscape'] = 'Paisagem'; @@ -25,3 +25,4 @@ $lang['usestyles'] = 'Você pode gerar uma lista de plugins separadas por vírgula nos quais style.css ou screen.css devem ser usadas para a gerar um PDF.'; $lang['qrcodescale'] = 'Escala de tamanho do código QR incorporado. Vazio ou 0 para desativar.'; $lang['showexportbutton'] = 'Mostrar botão de exportação de PDF (se suportado pelo modelo)'; +$lang['showexporttemplate'] = 'Mostrar seleção de template de exportação de PDF'; diff --git a/script.js b/script.js new file mode 100644 index 00000000..6d340607 --- /dev/null +++ b/script.js @@ -0,0 +1,9 @@ +/** + * includes all needed JavaScript for the move plugin + * + * be sure to touch this file when one of the scripts has been updated to refresh caching + */ + +jQuery(function() { + /* DOKUWIKI:include script/template.js */ +}); \ No newline at end of file diff --git a/script/template.js b/script/template.js new file mode 100644 index 00000000..eb0ec2bb --- /dev/null +++ b/script/template.js @@ -0,0 +1,107 @@ +/** + * Template dialog for end users + * + * @author Eduardo Mozart de Oliveira + */ +(function () { + if (!JSINFO || !JSINFO.plugins.dw2pdf.showexporttemplate) return; + + + // basic dialog template + let $dialogSelect = ''; + + const $dialog = jQuery( + '
' + + '
' + + '' + + '
' + + '
' + ); + + /** + * Executes the renaming based on the form contents + * @return {boolean} + */ + const templateFN = function () { + window.location.href = jQuery('#dokuwiki__pagetools .action.export_pdf a').attr("href") + "&tpl=" + $dialog.find('select').find(':selected').text(); + + return true; + }; + + /** + * Create the actual dialog modal and show it + */ + const showDialog = function () { + $dialog.dialog({ + title: LANG.plugins.dw2pdf.export_pdf_modal + ' ' + JSINFO.id, + width: 800, + height: 200, + dialogClass: 'plugin_dw2pdf_dialog', + modal: true, + buttons: [ + { + text: LANG.plugins.dw2pdf.cancel, + click: function () { + $dialog.dialog("close"); + } + }, + { + text: LANG.plugins.dw2pdf.export, + click: templateFN + } + ], + // remove HTML from DOM again + close: function () { + jQuery(this).remove(); + } + }); + }; + + /** + * Bind an event handler as the first handler + * + * @param {jQuery} $owner + * @param {string} event + * @param {function} handler + * @link https://stackoverflow.com/a/4700103 + */ + const bindFirst = function ($owner, event, handler) { + $owner.unbind(event, handler); + $owner.bind(event, handler); + + const events = jQuery._data($owner[0])['events'][event]; + events.unshift(events.pop()); + + jQuery._data($owner[0])['events'][event] = events; + }; + + + // attach handler to menu item + jQuery('#dokuwiki__pagetools .action.export_pdf a') + .show() + .click(function (e) { + e.preventDefault(); + showDialog(); + }); + + // attach handler to mobile menu entry + const $mobileMenuOption = jQuery('form select[name=do] option[value=export_pdf]'); + if ($mobileMenuOption.length === 1) { + bindFirst($mobileMenuOption.closest('select[name=do]'), 'change', function (e) { + const $select = jQuery(this); + if ($select.val() !== 'export_pdf') return; + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + $select.val(''); + showDialog(); + }); + } + +})(); \ No newline at end of file diff --git a/tpl/default/README.txt b/tpl/default/README.txt index 60216e48..ef391d6a 100644 --- a/tpl/default/README.txt +++ b/tpl/default/README.txt @@ -41,14 +41,16 @@ footer files. * ''@PAGE@'' -- current page number in the PDF * ''@PAGES@'' -- number of all pages in the PDF - * ''@TITLE@'' -- The article's title - * ''@WIKI@'' -- The wiki's title + * ''@TITLE@'' -- the article's title + * ''@WIKI@'' -- the wiki's title * ''@WIKIURL@'' -- URL to the wiki * ''@DATE@'' -- time when the PDF was created (might be in the past if cached) * ''@BASE@'' -- the wiki base directory * ''@INC@'' -- the absolute wiki install directory on the filesystem * ''@TPLBASE@'' -- the PDF template base directory (use to reference images) * ''@TPLINC@'' -- the absolute path to the PDF template directory on the filesystem + * ''@DATE([, ])@'' -- formats the given date with [[config:dformat]] or with the given format such as ''%Y-%m-%e'', e.g. this would give just the current year ''@DATE(@DATE@,%Y)@'' + * ''@USERNAME@'' -- name of the user who creates the PDF //Remark about Bookcreator//: The page depended replacements are only for ''citation.html'' updated for every page. @@ -58,12 +60,48 @@ In the headers and footers the ID of the bookmanager page of the Bookcreator is * ''@UPDATE@'' -- Time of the last update of the article * ''@QRCODE@'' -- QR code image pointing to the original page url (requires an online generator, see config setting) +===== Revisions Replacements ===== + +You can use ''@OLDREVISIONS@'' to display page changelog. Custom HTML can be provided +by using ''@OLDREVISIONS("")@''. You can display the first X revisions by using +''@OLDREVISIONS("",)@'', where '''' can also be a negative value. + +The following replacement patterns can be used within the revisions. + + * ''@REVDATE@'' -- date of revision. You can use modifiers to format the revision date, e.g. ''@REVDATE(%m)@'' returns the revision month + * ''@REVIP@'' -- ip from user name of revision + * ''@REVTYPE@'' -- type of revision (e.g. "E" [edit], "C" [create]) + * ''@REVID@'' -- id of revision + * ''@REVUSER@'' -- user name of revision + * ''@REVSUM@'' -- summary of revision + * ''@REVEXTRA@'' -- revision extra data + * ''@REVSIZECHANGE@'' -- size change of revision + ===== Styles ===== Custom stylings can be provided in the following file of your dw2pdf-template folder: * style.css +The custom PDF selector ''@page last-page :first'' allows you to customize the CSS of the last PDF page. + You can use all the CSS that is understood by mpdf (See http://mpdf1.com/manual/index.php?tid=34) +===== Fonts ===== + +You can use custom fonts with your template by creating file ''fonts.php'' within your template folder. E.g.: + + + [ + 'R' => 'Frutiger-Normal.ttf', + 'I' => 'FrutigerObl-Normal.ttf', + ] +]; + + +Copy the font files to the ''fonts/'' subfolder within your template folder. + +You can use them on your template by using CSS style ''font-family: asap'', where "asap" is the name of the font on your ''fonts.php'' file. diff --git a/tpl/default/citation.html b/tpl/default/citation.html index 02e7bfe4..4764e0c2 100644 --- a/tpl/default/citation.html +++ b/tpl/default/citation.html @@ -1,6 +1,6 @@

-
+