Better PDF export mechanism (part I) (#7399)

This commit is contained in:
Elian Doran
2025-10-20 14:37:35 +03:00
committed by GitHub
42 changed files with 882 additions and 627 deletions

View File

@@ -1,4 +1,3 @@
import type child_process from "child_process";
import { describe, beforeAll, afterAll } from "vitest";
let etapiAuthToken: string | undefined;
@@ -12,8 +11,6 @@ type SpecDefinitionsFunc = () => void;
function describeEtapi(description: string, specDefinitions: SpecDefinitionsFunc): void {
describe(description, () => {
let appProcess: ReturnType<typeof child_process.spawn>;
beforeAll(async () => {});
afterAll(() => {});

View File

@@ -1,6 +1,5 @@
import server from "../services/server.js";
import noteAttributeCache from "../services/note_attribute_cache.js";
import ws from "../services/ws.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import cssClassManager from "../services/css_class_manager.js";
import type { Froca } from "../services/froca-interface.js";
@@ -586,7 +585,7 @@ export default class FNote {
let childBranches = this.getChildBranches();
if (!childBranches) {
ws.logError(`No children for '${this.noteId}'. This shouldn't happen.`);
console.error(`No children for '${this.noteId}'. This shouldn't happen.`);
return [];
}

View File

@@ -138,7 +138,7 @@ export default class DesktopLayout {
.child(new PromotedAttributesWidget())
.child(<SqlTableSchemas />)
.child(new NoteDetailWidget())
.child(<NoteList />)
.child(<NoteList media="screen" />)
.child(<SearchResult />)
.child(<SqlResults />)
.child(<ScrollPadding />)

View File

@@ -66,6 +66,6 @@ export function applyModals(rootContainer: RootContainer) {
.child(<PopupEditorFormattingToolbar />)
.child(new PromotedAttributesWidget())
.child(new NoteDetailWidget())
.child(<NoteList displayOnlyCollections />))
.child(<NoteList media="screen" displayOnlyCollections />))
.child(<CallToActionDialog />);
}

View File

@@ -154,7 +154,7 @@ export default class MobileLayout {
.filling()
.contentSized()
.child(new NoteDetailWidget())
.child(<NoteList />)
.child(<NoteList media="screen" />)
.child(<FilePropertiesWrapper />)
)
.child(<MobileEditorToolbar />)

155
apps/client/src/print.css Normal file
View File

@@ -0,0 +1,155 @@
:root {
--print-font-size: 11pt;
--ck-content-color-image-caption-background: transparent !important;
}
html,
body {
width: 100%;
height: 100%;
color: black;
}
@page {
margin: 2cm;
}
.note-list-widget.full-height,
.note-list-widget.full-height .note-list-widget-content {
height: unset !important;
}
.component {
contain: none !important;
}
body[data-note-type="text"] .ck-content {
font-size: var(--print-font-size);
text-align: justify;
}
.ck-content figcaption {
font-style: italic;
}
.ck-content a {
text-decoration: none;
}
.ck-content a:not([href^="#root/"]) {
text-decoration: underline;
color: #374a75;
}
.ck-content .todo-list__label * {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
@supports selector(.todo-list__label__description:has(*)) and (height: 1lh) {
.ck-content .todo-list__label__description {
/* The percentage of the line height that the check box occupies */
--box-ratio: 0.75;
/* The size of the gap between the check box and the caption */
--box-text-gap: 0.25em;
--box-size: calc(1lh * var(--box-ratio));
--box-vert-offset: calc((1lh - var(--box-size)) / 2);
display: inline-block;
padding-inline-start: calc(var(--box-size) + var(--box-text-gap));
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-blank-outline/ */
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5C3.89%2c3 3%2c3.89 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5C21%2c3.89 20.1%2c3 19%2c3M19%2c5V19H5V5H19Z' /%3e%3c/svg%3e");
background-position: 0 var(--box-vert-offset);
background-size: var(--box-size);
background-repeat: no-repeat;
}
.ck-content .todo-list__label:has(input[type="checkbox"]:checked) .todo-list__label__description {
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-outline/ */
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5A2%2c2 0 0%2c0 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5A2%2c2 0 0%2c0 19%2c3M19%2c5V19H5V5H19M10%2c17L6%2c13L7.41%2c11.58L10%2c14.17L16.59%2c7.58L18%2c9' /%3e%3c/svg%3e");
}
.ck-content .todo-list__label input[type="checkbox"] {
display: none !important;
}
}
/* #region Footnotes */
.footnote-reference a,
.footnote-back-link a {
text-decoration: none !important;
}
li.footnote-item {
position: relative;
width: fit-content;
}
.ck-content .footnote-back-link {
margin-right: 0.25em;
}
.ck-content .footnote-content {
display: inline-block;
width: unset;
}
/* #endregion */
/* #region Widows and orphans */
p,
blockquote {
widows: 4;
orphans: 4;
}
pre > code {
widows: 6;
orphans: 6;
overflow: auto;
white-space: pre-wrap !important;
}
h1,
h2,
h3,
h4,
h5,
h6 {
page-break-after: avoid;
break-after: avoid;
}
/* #endregion */
/* #region Tables */
.table thead th,
.table td,
.table th {
/* Fix center vertical alignment of table cells */
vertical-align: middle;
}
pre {
box-shadow: unset !important;
border: 0.75pt solid gray !important;
border-radius: 2pt !important;
}
th,
span[style] {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
/* #endregion */
/* #region Page breaks */
.page-break {
page-break-after: always;
break-after: always;
}
.page-break > *,
.page-break::after {
display: none !important;
}
/* #endregion */

92
apps/client/src/print.tsx Normal file
View File

@@ -0,0 +1,92 @@
import FNote from "./entities/fnote";
import { render } from "preact";
import { CustomNoteList } from "./widgets/collections/NoteList";
import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
import content_renderer from "./services/content_renderer";
interface RendererProps {
note: FNote;
onReady: () => void;
}
async function main() {
const notePath = window.location.hash.substring(1);
const noteId = notePath.split("/").at(-1);
if (!noteId) return;
await import("./print.css");
const froca = (await import("./services/froca")).default;
const note = await froca.getNote(noteId);
render(<App note={note} noteId={noteId} />, document.body);
}
function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) {
const sentReadyEvent = useRef(false);
const onReady = useCallback(() => {
if (sentReadyEvent.current) return;
window.dispatchEvent(new Event("note-ready"));
window._noteReady = true;
sentReadyEvent.current = true;
}, []);
const props: RendererProps | undefined | null = note && { note, onReady };
if (!note || !props) return <Error404 noteId={noteId} />
useLayoutEffect(() => {
document.body.dataset.noteType = note.type;
}, [ note ]);
return (
<>
{note.type === "book"
? <CollectionRenderer {...props} />
: <SingleNoteRenderer {...props} />
}
</>
);
}
function SingleNoteRenderer({ note, onReady }: RendererProps) {
const containerRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
async function load() {
if (note.type === "text") {
await import("@triliumnext/ckeditor5/src/theme/ck-content.css");
}
const { $renderedContent } = await content_renderer.getRenderedContent(note, { noChildrenList: true });
containerRef.current?.replaceChildren(...$renderedContent);
}
load().then(() => requestAnimationFrame(onReady))
}, [ note ]);
return <>
<h1>{note.title}</h1>
<main ref={containerRef} />
</>;
}
function CollectionRenderer({ note, onReady }: RendererProps) {
return <CustomNoteList
isEnabled
note={note}
notePath={note.getBestNotePath().join("/")}
ntxId="print"
highlightedTokens={null}
media="print"
onReady={onReady}
/>;
}
function Error404({ noteId }: { noteId: string }) {
return (
<main>
<p>The note you are trying to print could not be found.</p>
<small>{noteId}</small>
</main>
)
}
main();

View File

@@ -40,20 +40,23 @@ class FrocaImpl implements Froca {
constructor() {
this.initializedPromise = this.loadInitialTree();
this.#clear();
}
async loadInitialTree() {
const resp = await server.get<SubtreeResponse>("tree");
// clear the cache only directly before adding new content which is important for e.g., switching to protected session
this.#clear();
this.addResp(resp);
}
#clear() {
this.notes = {};
this.branches = {};
this.attributes = {};
this.attachments = {};
this.blobPromises = {};
this.addResp(resp);
}
async loadSubTree(subTreeNoteId: string) {

View File

@@ -76,7 +76,7 @@ export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
// Load theme.
const currentThemeName = String(options.get("codeBlockTheme"));
loadHighlightingTheme(currentThemeName);
await loadHighlightingTheme(currentThemeName);
// Load mime types.
let mimeTypes: MimeType[];
@@ -98,17 +98,16 @@ export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
highlightingLoaded = true;
}
export function loadHighlightingTheme(themeName: string) {
export async function loadHighlightingTheme(themeName: string) {
const themePrefix = "default:";
let theme: Theme | null = null;
if (themeName.includes(themePrefix)) {
if (glob.device === "print") {
theme = Themes.vs;
} else if (themeName.includes(themePrefix)) {
theme = Themes[themeName.substring(themePrefix.length)];
}
if (!theme) {
theme = Themes.default;
}
loadTheme(theme);
await loadTheme(theme ?? Themes.default);
}
/**

View File

@@ -304,6 +304,8 @@ async function sendPing() {
}
setTimeout(() => {
if (glob.device === "print") return;
ws = connectWebSocket();
lastPingTs = Date.now();

View File

@@ -1,322 +0,0 @@
:root {
--main-background-color: white;
--root-background: var(--main-background-color);
--launcher-pane-background-color: var(--main-background-color);
--main-text-color: black;
--input-text-color: var(--main-text-color);
--print-font-size: 11pt;
}
@page {
margin: 2cm;
}
.ck-content {
font-size: var(--print-font-size);
text-align: justify;
}
.note-detail-readonly-text {
padding: 0 !important;
}
.no-print,
.no-print *,
.tab-row-container,
.tab-row-widget,
.title-bar-buttons,
#launcher-pane,
#left-pane,
#center-pane > *:not(.split-note-container-widget),
#right-pane,
.title-row .note-icon-widget,
.title-row .icon-action,
.ribbon-container,
.promoted-attributes-widget,
.scroll-padding-widget,
.note-list-widget,
.spacer {
display: none !important;
}
body.mobile #mobile-sidebar-wrapper,
body.mobile .classic-toolbar-widget,
body.mobile .action-button {
display: none !important;
}
body.mobile #detail-container {
max-height: unset;
}
body.mobile .note-title-widget {
padding: 0 !important;
}
body,
#root-widget,
#rest-pane > div.component:first-child,
.note-detail-printable,
.note-detail-editable-text-editor {
height: unset !important;
overflow: auto;
}
.ck.ck-editor__editable_inline {
overflow: hidden !important;
}
.note-title-widget input,
.note-detail-editable-text,
.note-detail-editable-text-editor {
padding: 0 !important;
}
html,
body {
width: unset !important;
height: unset !important;
overflow: visible;
position: unset;
/* https://github.com/zadam/trilium/issues/3202 */
color: black;
}
#root-widget,
#horizontal-main-container,
#rest-pane,
#vertical-main-container,
#center-pane,
.split-note-container-widget,
.note-split:not(.hidden-ext),
body.mobile #mobile-rest-container {
display: block !important;
overflow: auto;
border-radius: 0 !important;
}
#center-pane,
#rest-pane,
.note-split,
body.mobile #detail-container {
width: unset !important;
max-width: unset !important;
}
.component {
contain: none !important;
}
/* Respect page breaks */
.page-break {
page-break-after: always;
break-after: always;
}
.page-break > * {
display: none !important;
}
.relation-map-wrapper {
height: 100vh !important;
}
.table thead th,
.table td,
.table th {
/* Fix center vertical alignment of table cells */
vertical-align: middle;
}
pre {
box-shadow: unset !important;
border: 0.75pt solid gray !important;
border-radius: 2pt !important;
}
th,
span[style] {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
/*
* Text note specific fixes
*/
.ck-widget {
outline: none !important;
}
.ck-placeholder,
.ck-widget__type-around,
.ck-widget__selection-handle {
display: none !important;
}
.ck-widget.table td.ck-editor__nested-editable.ck-editor__nested-editable_focused,
.ck-widget.table td.ck-editor__nested-editable:focus,
.ck-widget.table th.ck-editor__nested-editable.ck-editor__nested-editable_focused,
.ck-widget.table th.ck-editor__nested-editable:focus {
background: unset !important;
outline: unset !important;
}
.include-note .include-note-content {
max-height: unset !important;
overflow: unset !important;
}
/* TODO: This will break once we translate the language */
.ck-content pre[data-language="Auto-detected"]:after {
display: none !important;
}
/*
* Code note specific fixes.
*/
.note-detail-code pre {
border: unset !important;
border-radius: unset !important;
}
/*
* Links
*/
.note-detail-printable a {
text-decoration: none;
}
.note-detail-printable a:not([href^="#root/"]) {
text-decoration: underline;
color: #374a75;
}
.note-detail-printable a::after {
/* Hide the external link trailing arrow */
display: none !important;
}
/*
* TODO list check boxes
*/
.note-detail-printable .todo-list__label * {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
@supports selector(.todo-list__label__description:has(*)) and (height: 1lh) {
.note-detail-printable .todo-list__label__description {
/* The percentage of the line height that the check box occupies */
--box-ratio: 0.75;
/* The size of the gap between the check box and the caption */
--box-text-gap: 0.25em;
--box-size: calc(1lh * var(--box-ratio));
--box-vert-offset: calc((1lh - var(--box-size)) / 2);
display: inline-block;
padding-inline-start: calc(var(--box-size) + var(--box-text-gap));
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-blank-outline/ */
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5C3.89%2c3 3%2c3.89 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5C21%2c3.89 20.1%2c3 19%2c3M19%2c5V19H5V5H19Z' /%3e%3c/svg%3e");
background-position: 0 var(--box-vert-offset);
background-size: var(--box-size);
background-repeat: no-repeat;
}
.note-detail-printable .todo-list__label:has(input[type="checkbox"]:checked) .todo-list__label__description {
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-outline/ */
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5A2%2c2 0 0%2c0 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5A2%2c2 0 0%2c0 19%2c3M19%2c5V19H5V5H19M10%2c17L6%2c13L7.41%2c11.58L10%2c14.17L16.59%2c7.58L18%2c9' /%3e%3c/svg%3e");
}
.note-detail-printable .todo-list__label input[type="checkbox"] {
display: none !important;
}
}
/*
* Blockquotes
*/
.note-detail-printable blockquote {
box-shadow: unset;
}
/*
* Figures
*/
.note-detail-printable figcaption {
--accented-background-color: transparent;
font-style: italic;
}
/*
* Footnotes
*/
.note-detail-printable .footnote-reference a,
.footnote-back-link a {
text-decoration: none;
}
/* Make the "^" link cover the whole area of the footnote item */
.footnote-section {
clear: both;
}
.note-detail-printable li.footnote-item {
position: relative;
width: fit-content;
}
.note-detail-printable .footnote-back-link,
.note-detail-printable .footnote-back-link *,
.note-detail-printable .footnote-back-link a {
display: block;
position: absolute;
top: 0;
inset-inline-start: 0;
width: 100%;
height: 100%;
}
.note-detail-printable .footnote-back-link a {
color: transparent;
}
.note-detail-printable .footnote-content {
display: inline-block;
width: unset;
}
/*
* Widows and orphans
*/
p,
blockquote {
widows: 4;
orphans: 4;
}
pre > code {
widows: 6;
orphans: 6;
overflow: auto;
white-space: pre-wrap !important;
}
h1,
h2,
h3,
h4,
h5,
h6 {
page-break-after: avoid;
break-after: avoid;
}

View File

@@ -2423,3 +2423,13 @@ footer.webview-footer button {
background: rgba(255, 100, 100, 0.5);
text-decoration: line-through;
}
iframe.print-iframe {
position: absolute;
top: 0;
left: -600px;
right: -600px;
bottom: 0;
width: 0;
height: 0;
}

View File

@@ -1722,7 +1722,9 @@
"window-on-top": "Keep Window on Top"
},
"note_detail": {
"could_not_find_typewidget": "Could not find typeWidget for type '{{type}}'"
"could_not_find_typewidget": "Could not find typeWidget for type '{{type}}'",
"printing": "Printing in progress...",
"printing_pdf": "Exporting to PDF in progress..."
},
"note_title": {
"placeholder": "type note's title here..."

View File

@@ -16,7 +16,7 @@ interface ElectronProcess {
interface CustomGlobals {
isDesktop: typeof utils.isDesktop;
isMobile: typeof utils.isMobile;
device: "mobile" | "desktop";
device: "mobile" | "desktop" | "print";
getComponentByEl: typeof appContext.getComponentByEl;
getHeaders: typeof server.getHeaders;
getReferenceLinkTitle: (href: string) => Promise<string>;
@@ -59,6 +59,9 @@ declare global {
process?: ElectronProcess;
glob?: CustomGlobals;
/** On the printing endpoint, set to true when the note has fully loaded and is ready to be printed/exported as PDF. */
_noteReady?: boolean;
EXCALIDRAW_ASSET_PATH?: string;
}

View File

@@ -1,4 +1,4 @@
import { allViewTypes, ViewModeProps, ViewTypeOptions } from "./interface";
import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface";
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "../react/hooks";
import FNote from "../../entities/fnote";
import "./NoteList.css";
@@ -22,9 +22,11 @@ interface NoteListProps {
displayOnlyCollections?: boolean;
isEnabled: boolean;
ntxId: string | null | undefined;
media: ViewModeMedia;
onReady?: () => void;
}
export default function NoteList<T extends object>(props: Pick<NoteListProps, "displayOnlyCollections">) {
export default function NoteList<T extends object>(props: Pick<NoteListProps, "displayOnlyCollections" | "media" | "onReady">) {
const { note, noteContext, notePath, ntxId } = useNoteContext();
const isEnabled = noteContext?.hasNoteList();
return <CustomNoteList note={note} isEnabled={!!isEnabled} notePath={notePath} ntxId={ntxId} {...props} />
@@ -34,7 +36,7 @@ export function SearchNoteList<T extends object>(props: Omit<NoteListProps, "isE
return <CustomNoteList {...props} isEnabled={true} />
}
function CustomNoteList<T extends object>({ note, isEnabled: shouldEnable, notePath, highlightedTokens, displayOnlyCollections, ntxId }: NoteListProps) {
export function CustomNoteList<T extends object>({ note, isEnabled: shouldEnable, notePath, highlightedTokens, displayOnlyCollections, ntxId, onReady, ...restProps }: NoteListProps) {
const widgetRef = useRef<HTMLDivElement>(null);
const viewType = useNoteViewType(note);
const noteIds = useNoteIds(note, viewType, ntxId);
@@ -76,7 +78,9 @@ function CustomNoteList<T extends object>({ note, isEnabled: shouldEnable, noteP
note, noteIds, notePath,
highlightedTokens,
viewConfig: viewModeConfig[0],
saveConfig: viewModeConfig[1]
saveConfig: viewModeConfig[1],
onReady: onReady ?? (() => {}),
...restProps
}
}
@@ -123,7 +127,7 @@ function useNoteViewType(note?: FNote | null): ViewTypeOptions | undefined {
}
}
function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined, ntxId: string | null | undefined) {
export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined, ntxId: string | null | undefined) {
const [ noteIds, setNoteIds ] = useState<string[]>([]);
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
@@ -187,7 +191,7 @@ function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOptions |
return noteIds;
}
function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
const [ viewConfig, setViewConfig ] = useState<[T | undefined, (data: T) => void]>();
useEffect(() => {

View File

@@ -3,6 +3,8 @@ import FNote from "../../entities/fnote";
export const allViewTypes = ["list", "grid", "calendar", "table", "geoMap", "board", "presentation"] as const;
export type ViewTypeOptions = typeof allViewTypes[number];
export type ViewModeMedia = "screen" | "print";
export interface ViewModeProps<T extends object> {
note: FNote;
notePath: string;
@@ -13,4 +15,6 @@ export interface ViewModeProps<T extends object> {
highlightedTokens: string[] | null | undefined;
viewConfig: T | undefined;
saveConfig(newConfig: T): void;
media: ViewModeMedia;
onReady(): void;
}

View File

@@ -1,4 +1,4 @@
import { ViewModeProps } from "../interface";
import { ViewModeMedia, ViewModeProps } from "../interface";
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import Reveal from "reveal.js";
import slideBaseStylesheet from "reveal.js/dist/reveal.css?raw";
@@ -14,11 +14,11 @@ import { t } from "../../../services/i18n";
import { DEFAULT_THEME, loadPresentationTheme } from "./themes";
import FNote from "../../../entities/fnote";
export default function PresentationView({ note, noteIds }: ViewModeProps<{}>) {
export default function PresentationView({ note, noteIds, media, onReady }: ViewModeProps<{}>) {
const [ presentation, setPresentation ] = useState<PresentationModel>();
const containerRef = useRef<HTMLDivElement>(null);
const [ api, setApi ] = useState<Reveal.Api>();
const stylesheets = usePresentationStylesheets(note);
const stylesheets = usePresentationStylesheets(note, media);
function refresh() {
buildPresentationModel(note).then(setPresentation);
@@ -33,31 +33,59 @@ export default function PresentationView({ note, noteIds }: ViewModeProps<{}>) {
useLayoutEffect(refresh, [ note, noteIds ]);
return presentation && stylesheets && (
useEffect(() => {
// We need to wait for Reveal.js to initialize (by setting api) and for the presentation to become available.
if (api && presentation) {
// Timeout is necessary because it otherwise can cause flakiness by rendering only the first slide.
setTimeout(onReady, 200);
}
}, [ api, presentation ]);
if (!presentation || !stylesheets) return;
const content = (
<>
{stylesheets.map(stylesheet => <style>{stylesheet}</style>)}
<Presentation presentation={presentation} setApi={setApi} />
</>
);
if (media === "screen") {
return (
<>
<ShadowDom
className="presentation-container"
containerRef={containerRef}
>
{stylesheets.map(stylesheet => <style>{stylesheet}</style>)}
<Presentation presentation={presentation} setApi={setApi} />
</ShadowDom>
>{content}</ShadowDom>
<ButtonOverlay containerRef={containerRef} api={api} />
</>
)
} else if (media === "print") {
// Printing needs a query parameter that is read by Reveal.js.
const url = new URL(window.location.href);
url.searchParams.set("print-pdf", "");
window.history.replaceState({}, '', url);
// Shadow DOM doesn't work well with Reveal.js's PDF printing mechanism.
return content;
}
}
function usePresentationStylesheets(note: FNote) {
function usePresentationStylesheets(note: FNote, media: ViewModeMedia) {
const [ themeName ] = useNoteLabelWithDefault(note, "presentation:theme", DEFAULT_THEME);
const [ stylesheets, setStylesheets ] = useState<string[]>();
useLayoutEffect(() => {
loadPresentationTheme(themeName).then((themeStylesheet) => {
setStylesheets([
let stylesheets = [
slideBaseStylesheet,
themeStylesheet,
slideCustomStylesheet
].map(stylesheet => stylesheet.replace(/:root/g, ":host")));
];
if (media === "screen") {
// We are rendering in the shadow DOM, so the global variables are not set correctly.
stylesheets = stylesheets.map(stylesheet => stylesheet.replace(/:root/g, ":host"));
}
setStylesheets(stylesheets);
});
}, [ themeName ]);
@@ -128,6 +156,7 @@ function Presentation({ presentation, setApi } : { presentation: PresentationMod
const api = new Reveal(containerRef.current, {
transition: "slide",
embedded: true,
pdfMaxPagesPerSlide: 1,
keyboardCondition(event) {
// Full-screen requests sometimes fail, we rely on the UI button instead.
if (event.key === "f") {

View File

@@ -28,11 +28,12 @@ import ContentWidgetTypeWidget from "./type_widgets/content_widget.js";
import AttachmentListTypeWidget from "./type_widgets/attachment_list.js";
import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
import MindMapWidget from "./type_widgets/mind_map.js";
import utils from "../services/utils.js";
import utils, { isElectron } from "../services/utils.js";
import type { NoteType } from "../entities/fnote.js";
import type TypeWidget from "./type_widgets/type_widget.js";
import { MermaidTypeWidget } from "./type_widgets/mermaid.js";
import AiChatTypeWidget from "./type_widgets/ai_chat.js";
import toast from "../services/toast.js";
const TPL = /*html*/`
<div class="note-detail">
@@ -140,6 +141,13 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
doRender() {
this.$widget = $(TPL);
this.contentSized();
if (utils.isElectron()) {
const { ipcRenderer } = utils.dynamicRequire("electron");
ipcRenderer.on("print-done", () => {
toast.closePersistent("printing");
});
}
}
async refresh() {
@@ -297,18 +305,53 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
return;
}
// Trigger in timeout to dismiss the menu while printing.
setTimeout(window.print, 0);
toast.showPersistent({
icon: "bx bx-loader-circle bx-spin",
message: t("note_detail.printing"),
id: "printing"
});
if (isElectron()) {
const { ipcRenderer } = utils.dynamicRequire("electron");
ipcRenderer.send("print-note", {
notePath: this.notePath
});
} else {
const iframe = document.createElement('iframe');
iframe.src = `?print#${this.notePath}`;
iframe.className = "print-iframe";
document.body.appendChild(iframe);
iframe.onload = () => {
if (!iframe.contentWindow) {
toast.closePersistent("printing");
document.body.removeChild(iframe);
return;
}
iframe.contentWindow.addEventListener("note-ready", () => {
toast.closePersistent("printing");
iframe.contentWindow?.print();
document.body.removeChild(iframe);
});
};
}
}
async exportAsPdfEvent() {
if (!this.noteContext?.isActive() || !this.note) {
if (!this.noteContext?.isActive() || !this.note || !this.notePath) {
return;
}
toast.showPersistent({
icon: "bx bx-loader-circle bx-spin",
message: t("note_detail.printing_pdf"),
id: "printing"
});
const { ipcRenderer } = utils.dynamicRequire("electron");
ipcRenderer.send("export-as-pdf", {
title: this.note.title,
notePath: this.notePath,
pageSize: this.note.getAttributeValue("label", "printPageSize") ?? "Letter",
landscape: this.note.hasAttribute("label", "printLandscape")
});

View File

@@ -47,7 +47,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type);
const isInOptions = note.noteId.startsWith("_options");
const isPrintable = ["text", "code"].includes(note.type);
const isPrintable = ["text", "code"].includes(note.type) || (note.type === "book" && note.getLabelValue("viewType") === "presentation");
const isElectron = getIsElectron();
const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type);

View File

@@ -54,6 +54,7 @@ export default function SearchResult() {
{state === SearchResultState.GOT_RESULTS && (
<SearchNoteList
media="screen"
note={note}
notePath={notePath}
highlightedTokens={highlightedTokens}

View File

@@ -76,7 +76,8 @@ export default defineConfig(() => ({
setup: join(__dirname, "src", "setup.ts"),
share: join(__dirname, "src", "share.ts"),
set_password: join(__dirname, "src", "set_password.ts"),
runtime: join(__dirname, "src", "runtime.ts")
runtime: join(__dirname, "src", "runtime.ts"),
print: join(__dirname, "src", "print.tsx")
},
output: {
entryFileNames: "src/[name].js",

File diff suppressed because one or more lines are too long

View File

@@ -302,7 +302,9 @@
<td><code>color</code>
</td>
<td>defines color of the note in note tree, links etc. Use any valid CSS color
value like 'red' or #a13d5f</td>
value like 'red' or #a13d5f
<br>Note: this color may be automatically adjusted when displayed to ensure
sufficient contrast with the background.</td>
</tr>
<tr>
<td><code>keyboardShortcut</code>

View File

@@ -1,42 +0,0 @@
<p>
<img src="Export as PDF_image.png">
</p>
<p>Screenshot of the note contextual menu indicating the “Export as PDF”
option.</p>
<p>On the desktop application of Trilium it is possible to export a note
as PDF. On the server or PWA (mobile), the option is not available due
to technical constraints and it will be hidden.</p>
<p>To print a note, select the
<img src="1_Export as PDF_image.png">button to the right of the note and select <em>Export as PDF</em>.</p>
<p>Afterwards you will be prompted to select where to save the PDF file.</p>
<h2>Automatic opening of the file</h2>
<p>When the PDF is exported, it is automatically opened with the system default
application for easy preview.</p>
<p>Note that if you are using Linux with the GNOME desktop environment, sometimes
the default application might seem incorrect (such as opening in GIMP).
This is because it uses Gnome's “Recommended applications” list.</p>
<p>To solve this, you can change the recommended application for PDFs via
this command line. First, list the available applications via <code>gio mime application/pdf</code> and
then set the desired one. For example to use GNOME's Evince:</p><pre><code class="language-text-x-trilium-auto">gio mime application/pdf</code></pre>
<h2>Reporting issues with the rendering</h2>
<p>Should you encounter any visual issues in the resulting PDF file (e.g.
a table does not fit properly, there is cut off text, etc.) feel free to
<a
href="#root/_help_wy8So3yZZlH9">report the issue</a>. In this case, it's best to offer a sample note (click
on the
<img src="1_Export as PDF_image.png">button, select Export note → This note and all of its descendants → HTML
in ZIP archive). Make sure not to accidentally leak any personal information.</p>
<h2>Landscape mode</h2>
<p>When exporting to PDF, there are no customizable settings such as page
orientation, size, etc. However, it is possible to specify a given note
to be printed as a PDF in landscape mode by adding the <code>#printLandscape</code> attribute
to it (see&nbsp;<a class="reference-link" href="#root/_help_zEY4DaJG4YT5">Attributes</a>).</p>
<h2>Page size</h2>
<p>By default, the resulting PDF will be in Letter format. It is possible
to adjust it to another page size via the <code>#printPageSize</code> attribute,
with one of the following values: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.</p>
<h2>Keyboard shortcut</h2>
<p>It's possible to trigger the export to PDF from the keyboard by going
to&nbsp;<em>Keyboard shortcuts</em>&nbsp;in&nbsp;<a class="reference-link"
href="#root/_help_4TIF1oA4VQRO">Options</a>&nbsp;and assigning a key combination
for the <code>exportAsPdf</code> action.</p>

View File

@@ -0,0 +1,111 @@
<figure class="image">
<img style="aspect-ratio:951/432;" src="Printing & Exporting as PD.png"
width="951" height="432">
<figcaption>Screenshot of the note contextual menu indicating the “Export as PDF”
option.</figcaption>
</figure>
<h2>Printing</h2>
<p>This feature allows printing of notes. It works on both the desktop client,
but also on the web.</p>
<p>Note that not all note types are printable as of now. We do plan to increase
the coverage of supported note types in the future.</p>
<p>To print a note, select the
<img src="1_Printing & Exporting as PD.png"
width="29" height="31">button to the right of the note and select <em>Print note</em>. Depending
on the size and type of the note, this can take up to a few seconds. Afterwards
you will be redirected to the system/browser printing dialog.</p>
<aside
class="admonition note">
<p>Printing and exporting as PDF are not perfect. Due to technical limitations,
and sometimes even browser glitches the text might appear cut off in some
circumstances.&nbsp;</p>
</aside>
<h2>Reporting issues with the rendering</h2>
<p>Should you encounter any visual issues in the resulting PDF file (e.g.
a table does not fit properly, there is cut off text, etc.) feel free to
<a
href="#root/_help_wy8So3yZZlH9">report the issue</a>. In this case, it's best to offer a sample note (click
on the
<img src="1_Printing & Exporting as PD.png" width="29" height="31">button, select Export note → This note and all of its descendants → HTML
in ZIP archive). Make sure not to accidentally leak any personal information.</p>
<p>Consider adjusting font sizes and using <a href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/_help_CohkqWQC1iBv">page breaks</a> to
work around the layout.</p>
<h2>Exporting as PDF</h2>
<p>On the desktop application of Trilium it is possible to export a note
as PDF. On the server or PWA (mobile), the option is not available due
to technical constraints and it will be hidden.</p>
<p>To print a note, select the
<img src="1_Printing & Exporting as PD.png">button to the right of the note and select <em>Export as PDF</em>. Afterwards
you will be prompted to select where to save the PDF file.</p>
<h3>Automatic opening of the file</h3>
<p>When the PDF is exported, it is automatically opened with the system default
application for easy preview.</p>
<p>Note that if you are using Linux with the GNOME desktop environment, sometimes
the default application might seem incorrect (such as opening in GIMP).
This is because it uses Gnome's “Recommended applications” list.</p>
<p>To solve this, you can change the recommended application for PDFs via
this command line. First, list the available applications via <code>gio mime application/pdf</code> and
then set the desired one. For example to use GNOME's Evince:</p><pre><code class="language-text-x-trilium-auto">gio mime application/pdf</code></pre>
<h3>Customizing exporting as PDF</h3>
<p>When exporting to PDF, there are no customizable settings such as page
orientation, size. However, there are a few&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_zEY4DaJG4YT5">Attributes</a>&nbsp;to
adjust some of the settings:</p>
<ul>
<li data-list-item-id="e76a765fe309058890d4f4d3e63bca578">To print in landscape mode instead of portrait (useful for big diagrams
or slides), add <code>#printLandscape</code>.</li>
<li data-list-item-id="e78123a3a12954d7c9f520f4e75ed375d">By default, the resulting PDF will be in Letter format. It is possible
to adjust it to another page size via the <code>#printPageSize</code> attribute,
with one of the following values: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.</li>
</ul>
<aside class="admonition note">
<p>These options have no effect when used with the printing feature, since
the user-defined settings are used instead.</p>
</aside>
<h2>Keyboard shortcut</h2>
<p>It's possible to trigger both printing and export as PDF from the keyboard
by going to&nbsp;<em>Keyboard shortcuts</em>&nbsp;in&nbsp;<a class="reference-link"
href="#root/_help_4TIF1oA4VQRO">Options</a>&nbsp;and assigning a key combination
for:</p>
<ul>
<li class="ck-list-marker-italic" data-list-item-id="e52b4441f2a3af7773585b19c8f796c8d"><em>Print Active Note</em>
</li>
<li class="ck-list-marker-italic" data-list-item-id="e40e1f6f480c7857100f64c2f63e062fb"><em>Export Active Note as PDF</em>
</li>
</ul>
<h2>Constraints &amp; limitations</h2>
<p>Not all&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/_help_KSZ04uQ2D1St">Note Types</a>&nbsp;are
supported when printing, in which case the <em>Print</em> and <em>Export as PDF</em> options
will be disabled.</p>
<ul>
<li data-list-item-id="e448a833da1bb1643181c72779382d1b6">For&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a>&nbsp;notes:
<ul>
<li data-list-item-id="e6b699104a21930de7c2618123d0d5750">Line numbers are not printed.</li>
<li data-list-item-id="e342cba939534f0989c9620520c7b87a3">Syntax highlighting is enabled, however a default theme (Visual Studio)
is enforced.</li>
</ul>
</li>
<li data-list-item-id="ea23948a21d6ad2a01d70dfbdfa9bd62f">For&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_GTwFsgaA0lCt">Collections</a>:
<ul>
<li data-list-item-id="e387ee52424d8b78fd61c3a1e0d395d3e">Only&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/GTwFsgaA0lCt/_help_zP3PMqaG71Ct">Presentation View</a>&nbsp;is
currently supported.</li>
<li data-list-item-id="e94fa0cced6a7668c96cf7513557f2906">We plan to add support for all the collection types at some point.</li>
</ul>
</li>
<li data-list-item-id="e8b17c931e05f9d579767536088a108da">Using&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/pKK96zzmvBGf/_help_AlhDUqhENtH7">Custom app-wide CSS</a>&nbsp;for
printing is not longer supported, due to a more stable but isolated mechanism.
<ul>
<li data-list-item-id="eae91219e277cc2366d86d47291ed9cec">We plan to introduce a new mechanism specifically for a print CSS.</li>
</ul>
</li>
</ul>
<h2>Under the hood</h2>
<p>Both printing and exporting as PDF use the same mechanism: a note is rendered
individually in a separate webpage that is then sent to the browser or
the Electron application either for printing or exporting as PDF.</p>
<p>The webpage that renders a single note can actually be accessed in a web
browser. For example <code>http://localhost:8080/#root/WWRGzqHUfRln/RRZsE9Al8AIZ?ntxId=0o4fzk</code> becomes <code>http://localhost:8080/?print#root/WWRGzqHUfRln/RRZsE9Al8AIZ</code>.</p>
<p>Accessing the print note in a web browser allows for easy debugging to
understand why a particular note doesn't render well. The mechanism for
rendering is similar to the one used in&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_0ESUbbAxVnoK">Note List</a>.</p>

View File

@@ -6,33 +6,31 @@
within Trilium.</p>
<h2>How it works</h2>
<ul>
<li data-list-item-id="e51cbe078fb06e2bfdfb1f2bf6fd82225">Each slide is a child note of the collection.</li>
<li data-list-item-id="efffc6f15623109770c57338c61b4ccb6">The order of the child notes determines the order of the slides.</li>
<li
data-list-item-id="e1a795af0f85ba888f84586be6ed2de2a">Unlike traditional presentation software, slides can be laid out both
<li>Each slide is a child note of the collection.</li>
<li>The order of the child notes determines the order of the slides.</li>
<li>Unlike traditional presentation software, slides can be laid out both
horizontally and vertically (see belwo for more information).</li>
<li data-list-item-id="e9a2a74d8e19974766f65416100e8f877">Direct children will be laid out horizontally and the children of those
<li>Direct children will be laid out horizontally and the children of those
will be laid out vertically. Children deeper than two levels of nesting
are ignored.</li>
</ul>
<h2>Interaction and navigation</h2>
<p>In the floating buttons section (top-right):</p>
<ul>
<li data-list-item-id="ee6dd604137e6918dcfac24fe271b05bf">Edit button to go to the corresponding note of the current slide.</li>
<li
data-list-item-id="e4805f237077e9dc8ddbed2cb0b56e585">Press Overview button (or the <kbd>O</kbd> key) to show a birds-eye view
<li>Edit button to go to the corresponding note of the current slide.</li>
<li>Press Overview button (or the <kbd>O</kbd> key) to show a birds-eye view
of the slides. Press the button again to disable it.</li>
<li data-list-item-id="ee714da289257895faf87a26f6849e050">Press the “Start presentation” button to show the presentation in full-screen.</li>
<li>Press the “Start presentation” button to show the presentation in full-screen.</li>
</ul>
<p>The following keyboard shortcuts are supported:</p>
<ul>
<li data-list-item-id="e5a34fbaa9c98cd91ffac8301e153a083">Press <kbd></kbd> and <kbd></kbd> (or <kbd>H</kbd> and <kbd>L</kbd>) to go
<li>Press <kbd></kbd> and <kbd></kbd> (or <kbd>H</kbd> and <kbd>L</kbd>) to go
to the slide on the left or on the right (horizontal).</li>
<li data-list-item-id="e39394b060a9b767d04c466440106cbf0">Press <kbd></kbd> and <kbd></kbd> &nbsp;(or <kbd>K</kbd> and <kbd>J</kbd>)
<li>Press <kbd></kbd> and <kbd></kbd> &nbsp;(or <kbd>K</kbd> and <kbd>J</kbd>)
to go to the upward or downward slide (vertical).</li>
<li data-list-item-id="e17441e9598f687e89a161a8afe2f703d">Press <kbd>Space</kbd> and <kbd>Shift</kbd> + <kbd>Space</kbd> or &nbsp;to go
<li>Press <kbd>Space</kbd> and <kbd>Shift</kbd> + <kbd>Space</kbd> or &nbsp;to go
to the next/previous slide in order.</li>
<li data-list-item-id="e4212130fc1fdc5de980e2e40feae68c2">And a few more, press <kbd>?</kbd> to display a popup with all the supported
<li>And a few more, press <kbd>?</kbd> to display a popup with all the supported
keyboard combinations.</li>
</ul>
<h2>Vertical slides and nesting</h2>
@@ -42,15 +40,15 @@
<p>This horizontal/vertical organization affects transitions (especially
on the “slide” transition), however it is most noticeable in navigation.</p>
<ul>
<li data-list-item-id="e9245eba99da45713930c1714202add31">Pressing <kbd></kbd> and <kbd></kbd> will navigate through slides horizontally,
<li>Pressing <kbd></kbd> and <kbd></kbd> will navigate through slides horizontally,
thus skipping vertical notes under the current slide. This is useful to
skip entire chapters/related slides.&nbsp;</li>
<li data-list-item-id="ef9aedf69e5e2a599e9fbd0de1b89b4ad">Pressing <kbd></kbd> and <kbd></kbd> will navigate through the vertical
skip entire chapters/related slides.</li>
<li>Pressing <kbd></kbd> and <kbd></kbd> will navigate through the vertical
slides at the current level.</li>
<li data-list-item-id="e436fc36f74a22fdefe31e498684e23b3">Pressing <kbd>Space</kbd> and <kbd>Shift</kbd> + <kbd>Space</kbd> will go to
<li>Pressing <kbd>Space</kbd> and <kbd>Shift</kbd> + <kbd>Space</kbd> will go to
the next/previous slide in order, regardless of the direction. This is
generally the key combination to use when presenting.</li>
<li data-list-item-id="e9c5dcf5efec250876bd2c527082e76d7">The arrows on the bottom-right of the slide will also reflect this navigation
<li>The arrows on the bottom-right of the slide will also reflect this navigation
scheme.</li>
</ul>
<figure class="image image-style-align-right image_resized" style="width:55.57%;">
@@ -62,19 +60,19 @@
slides.</p>
<p>In the following example, the note structure is as follows:</p>
<ul>
<li data-list-item-id="e4d5d440ec56a9c81b7c8323ab142478d">Presentation collection
<li>Presentation collection
<ul>
<li data-list-item-id="e255021a351d18e2792c15ab2b80c0a57">Trilium Notes (demo page)</li>
<li data-list-item-id="ef6f95ec54572aa247a13e8104a2db0c3">“Introduction” slide
<li>Trilium Notes (demo page)</li>
<li>“Introduction” slide
<ul>
<li data-list-item-id="eccb526d7f2dd67ac686f8e963e660a77">“The challenge of personal knowledge management”</li>
<li data-list-item-id="e0aca477122bb0b00ab9b3bc163436b5f">“Note-taking structures”</li>
<li>“The challenge of personal knowledge management”</li>
<li>“Note-taking structures”</li>
</ul>
</li>
<li data-list-item-id="e3bbbe33c8d7a18cb17cd0de29b6eff05">“Demo &amp; Feature highlights” slide
<li>“Demo &amp; Feature highlights” slide
<ul>
<li data-list-item-id="ebf5e3fe8a8b4400f21d5cc99b8198898">“Really fast installation process”</li>
<li data-list-item-id="e0b6885a0bf7f76fa2ce7801f004a2d42">Video slide</li>
<li>“Really fast installation process”</li>
<li>Video slide</li>
</ul>
</li>
</ul>
@@ -83,46 +81,44 @@
<h2>Customization</h2>
<p>At collection level, it's possible to adjust:</p>
<ul>
<li data-list-item-id="edc0a7c71ee836a225a8793e3cb1e29e8">The theme of the entire presentation to one of the predefined themes by
going to the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_BlN9DFI679QC">Ribbon</a>&nbsp;and
<li>The theme of the entire presentation to one of the predefined themes by
going to the&nbsp;<a class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a>&nbsp;and
looking for the <em>Collection Properties</em> tab.</li>
<li data-list-item-id="ed04e1bd7a997de717d8b8b8b90f19e7f">It's currently not possible to create custom themes, although it is planned.</li>
<li
data-list-item-id="edb37c7902c9e464de4555ec3ede05403">Note that it is note possible to alter the CSS via&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/pKK96zzmvBGf/_help_AlhDUqhENtH7">Custom app-wide CSS</a>&nbsp;because
the slides are rendered isolated (in a shadow DOM).</li>
<li>It's currently not possible to create custom themes, although it is planned.</li>
<li>Note that it is note possible to alter the CSS via&nbsp;<a class="reference-link"
href="#root/_help_AlhDUqhENtH7">Custom app-wide CSS</a>&nbsp;because the
slides are rendered isolated (in a shadow DOM).</li>
</ul>
<p>At slide level:</p>
<ul>
<li data-list-item-id="eb9c23ec94dcd00a3a8539d3cd633d7df">It's possible to adjust the background color of a slide by using the
<li>It's possible to adjust the background color of a slide by using the
<a
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">predefined promoted attribute</a>for the color or manually setting <code>#slide:background</code> to
href="#root/_help_OFXdgB2nNk1F">predefined promoted attribute</a>for the color or manually setting <code>#slide:background</code> to
a hex color.</li>
<li data-list-item-id="e70af66a7d3468b7fa86badb1e2c93cc9">More complex backgrounds can be achieved via gradients. There's no UI
<li>More complex backgrounds can be achieved via gradients. There's no UI
for it; it has to be set via <code>#slide:background</code> to a CSS gradient
definition such as: <code>linear-gradient(to bottom, #283b95, #17b2c3)</code>.</li>
</ul>
<h2>Tips and tricks</h2>
<ul>
<li data-list-item-id="ec501025735d0063969f2a48eedb651dc">Text notes generally respect the formatting (bold, italic, foreground
<li>Text notes generally respect the formatting (bold, italic, foreground
and background colors) and font size. Code blocks and tables also work.</li>
<li
data-list-item-id="e8acd457a2660726905aee30a9325a620">Try using more than just text notes, the presentation uses the same mechanism
as <a href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_R9pX4DGra2Vt">shared notes</a> and&nbsp;
<li>Try using more than just text notes, the presentation uses the same mechanism
as <a href="#root/_help_R9pX4DGra2Vt">shared notes</a> and&nbsp;<a class="reference-link"
href="#root/_help_0ESUbbAxVnoK">Note List</a>&nbsp;so it should be able
to display&nbsp;<a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>,&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_0ESUbbAxVnoK">Note List</a>&nbsp;so it should be able to display&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_s1aBHPd79XYj">Mermaid Diagrams</a>,&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_grjYqerjn243">Canvas</a>&nbsp;and&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_gBbsAeiuUxI5">Mind Map</a>&nbsp;in
class="reference-link" href="#root/_help_grjYqerjn243">Canvas</a>&nbsp;and&nbsp;<a class="reference-link" href="#root/_help_gBbsAeiuUxI5">Mind Map</a>&nbsp;in
full-screen (without the interactivity).
<ul>
<li data-list-item-id="e91cdf4823552f771ed802de7fd6330e4">Consider using a transparent background for&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_grjYqerjn243">Canvas</a>, if
the slides have a custom background (go to the hamburger menu in the Canvas,
press the button select a custom color and write <code>transparent</code>).</li>
<li
data-list-item-id="ebed408174c89a15dc9b0ee74d36e2e70">
<p>For&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_s1aBHPd79XYj">Mermaid Diagrams</a>,
<li>
<p>Consider using a transparent background for&nbsp;<a class="reference-link"
href="#root/_help_grjYqerjn243">Canvas</a>, if the slides have a custom
background (go to the hamburger menu in the Canvas, press the button select
a custom color and write <code>transparent</code>).</p>
</li>
<li>
<p>For&nbsp;<a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>,
some of them have a predefined background which can be changed via the
frontmatter. For example, for XY-charts:</p><pre><code class="language-text-x-trilium-auto">---
config:

View File

@@ -1,11 +1,10 @@
<p>It is possible to provide a CSS file to be used regardless of the theme
set by the user.</p>
<figure class="table">
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
@@ -32,7 +31,7 @@
</tr>
</tbody>
</table>
</figure>
<h2>Seeing the changes</h2>
<p>Adding a new <em>app CSS note</em> or modifying an existing one does not
immediately apply changes. To see the changes, press Ctrl+Shift+R to refresh
@@ -54,10 +53,9 @@
workspaces.</p>
<p>To do so:</p>
<ol>
<li data-list-item-id="eaca1b6777262e20c38dae5e2c2ef8496">In the note with <code>#workspace</code>, add an inheritable attribute <code>#cssClass(inheritable)</code> with
<li>In the note with <code>#workspace</code>, add an inheritable attribute <code>#cssClass(inheritable)</code> with
a value that uniquely identifies the workspace (say <code>my-workspace</code>).</li>
<li
data-list-item-id="e01663cf2128c10a0cd0cab1bb27fd44d">Anywhere in the note structure, create a CSS note with <code>#appCss</code>.</li>
<li>Anywhere in the note structure, create a CSS note with <code>#appCss</code>.</li>
</ol>
<h4>Change the color of the icons in the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a></h4><pre><code class="language-text-x-trilium-auto">.fancytree-node.my-workspace.fancytree-custom-icon {
color: #ff0000;
@@ -73,8 +71,8 @@
width="641" height="630">
</figure>
<ol>
<li data-list-item-id="e6e7ec9751bdc6f7846d10bf42c42c9b1">Insert an image in any note and take the URL of the image.</li>
<li data-list-item-id="edc7f77ed718593d91bda3b2983b81bed">Use the following CSS, adjusting the <code>background-image</code> and <code>width</code> and <code>height</code> to
<li>Insert an image in any note and take the URL of the image.</li>
<li>Use the following CSS, adjusting the <code>background-image</code> and <code>width</code> and <code>height</code> to
the desired values.</li>
</ol><pre><code class="language-text-x-trilium-auto">.note-split.my-workspace .scrolling-container:after {
position: fixed;
@@ -94,5 +92,5 @@
<p>Some parts of the application can't be styled directly via custom CSS
because they are rendered in an isolated mode (shadow DOM), more specifically:</p>
<ul>
<li data-list-item-id="e3ce2c089fe536bc42856bb1b5edc8c8e">The slides in a&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/GTwFsgaA0lCt/_help_zP3PMqaG71Ct">Presentation View</a>.</li>
<li>The slides in a&nbsp;<a class="reference-link" href="#root/_help_zP3PMqaG71Ct">Presentation View</a>.</li>
</ul>

View File

@@ -373,7 +373,8 @@
"export_filter": "PDF Document (*.pdf)",
"unable-to-export-message": "The current note could not be exported as a PDF.",
"unable-to-export-title": "Unable to export as PDF",
"unable-to-save-message": "The selected file could not be written to. Try again or select another destination."
"unable-to-save-message": "The selected file could not be written to. Try again or select another destination.",
"unable-to-print": "Unable to print the note"
},
"tray": {
"tooltip": "Trilium Notes",

View File

@@ -3,7 +3,7 @@
window.glob = {
device: "<%= device %>",
baseApiUrl: 'api/',
baseApiUrl: "<%= baseApiUrl %>",
activeDialog: null,
maxEntityChangeIdAtLoad: <%= maxEntityChangeIdAtLoad %>,
maxEntityChangeSyncIdAtLoad: <%= maxEntityChangeSyncIdAtLoad %>,

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
<link rel="manifest" crossorigin="use-credentials" href="../manifest.webmanifest">
<title>Trilium Notes</title>
<script src="../<%= appPath %>/runtime.js" crossorigin type="module"></script>
</head>
<body
id="trilium-print"
lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>"
>
<noscript><%= t("javascript-required") %></noscript>
<script>
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
document.getElementsByTagName("body")[0].style.display = "none";
</script>
<%- include("./partials/windowGlobal.ejs", locals) %>
<!-- Required for correct loading of scripts in Electron -->
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script src="../<%= appPath %>/print.js" crossorigin type="module"></script>
</body>
</html>

View File

@@ -16,9 +16,11 @@ import type { Request, Response } from "express";
import type BNote from "../becca/entities/bnote.js";
import { getCurrentLocale } from "../services/i18n.js";
type View = "desktop" | "mobile" | "print";
function index(req: Request, res: Response) {
const options = optionService.getOptionMap();
const view = getView(req);
const options = optionService.getOptionMap();
//'overwrite' set to false (default) => the existing token will be re-used and validated
//'validateOnReuse' set to false => if validation fails, generate a new token instead of throwing an error
@@ -57,13 +59,19 @@ function index(req: Request, res: Response) {
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
maxContentWidth: Math.max(640, parseInt(options.maxContentWidth)),
triliumVersion: packageJson.version,
assetPath: assetPath,
appPath: appPath,
assetPath,
appPath,
baseApiUrl: 'api/',
currentLocale: getCurrentLocale()
});
}
function getView(req: Request): "desktop" | "mobile" {
function getView(req: Request): View {
// Special override for printing.
if ("print" in req.query) {
return "print";
}
// Electron always uses the desktop view.
if (isElectron) {
return "desktop";

View File

@@ -378,8 +378,6 @@ function register(app: express.Application) {
asyncApiRoute(PST, "/api/llm/chat/:chatNoteId/messages", llmRoute.sendMessage);
asyncApiRoute(PST, "/api/llm/chat/:chatNoteId/messages/stream", llmRoute.streamMessage);
// LLM provider endpoints - moved under /api/llm/providers hierarchy
asyncApiRoute(GET, "/api/llm/providers/ollama/models", ollamaRoute.listModels);
asyncApiRoute(GET, "/api/llm/providers/openai/models", openaiRoute.listModels);

View File

@@ -8,10 +8,12 @@ import sqlInit from "./sql_init.js";
import cls from "./cls.js";
import keyboardActionsService from "./keyboard_actions.js";
import electron from "electron";
import type { App, BrowserWindowConstructorOptions, BrowserWindow, WebContents } from "electron";
import type { App, BrowserWindowConstructorOptions, BrowserWindow, WebContents, IpcMainEvent } from "electron";
import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js";
import { t } from "i18next";
import { RESOURCE_DIR } from "./resource_dir.js";
import { PerformanceObserverEntryList } from "perf_hooks";
import options from "./options.js";
// Prevent the window being garbage collected
let mainWindow: BrowserWindow | null;
@@ -67,20 +69,35 @@ electron.ipcMain.on("create-extra-window", (event, arg) => {
createExtraWindow(arg.extraWindowHash);
});
interface PrintOpts {
notePath: string;
printToPdf: boolean;
}
interface ExportAsPdfOpts {
notePath: string;
title: string;
landscape: boolean;
pageSize: "A0" | "A1" | "A2" | "A3" | "A4" | "A5" | "A6" | "Legal" | "Letter" | "Tabloid" | "Ledger";
}
electron.ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => {
const browserWindow = electron.BrowserWindow.fromWebContents(e.sender);
if (!browserWindow) {
return;
electron.ipcMain.on("print-note", async (e, { notePath }: PrintOpts) => {
const browserWindow = await getBrowserWindowForPrinting(e, notePath);
browserWindow.webContents.print({}, (success, failureReason) => {
if (!success) {
electron.dialog.showErrorBox(t("pdf.unable-to-print"), failureReason);
}
e.sender.send("print-done");
browserWindow.destroy();
});
});
electron.ipcMain.on("export-as-pdf", async (e, { title, notePath, landscape, pageSize }: ExportAsPdfOpts) => {
const browserWindow = await getBrowserWindowForPrinting(e, notePath);
async function print() {
const filePath = electron.dialog.showSaveDialogSync(browserWindow, {
defaultPath: formatDownloadTitle(opts.title, "file", "application/pdf"),
defaultPath: formatDownloadTitle(title, "file", "application/pdf"),
filters: [
{
name: t("pdf.export_filter"),
@@ -88,15 +105,13 @@ electron.ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => {
}
]
});
if (!filePath) {
return;
}
if (!filePath) return;
let buffer: Buffer;
try {
buffer = await browserWindow.webContents.printToPDF({
landscape: opts.landscape,
pageSize: opts.pageSize,
landscape,
pageSize,
generateDocumentOutline: true,
generateTaggedPDF: true,
printBackground: true,
@@ -120,8 +135,36 @@ electron.ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => {
}
electron.shell.openPath(filePath);
}
try {
await print();
} finally {
e.sender.send("print-done");
browserWindow.destroy();
}
});
async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string) {
const browserWindow = new electron.BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
offscreen: true,
session: e.sender.session
},
});
await browserWindow.loadURL(`http://127.0.0.1:${port}/?print#${notePath}`);
await browserWindow.webContents.executeJavaScript(`
new Promise(resolve => {
if (window._noteReady) return resolve();
window.addEventListener("note-ready", () => resolve());
});
`);
return browserWindow;
}
async function createMainWindow(app: App) {
if ("setUserTasks" in app) {
app.setUserTasks([

View File

@@ -3472,7 +3472,7 @@
"BFs8mudNFgCS",
"NRnIZmSMc5sj"
],
"title": "Export as PDF",
"title": "Printing & Exporting as PDF",
"notePosition": 120,
"prefix": null,
"isExpanded": false,
@@ -3503,13 +3503,62 @@
{
"type": "label",
"name": "iconClass",
"value": "bx bxs-file-pdf",
"value": "bx bx-printer",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "CohkqWQC1iBv",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "0ESUbbAxVnoK",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "KSZ04uQ2D1St",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "6f9hih2hXXZk",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "AlhDUqhENtH7",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "GTwFsgaA0lCt",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "zP3PMqaG71Ct",
"isInheritable": false,
"position": 100
}
],
"format": "markdown",
"dataFileName": "Export as PDF.md",
"dataFileName": "Printing & Exporting as PDF.md",
"attachments": [
{
"attachmentId": "NfSjRsArIQHy",
@@ -3517,7 +3566,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Export as PDF_image.png"
"dataFileName": "Printing & Exporting as PD.png"
},
{
"attachmentId": "Om2EmdZr54vy",
@@ -3525,7 +3574,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Export as PDF_image.png"
"dataFileName": "1_Printing & Exporting as PD.png"
}
]
},
@@ -8758,51 +8807,51 @@
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bx-slideshow",
"type": "relation",
"name": "internalLink",
"value": "BlN9DFI679QC",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "BlN9DFI679QC",
"value": "OFXdgB2nNk1F",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "OFXdgB2nNk1F",
"value": "R9pX4DGra2Vt",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "R9pX4DGra2Vt",
"value": "0ESUbbAxVnoK",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "0ESUbbAxVnoK",
"value": "grjYqerjn243",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "s1aBHPd79XYj",
"value": "AlhDUqhENtH7",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "grjYqerjn243",
"value": "s1aBHPd79XYj",
"isInheritable": false,
"position": 70
},
@@ -8814,11 +8863,11 @@
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "AlhDUqhENtH7",
"type": "label",
"name": "iconClass",
"value": "bx bx-slideshow",
"isInheritable": false,
"position": 90
"position": 10
}
],
"format": "markdown",

View File

@@ -1,38 +0,0 @@
# Export as PDF
![](Export%20as%20PDF_image.png)
Screenshot of the note contextual menu indicating the “Export as PDF” option.
On the desktop application of Trilium it is possible to export a note as PDF. On the server or PWA (mobile), the option is not available due to technical constraints and it will be hidden.
To print a note, select the ![](1_Export%20as%20PDF_image.png) button to the right of the note and select _Export as PDF_.
Afterwards you will be prompted to select where to save the PDF file.
## Automatic opening of the file
When the PDF is exported, it is automatically opened with the system default application for easy preview.
Note that if you are using Linux with the GNOME desktop environment, sometimes the default application might seem incorrect (such as opening in GIMP). This is because it uses Gnome's “Recommended applications” list.
To solve this, you can change the recommended application for PDFs via this command line. First, list the available applications via `gio mime application/pdf` and then set the desired one. For example to use GNOME's Evince:
```
gio mime application/pdf
```
## Reporting issues with the rendering
Should you encounter any visual issues in the resulting PDF file (e.g. a table does not fit properly, there is cut off text, etc.) feel free to [report the issue](../../Troubleshooting/Reporting%20issues.md). In this case, it's best to offer a sample note (click on the ![](1_Export%20as%20PDF_image.png) button, select Export note → This note and all of its descendants → HTML in ZIP archive). Make sure not to accidentally leak any personal information.
## Landscape mode
When exporting to PDF, there are no customizable settings such as page orientation, size, etc. However, it is possible to specify a given note to be printed as a PDF in landscape mode by adding the `#printLandscape` attribute to it (see <a class="reference-link" href="../../Advanced%20Usage/Attributes.md">Attributes</a>).
## Page size
By default, the resulting PDF will be in Letter format. It is possible to adjust it to another page size via the `#printPageSize` attribute, with one of the following values: `A0`, `A1`, `A2`, `A3`, `A4`, `A5`, `A6`, `Legal`, `Letter`, `Tabloid`, `Ledger`.
## Keyboard shortcut
It's possible to trigger the export to PDF from the keyboard by going to _Keyboard shortcuts_ in <a class="reference-link" href="../UI%20Elements/Options.md">Options</a> and assigning a key combination for the `exportAsPdf` action.

View File

@@ -0,0 +1,75 @@
# Printing & Exporting as PDF
<figure class="image"><img style="aspect-ratio:951/432;" src="Printing &amp; Exporting as PD.png" width="951" height="432"><figcaption>Screenshot of the note contextual menu indicating the “Export as PDF” option.</figcaption></figure>
## Printing
This feature allows printing of notes. It works on both the desktop client, but also on the web.
Note that not all note types are printable as of now. We do plan to increase the coverage of supported note types in the future.
To print a note, select the <img src="1_Printing &amp; Exporting as PD.png" width="29" height="31"> button to the right of the note and select _Print note_. Depending on the size and type of the note, this can take up to a few seconds. Afterwards you will be redirected to the system/browser printing dialog.
> [!NOTE]
> Printing and exporting as PDF are not perfect. Due to technical limitations, and sometimes even browser glitches the text might appear cut off in some circumstances. 
## Reporting issues with the rendering
Should you encounter any visual issues in the resulting PDF file (e.g. a table does not fit properly, there is cut off text, etc.) feel free to [report the issue](../../Troubleshooting/Reporting%20issues.md). In this case, it's best to offer a sample note (click on the <img src="1_Printing &amp; Exporting as PD.png" width="29" height="31"> button, select Export note → This note and all of its descendants → HTML in ZIP archive). Make sure not to accidentally leak any personal information.
Consider adjusting font sizes and using [page breaks](../../Note%20Types/Text/Insert%20buttons.md) to work around the layout.
## Exporting as PDF
On the desktop application of Trilium it is possible to export a note as PDF. On the server or PWA (mobile), the option is not available due to technical constraints and it will be hidden.
To print a note, select the ![](1_Printing%20&%20Exporting%20as%20PD.png) button to the right of the note and select _Export as PDF_. Afterwards you will be prompted to select where to save the PDF file.
### Automatic opening of the file
When the PDF is exported, it is automatically opened with the system default application for easy preview.
Note that if you are using Linux with the GNOME desktop environment, sometimes the default application might seem incorrect (such as opening in GIMP). This is because it uses Gnome's “Recommended applications” list.
To solve this, you can change the recommended application for PDFs via this command line. First, list the available applications via `gio mime application/pdf` and then set the desired one. For example to use GNOME's Evince:
```
gio mime application/pdf
```
### Customizing exporting as PDF
When exporting to PDF, there are no customizable settings such as page orientation, size. However, there are a few <a class="reference-link" href="../../Advanced%20Usage/Attributes.md">Attributes</a> to adjust some of the settings:
* To print in landscape mode instead of portrait (useful for big diagrams or slides), add `#printLandscape`.
* By default, the resulting PDF will be in Letter format. It is possible to adjust it to another page size via the `#printPageSize` attribute, with one of the following values: `A0`, `A1`, `A2`, `A3`, `A4`, `A5`, `A6`, `Legal`, `Letter`, `Tabloid`, `Ledger`.
> [!NOTE]
> These options have no effect when used with the printing feature, since the user-defined settings are used instead.
## Keyboard shortcut
It's possible to trigger both printing and export as PDF from the keyboard by going to _Keyboard shortcuts_ in <a class="reference-link" href="../UI%20Elements/Options.md">Options</a> and assigning a key combination for:
* _Print Active Note_
* _Export Active Note as PDF_
## Constraints & limitations
Not all <a class="reference-link" href="../../Note%20Types.md">Note Types</a> are supported when printing, in which case the _Print_ and _Export as PDF_ options will be disabled.
* For <a class="reference-link" href="../../Note%20Types/Code.md">Code</a> notes:
* Line numbers are not printed.
* Syntax highlighting is enabled, however a default theme (Visual Studio) is enforced.
* For <a class="reference-link" href="../../Note%20Types/Collections.md">Collections</a>:
* Only <a class="reference-link" href="../../Note%20Types/Collections/Presentation%20View.md">Presentation View</a> is currently supported.
* We plan to add support for all the collection types at some point.
* Using <a class="reference-link" href="../../Theme%20development/Custom%20app-wide%20CSS.md">Custom app-wide CSS</a> for printing is not longer supported, due to a more stable but isolated mechanism.
* We plan to introduce a new mechanism specifically for a print CSS.
## Under the hood
Both printing and exporting as PDF use the same mechanism: a note is rendered individually in a separate webpage that is then sent to the browser or the Electron application either for printing or exporting as PDF.
The webpage that renders a single note can actually be accessed in a web browser. For example `http://localhost:8080/#root/WWRGzqHUfRln/RRZsE9Al8AIZ?ntxId=0o4fzk` becomes `http://localhost:8080/?print#root/WWRGzqHUfRln/RRZsE9Al8AIZ`.
Accessing the print note in a web browser allows for easy debugging to understand why a particular note doesn't render well. The mechanism for rendering is similar to the one used in <a class="reference-link" href="Note%20List.md">Note List</a>.

View File

@@ -19,7 +19,7 @@ This section presents the most important changes by version. For a full set of c
* v0.92.4:
* macOS binaries are now signed.
* <a class="reference-link" href="Note%20Types/Text.md">Text</a> notes can now have adjustable <a class="reference-link" href="Note%20Types/Text/Content%20language%20%26%20Right-to-le.md">Content language &amp; Right-to-left support</a>.
* <a class="reference-link" href="Basic%20Concepts%20and%20Features/Notes/Export%20as%20PDF.md">Export as PDF</a>
* <a class="reference-link" href="Basic%20Concepts%20and%20Features/Notes/Printing%20%26%20Exporting%20as%20PDF.md">Export as PDF</a>
* <a class="reference-link" href="Basic%20Concepts%20and%20Features/Zen%20mode.md">Zen mode</a>
* <a class="reference-link" href="Note%20Types/Collections/Calendar%20View.md">Calendar View</a>, allowing notes to be displayed in a monthly grid based on start and end dates.
* v0.91.5:

View File

@@ -53,7 +53,7 @@ Alternatively, it's possible to insert a horizontal ruler by typing `---`.
<figure class="image image-style-align-right"><img style="aspect-ratio:371/79;" src="8_Insert buttons_image.png" width="371" height="79"></figure>
Page breaks provide a way to force the next paragraph or block (table, image, etc.) to be displayed onto the next page when printing (either to a real printer to [when exporting to PDF](../../Basic%20Concepts%20and%20Features/Notes/Export%20as%20PDF.md)).
Page breaks provide a way to force the next paragraph or block (table, image, etc.) to be displayed onto the next page when printing (either to a real printer to [when exporting to PDF](../../Basic%20Concepts%20and%20Features/Notes/Printing%20%26%20Exporting%20as%20PDF.md)).
Page breaks are marked in the editor with the words _Page break_, but they will not actually be shown when printed.