diff --git a/src/editor/extensions.ts b/src/editor/extensions.ts index 82ce6c8..23a17e3 100644 --- a/src/editor/extensions.ts +++ b/src/editor/extensions.ts @@ -2,6 +2,7 @@ import type { Extensions } from "@tiptap/core"; import StarterKit from "@tiptap/starter-kit"; import Placeholder from "@tiptap/extension-placeholder"; import { Figure } from "./figure"; +import { ParagraphIndent } from "./indent"; export const editorExtensions: Extensions = [ StarterKit.configure({ @@ -13,4 +14,5 @@ export const editorExtensions: Extensions = [ placeholder: ({ node }) => (node.type.name === "heading" ? "" : "Begin your chapter…"), }), Figure, + ParagraphIndent, ]; diff --git a/src/editor/indent.ts b/src/editor/indent.ts new file mode 100644 index 0000000..1ceeb7a --- /dev/null +++ b/src/editor/indent.ts @@ -0,0 +1,42 @@ +import { Extension } from "@tiptap/core"; + +export const ParagraphIndent = Extension.create({ + name: "paragraphIndent", + + addGlobalAttributes() { + return [ + { + types: ["paragraph"], + attributes: { + indent: { + default: false, + keepOnSplit: false, + parseHTML: (el) => el.getAttribute("data-indent") === "true", + renderHTML: (attrs) => (attrs.indent ? { "data-indent": "true" } : {}), + }, + }, + }, + ]; + }, + + addKeyboardShortcuts() { + return { + Tab: () => { + if (this.editor.isActive("listItem")) return false; + this.editor.commands.updateAttributes("paragraph", { indent: true }); + return true; + }, + "Shift-Tab": () => { + if (this.editor.isActive("listItem")) return false; + this.editor.commands.updateAttributes("paragraph", { indent: false }); + return true; + }, + Backspace: () => { + const { $from, empty } = this.editor.state.selection; + if (!empty || $from.parentOffset !== 0) return false; + if ($from.parent.type.name !== "paragraph" || !$from.parent.attrs.indent) return false; + return this.editor.commands.updateAttributes("paragraph", { indent: false }); + }, + }; + }, +}); diff --git a/src/export/epub.ts b/src/export/epub.ts index 5309360..97dd741 100644 --- a/src/export/epub.ts +++ b/src/export/epub.ts @@ -80,8 +80,11 @@ function figure(node: JSONContent, paths: Map): string { function block(node: JSONContent, paths: Map): string { switch (node.type) { - case "paragraph": - return `

${inlines(node.content)}

`; + case "paragraph": { + const body = inlines(node.content); + if (!body.trim()) return "

 

"; + return `${body}

`; + } case "heading": return node.attrs?.level === 3 ? `

${inlines(node.content)}

` @@ -363,14 +366,16 @@ body { p { margin: 0; text-align: justify; - text-indent: 1.3em; hyphens: auto; -webkit-hyphens: auto; -epub-hyphens: auto; } +p[data-indent="true"] { + text-indent: 1.3em; +} + h1, h2, h3 { - text-indent: 0; hyphens: none; line-height: 1.25; font-weight: 500; @@ -380,25 +385,11 @@ h2, h3 { margin: 1.4em 0 0.6em; } -p + p { - text-indent: 1.3em; -} - .chapter-opener { text-align: center; margin: 3em 0 2.2em; } -.chapter-opener p, -header + p, -h2 + p, -h3 + p, -blockquote + p, -figure + p, -hr + p { - text-indent: 0; -} - .eyebrow { text-transform: uppercase; letter-spacing: 0.28em; diff --git a/src/export/typst.ts b/src/export/typst.ts index 4ef064a..65c73ff 100644 --- a/src/export/typst.ts +++ b/src/export/typst.ts @@ -66,8 +66,11 @@ function figure(node: JSONContent, paths: Map): string { function block(node: JSONContent, paths: Map): string { switch (node.type) { - case "paragraph": - return guardLineStart(inlines(node.content)); + case "paragraph": { + const body = guardLineStart(inlines(node.content)); + if (!body.trim()) return "~"; + return node.attrs?.indent ? `#h(1.3em)${body}` : body; + } case "heading": return `#heading(level: ${node.attrs?.level ?? 2})[${inlines(node.content)}]`; case "blockquote": @@ -104,7 +107,7 @@ function preamble(book: Book): string { binding: left, ) #set text(font: "Literata", size: 11pt, lang: ${str(meta.language || "en")}, hyphenate: true) -#set par(justify: true, leading: 0.72em, spacing: 0.72em, first-line-indent: (amount: 1.3em, all: false)) +#set par(justify: true, leading: 0.72em, spacing: 0.72em) #show heading: set text(font: "Literata", weight: "medium") #show heading.where(level: 1): set text(size: 22pt) #show heading.where(level: 2): set block(above: 1.4em, below: 0.6em) diff --git a/src/styles/app.css b/src/styles/app.css index cbfc914..9c48163 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -393,12 +393,10 @@ button { text-align: justify; hyphens: auto; -webkit-hyphens: auto; - text-indent: 1.3em; } -.prose p:first-of-type, -.prose .first { - text-indent: 0; +.prose p[data-indent="true"] { + text-indent: 1.3em; } .prose .first::first-letter { @@ -530,11 +528,14 @@ button { line-height: 1.5; text-align: justify; hyphens: auto; - text-indent: 1.2em; margin: 0; color: #2b2720; } +.page p[data-indent="true"] { + text-indent: 1.2em; +} + .page .folio { text-align: center; font-family: var(--font-book); @@ -596,10 +597,6 @@ button { min-height: 62vh; } -.ProseMirror > p:first-child { - text-indent: 0; -} - .ProseMirror hr { border: none; border-top: 1px solid var(--line-strong);