your personal website on atproto - mirror blento.app
at fix-formatting 135 lines 2.9 kB view raw
1<script lang="ts"> 2 import { onDestroy, onMount } from 'svelte'; 3 import { Editor, type Content, type Extensions } from '@tiptap/core'; 4 import StarterKit from '@tiptap/starter-kit'; 5 import Image from '@tiptap/extension-image'; 6 import Placeholder from '@tiptap/extension-placeholder'; 7 import Link from '@tiptap/extension-link'; 8 import { marked } from 'marked'; 9 import { generateJSON } from '@tiptap/core'; 10 import TurndownService from 'turndown'; 11 import { RichTextLink } from './extensions/RichTextLink'; 12 import type { Item } from '$lib/types'; 13 14 let element: HTMLElement | undefined = $state(); 15 16 let { 17 editor = $bindable(), 18 contentDict = $bindable(), 19 key = 'text', 20 placeholder = '', 21 defaultContent = '', 22 class: className 23 }: { 24 editor: Editor | null; 25 contentDict: Record<string, any>; 26 key: string; 27 placeholder?: string; 28 defaultContent?: string; 29 class?: string; 30 } = $props(); 31 32 const update = async () => { 33 if (!editor) return {}; 34 35 const html = editor.getHTML(); 36 37 var turndownService = new TurndownService({ 38 headingStyle: 'atx', 39 bulletListMarker: '-' 40 }); 41 const markdown = turndownService.turndown(html); 42 43 contentDict[key] = markdown; 44 }; 45 46 onMount(async () => { 47 if (!element || editor) return; 48 49 let json: Content = ''; 50 51 try { 52 let html = await marked.parse(contentDict[key] ?? (defaultContent as string)); 53 54 // parse to json 55 json = generateJSON(html, [ 56 StarterKit.configure({ 57 heading: false, 58 bulletList: false, 59 codeBlock: false 60 }), 61 Image.configure(), 62 RichTextLink.configure({ 63 openOnClick: false 64 }) 65 ]); 66 } catch (error) { 67 console.error(error); 68 } 69 70 let extensions: Extensions = [ 71 StarterKit.configure({ 72 heading: false, 73 bulletList: false, 74 codeBlock: false, 75 dropcursor: false 76 }), 77 Image.configure(), 78 Link.configure({ 79 openOnClick: false 80 }) 81 ]; 82 83 if (placeholder) { 84 extensions.push( 85 Placeholder.configure({ 86 placeholder: placeholder 87 }) 88 ); 89 } 90 91 editor = new Editor({ 92 element: element, 93 extensions: extensions, 94 onTransaction: () => { 95 editor = editor; 96 }, 97 onUpdate: () => { 98 update(); 99 }, 100 onDrop: () => { 101 return false; 102 }, 103 content: json, 104 105 editorProps: { 106 attributes: { 107 class: 108 'outline-none w-full text-base-600 dark:text-base-400 prose dark:prose-invert prose-a:text-accent-500 prose-a:no-underline' 109 }, 110 handleDOMEvents: { drop: () => false } 111 } 112 }); 113 }); 114 115 onDestroy(() => { 116 if (editor) { 117 editor.destroy(); 118 } 119 }); 120</script> 121 122<div class={['w-full cursor-text', className]} bind:this={element}></div> 123 124<style> 125 :global(.tiptap p.is-editor-empty:first-child::before) { 126 color: var(--color-base-800); 127 content: attr(data-placeholder); 128 float: left; 129 height: 0; 130 pointer-events: none; 131 } 132 :global(.dark .tiptap p.is-editor-empty:first-child::before) { 133 color: var(--color-base-200); 134 } 135</style>