Browse Source

Support rich formatting in editor preview

master
buttercat1791 1 year ago
parent
commit
dd27f6d529
  1. 2
      package.json
  2. 17
      pnpm-lock.yaml
  3. 12
      src/app.css
  4. 36
      src/lib/articleParser.ts
  5. 16
      src/lib/components/Preview.svelte
  6. 20
      src/lib/parser.ts
  7. 29
      src/routes/new/edit/+page.svelte

2
package.json

@ -20,6 +20,7 @@
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"asciidoctor": "^3.0.4", "asciidoctor": "^3.0.4",
"he": "^1.2.0",
"markdown-it": "^14.0.0", "markdown-it": "^14.0.0",
"markdown-it-plain-text": "^0.3.0", "markdown-it-plain-text": "^0.3.0",
"marked": "^11.1.1", "marked": "^11.1.1",
@ -31,6 +32,7 @@
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.1.1", "@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/kit": "^2.4.3", "@sveltejs/kit": "^2.4.3",
"@types/he": "^1.2.3",
"@types/markdown-it": "^13.0.7", "@types/markdown-it": "^13.0.7",
"@types/node": "^22.5.4", "@types/node": "^22.5.4",
"@types/showdown": "^2.0.6", "@types/showdown": "^2.0.6",

17
pnpm-lock.yaml

@ -29,6 +29,9 @@ importers:
asciidoctor: asciidoctor:
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.4(chokidar@3.6.0) version: 3.0.4(chokidar@3.6.0)
he:
specifier: ^1.2.0
version: 1.2.0
markdown-it: markdown-it:
specifier: ^14.0.0 specifier: ^14.0.0
version: 14.1.0 version: 14.1.0
@ -57,6 +60,9 @@ importers:
'@sveltejs/kit': '@sveltejs/kit':
specifier: ^2.4.3 specifier: ^2.4.3
version: 2.5.25(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.2(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.2(@types/node@22.5.4)) version: 2.5.25(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.2(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.2(@types/node@22.5.4))
'@types/he':
specifier: ^1.2.3
version: 1.2.3
'@types/markdown-it': '@types/markdown-it':
specifier: ^13.0.7 specifier: ^13.0.7
version: 13.0.9 version: 13.0.9
@ -572,6 +578,9 @@ packages:
'@types/estree@1.0.5': '@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
'@types/he@1.2.3':
resolution: {integrity: sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==}
'@types/linkify-it@3.0.5': '@types/linkify-it@3.0.5':
resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==}
@ -1208,6 +1217,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
htmlparser2@9.1.0: htmlparser2@9.1.0:
resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==}
@ -2614,6 +2627,8 @@ snapshots:
'@types/estree@1.0.5': {} '@types/estree@1.0.5': {}
'@types/he@1.2.3': {}
'@types/linkify-it@3.0.5': {} '@types/linkify-it@3.0.5': {}
'@types/markdown-it@13.0.9': '@types/markdown-it@13.0.9':
@ -3329,6 +3344,8 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
he@1.2.0: {}
htmlparser2@9.1.0: htmlparser2@9.1.0:
dependencies: dependencies:
domelementtype: 2.3.0 domelementtype: 2.3.0

12
src/app.css

@ -48,8 +48,16 @@
} }
/* Content */ /* Content */
div.note-leather { div.note-leather,
@apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300 hover:bg-primary-100 dark:hover:bg-primary-800 p-2 rounded; p.note-leather,
section.note-leather {
@apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300 p-2 rounded;
}
div.note-leather:hover:not(:has(.note-leather:hover)),
p.note-leather:hover:not(:has(.note-leather:hover)),
section.note-leather:hover:not(:has(.note-leather:hover)) {
@apply hover:bg-primary-100 dark:hover:bg-primary-800 ;
} }
/* Heading */ /* Heading */

36
src/lib/articleParser.ts

@ -1,36 +0,0 @@
import MarkdownIt from 'markdown-it';
import LinkToArticle from '$components/LinkToArticle.svelte';
import plainText from 'markdown-it-plain-text';
const md = new MarkdownIt();
const mdTxt = new MarkdownIt().use(plainText);
export function parse(markdown: string) {
let parsedMarkdown = md.render(markdown);
parsedMarkdown = parsedMarkdown.replace(/\[\[(.*?)\]\]/g, (match: any, content: any) => {
const container = document.createElement('span');
const linkToArticle = new LinkToArticle({
target: container,
props: {
content: content
}
});
return container.outerHTML;
});
return parsedMarkdown;
}
export function parsePlainText(markdown: string) {
mdTxt.render(markdown);
/* @ts-ignore */ // markdown-it-plain-text doesnt have typescript support??
let parsedText = mdTxt.plainText.replace(/\[\[(.*?)\]\]/g, (match: any, content: any) => {
return content;
});
return parsedText;
}

