PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.1
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.1
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / FrontEnd / PrintTemplate.php

PrintTemplate.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.1, at includes/FrontEnd/PrintTemplate.php

347 lines 11.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\FrontEnd;
4
5 /**
6 * Print template — the logo header and page footer shown when a visitor prints
7 * (or saves as PDF) a page.
8 *
9 * There are two ways a doc gets printed, and this class feeds both from the
10 * same settings so they render identically:
11 *
12 * 1. The browser's own Print command (Ctrl/Cmd + P) on the live page. Handled
13 * here: the markup is emitted on `wp_footer` and pushed into the `@page`
14 * margin box with `position: fixed`, which browsers repeat on every printed
15 * page. This is the only route available on pages that carry no BetterDocs
16 * print button — the API Reference pages, FSE templates, archives, and any
17 * other page a reader decides to print.
18 * 2. BetterDocs' own print icon, which opens a popup holding just the doc
19 * content. That route builds its own header/footer in `betterdocs.js` from
20 * `betterdocsConfig.print`, using a table `<tfoot>` — it owns the whole
21 * document there, and `<tfoot>` reserves space more reliably than a fixed
22 * element when the footer text wraps to several lines.
23 *
24 * The popup appends its own `@page` rule *after* the styles it copies from this
25 * page, so route 2's margins keep winning inside the popup and the two
26 * mechanisms never fight.
27 *
28 * @since 4.9.1
29 */
30 class PrintTemplate {
31 /**
32 * Vertical space reserved inside the printed page margins, in millimetres.
33 * Keep these in sync with the offsets in {@see self::styles()}.
34 */
35 const HEADER_SPACE = 22;
36 const FOOTER_SPACE = 18;
37 const EDGE_SPACE = 12;
38
39 public function __construct() {
40 add_action( 'wp_footer', [ $this, 'render' ], 99 );
41 }
42
43 /**
44 * Whether the print template should be emitted on the current request.
45 *
46 * Defaults to every front-end page — a reader can print any page, and the
47 * whole point of this route is to cover the ones without a print button.
48 * Filter `betterdocs_print_template_enabled` to narrow it, e.g. to docs
49 * only:
50 *
51 * add_filter( 'betterdocs_print_template_enabled', function ( $on ) {
52 * return $on && ( is_singular( 'docs' ) || is_post_type_archive( 'docs' ) );
53 * } );
54 *
55 * @return bool
56 */
57 public static function is_enabled() {
58 $enabled = ! is_admin() && ! is_feed() && ! is_embed();
59
60 return (bool) apply_filters( 'betterdocs_print_template_enabled', $enabled );
61 }
62
63 /**
64 * Resolved logo URL for the printed page, or '' when disabled/unavailable.
65 *
66 * Resolution order: the `print_logo` setting, the theme's Custom Logo, the
67 * Elementor kit's site logo (themes without custom-logo support keep theirs
68 * there), then the Site Icon.
69 *
70 * @return string
71 */
72 public static function get_logo() {
73 $enabled = apply_filters(
74 'betterdocs_print_enable_logo',
75 betterdocs()->settings->get( 'print_enable_logo', false )
76 );
77
78 if ( ! $enabled ) {
79 return '';
80 }
81
82 $logo = '';
83
84 $setting = betterdocs()->settings->get( 'print_logo' );
85 if ( ! empty( $setting['url'] ) ) {
86 $logo = (string) $setting['url'];
87 }
88
89 if ( '' === $logo ) {
90 $logo_id = (int) get_theme_mod( 'custom_logo' );
91
92 if ( ! $logo_id ) {
93 $kit_id = (int) get_option( 'elementor_active_kit' );
94 if ( $kit_id ) {
95 $kit_settings = get_post_meta( $kit_id, '_elementor_page_settings', true );
96 if ( ! empty( $kit_settings['site_logo']['id'] ) ) {
97 $logo_id = (int) $kit_settings['site_logo']['id'];
98 }
99 }
100 }
101
102 if ( $logo_id ) {
103 $logo = (string) wp_get_attachment_image_url( $logo_id, 'medium' );
104 }
105 }
106
107 if ( '' === $logo && has_site_icon() ) {
108 $logo = get_site_icon_url( 512 );
109 }
110
111 return (string) apply_filters( 'betterdocs_print_logo', $logo );
112 }
113
114 /**
115 * Resolved footer markup for the printed page, or '' when disabled.
116 *
117 * Falls back to "© <year> <site name>" when the setting is left empty.
118 *
119 * @return string
120 */
121 public static function get_footer() {
122 $enabled = apply_filters(
123 'betterdocs_print_enable_footer',
124 betterdocs()->settings->get( 'print_enable_footer', false )
125 );
126
127 if ( ! $enabled ) {
128 return '';
129 }
130
131 $footer = (string) betterdocs()->settings->get( 'print_footer_text', '' );
132
133 if ( '' === trim( $footer ) ) {
134 $footer = sprintf( '&copy; %1$s %2$s', gmdate( 'Y' ), get_bloginfo( 'name' ) );
135 }
136
137 return (string) apply_filters( 'betterdocs_print_footer', $footer );
138 }
139
140 /**
141 * Fallback print CSS, used when the table wrap below never runs.
142 *
143 * `position: fixed` is what makes a browser repeat an element on every
144 * printed page, and the negative offsets lift the boxes out of the content
145 * flow into the margin `@page` reserves for them.
146 *
147 * This is a fallback and not the primary mechanism because Chrome drops one
148 * instance at each end: measured over an 8-page print, the header rendered
149 * on pages 1-7 and the footer on pages 2-8 — the last page lost its logo and
150 * the first page lost its footer. Laying the boxes out inside the page area
151 * instead (`top:0`/`bottom:0`) does repeat on every page, but then the text
152 * runs underneath them. `transform: translateY()` in place of the negative
153 * offsets behaves identically to the negative offsets. The correct CSS
154 * answer — `@page` margin boxes such as `@top-center` — is implemented by no
155 * browser, so {@see self::script()} does the job properly and this rule set
156 * only covers the case where its events never fire.
157 *
158 * @param bool $has_logo
159 * @param bool $has_footer
160 * @return string
161 */
162 protected static function styles( $has_logo, $has_footer ) {
163 $top = $has_logo ? self::HEADER_SPACE : self::EDGE_SPACE;
164 $bottom = $has_footer ? self::FOOTER_SPACE : self::EDGE_SPACE;
165
166 $css = '.betterdocs-print-running-header,.betterdocs-print-running-footer{display:none}'
167 . '@media print{'
168 . sprintf( '@page{margin:%1$dmm %2$dmm %3$dmm}', $top, self::EDGE_SPACE, $bottom )
169 . '.betterdocs-print-running-header,.betterdocs-print-running-footer{'
170 . 'display:block;position:fixed;left:0;right:0;margin:0;padding:0;'
171 . 'text-align:center;background:none;border:0}';
172
173 if ( $has_logo ) {
174 $css .= '.betterdocs-print-running-header{top:-16mm;height:14mm;line-height:0}'
175 . '.betterdocs-print-running-header img{'
176 . 'max-height:14mm;max-width:60%;width:auto;height:auto;display:inline-block}';
177 }
178
179 if ( $has_footer ) {
180 $css .= '.betterdocs-print-running-footer{'
181 . 'bottom:-13mm;height:11mm;overflow:hidden;'
182 . 'font-size:9pt;line-height:1.35;color:#555}'
183 . '.betterdocs-print-running-footer p{margin:0;font-size:inherit;line-height:inherit}';
184 }
185
186 return $css . '}';
187 }
188
189 /**
190 * Print CSS for the wrapped (table) layout.
191 *
192 * Emitted inert as `media="not all"`; {@see self::script()} flips it to
193 * `print` once it has actually built the table, and back again afterwards.
194 * Because this sheet comes after the fallback one, its `@page` margin and
195 * its `position: static` reset both win while the wrap is in place.
196 *
197 * Browsers repeat `table-header-group` / `table-footer-group` on every page
198 * *and* keep the flowing content clear of them, which is the behaviour the
199 * fixed-position fallback cannot deliver.
200 *
201 * @param bool $has_logo
202 * @param bool $has_footer
203 * @return string
204 */
205 protected static function wrapped_styles( $has_logo, $has_footer ) {
206 $css = sprintf( '@page{margin:%1$dmm}', self::EDGE_SPACE )
207 . '.betterdocs-print-running-header,.betterdocs-print-running-footer{'
208 . 'display:block;position:static;top:auto;bottom:auto;left:auto;right:auto;'
209 . 'height:auto;transform:none;text-align:center}'
210 . '#betterdocs-print-layout{width:100%;border-collapse:collapse;border:0}'
211 . '#betterdocs-print-layout>thead>tr>td,'
212 . '#betterdocs-print-layout>tfoot>tr>td,'
213 . '#betterdocs-print-layout>tbody>tr>td{padding:0;border:0}'
214 . '#betterdocs-print-layout>thead{display:table-header-group}'
215 . '#betterdocs-print-layout>tfoot{display:table-footer-group}';
216
217 if ( $has_logo ) {
218 $css .= '.betterdocs-print-running-header{padding:0 0 6mm;line-height:0}'
219 . '.betterdocs-print-running-header img{'
220 . 'max-height:16mm;max-width:60%;width:auto;height:auto;display:inline-block}';
221 }
222
223 if ( $has_footer ) {
224 $css .= '.betterdocs-print-running-footer{'
225 . 'padding:5mm 0 0;font-size:9pt;line-height:1.35;color:#555}'
226 . '.betterdocs-print-running-footer p{margin:0;font-size:inherit;line-height:inherit}';
227 }
228
229 return $css;
230 }
231
232 /**
233 * The wrap/unwrap helper.
234 *
235 * Inlined rather than shipped as a bundle on purpose: it has to be present
236 * on every front-end page, and a separate request site-wide for a
237 * print-only helper of this size costs more than it saves.
238 *
239 * On `beforeprint` it moves the page's rendered content into a single table
240 * cell, with the logo as `<thead>` and the footer as `<tfoot>`, then
241 * activates {@see self::wrapped_styles()}. On `afterprint` it puts
242 * everything back. `matchMedia('print')` covers Safari, which has no
243 * `beforeprint`; the `state` guard makes a double fire harmless.
244 *
245 * @return string
246 */
247 protected static function script() {
248 return <<<'JS'
249 (function(){
250 "use strict";
251 var SKIP={SCRIPT:1,STYLE:1,LINK:1,TEMPLATE:1,NOSCRIPT:1},state=null;
252 function cell(parent,tag){
253 var section=document.createElement(tag),row=document.createElement("tr"),td=document.createElement("td");
254 row.appendChild(td);section.appendChild(row);parent.appendChild(section);return td;
255 }
256 function wrap(){
257 if(state||!document.body){return;}
258 var header=document.querySelector(".betterdocs-print-running-header");
259 var footer=document.querySelector(".betterdocs-print-running-footer");
260 if(!header&&!footer){return;}
261 var body=document.body,content=[],node=body.firstChild;
262 while(node){
263 if(node!==header&&node!==footer&&!(node.nodeType===1&&SKIP[node.tagName])){content.push(node);}
264 node=node.nextSibling;
265 }
266 if(!content.length){return;}
267 var table=document.createElement("table");
268 table.id="betterdocs-print-layout";
269 if(header){cell(table,"thead").appendChild(header);}
270 if(footer){cell(table,"tfoot").appendChild(footer);}
271 var host=cell(table,"tbody");
272 body.insertBefore(table,content[0]);
273 for(var i=0;i<content.length;i++){host.appendChild(content[i]);}
274 var sheet=document.getElementById("betterdocs-print-template-wrapped");
275 if(sheet){sheet.media="print";}
276 state={table:table,host:host,header:header,footer:footer,sheet:sheet};
277 }
278 function unwrap(){
279 if(!state){return;}
280 var body=document.body,table=state.table;
281 while(state.host.firstChild){body.insertBefore(state.host.firstChild,table);}
282 if(state.header){body.insertBefore(state.header,table);}
283 if(state.footer){body.insertBefore(state.footer,table);}
284 body.removeChild(table);
285 if(state.sheet){state.sheet.media="not all";}
286 state=null;
287 }
288 window.addEventListener("beforeprint",wrap);
289 window.addEventListener("afterprint",unwrap);
290 if(window.matchMedia){
291 var mq=window.matchMedia("print"),onChange=function(e){e.matches?wrap():unwrap();};
292 if(mq.addEventListener){mq.addEventListener("change",onChange);}
293 else if(mq.addListener){mq.addListener(onChange);}
294 }
295 })();
296 JS;
297 }
298
299 /**
300 * Emit the running header/footer, their print styles, and the wrap helper
301 * just before </body>.
302 *
303 * @return void
304 */
305 public function render() {
306 if ( ! self::is_enabled() ) {
307 return;
308 }
309
310 $logo = self::get_logo();
311 $footer = self::get_footer();
312
313 if ( '' === $logo && '' === $footer ) {
314 return;
315 }
316
317 $has_logo = '' !== $logo;
318 $has_footer = '' !== $footer;
319
320 if ( $has_logo ) {
321 printf(
322 '<div class="betterdocs-print-running-header" aria-hidden="true"><img src="%s" alt="" /></div>',
323 esc_url( $logo )
324 );
325 }
326
327 if ( $has_footer ) {
328 printf(
329 '<div class="betterdocs-print-running-footer" aria-hidden="true">%s</div>',
330 wp_kses_post( $footer )
331 );
332 }
333
334 printf(
335 '<style id="betterdocs-print-template">%s</style>',
336 self::styles( $has_logo, $has_footer )
337 );
338
339 printf(
340 '<style id="betterdocs-print-template-wrapped" media="not all">%s</style>',
341 self::wrapped_styles( $has_logo, $has_footer )
342 );
343
344 wp_print_inline_script_tag( self::script(), [ 'id' => 'betterdocs-print-template-js' ] );
345 }
346 }
347