Skip to content

Instantly share code, notes, and snippets.

@whiteball
Last active December 17, 2023 14:30
Show Gist options
  • Save whiteball/4e5075cbdecdf8e521acf8ba51c61478 to your computer and use it in GitHub Desktop.
Save whiteball/4e5075cbdecdf8e521acf8ba51c61478 to your computer and use it in GitHub Desktop.
AIのべりすとのリーディングモードに画像を挿入するためのスクリプトです。リッチテキストとしてのコピーもできます。AIのべりすとユーティリティ(拙作UserScript)と互換性があります。※Chrome/Firefoxで動作確認
// ==UserScript==
// @name AIのべりすと リーディングモードに画像挿入
// @namespace https://ai-novelist-share.geo.jp/
// @version 0.1.0
// @description AIのべりすとのリーディングモードに画像を挿入するためのスクリプトです。リッチテキストとしてのコピーもできます。AIのべりすとユーティリティ(拙作UserScript)と互換性があります。※Chrome/Firefoxで動作確認
// @author しらたま
// @match https://ai-novel.com/novel_read.php
// @icon https://www.google.com/s2/favicons?sz=64&domain=ai-novel.com
// @updateURL https://gist.github.com/whiteball/4e5075cbdecdf8e521acf8ba51c61478/raw/ai_novelist_utility.user.js
// @downloadURL https://gist.github.com/whiteball/4e5075cbdecdf8e521acf8ba51c61478/raw/ai_novelist_utility.user.js
// @supportURL https://gist.github.com/whiteball/4e5075cbdecdf8e521acf8ba51c61478
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @grant none
// ==/UserScript==
(function () {
'use strict';
document.querySelector('#acMenu > dd')
.insertAdjacentHTML('beforeend', `<h3>ユーザースクリプト</h3>
<h4>画像の挿入
<span id="tooltips">
<span data-text="本文欄の中の指定した名前を、画像に置き換えます。チャット用プロンプトなどと組み合わせてお使い下さい。名前入力欄の隣のチェックボックスにチェックすると、本文に置換対象の名前を残します。置換対象を決める正規表現は調整できます。設定の保存ボタンから名前と画像の組み合わせデータをダウンロード可能です。" id="help_mod_insert_image"><img src="images/icon_popup.png" width="20" height="20" id="help_mod_insert_image_icon" class="help_popup" style="margin-left:10px; margin-top: -5px; vertical-align:middle;" aria-describedby="tooltip_mod_insert_image" onclick="return false;"></span>
</span></h4>
<div id="mod_insert_image_items" style="margin-bottom: 1rem">
<p>幅と高さについて
<span id="tooltips">
<span data-text="幅と高さ両方に1以上の数値を設定すると、その幅と高さで本文に挿入します(アスペクト比が変わります)。どちらかを0にすると、入力した幅または高さを最大値としたサイズに合わせて挿入します(アスペクト比は維持します)。両方0にすると、元の画像サイズのまま挿入します。" id="help_mod_insert_image_items_wh"><img src="images/icon_popup.png" width="20" height="20" id="help_mod_insert_image_items_wh_icon" class="help_popup" style="margin-left:10px; margin-top: -5px; vertical-align:middle;" aria-describedby="tooltip_mod_insert_image_items_wh" onclick="return false;"></span>
</span></p>
<div id="mod_insert_image_items_inner" style="margin-bottom: 1rem">
</div>
<div>
<button id="mod_insert_image_item_add" data-next-id="0">入力枠を追加</button>
</div>
</div>
<div style="margin-bottom: 1rem">
<h5>置換用のパターンの設定
<span id="tooltips">
<span data-text="「置換用のパターン」の文字列で本文を検索します。「{{name}}」は検索前に画像の名前に置換されます。「置換用のパターン」のパターンにキャプチャなどの正規表現を使う場合、「置換後のフォーマット」に置換後の文字列を指定してください。その際、キャプチャは「$数字」のスタイルではなく、「$<文字列>」のスタイルにしてください。なお、_から始まるキャプチャ名(例えば「$<_line>」)はシステム予約となりますので、使用しないでください。" id="help_mod_insert_image_format"><img src="images/icon_popup.png" width="20" height="20" id="help_mod_insert_image_format_icon" class="help_popup" style="margin-left:10px; margin-top: -5px; vertical-align:middle;" aria-describedby="tooltip_mod_insert_image_format" onclick="return false;"></span>
</span></h5>
<div style="display:inline-block">
置換用のパターン<br>
<input type="text" style="font-size: 18px;" id="mod_insert_image_format" value="{{name}}「" placeholder="例:{{name}}「">
</div>
<div style="display:inline-block">
置換後のフォーマット(任意)<br>
<input type="text" style="font-size: 18px;" id="mod_insert_image_format_after" value="" placeholder="例:{{name}}「">
</div>
</div>
<div>
<div style="margin-bottom: 1rem;"><button id="mod_insert_image_apply">設定を本文に適用する</button></div>
<div style="display: inline-block;margin-right: 2rem;"><button id="mod_insert_image_save">設定の保存</button></div>
<div style="display: inline-block;">設定の読込:<input type="file" id="mod_insert_image_load" accept=".zip"></div>
</div>
</div>
`)
// style追加
// mod_insert_image_content_newクラスは、画像がある行に新規の出力文が追加される場合に上手く動かないので未使用
document.getElementsByTagName('head')[0].insertAdjacentHTML('beforeend', `<style type="text/css">
.mod_insert_image_content_new {
display: inline-flex;
margin: 5px 0;
align-items: center;
}
.mod_insert_image_content_new .name{
white-space: nowrap;
margin: 0 5px;
}
.mod_insert_image_content {
display: inline-block;margin:5px 0;
}
.mod_insert_image_content img{
vertical-align: middle;
}
</style>`)
// ツールチップ表示のイベント
window.jQuery("#help_mod_insert_image, #help_mod_insert_image_format, #help_mod_insert_image_items_wh, #help_mod_insert_image_scroll_to_button").on({
'mouseenter': function () {
const $ = window.jQuery
var text = $(this).attr('data-text')
$(this).append('<div class="tooltips_body">' + text + '</div>')
},
'mouseleave': function () {
window.jQuery(this).find(".tooltips_body").remove()
}
})
/**
* テキストから画像挿入関係のタグを削除する。
*/
const removeInsertImage = function (txt) {
return txt.replace(/<img[^>]*>/gi, '')
.replace(/<span class="name"[^>]*>([^<]*)<\/span>/gi, '$1')
.replace(/<span class="mod_insert_image_content(?:_new)?"[^>]*>([^<]*)<\/span>/gi, '$1')
}
// 入力枠の追加処理
const mod_insert_image_item_add = document.getElementById('mod_insert_image_item_add')
/**
* 画像挿入設定の入力枠を追加する。
* @returns {Number} 次のID
*/
const addInsertImageUI = function () {
const id = mod_insert_image_item_add.getAttribute('data-next-id'),
mod_insert_image_items_inner = document.getElementById('mod_insert_image_items_inner')
if (mod_insert_image_items_inner) {
let init_w = 64, init_h = 64
const currentElemCount = mod_insert_image_items_inner.childElementCount
if (currentElemCount > 0) {
const last_id = mod_insert_image_items_inner.children[mod_insert_image_items_inner.childElementCount - 1].getAttribute('data-id')
const mod_insert_image_item_width = document.getElementById('mod_insert_image_item_width[' + last_id + ']'),
mod_insert_image_item_height = document.getElementById('mod_insert_image_item_height[' + last_id + ']')
if (mod_insert_image_item_width) {
init_w = mod_insert_image_item_width.value
}
if (mod_insert_image_item_height) {
init_h = mod_insert_image_item_height.value
}
}
mod_insert_image_items_inner.insertAdjacentHTML('beforeend', `<div id="mod_insert_image_item[${id}]" class="mod_insert_image_item" style="margin-bottom: 0.5rem" data-id="${id}">
<span style="vertical-align: middle;">
<input type="text" style="font-size: 18px;width:10rem;" id="mod_insert_image_item_name[${id}]" placeholder="名前を入力...">
<input type="checkbox" style="font-size: 18px; transform:scale(1.5);" id="mod_insert_image_item_display[${id}]" >
幅:
<input type="text" style="font-size: 18px;width:3.5rem;" id="mod_insert_image_item_width[${id}]" value="${init_w}" placeholder="64">
高さ:
<input type="text" style="font-size: 18px;width:3.5rem;" id="mod_insert_image_item_height[${id}]" value="${init_h}" placeholder="64">
<input type="file" id="mod_insert_image_item_file[${id}]" accept="image/*">
</span>
<span style="vertical-align:middle;display:none;">
<img id="mod_insert_image_item_image[${id}]" style="vertical-align:middle;max-height:64px;max-width:64px;">
<button style="padding:3px;">×</button>
</span>
</div>`)
const mod_insert_image_item = document.getElementById('mod_insert_image_item_file[' + id + ']')
const mod_insert_image_item_image = document.getElementById('mod_insert_image_item_image[' + id + ']')
if (mod_insert_image_item && mod_insert_image_item_image) {
mod_insert_image_item.addEventListener('change', function () {
const reader = new FileReader();
reader.addEventListener('load', function () {
mod_insert_image_item_image.src = reader.result
mod_insert_image_item_image.parentElement.style.display = ''
mod_insert_image_item.value = ''
})
reader.readAsDataURL(this.files[0]);
})
mod_insert_image_item_image.nextElementSibling.addEventListener('click', function () {
mod_insert_image_item_image.parentElement.style.display = 'none'
mod_insert_image_item_image.src = ''
mod_insert_image_item.value = ''
})
}
mod_insert_image_item_add.setAttribute('data-next-id', Number(id) + 1)
document.getElementById('vis_fontsize').dispatchEvent(new Event('input'))
return Number(id)
}
return NaN
}
addInsertImageUI()
if (mod_insert_image_item_add) {
mod_insert_image_item_add.addEventListener('click', function () {
addInsertImageUI()
})
}
/**
* 本文入力枠のテキストに画像を挿入する。
* target_textが与えられた場合は、本文入力枠ではなくそのテキストに対して挿入する。
* target_textは出力テキスト(ai_outputの中身)を与えることを想定している。
* @param {string} target_text 画像を挿入する対象のテキスト
* @returns {string} target_textが与えられた場合は、挿入後のテキスト。そうでなければ空。
*/
const applyInsertImage = function (target_text = '') {
// 正規表現が登録されているかチェック
const mod_insert_image_format = document.getElementById('mod_insert_image_format')
if (!mod_insert_image_format || mod_insert_image_format.value === '') {
console.error('mod_insert_image_format is empty')
return target_text
}
// 画像を適用する
const mod_insert_image_format_base = mod_insert_image_format.value
const replace_args = []
for (const mod_insert_image_item of document.getElementsByClassName('mod_insert_image_item')) {
const id = mod_insert_image_item.getAttribute('data-id')
const mod_insert_image_item_name = document.getElementById('mod_insert_image_item_name[' + id + ']'),
mod_insert_image_item_image = document.getElementById('mod_insert_image_item_image[' + id + ']')
if (!(mod_insert_image_item_name && mod_insert_image_item_image && mod_insert_image_item_name.value !== '' && mod_insert_image_item_image.src !== location.href)) {
continue
}
try {
const text = mod_insert_image_item_name.value.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
const reg = new RegExp('(?<_preceding>^|<p style="margin:0px;">)' + mod_insert_image_format_base.replace('{{name}}', text) + '(?<_line>.*?)(?=\\<|$)', 'gm')
let img_h = 'max-height:64px;', img_w = 'max-width:64px;', display = 'none'
const mod_insert_image_item_width = document.getElementById('mod_insert_image_item_width[' + id + ']'),
mod_insert_image_item_height = document.getElementById('mod_insert_image_item_height[' + id + ']'),
mod_insert_image_item_display = document.getElementById('mod_insert_image_item_display[' + id + ']')
const width = mod_insert_image_item_width ? Number(mod_insert_image_item_width.value) : 0,
height = mod_insert_image_item_height ? Number(mod_insert_image_item_height.value) : 0
if (width === 0) {
if (height === 0) {
img_w = ''
img_h = ''
} else {
img_w = ''
img_h = 'max-height:' + height + 'px;'
}
} else if (width > 0) {
if (height > 0) {
img_w = 'width:' + width + 'px;'
img_h = 'height:' + height + 'px;'
} else {
img_w = 'max-width:' + width + 'px;'
img_h = ''
}
}
if (mod_insert_image_item_display && mod_insert_image_item_display.checked) {
display = 'inline'
}
const mod_insert_image_format_after = document.getElementById('mod_insert_image_format_after')
const mod_insert_image_format_base_after = (mod_insert_image_format_after && mod_insert_image_format_after.value !== '') ? mod_insert_image_format_after.value : mod_insert_image_format_base
const after_text = '<img src="' + mod_insert_image_item_image.src + '" title="' + text + '" alt="' + text + '" style="' + img_h + img_w + '"><span class="name" style="display:' + display + ';">' + text + '</span>'
replace_args.push({
reg: reg,
text: '$<_preceding><span class="mod_insert_image_content">' + mod_insert_image_format_base_after.replace('{{name}}', after_text) + '$<_line></span>',
})
} catch (error) {
console.error(error)
}
}
if (target_text === '') {
let target_list = document.getElementsByClassName('data_edit')
for (const data_edit of target_list) {
let innerHTML = data_edit.innerHTML
for (const args of replace_args) {
innerHTML = innerHTML.replace(args.reg, args.text)
}
if (data_edit.innerHTML !== innerHTML) {
data_edit.innerHTML = innerHTML
}
}
return ''
} else {
for (const args of replace_args) {
target_text = target_text.replace(args.reg, args.text)
}
return target_text
}
}
// 強制的にTextShardingを呼び出して、画像挿入を適用するボタン
const mod_insert_image_apply = document.getElementById('mod_insert_image_apply')
if (mod_insert_image_apply) {
mod_insert_image_apply.addEventListener('click', function () {
// 各入力枠の画像を削除
for (const data_edit of document.getElementsByClassName('data_edit')) {
const temp_html = removeInsertImage(data_edit.innerHTML)
if (data_edit.innerHTML !== temp_html) {
data_edit.innerHTML = temp_html
}
}
applyInsertImage()
})
}
// 設定の保存
const mod_insert_image_save = document.getElementById('mod_insert_image_save')
if (mod_insert_image_save) {
mod_insert_image_save.addEventListener('click', function () {
const save_date = []
// 画像設定を列挙
for (const mod_insert_image_item of document.getElementsByClassName('mod_insert_image_item')) {
const id = mod_insert_image_item.getAttribute('data-id')
const mod_insert_image_item_name = document.getElementById('mod_insert_image_item_name[' + id + ']'),
mod_insert_image_item_image = document.getElementById('mod_insert_image_item_image[' + id + ']'),
mod_insert_image_item_width = document.getElementById('mod_insert_image_item_width[' + id + ']'),
mod_insert_image_item_height = document.getElementById('mod_insert_image_item_height[' + id + ']'),
mod_insert_image_item_display = document.getElementById('mod_insert_image_item_display[' + id + ']')
if (!(mod_insert_image_item_name && mod_insert_image_item_image && mod_insert_image_item_width && mod_insert_image_item_height && mod_insert_image_item_display)) {
continue
}
save_date.push({
'name': mod_insert_image_item_name.value,
'img': mod_insert_image_item_image.src === location.href ? '' : mod_insert_image_item_image.src,
'width': mod_insert_image_item_width.value,
'height': mod_insert_image_item_height.value,
'display': mod_insert_image_item_display.checked ? 1 : 0,
})
}
// その他設定
const mod_insert_image_format = document.getElementById('mod_insert_image_format'),
mod_insert_image_format_after = document.getElementById('mod_insert_image_format_after')
// ZIP準備
const zip = new JSZip()
zip.file('setting.json', JSON.stringify({
'option': {
'format': mod_insert_image_format ? mod_insert_image_format.value : '',
'format_after': mod_insert_image_format_after ? mod_insert_image_format_after.value : '',
}, 'list': save_date
}))
zip.generateAsync({
type: 'blob',
compression: 'DEFLATE',
compressionOptions: {
level: 6,
}
}).then(function (blob) {
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
document.body.appendChild(a)
a.download = 'AIのべりすと拡張_画像設定.zip'
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(a.href)
})
})
}
// 設定の読み込み
const mod_insert_image_load = document.getElementById('mod_insert_image_load')
if (mod_insert_image_load) {
mod_insert_image_load.addEventListener('change', function (event) {
// 項目設定用関数
const setInsertImageData = function (id, data) {
const mod_insert_image_item_name = document.getElementById('mod_insert_image_item_name[' + id + ']'),
mod_insert_image_item_file = document.getElementById('mod_insert_image_item_file[' + id + ']'),
mod_insert_image_item_image = document.getElementById('mod_insert_image_item_image[' + id + ']'),
mod_insert_image_item_width = document.getElementById('mod_insert_image_item_width[' + id + ']'),
mod_insert_image_item_height = document.getElementById('mod_insert_image_item_height[' + id + ']'),
mod_insert_image_item_display = document.getElementById('mod_insert_image_item_display[' + id + ']')
if (!(mod_insert_image_item_name && mod_insert_image_item_file && mod_insert_image_item_image && mod_insert_image_item_width && mod_insert_image_item_height && mod_insert_image_item_display)) {
return false
}
mod_insert_image_item_name.value = data.name
mod_insert_image_item_file.value = ''
mod_insert_image_item_image.src = data.img
mod_insert_image_item_image.parentElement.style.display = ''
mod_insert_image_item_width.value = data.width
mod_insert_image_item_height.value = data.height
mod_insert_image_item_display.checked = data.display !== 0
return true
}
if (event.target.files.length === 0) {
return
}
const zip_file = event.target.files[0];
JSZip.loadAsync(zip_file).then(function (zip) {
return zip.files['setting.json'].async('text')
}).then(function (text) {
return JSON.parse(text)
}).then(function (save_data) {
if (!(save_data && save_data.list && save_data.list.length > 0)) {
return
}
// 既存の項目に上書き
for (const mod_insert_image_item of document.getElementsByClassName('mod_insert_image_item')) {
const id = mod_insert_image_item.getAttribute('data-id')
if (setInsertImageData(id, save_data.list[0])) {
save_data.list.shift()
}
}
if (save_data.list.length > 0) {
// 項目が足りないので追加する
for (const date of save_data.list) {
const id = addInsertImageUI()
setInsertImageData(id, date)
}
}
if (save_data.option) {
for (const setting of [['format', 'mod_insert_image_format'], ['format_after', 'mod_insert_image_format_after']]) {
if (save_data.option[setting[0]]) {
const element = document.getElementById(setting[1])
if (element) {
element.value = save_data.option[setting[0]]
}
}
}
}
// 読み込み終わったらファイルは消す
mod_insert_image_load.value = ''
})
})
}
document.getElementById('allcopy').insertAdjacentHTML('afterend', `<input type="button" id="allcopy_img" value="書式(画像)付きで本文をコピーする" class="btn-square" style="width:70vw; min-width:300px; margin: 15px; font-size: 16px; color: #777777; background-color:white; border-color: #777777; width:45vw; margin-right:1vw; font-family: Quicksand;">`)
document.getElementById('allcopy_img').addEventListener('click', function () {
if (window.ClipboardItem && navigator.clipboard.write) {
const html = document.querySelector('#data_edit').innerHTML.replace(/<\/p><p style="margin:0px;">/, '<br>').replace(/^<p style="margin:0px;">/, '').replace(/<\/p>$/, ''),
text = localStorage.textdata.replace(/(<br*?>)/gi, '\n')
.replace(/(<div.*?>)/gi, '\n')
.replace(/(&nbsp;)/gi, ' ')
.replace(/(&amp;)/gi, '&')
.replace(/(&lt;)/gi, '<')
.replace(/(&gt;)/gi, '>')
.replace(/<("[^"]*"|'[^']*'|[^'">])*>/gi, ''),
blob_html = new Blob([html], { type: 'text/html' }),
blob_text = new Blob([text], { type: 'text/plain' }),
item = [new window.ClipboardItem({ 'text/html': blob_html, 'text/plain': blob_text })]
navigator.clipboard.write(item).then(() => {
this.setAttribute('value', '書式(画像)付きでクリップボードにコピーしました!')
})
} else {
const selection = document.getSelection()
const range = selection.getRangeAt(0)
selection.removeAllRanges()
selection.selectAllChildren(document.querySelector('#data_edit'))
document.execCommand("Copy");
this.setAttribute('value', '書式(画像)付きでクリップボードにコピーしました!')
selection.removeAllRanges()
selection.addRange(range)
}
})
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment