├── .gitignore ├── src ├── js │ └── popup.js ├── html │ └── popup.html ├── css │ └── main.css ├── manifest.json └── ts │ └── main.ts ├── tsconfig.json ├── package.json ├── README.md ├── webpack.config.js └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | window.open("https://yuki0311.com/", "_blank"); -------------------------------------------------------------------------------- /src/html/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | .d2p-tag { 2 | line-height: 1.25em; 3 | overflow-wrap: break-word; 4 | margin-left: 0.4em; 5 | } 6 | 7 | .d2p-option { 8 | user-select: none; 9 | display: inline-block; 10 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "rootDir": "src", 7 | "esModuleInterop": true, 8 | "typeRoots": [ "node_modules/@types"] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "danbooru-to-prompt", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/chrome": "^0.0.197", 14 | "copy-webpack-plugin": "^11.0.0", 15 | "ts-loader": "^9.4.1", 16 | "typescript": "^4.8.4", 17 | "webpack": "^5.74.0", 18 | "webpack-cli": "^4.10.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "danbooru-to-prompt", 3 | "description": "Assist tool to create prompt running on danbooru", 4 | "version": "1.2.0", 5 | "manifest_version": 3, 6 | "action": { 7 | "default_popup": "html/popup.html" 8 | }, 9 | "permissions": [ 10 | "clipboardWrite" 11 | ], 12 | "content_scripts": [ 13 | { 14 | "matches": [ 15 | "https://danbooru.donmai.us/*" 16 | ], 17 | "js": [ 18 | "js/main.js" 19 | ], 20 | "css": [ 21 | "css/main.css" 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # danbooru-to-prompt 2 | 3 | ![](https://img.shields.io/github/downloads/fa0311/danbooru-to-prompt/total) 4 | 5 | Assist tool to create prompt running on [danbooru](https://danbooru.donmai.us/)
6 | 7 | [demo video](https://twitter.com/faa0311/status/1579917156657303552) 8 | 9 | > ※Photos in conflict with copyright laws have been blurred out. 10 | 11 | # Install 12 | 13 | Select manifest_v3.zip for [releases](https://github.com/fa0311/danbooru-to-prompt/releases) and download the file. 14 | Select Manage extensions in Chrome and enable Developer Mode, then drag the downloaded zip file onto the page. 15 | 16 | # License 17 | 18 | danbooru-to-prompt is under MIT License 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV || "development", 6 | entry: { 7 | main: path.join(__dirname, "src/ts/main.ts"), 8 | }, 9 | output: { 10 | path: path.join(__dirname, "dist/js"), 11 | filename: "[name].js", 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.ts$/, 17 | use: "ts-loader", 18 | exclude: /node_modules/, 19 | }, 20 | ], 21 | }, 22 | resolve: { 23 | extensions: [".ts", ".js"], 24 | }, 25 | plugins: [ 26 | new CopyPlugin({ 27 | patterns: [ 28 | { from: "html", to: "../html", context: "src" }, 29 | { from: "css", to: "../css", context: "src" }, 30 | { from: "js", to: "../js", context: "src" }, 31 | { from: "manifest.json", to: "../", context: "src" }, 32 | { from: "LICENSE", to: "../" }, 33 | ], 34 | }), 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 yuki 4 | 5 | https://yuki0311.com/ 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/ts/main.ts: -------------------------------------------------------------------------------- 1 | const HEADER = "[danbooru-to-prompt]"; 2 | 3 | window.addEventListener("load", (e) => { 4 | const tagListQuery = "div.tag-list.categorized-tag-list"; 5 | const checkedQuery = 'input[type="checkbox"]:checked'; 6 | const checkboxQuery = 'input[type="checkbox"]'; 7 | const tagQuery = "li.tag-type-0"; 8 | const InsertElement = ` 9 |
10 | 11 | 12 | 13 | 14 | 1.0 15 | 16 | 17 | 18 | 19 |
`; 20 | 21 | const heading: Element | null = document.querySelector(tagListQuery); 22 | if (heading == null) return; 23 | 24 | heading.insertAdjacentHTML( 25 | "afterbegin", 26 | `` 34 | ); 35 | 36 | heading.insertAdjacentHTML( 37 | "afterbegin", 38 | `

Quality Tag

` 39 | ); 40 | 41 | heading.insertAdjacentHTML( 42 | "afterbegin", 43 | `` 53 | ); 54 | 55 | heading.insertAdjacentHTML( 56 | "afterbegin", 57 | `

Danbooru To Prompt

` 58 | ); 59 | 60 | const listItemQuery = "ul>li"; 61 | const emphasisUpQuery = ".emphasis-up-icon"; 62 | const emphasisDownQuery = ".emphasis-down-icon"; 63 | const emphasisSpanQuery = "span.data-tag-emphasis"; 64 | heading.querySelectorAll(listItemQuery).forEach((li) => { 65 | li.insertAdjacentHTML("afterbegin", InsertElement); 66 | }); 67 | heading.querySelectorAll(emphasisUpQuery).forEach((icon) => { 68 | icon.addEventListener("click", (e) => { 69 | let emphasis = icon.parentElement?.querySelector(emphasisSpanQuery); 70 | if (emphasis === undefined) return; 71 | if (emphasis === null) return; 72 | let strength = parseFloat(emphasis.getAttribute("emphasis") ?? "1") + 0.1; 73 | if (strength >= 1.8) return; 74 | emphasis.setAttribute("emphasis", strength.toFixed(1)); 75 | emphasis.innerHTML = strength.toFixed(1); 76 | }); 77 | }); 78 | heading.querySelectorAll(emphasisDownQuery).forEach((icon) => { 79 | icon.addEventListener("click", (e) => { 80 | let emphasis = icon.parentElement?.querySelector(emphasisSpanQuery); 81 | if (emphasis === undefined) return; 82 | if (emphasis === null) return; 83 | let strength = parseFloat(emphasis.getAttribute("emphasis") ?? "1") - 0.1; 84 | if (strength <= 0.01) return; 85 | emphasis.setAttribute("emphasis", strength.toFixed(1)); 86 | emphasis.innerHTML = strength.toFixed(1); 87 | }); 88 | }); 89 | 90 | const getChecked = (): string[] => { 91 | return sort( 92 | ( 93 | [...heading.querySelectorAll(checkedQuery)] 94 | .map((input) => input?.parentElement?.parentElement) 95 | .filter((value) => value !== null) 96 | ) 97 | ).map((value) => 98 | emphasis( 99 | escape(value.getAttribute("data-tag-name")!), 100 | parseFloat( 101 | value.querySelector(emphasisSpanQuery)?.getAttribute("emphasis") ?? 102 | "1.0" 103 | ) 104 | ) 105 | ); 106 | }; 107 | 108 | const toSimply = (word: Element[]): Element[] => { 109 | return word.filter((value) => 110 | word 111 | .map((text) => { 112 | let textAttr = text.getAttribute("data-tag-name"); 113 | let valueAttr = value.getAttribute("data-tag-name"); 114 | if (textAttr == null) return false; 115 | if (valueAttr == null) return false; 116 | if (valueAttr == textAttr) return false; 117 | return ["", "ing", "ed", "s"] 118 | .map((end) => { 119 | if (!valueAttr!.endsWith(end)) return false; 120 | return textAttr!.includes( 121 | valueAttr!.replace(new RegExp(end + "$"), "") 122 | ); 123 | }) 124 | .some((v) => v === true); 125 | }) 126 | .every((v) => v === false) 127 | ); 128 | }; 129 | 130 | const emphasis = (word: string, factor: number): string => { 131 | if (factor == 1) return word; 132 | if (factor == 1.1) return "(" + word + ")"; 133 | return "(" + word + ":" + factor.toFixed(1) + ")"; 134 | }; 135 | 136 | const escape = (word: string): string => { 137 | return word 138 | .replace("(", "\\(") 139 | .replace(")", "\\)") 140 | .replace("[", "\\[") 141 | .replace("]", "\\]"); 142 | }; 143 | 144 | const sort = (value: Element[]): Element[] => { 145 | return value.sort((a: Element, b: Element) => { 146 | const titleA: Number = Number( 147 | a.querySelector("span[title]")?.getAttribute("title") 148 | ); 149 | const titleB: Number = Number( 150 | b.querySelector("span[title]")?.getAttribute("title") 151 | ); 152 | if (titleA > titleB) return -1; 153 | else if (titleA < titleB) return 1; 154 | else return 0; 155 | }); 156 | }; 157 | 158 | const clacTokenSize = (value: string): number => { 159 | const re = new RegExp("([0-90-9]{1,4}|[a-zA-Z]{1,4})", "g"); 160 | 161 | return value.match(re)?.length ?? 0; 162 | }; 163 | const getTokenSize = (): number => { 164 | return Math.floor( 165 | getChecked() 166 | .map((value) => Number(clacTokenSize(value))) 167 | .reduce((sum, element) => sum + element, 0) * 0.9 168 | ); 169 | }; 170 | 171 | const changeTokenSize = () => { 172 | const text: string | undefined = getTokenSize().toString(); 173 | const tokenSize: Element | null = document.getElementById("d2p-token-size"); 174 | if (text === undefined) text == "error"; 175 | if (tokenSize === null) return; 176 | tokenSize.innerHTML = text; 177 | }; 178 | 179 | heading.querySelectorAll(checkboxQuery).forEach((e) => { 180 | e.addEventListener("change", (_) => changeTokenSize()); 181 | }); 182 | 183 | document 184 | .getElementById("d2p-auto-select") 185 | ?.addEventListener("click", (button: MouseEvent) => { 186 | const tagList = [...heading.querySelectorAll(tagQuery)] 187 | .filter((tag) => tag.getAttribute("data-tag-name") !== null) 188 | .filter((tag) => tag.querySelector(checkboxQuery) !== null); 189 | 190 | tagList.forEach((value) => { 191 | (value.querySelector(checkboxQuery) as HTMLInputElement).checked = 192 | false; 193 | }); 194 | let length = getTokenSize(); 195 | sort(toSimply(tagList)).forEach((value: Element) => { 196 | const input = value.getAttribute("data-tag-name"); 197 | if (input == null) return; 198 | if (length + clacTokenSize(input) > 75) return; 199 | (value.querySelector(checkboxQuery) as HTMLInputElement).checked = true; 200 | length += clacTokenSize(input); 201 | }); 202 | changeTokenSize(); 203 | }); 204 | 205 | document 206 | .getElementById("d2p-all-select") 207 | ?.addEventListener("click", (button: MouseEvent) => { 208 | const tagList = [...heading.querySelectorAll(tagQuery)] 209 | .filter((tag) => tag.getAttribute("data-tag-name") !== null) 210 | .filter((tag) => tag.querySelector(checkboxQuery) !== null); 211 | 212 | tagList.forEach((value) => { 213 | (value.querySelector(checkboxQuery) as HTMLInputElement).checked = true; 214 | }); 215 | changeTokenSize(); 216 | }); 217 | 218 | document 219 | .getElementById("d2p-copy") 220 | ?.addEventListener("click", (button: MouseEvent) => { 221 | const output = getChecked().join(" "); 222 | console.log(`${HEADER}\n${output}`); 223 | return navigator.clipboard 224 | .writeText(output) 225 | .then( 226 | () => { 227 | console.log(`${HEADER} success`); 228 | (button.target as Element).innerHTML = "success"; 229 | }, 230 | () => { 231 | console.log(`${HEADER} failed`); 232 | (button.target as Element).innerHTML = "failed"; 233 | } 234 | ) 235 | .then(() => 236 | setTimeout( 237 | () => ((button.target as Element).innerHTML = "copy to clipboard"), 238 | 2000 239 | ) 240 | ); 241 | }); 242 | }); 243 | --------------------------------------------------------------------------------