Last active 1 month ago

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

Revision 1ad077f1d938a12d216d357bd455adc06deb1c7a

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