Last active 1 month ago

makes the ao3 editor into a markdown editor instead. plain text forever

Revision c4e234c27d7daaf5b94c6caac9ba04ff52069065

ao3markdown.user.js Raw
1// ==UserScript==
2// @name AO3 Markdown Editor
3// @namespace https://veryroundbird.house
4// @version 2026-03-31
5// @description replaces the standard AO3 editor with a markdown editor that has syntax highlighting and a previewer for people with my brain disease
6// @author You
7// @match https://archiveofourown.org/works/*
8// @icon https://www.google.com/s2/favicons?sz=64&domain=archiveofourown.org
9// @require https://unpkg.com/turndown/dist/turndown.js
10// @require https://unpkg.com/@highlightjs/cdn-assets@11.11.1/highlight.min.js
11// @require https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js
12// ==/UserScript==
13
14const addCss = (url, container) => {
15 const css = document.createElement('link');
16 css.setAttribute('rel', 'stylesheet');
17 css.setAttribute('type', 'text/css');
18 css.setAttribute('href', url);
19 container.appendChild(css);
20 return css;
21}
22
23
24(() => {
25 'use strict';
26
27 addCss('https://unpkg.com/@highlightjs/cdn-assets@11.11.1/styles/base16/cupcake.min.css', document.head);
28 addCss('https://unpkg.com/@fontsource/maple-mono@5.2.6/index.css', document.head);
29 const styles = document.createElement('style');
30 styles.setAttribute('type', 'text/css');
31 styles.innerHTML = `
32 #editor-tabs {
33 display: flex;
34 gap: 10px;
35 }
36
37 #editor-tabs button {
38 border-radius: 10px 10px 0 0;
39 box-shadow: none;
40 }
41
42 #editor-tabs button[disabled] {
43 opacity: 0.5;
44 }
45
46 #editor-container,
47 #preview {
48 border: 1px white solid;
49 }
50
51 #editor,
52 #preview {
53 height: 500px;
54 overflow: auto;
55 display: block;
56 }
57
58 #editor {
59 font-family: 'Maple Mono', monospace;
60 font-size: 16px;
61 }
62
63 #preview {
64 padding: 20px;
65 }
66
67 #preview p {
68 padding: 0;
69 margin: 1em 0;
70 }
71 `;
72 document.head.appendChild(styles);
73
74 const content = document.getElementById('content');
75 const buttons = document.querySelector('.rtf-html-switch');
76 if (buttons) {
77 buttons.style.display = 'none';
78 }
79 const turndownService = new TurndownService({
80 headingStyle: 'atx',
81 emDelimiter: '*',
82 codeBlockStyle: 'fenced'
83 });
84 if (content) {
85 content.style.display = 'none';
86 const initialText = turndownService.turndown(content.textContent);
87
88 const wrapper = document.createElement('div');
89 wrapper.id = 'editor-wrapper';
90
91 const tabs = document.createElement('div');
92 tabs.id = 'editor-tabs';
93
94 const mdBtn = document.createElement('button');
95 mdBtn.setAttribute('type', 'button');
96 mdBtn.textContent = "Markdown";
97 mdBtn.setAttribute("disabled", "disabled");
98 mdBtn.id = 'editor-btn-markdown';
99 tabs.appendChild(mdBtn);
100
101 const htmlBtn = document.createElement('button');
102 htmlBtn.setAttribute('type', 'button');
103 htmlBtn.id = 'editor-btn-html';
104 htmlBtn.textContent = "Preview";
105 tabs.appendChild(htmlBtn);
106
107 wrapper.appendChild(tabs);
108
109 const editorContainer = document.createElement('pre');
110 editorContainer.id = 'editor-container';
111
112 const editor = document.createElement('code');
113 editor.id = 'editor';
114 editor.classList.add('language-markdown');
115 editor.setAttribute("contenteditable", "plaintext-only");
116 editor.textContent = initialText;
117 editorContainer.appendChild(editor);
118 wrapper.appendChild(editorContainer);
119
120 const preview = document.createElement('div');
121 preview.id = 'preview';
122 preview.style.display = "none";
123 preview.innerHTML = content.value;
124 wrapper.appendChild(preview);
125
126 content.after(wrapper);
127
128 mdBtn.addEventListener('click', (e) => {
129 e.preventDefault();
130 htmlBtn.removeAttribute("disabled");
131 mdBtn.setAttribute("disabled", "disabled");
132 editorContainer.style.display = "block";
133 preview.style.display = "none";
134 });
135
136 htmlBtn.addEventListener('click', (e) => {
137 e.preventDefault();
138 mdBtn.removeAttribute("disabled");
139 htmlBtn.setAttribute("disabled", "disabled");
140 editorContainer.style.display = "none";
141 preview.style.display = "block";
142 });
143
144 editor.addEventListener('input', (_e) => {
145 const text = marked.parse(editor.innerHTML.replaceAll(/<\/?span(\sclass="hljs-[a-z]+")>/g, ""));
146 preview.innerHTML = text;
147 content.value = text;
148 });
149
150 hljs.highlightAll();
151 }
152})();