16
src/lib/components/Preview.svelte

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Pharos from "$lib/parser"; import Pharos from "$lib/parser";
import { Heading } from "flowbite-svelte"; import { Heading, P } from "flowbite-svelte";
export let parser: Pharos; export let parser: Pharos;
export let rootIndexId: string; export let rootIndexId: string;
@ -27,17 +27,21 @@
}; };
</script> </script>
<div> <section class='note-leather flex flex-col space-y-2'>
{#if depth < 4} {#if depth < 4}
<Heading tag={getHeadingTag(depth)}>{title}</Heading> <Heading tag={getHeadingTag(depth)}>{title}</Heading>
{#each orderedChildren as id} {#each orderedChildren as id, index}
{#if childIndices.includes(id)} {#if childIndices.includes(id)}
<svelte:self {parser} rootIndexId={id} depth={depth + 1} /> <svelte:self {parser} rootIndexId={id} depth={depth + 1} />
{:else if (childZettels.includes(id))} {:else if (childZettels.includes(id))}
{@html parser.getHtmlContent(id)} <P class='note-leather' firstupper={index === 0}>
{@html parser.getContent(id)}
</P>
{/if} {/if}
{/each} {/each}
{:else} {:else}
{@html parser.getHtmlContent(rootIndexId)} <P class='note-leather' firstupper>
{@html parser.getContent(rootIndexId)}
</P>
{/if} {/if}
</div> </section>

20
src/lib/parser.ts

@ -9,6 +9,7 @@ import asciidoctor, {
Section, Section,
type ProcessorOptions type ProcessorOptions
} from 'asciidoctor'; } from 'asciidoctor';
import he from 'he';
interface IndexMetadata { interface IndexMetadata {
authors?: string[]; authors?: string[];
@ -146,7 +147,8 @@ export default class Pharos {
*/ */
getIndexTitle(id: string): string | undefined { getIndexTitle(id: string): string | undefined {
const section = this.nodes.get(id) as Section; const section = this.nodes.get(id) as Section;
return section.getTitle(); const title = section.getTitle() ?? '';
return he.decode(title);
} }
/** /**
@ -173,10 +175,20 @@ export default class Pharos {
} }
/** /**
* @returns The converted content of the node with the given ID. * @returns The content of the node with the given ID. The presentation of the returned content
* varies by the node's context.
* @remarks By default, the content is returned as HTML produced by the
* Asciidoctor converter. However, other formats are returned for specific contexts:
* - Paragraph: The content is returned as a plain string.
*/ */
getHtmlContent(id: string): string { getContent(id: string): string {
const block = this.nodes.get(id) as Block; const block = this.nodes.get(id) as AbstractBlock;
switch (block.getContext()) {
case 'paragraph':
return block.getContent() ?? '';
}
return block.convert(); return block.convert();
} }

29
src/routes/new/edit/+page.svelte

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Input, Label, Textarea, Toolbar, ToolbarButton } from "flowbite-svelte"; import { Heading, Textarea, Toolbar, ToolbarButton } from "flowbite-svelte";
import { CodeOutline, EyeSolid } from "flowbite-svelte-icons"; import { CodeOutline, EyeSolid } from "flowbite-svelte-icons";
import { editorText } from "$lib/stores"; import { editorText } from "$lib/stores";
import Preview from "$lib/components/Preview.svelte"; import Preview from "$lib/components/Preview.svelte";
@ -25,14 +25,7 @@
<main class='w-full flex justify-center'> <main class='w-full flex justify-center'>
<form class='max-w-2xl w-full'> <form class='max-w-2xl w-full'>
<div class='flex flex-col space-y-4'> <div class='flex flex-col space-y-4'>
<div> <Heading tag='h1' class='mb-2'>New Article</Heading>
<Label for='article-title' class='mb-2'>Article Title</Label>
<Input type='text' id='article-title' placeholder='Title' required />
</div>
<div>
<Label for='article-author' class='mb-2'>Author Name</Label>
<Input type='text' id='article-author' placeholder='Author' required />
</div>
{#if isEditing} {#if isEditing}
<Textarea <Textarea
id='article-content' id='article-content'
@ -47,16 +40,14 @@
</Toolbar> </Toolbar>
</Textarea> </Textarea>
{:else} {:else}
<div> <Toolbar>
<Toolbar> <ToolbarButton name='Edit' on:click={hidePreview}>
<ToolbarButton name='Edit' on:click={hidePreview}> <CodeOutline class='w-6 h-6' />
<CodeOutline class='w-6 h-6' /> </ToolbarButton>
</ToolbarButton> </Toolbar>
</Toolbar> {#if rootIndexId}
{#if rootIndexId} <Preview {parser} {rootIndexId} />
<Preview {parser} {rootIndexId} /> {/if}
{/if}
</div>
{/if} {/if}
</div> </div>
</form> </form>

Loading…
Cancel
Save