// ==UserScript== // @name Tategaki Novel // @namespace http://userscripts.org/users/121129 // @description 小説投稿サイトに縦書き表示機能を追加 // @include http://ncode.syosetu.com/n* // @include http://novel18.syosetu.com/n* // @include http://www.mai-net.net/bbs/sst/sst.php?* // @include http://novel.syosetu.org/* // @include http://www.pixiv.net/novel/show.php?* // @include http://www.akatsuki-novels.com/stories/view/* // @version 13 // @license MIT // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @downloadURL none // ==/UserScript== ;(function() { 'use strict' var onIFrame = window.top !== window.self if (onIFrame) return var Default = { LINE_NUM: 22 , LINE_CHAR_NUM: 28 , FONT_TYPE: 'gothic' , GOTHIC_FONT_FAMILY: [ '"メイリオ"' , '"IPAexゴシック"' , '"IPAゴシック"' , '"MS ゴシック"' , '"SimSun"' , 'monospace' ].join(', ') , MINCHO_FONT_FAMILY: [ '"IPAex明朝"' , '"IPA明朝"' , '"MS 明朝"' , '"SimSun"' , 'serif' ].join(', ') , FONT_SIZE: '20px' , FONT_WEIGHT: 'normal' , CHAR_HEIGHT: '1.1em' , MARGIN_TOP: '50px' , SPACE_BETWEEN_LINES: '1em' , COLOR: '#2F4F4F' , BACKGROUND_COLOR: '#D3D3D3' , TOOLBAR_VISIBLE: true , AUTO_VERTICAL_WRITING: true } var _getValue, _setValue, _deleteValue ;(function() { function addPrefix(name) { return 'http://userscripts.org/users/121129/Tategaki Novel.' + name } if (typeof(GM_deleteValue) === 'undefined') { _getValue = function(name, def) { var v = localStorage.getItem(addPrefix(name)) return v === null ? def : JSON.parse(v) } _setValue = function(name, value) { localStorage.setItem(addPrefix(name), JSON.stringify(value)) } _deleteValue = function(name) { localStorage.removeItem(addPrefix(name)) } } else { _getValue = GM_getValue _setValue = GM_setValue _deleteValue = GM_deleteValue } })() function GM_getLineNum() { return _getValue('lineNum', Default.LINE_NUM) } function GM_getLineCharNum() { return _getValue('lineCharNum', Default.LINE_CHAR_NUM) } function GM_getFontType() { return _getValue('fontType', Default.FONT_TYPE) } function GM_getGothicFontFamily() { return _getValue('gothicFontFamily', Default.GOTHIC_FONT_FAMILY) } function GM_getMinchoFontFamily() { return _getValue('minchoFontFamily', Default.MINCHO_FONT_FAMILY) } function GM_getFontFamily(fontType) { switch (fontType || GM_getFontType()) { case 'gothic': return GM_getGothicFontFamily() case 'mincho': return GM_getMinchoFontFamily() default: throw new Error(fontType || GM_getFontType()) } } function GM_getColor() { return _getValue('color', Default.COLOR) } function GM_getBackgroundColor() { return _getValue('backgroundColor', Default.BACKGROUND_COLOR) } function GM_isToolbarVisible() { return _getValue('toolbarVisible', Default.TOOLBAR_VISIBLE) } function GM_isAutoVerticalWriting() { return _getValue('autoVerticalWriting', Default.AUTO_VERTICAL_WRITING) } function GM_getFontSize() { return _getValue('fontSize', Default.FONT_SIZE) } function GM_getCharHeight() { return _getValue('charHeight', Default.CHAR_HEIGHT) } function GM_getMarginTop() { return _getValue('marginTop', Default.MARGIN_TOP) } function GM_getSpaceBetweenLines() { return _getValue('spaceBetweenLines', Default.SPACE_BETWEEN_LINES) } function GM_getFontWeight() { return _getValue('fontWeight', Default.FONT_WEIGHT) } function GM_clear() { ;[ 'lineNum' , 'lineCharNum' , 'fontType' , 'gothicFontFamily' , 'minchoFontFamily' , 'color' , 'backgroundColor' , 'toolbarVisible' , 'fontSize' , 'charHeight' , 'marginTop' , 'spaceBetweenLines' , 'auto' , 'autoVerticalWriting' , 'fontWeight' ].forEach(function(name) { _deleteValue(name) }) } function hasParam(location, name, val) { return location.search.substring(1).split('&').some(function(param) { var i = param.indexOf('=') if (name !== param.substring(0, i)) return false var v = param.substring(i + 1) return val instanceof RegExp ? val.test(v) : val === v }) } function getHRef(selector, textContent) { var anchors = document.querySelectorAll(selector) for (var i = 0; i < anchors.length; i++) { var a = anchors[i] if (!textContent || a.textContent === textContent) return a.href } return '' } function insertCell(row, child) { var result = row.insertCell(-1) result.style.border = '1px solid' result.style.padding = '5px' result.style.fontSize = '18px' if (child) { if (child.nodeType) result.appendChild(child) else result.textContent = child } return result } var newTextNode = document.createTextNode.bind(document) function getLine(selector) { var n = document.querySelector(selector) return n ? Line.parse(n.childNodes) : null } function getLines(selector) { var n = document.querySelector(selector) return n ? Lines.parse(n.childNodes) : [] } function newRotatedElem(text, doc) { var result = (doc || document).createElement('span') result.textContent = text result.style.display = 'inline-block' result.style.transform = 'rotate(90deg)' result.style.webkitTransform = 'rotate(90deg)' return result } var pixelCharSize = (function() { var doc = document var height = 0 var char2width = Object.create(null) function calcHeight() { var e = doc.createElement('div') e.style.position = 'absolute' e.style.fontSize = GM_getFontSize() e.style.lineHeight = GM_getCharHeight() doc.body.appendChild(e) try { var s = doc.defaultView.getComputedStyle(e) var h = s.lineHeight === 'normal' ? s.fontSize : s.lineHeight return parseFloat(h) } finally { doc.body.removeChild(e) } } function calcWidth(ch) { var e = doc.createElement('pre') e.style.position = 'absolute' e.appendChild(doc.createTextNode(ch)) e.style.fontFamily = GM_getFontFamily() e.style.fontSize = GM_getFontSize() e.style.fontWeight = GM_getFontWeight() doc.body.appendChild(e) try { return parseFloat(doc.defaultView.getComputedStyle(e).width) } finally { doc.body.removeChild(e) } } return { get doc() { return doc }, set doc(v) { doc = v || document }, height: function() { return height || (height = calcHeight()) }, width: function(ch) { return char2width[ch] || (char2width[ch] = calcWidth(ch)) }, clearCache: function() { height = 0 char2width = Object.create(null) } } })() function Block() {} Block.prototype.startsWithBadChar = function() { return false } Block.prototype.endsWithBadChar = function() { return false } Block.prototype.setLine = function(line) { return this } Block.prototype.trimHead = function() { return this } Block.prototype.isEmpty = function() { return false } var Text = (function() { var replaceByVerticalChars = (function() { var map = { '「':'﹁', '」':'﹂', '「':'﹁', '」':'﹂' , '『':'﹃', '』':'﹄', '(':'︵', ')':'︶' , '{':'︷', '}':'︸', '〈':'︿', '〉':'﹀' , '<':'︿', '>':'﹀', '《':'︽', '》':'︾' , '≪':'︽', '≫':'︾', '〔':'︹', '〕':'︺' , '【':'︻', '】':'︼', '-':'︱', '―':'︱' , '─':'︱', '—':'︱', 'ー':'丨', '−':'︱' , '…':'︙', '‥':'︰' , '、':'︑', '。':'︒', ',':'︐' } , regExp = new RegExp(Object.keys(map).join('|'), 'g') return function(str) { return str.replace(regExp, function(match) { return map[match] }) } })() var replaceIsolatedAsciiByZenkaku = (function() { var regExp = /(^|[^\x20-\x7e])([\x21-\x7e])(?=$|[^\x20-\x7e])/g , diff = '!'.charCodeAt(0) - '!'.charCodeAt(0) return function(str) { return str.replace(regExp, function(match, p1, p2) { return p1 + String.fromCharCode(p2.charCodeAt(0) + diff) }) } })() function removeAllWhiteSpace(str) { return str.replace(/[ \t\r\n\u00A0]/g, '') } var replaceTwoExclamationOrQuestion = (function() { var map = { '!!': '‼', '??': '⁇', '?!': '⁈', '!?': '⁉' } , regExp = /(^|[^!?])([!?]{2})(?=$|[^!?])/g return function(str) { return str.replace(regExp, function(match, p1, p2) { return p1 + map[p2] }) } })() var reverseQuotationMark = (function() { var map = { '“':'”', '”':'“', '‘':'’', '’':'‘' , '〝':'”', '〟':'“', '〞':'“' } , regExp = new RegExp(Object.keys(map).join('|'), 'g') return function(str) { return str.replace(regExp, function(match) { return map[match] }) } })() var replaceHalfwidthKatakanaByFullwidth = (function() { var fullwidth = '。「」、・ヲァィゥェォャュョッー' + 'アイウエオカキクケコサシスセソタチツテト' + 'ナニヌネノハヒフヘホマミムメモヤユヨラリルレロ' + 'ワン゛゜' , halfwidth = /([。-゚])([゙゚]?)/g , dakuten_able = /[カ-チツ-トハ-ホ]/ , handakuten_able = /[ハ-ホ]/ , halfwidthBase = '。'.charCodeAt(0) function plusCharCode(ch, n) { return String.fromCharCode(ch.charCodeAt(0) + n) } return function(str) { return str.replace(halfwidth, function(match, p1, p2) { var c = fullwidth.charAt(p1.charCodeAt(0) - halfwidthBase) if (!p2) return c if (p2 === '゙') { if (dakuten_able.test(c)) return plusCharCode(c, 1) switch (c) { case 'ウ': return 'ヴ' case 'ワ': return 'ヷ' case 'ヲ': return 'ヺ' } return c + '゛' } if (handakuten_able.test(c)) return plusCharCode(c, 2) return c + '゜' }) } })() var rotateArrows = (function() { var map = { '↑': '→', '↓': '←', '→': '↓', '←': '↑' } , regExp = new RegExp(Object.keys(map).join('|'), 'g') return function(str) { return str.replace(regExp, function(match) { return map[match] }) } })() var insertFullwidthSpaceAfterExclamationOfQuestion = (function() { var re = /[!?‼⁇⁈⁉](?![!?‼⁇⁈⁉」』)}〉>》≫〕】”’〟〞\u3000]|$)/g return function(str) { return str.replace(re, function(match) { return match + ' ' }) } })() function replace(text) { var str = removeAllWhiteSpace(text) str = replaceIsolatedAsciiByZenkaku(str) str = replaceHalfwidthKatakanaByFullwidth(str) str = insertFullwidthSpaceAfterExclamationOfQuestion(str) str = replaceByVerticalChars(str) str = replaceTwoExclamationOrQuestion(str) str = reverseQuotationMark(str) str = rotateArrows(str) return str } function Text(text, replaced) { this.val = replaced ? text : replace(text) Object.freeze(this) } Text.prototype = Object.create(Block.prototype) Text.prototype.isEmpty = function() { return !this.val } Text.prototype.getLength = function() { return this.val.length } Text.prototype.canCut = function(index) { return 0 < index && index < this.getLength() } Text.prototype.cut = function(index) { return { before: new Text(this.val.substring(0, index), true) , after: new Text(this.val.substring(index), true) } } Text.prototype.startsWithBadChar = (function() { var regExp = new RegExp('[﹂﹄︶︸﹀︾︺︼︑︒︐]“‘!?‼⁇⁈⁉丨' + 'ぁぃぅぇぉっゃゅょゎァィゥェォッャュョヮヵヶ' + '〜~]') return function() { return regExp.test(this.val[0]) } })() Text.prototype.endsWithBadChar = function() { return /[﹁﹃︵︷︿︽︹︻[”’]/.test(this.val[this.val.length - 1]) } Text.prototype.trimHead = function() { return /^\u3000+/.test(this.val) ? new Text(this.val.trimLeft(), true) : this } Text.prototype.newNode = (function() { function replaceMatchedTextByCreatedElem(textNode , regExp , createElem , doc) { var result = doc.createDocumentFragment() , begin = 0 , text = textNode.nodeValue for (var r; r = regExp.exec(text);) { result.appendChild(doc.createTextNode(text.substring(begin , r.index))) result.appendChild(createElem(r[0], doc)) begin = regExp.lastIndex } result.appendChild(doc.createTextNode(text.substring(begin))) result.normalize() return result } function replaceTextNodeIfMatched(node, regExp, createElem, doc) { Array.prototype.filter.call(node.childNodes, function(child) { return child.nodeType === Node.TEXT_NODE }).forEach(function(textNode) { node.replaceChild(replaceMatchedTextByCreatedElem(textNode , regExp , createElem , doc) , textNode) }) return node } function rotateNoVerticalChars(node, doc) { return replaceTextNodeIfMatched(node , /[[]=:;〜~]/g , newRotatedElem , doc) } var translateSutegana = (function() { var regExp = /[ぁぃぅぇぉっゃゅょゎァィゥェォッャュョヮヵヶ]/g function newSuteganaElem(text, doc) { var result = doc.createElement('span') result.textContent = text result.style.position = 'relative' result.style.left = '0.1em' result.style.top = '-0.1em' return result } return function(node, doc) { return replaceTextNodeIfMatched(node, regExp, newSuteganaElem, doc) } })() var translateQuotationMark = (function() { var regExp = /[”“’‘]/g function newQuotationMarkElem(text, doc) { var result = doc.createElement('span') result.textContent = text result.style.position = 'relative' if (text === '”' || text === '’') { result.style.top = '0.5em' if (text === '”') result.style.left = '0.5em' else result.style.left = '0.7em' } else { result.style.top = '0.2em' if (text === '“') result.style.right = '0.5em' else result.style.right = '0.7em' } return result } return function(node, doc) { return replaceTextNodeIfMatched(node , regExp , newQuotationMarkElem , doc) } })() return function(doc) { doc = doc || document var df = doc.createDocumentFragment() df.appendChild(doc.createTextNode(this.val)) df = rotateNoVerticalChars(df, doc) df = translateSutegana(df, doc) df = translateQuotationMark(df, doc) return df } })() return Text })() var Ascii = (function() { function toIntEmLength(pxLength) { return Math.ceil(pxLength / pixelCharSize.height()) } function getTextPxWidth(str) { return Array.prototype.reduce.call(str, function(pre, ch) { return pre + pixelCharSize.width(ch) }, 0) } function getIntEmLength(str) { return toIntEmLength(getTextPxWidth(str)) } function compressWhiteSpaces(str) { return str.replace(/[ \t\r\n]+/g, ' ') } function Ascii(asciiText) { this.val = compressWhiteSpaces(asciiText) Object.freeze(this) } Ascii.prototype = Object.create(Block.prototype) Ascii.prototype.getLength = function() { return getIntEmLength(this.val) } Ascii.prototype.canCut = function(index) { var v = this.val, i = v.indexOf(' ') if (i === -1) return false return getIntEmLength(v.substring(0, i)) <= index } Ascii.prototype.cut = function(index) { var sub = this.val.split(' ') for (var i = 2, n = sub.length; i <= n; i++) { var pxLen = getTextPxWidth(sub.slice(0, i).join(' ')) if (toIntEmLength(pxLen) >= index) break } return { before: new Ascii(sub.slice(0, i - 1).join(' ')) , after: new Ascii(sub.slice(i - 1).join(' ')) } } Ascii.prototype.newNode = function(doc) { var result = newRotatedElem(this.val, doc) result.style.transformOrigin = '0.5em 0.5em 0px' result.style.webkitTransformOriginX = '0.5em' result.style.webkitTransformOriginY = '0.5em' result.style.textAlign = 'center' result.style.lineHeight = '1' var len = this.getLength() var h = pixelCharSize.height() result.style.width = len * h + 'px' result.style.marginBottom = (len - 1) * h + 'px' return result } return Ascii })() var Ruby = (function() { function rubyCharPxHeight() { return pixelCharSize.height() / 2 } function Ruby(base, val, line) { this.base = base this.val = val this.line = line Object.freeze(this) } Ruby.parse = (function() { var map = { 'ぁ': 'あ', 'ぃ': 'い', 'ぅ': 'う', 'ぇ': 'え', 'ぉ': 'お' , 'っ': 'つ', 'ゃ': 'や', 'ゅ': 'ゆ', 'ょ': 'よ', 'ゎ': 'わ' , 'ァ': 'ア', 'ィ': 'イ', 'ゥ': 'ウ', 'ェ': 'エ', 'ォ': 'オ' , 'ッ': 'ツ', 'ャ': 'ヤ', 'ュ': 'ユ', 'ョ': 'ヨ', 'ヮ': 'ワ' , 'ヵ': 'カ', 'ヶ': 'ケ' } var re = new RegExp(Object.keys(map).join('|'), 'g') function replaceSuteganaByBigChar(str) { return str.replace(re, function(match) { return map[match] }) } return function(rubyElem) { var rb = '', rt = '' Array.prototype.forEach.call(rubyElem.childNodes, function(n) { if (n.tagName === 'RB') rb += n.textContent else if (n.tagName === 'RT') rt += n.textContent else if (n.nodeType === Node.TEXT_NODE) rb += n.nodeValue }) rb = rb.trim() rt = replaceSuteganaByBigChar(rt.trim()) return rb && rt ? new Ruby(Line.newByStr(rb), Line.newByStr(rt)) : null } })() Ruby.prototype = Object.create(Block.prototype) Ruby.prototype.hasRubyNeighbor = function() { if (!this.line) return false var blocks = this.line.blocks, i = blocks.indexOf(this) if (i === -1) return false return (blocks[i - 1] && blocks[i - 1] instanceof Ruby) || (blocks[i + 1] && blocks[i + 1] instanceof Ruby) } Ruby.prototype.getLength = function() { var rubyLen = this.val.getLength() var baseLen = this.base.getLength() var rubyLenCapacity = this.base.getLength() * 2 + (this.hasRubyNeighbor() ? 0 : 2) if (rubyLen <= rubyLenCapacity) return baseLen return baseLen + Math.ceil((rubyLen - rubyLenCapacity) / 2) } Ruby.prototype.canCut = function(index) { return false } Ruby.prototype.getRubyNodeTop = function() { var h = rubyCharPxHeight() var diff = this.base.getLength() * 2 - this.val.getLength() if (diff >= 0) return diff * h / 2 + 'px' var top = diff % 2 ? -h / 2 : -h return (this.hasRubyNeighbor() ? top + h : top) + 'px' } Ruby.prototype.newRubyNode = function(doc) { var result = doc.createElement('div') result.style.position = 'absolute' result.style.width = '1em' result.style.lineHeight = rubyCharPxHeight() + 'px' result.style.fontSize = '0.5em' result.style.top = this.getRubyNodeTop() result.style.left = '2em' ;[].slice.call(this.val.newNode(doc).childNodes).forEach(function(n) { result.appendChild(n) }) return result } Ruby.prototype.newBaseNode = function(doc) { var result = doc.createDocumentFragment() ;[].slice.call(this.base.newNode(doc).childNodes).forEach(function(n) { result.appendChild(n) }) return result } Ruby.prototype.getNodePadding = function() { var h = pixelCharSize.height() var p = (this.getLength() - this.base.getLength()) * h / 2 return p + 'px 0' } Ruby.prototype.newNode = function(doc) { doc = doc || document var result = doc.createElement('span') result.style.display = 'inline-block' result.style.position = 'relative' result.style.width = '1em' result.style.padding = this.getNodePadding() result.appendChild(this.newBaseNode(doc)) result.appendChild(this.newRubyNode(doc)) return result } Ruby.prototype.setLine = function(line) { return new Ruby(this.base, this.val, line) } Ruby.prototype.isEmphasized = function() { var bbs = this.base.blocks, rbs = this.val.blocks if (!(bbs.length === 1 && rbs.length === 1)) return false var bb = bbs[0], rb = rbs[0] if (!(bb instanceof Text && rb instanceof Text)) return false var bv = bb.val, rv = rb.val return bv.length === rv.length && /^([・﹅])\1*$/.test(rv) } Ruby.prototype.splitIntoEmphasisDots = function() { var bv = this.base.blocks[0].val return Array.prototype.map.call(bv, function(c) { return new Ruby(Line.newByStr(c), Line.newByStr('﹅'), this.line) }, this) } return Ruby })() var Line = (function() { function addTextIfNotEmpty(blocks, str) { if (!str) return var t = new Text(str) if (!t.isEmpty()) blocks.push(t) } function parseText(blocks, text) { var re = /[\x21-\x7e][\x20-\x7e\n\t\r]*[\x21-\x7e]/g, begin = 0 for (var r; r = re.exec(text);) { addTextIfNotEmpty(blocks, text.substring(begin, r.index)) blocks.push(new Ascii(r[0])) begin = re.lastIndex } addTextIfNotEmpty(blocks, text.substring(begin)) } function Line(blocks) { this.blocks = Object.freeze((blocks || []).map(function(b) { return b.setLine(this) }, this)) Object.freeze(this) } Line.EMPTY = new Line() Line.parse = function(childNodes) { var blocks = [], text = '' Array.prototype.forEach.call(childNodes, function(n) { if (n.tagName === 'RUBY') { var ruby = Ruby.parse(n) if (ruby) { parseText(blocks, text) text = '' Array.prototype.push.apply(blocks, ruby.isEmphasized() ? ruby.splitIntoEmphasisDots() : [ruby]) } else { text += n.textContent } } else { text += n.textContent } }) parseText(blocks, text) return blocks.length ? new Line(blocks) : Line.EMPTY } Line.newByStr = function(str) { return Line.parse([newTextNode(str)]) } Line.prototype.getLength = function() { return this.blocks.reduce(function(pre, block) { return pre + block.getLength() }, 0) } Line.prototype.cut = (function() { function cut(block, index, before, after) { var o = block.cut(index) before.push(o.before) after.unshift(o.after) } function endsWithBadChar(blocks) { if (!blocks.length) return false var last = blocks[blocks.length - 1] return last.endsWithBadChar() && !(last.getLength() === 1 && blocks.length === 1) } function startsWithBadChar(after) { return after.length && after[0].startsWithBadChar() } function pollLastChar(before, after) { var b = before.pop() if (b.getLength() === 1) after.unshift(b) else cut(b, b.getLength() - 1, before, after) } function pollTopChar(after, before) { var b = after.shift() if (b.getLength() === 1) before.push(b) else cut(b, 1, before, after) } function trimHead(after) { if (!after.length) return var b = after.shift().trimHead() if (!b.isEmpty()) after.unshift(b) } var MAX_POLL_NUM = 3 return function(index) { var after = this.blocks.slice(), before = [] for (var b, begin = 0; b = after.shift(); begin += b.getLength()) { var end = begin + b.getLength() if (end === index) { before.push(b) break } if (begin <= index && index < end) { if (b.canCut(index - begin)) cut(b, index - begin, before, after) else if (begin === 0) before.push(b) else after.unshift(b) break } before.push(b) } for (var i = 0; i < MAX_POLL_NUM && endsWithBadChar(before); i++) { pollLastChar(before, after) } for (var i = 0; i < MAX_POLL_NUM && startsWithBadChar(after); i++) { pollTopChar(after, before) } trimHead(after) return after.length ? { before: new Line(before), after: new Line(after) } : { before: this, after: null } } })() Line.prototype.split = function(lineCharNum) { var result = [], line = this do { var o = line.cut(lineCharNum) result.push(o.before) } while (line = o.after) return result } Line.prototype.isEmpty = function() { return !this.blocks.length } Line.prototype.newNode = function(doc) { doc = doc || document var result = doc.createElement('div') result.style.cssFloat = 'right' result.style.width = '1em' result.style.wordWrap = 'break-word' if (this.isEmpty()) { result.appendChild(doc.createTextNode('\u3000')) } else { this.blocks.forEach(function(b) { result.appendChild(b.newNode(doc)) }) } return result } return Line })() var Lines = { trim: function(lines) { var begin = 0, end = lines.length while (lines[begin] && lines[begin].isEmpty()) begin++ while (lines[end - 1] && lines[end - 1].isEmpty()) end-- return lines.slice(begin, end) } , parse: function(childNodes) { var result = [], sub = [] Array.prototype.forEach.call(childNodes, function(n) { if (n.tagName === 'BR' || n.tagName === 'P') { result.push(Line.parse(sub)) sub = [] if (n.tagName === 'P') { result.push(Line.EMPTY) result.push(Line.parse([n])) result.push(Line.EMPTY) } } else { sub.push(n) } }) result.push(Line.parse(sub)) return Lines.trim(result) } , split: function(lines, lineCharNum) { var result = [] lines.forEach(function(line) { Array.prototype.push.apply(result, line.split(lineCharNum)) }) return result } } function Episode(obj) { this.novelTitle = obj.novelTitle || null this.chapterTitle = obj.chapterTitle || null this.author = obj.author || null this.title = obj.title || null this.text = Object.freeze((obj.text || []).slice()) this.preface = Object.freeze((obj.preface || []).slice()) this.postscript = Object.freeze((obj.postscript || []).slice()) this.nextURL = obj.nextURL || '' this.prevURL = obj.prevURL || '' Object.freeze(this) } function Site() {} Site.prototype.newButton = function() { var result = document.createElement('button') result.textContent = '縦書き' result.style.padding = '0px 6px' result.addEventListener('click' , this.showBookView.bind(this, function(){})) result.addEventListener('click', function() { result.blur() }) return result } Site.prototype.showBookView = function(done) { var iframe = document.createElement('iframe') iframe.style.position = 'fixed' iframe.style.top = '0px' iframe.style.left = '0px' iframe.style.width = '100%' iframe.style.height = '100%' iframe.style.zIndex = '9999' iframe.style.borderWidth = '0px' iframe.addEventListener('load', (function() { pixelCharSize.doc = iframe.contentDocument var book = new Book(this.parse()) var view = new BookView(book, iframe.contentDocument) book.setView(view) document.documentElement.style.overflowY = 'hidden' view.onhide = function() { document.documentElement.style.overflowY = '' iframe.parentNode.removeChild(iframe) } view.show() iframe.focus() if (done) done(iframe) }).bind(this)) document.body.appendChild(iframe) iframe.focus() } var Narou = (function() { function isShortNovel() { return Boolean(document.querySelector('.novel_writername')) } function getParser() { return isShortNovel() ? new ShortParser() : new Parser() } function getNovelTitleDivSelector() { return isShortNovel() ? '.novel_title' : '.novel_subtitle' } function Parser() {} Parser.prototype.novelTitleSelector = '.contents1 > a:first-of-type' Parser.prototype.titleSelector = '.novel_subtitle' Parser.prototype.getTitle = function() { var n = document.querySelector(this.titleSelector) return n && n.firstChild ? Line.parse([n.firstChild]) : null } Parser.prototype.getChapterTitle = function() { return getLine('.chapter_title') } Parser.prototype.getAuthor = function() { var l = document.querySelectorAll('.contents1 > a') if (!l.length) return null if (l.length >= 2) return Line.parse(l[1].childNodes) var a = [].filter.call(l[0].parentNode.childNodes, function(n) { return n.nodeType === Node.TEXT_NODE && n.data.indexOf('作者:') >= 0 }) return a.length ? Line.newByStr(a[0].data.slice(a[0].data.indexOf('作者:') + 3)) : null } Parser.prototype.parse = function() { var navSelector = '.novel_bn > a' return new Episode({ novelTitle: getLine(this.novelTitleSelector) , chapterTitle: this.getChapterTitle() , author: this.getAuthor() , title: this.getTitle() , text: getLines('#novel_honbun') , preface: getLines('#novel_p') , postscript: getLines('#novel_a') , nextURL: getHRef(navSelector, '次の話\xA0>>') , prevURL: getHRef(navSelector, '<<\xA0前の話') }) } function ShortParser() {} ShortParser.prototype = Object.create(Parser.prototype) ShortParser.prototype.novelTitleSelector = '.series_title > a' ShortParser.prototype.titleSelector = '.novel_title' ShortParser.prototype.getChapterTitle = function() { return null } ShortParser.prototype.getAuthor = function() { return getLine('.novel_writername > a') } function Narou() {} Narou.prototype = Object.create(Site.prototype) Narou.prototype.is = function(location) { var h = location.hostname return (h === 'ncode.syosetu.com' || h === 'novel18.syosetu.com') && /^\/n\d+[a-z]+/.test(location.pathname) && Boolean(document.getElementById('novel_honbun')) } Narou.prototype.parse = function() { return getParser().parse() } Narou.prototype.addButton = function() { var d = document.querySelector(getNovelTitleDivSelector()) d.appendChild(this.newButton()) } return Narou })() var Arcadia = (function() { function getAuthor() { var n = document.querySelector('.bgc > table td:first-child tt') var s = /^Name: ([^◆]+)/.exec(n.textContent)[1] return Line.parse([newTextNode(s)]) } function getTitleFontElem() { return document.querySelector('.bgb font') } function Arcadia() {} Arcadia.prototype = Object.create(Site.prototype) Arcadia.prototype.is = function(location) { return location.hostname === 'www.mai-net.net' && location.pathname === '/bbs/sst/sst.php' && hasParam(location, 'act', 'dump') && hasParam(location, 'all', /[0-9]+/) } Arcadia.prototype.parse = function() { var navSelector = '.bgc > table td[align="right"] a' return new Episode({ author: getAuthor() , title: Line.parse(getTitleFontElem().childNodes) , text: getLines('.bgc blockquote div') , nextURL: getHRef(navSelector, '次を表示する') , prevURL: getHRef(navSelector, '前を表示する') }) } Arcadia.prototype.addButton = function() { getTitleFontElem().parentNode.appendChild(this.newButton()) } return Arcadia })() var Hameln = (function() { function isEpisodeListPage() { return Boolean(document.querySelector('#maind > .ss > table')) } function getTextLines() { var n = document.querySelector('.ss > font'), nodes = [] for (var s = n; (s = s.nextSibling) && s.id !== 'atogaki';) nodes.push(s) return Lines.parse(nodes) } var novelTitleAnchorSelector = '.ss > p > font > a' function Hameln() {} Hameln.prototype = Object.create(Site.prototype) Hameln.prototype.is = function(location) { return location.hostname === 'novel.syosetu.org' && /^\/\d+\/(\d+\.html)?/.test(location.pathname) && !isEpisodeListPage() } Hameln.prototype.parse = function() { var navSelector = '.ss > div:last-of-type > a' return new Episode({ novelTitle: getLine(novelTitleAnchorSelector) , author: getLine('.ss > p > a:first-of-type') , title: getLine('.ss > font') , text: getTextLines() , preface: getLines('#maegaki') , postscript: getLines('#atogaki') , nextURL: getHRef(navSelector, '次の話 >>') , prevURL: getHRef(navSelector, '<< 前の話') }) } Hameln.prototype.addButton = function() { var a = document.querySelector(novelTitleAnchorSelector) var fontElem = a.parentNode fontElem.parentNode.insertBefore(this.newButton(), fontElem.nextSibling) } return Hameln })() var Pixiv = (function() { var titleSelector = '.work-info .title' , novelArticleSelector = '#preview_area > .novel_article' function getTitle() { var e = document.querySelector(titleSelector) return e && e.firstChild ? Line.parse([e.firstChild]) : null } function newBR() { return document.createElement('br') } function isNovelArticleCreated() { return Boolean(document.querySelector(novelArticleSelector)) } function getTextLinesByNovelArticles() { var nodes = document.querySelectorAll(novelArticleSelector) var children = [] Array.prototype.forEach.call(nodes, function(node) { Array.prototype.push.apply(children, node.childNodes) children.push(newBR(), newBR()) }) return Lines.parse(children) } function getTextLinesByPlainText() { var text = document.querySelector('#novel_text') , lines = [] text.value.split('\n').forEach(function(line) { var r = null if (/^\s*\[newpage\]\s*$/.test(line)) { lines.push(Line.EMPTY) } else if (r = /^\s*\[chapter:(.+?)\]\s*$/.exec(line)) { lines.push(Line.EMPTY) lines.push(Line.newByStr(r[1])) lines.push(Line.EMPTY) } else { lines.push(Line.newByStr(line)) } }) return Lines.trim(lines) } function getTextLines() { if (isNovelArticleCreated()) return getTextLinesByNovelArticles() return getTextLinesByPlainText() } function Pixiv() {} Pixiv.prototype = Object.create(Site.prototype) Pixiv.prototype.is = function(location) { return location.hostname === 'www.pixiv.net' && location.pathname === '/novel/show.php' && hasParam(location, 'id', /\d+/) } Pixiv.prototype.parse = function() { return new Episode({ title: getTitle() , author: getLine('.user') , text: getTextLines() , nextURL: getHRef('.before a') , prevURL: getHRef('.after a') , preface: getLines('.work-info .caption') }) } Pixiv.prototype.addButton = function() { document.querySelector(titleSelector).appendChild(this.newButton()) } return Pixiv })() var Akatsuki = (function() { function selector(suffix) { return '#contents-inner2 > div.box.story > div.box.story ' + suffix } function getH2() { var h2 = document.querySelector(selector('h2')) var s = '' for (var n = h2.firstChild; n !== h2.lastChild; n = n.nextSibling) { if (n.tagName === 'BR') s += ' ' else s += n.textContent } return { title: Line.parse([h2.lastChild]) , chapterTitle: s ? Line.newByStr(s.trim()) : null } } function href(selector) { var a = document.querySelector(selector) return a ? a.href : '' } function getNovelTitle() { var h1 = document.querySelector(selector('h1')) return Line.newByStr(h1.firstChild.textContent) } function getUnbalancedContent(nodeList) { if (nodeList[0].previousSibling.textContent === '前書き') { return { preface: Lines.parse(nodeList[0].childNodes) , text: Lines.parse(nodeList[1].childNodes) , postscript: null } } return { preface: null , text: Lines.parse(nodeList[0].childNodes) , postscript: Lines.parse(nodeList[1].childNodes) } } function getContent() { var nl = document.querySelectorAll(selector('.body-novel')) switch (nl.length) { case 1: return { text: Lines.parse(nl[0].childNodes) , preface: null , postscript: null } case 2: return getUnbalancedContent(nl) case 3: return { text: Lines.parse(nl[1].childNodes) , preface: Lines.parse(nl[0].childNodes) , postscript: Lines.parse(nl[2].childNodes) } default: throw new Error(nl.length) } } function Akatsuki() {} Akatsuki.prototype = Object.create(Site.prototype) Akatsuki.prototype.is = function(location) { return location.hostname === 'www.akatsuki-novels.com' && /\/stories\/view\/\d+\/novel_id~\d+/.test(location.pathname) } Akatsuki.prototype.parse = function() { var h2 = getH2(), c = getContent() return new Episode({ novelTitle: getNovelTitle() , chapterTitle: h2.chapterTitle , title: h2.title , author: getLine(selector('a[href^="/users/view/"]')) , text: c.text , preface: c.preface , postscript: c.postscript , nextURL: href(selector('.paging_for_view .next a')) , prevURL: href(selector('.paging_for_view .prev a')) }) } Akatsuki.prototype.addButton = function() { document.querySelector(selector('h1')).appendChild(this.newButton()) } return Akatsuki })() var Book = (function() { function newPageLines(lines, lineNum) { var result = [] for (var i = 0, n = lines.length; i < n; i += lineNum) { result.push(lines.slice(i, i + lineNum)) } return result } function joinTopPageItems(episode) { return [ episode.novelTitle , episode.chapterTitle , episode.title , episode.author ].filter(function(line) { return line !== null }) } function RangedLine(begin, pageLines) { this.begin = begin this.end = begin + pageLines.length this.pageLines = Object.freeze(pageLines.slice()) Object.freeze(this) } RangedLine.prototype.contain = function(pageIndex) { return this.begin <= pageIndex && pageIndex < this.end } RangedLine.prototype.getLines = function(pageIndex) { return this.pageLines[pageIndex - this.begin] } function Book(episode) { this.episode = episode this.topLines = joinTopPageItems(episode) this.prefaceLines = episode.preface.length ? [ Line.newByStr('まえがき') , Line.EMPTY ].concat(episode.preface) : [] this.postscriptLines = episode.postscript.length ? [ Line.newByStr('あとがき') , Line.EMPTY ].concat(episode.postscript) : [] this.lineCharNum = GM_getLineCharNum() this.lineNum = GM_getLineNum() this.rangedLines = this.newRangedLines() this.pageNum = this.rangedLines[this.rangedLines.length - 1].end this.pageIndex = 0 this.view = null Object.seal(this) } Book.prototype.setView = function(view) { this.view = view view.update() } Book.prototype.newRangedLine = function(begin, lines) { return new RangedLine(begin , newPageLines(Lines.split(lines, this.lineCharNum) , this.lineNum)) } Book.prototype.newRangedLines = function() { var top = this.newRangedLine(0, this.topLines) var preface = this.newRangedLine(top.end, this.prefaceLines) var text = this.newRangedLine(preface.end, this.episode.text) var postscript = this.newRangedLine(text.end, this.postscriptLines) return [top, preface, text, postscript] } Book.prototype.update = function() { this.rangedLines = this.newRangedLines() this.pageNum = this.rangedLines[this.rangedLines.length - 1].end this.pageIndex = Math.min(this.pageIndex, this.pageNum - 1) this.view.update() } Book.prototype.setLineCharNum = function(lineCharNum) { var n = Math.max(lineCharNum, 1) if (this.lineCharNum === n) return this.lineCharNum = n this.update() } Book.prototype.setLineNum = function(lineNum) { var n = Math.max(lineNum, 1) if (this.lineNum === n) return this.lineNum = n this.update() } Book.prototype.getRangedLine = function() { var rls = this.rangedLines for (var i = 0, n = rls.length; i < n; i++) { var rl = rls[i] if (rl.contain(this.pageIndex)) return rl } throw new Error(this.pageIndex) } Book.prototype.setPageIndex = function(pageIndex) { var i = Math.min(Math.max(pageIndex, 0), this.pageNum - 1) if (this.pageIndex === i) return this.pageIndex = i this.view.update() } Book.prototype.turnPage = function() { this.setPageIndex(this.pageIndex + 1) } Book.prototype.returnPage = function() { this.setPageIndex(this.pageIndex - 1) } Book.prototype.begin = function() { this.setPageIndex(0) } Book.prototype.end = function() { this.setPageIndex(this.pageNum - 1) } Book.prototype.loadNextEpisode = function() { var u = this.episode.nextURL if (!u) return _setValue('auto', true) window.location.assign(u) } Book.prototype.loadPrevEpisode = function() { var u = this.episode.prevURL if (!u) return _setValue('auto', true) window.location.assign(u) } Book.prototype.isTop = function() { return this.pageIndex === 0 } Book.prototype.isLast = function() { return this.pageIndex === this.pageNum - 1 } Book.prototype.getLines = function() { return this.getRangedLine().getLines(this.pageIndex) } return Book })() var Key = { ESCAPE: 27 , SPACE: 32 , END: 35 , HOME: 36 , LEFT: 37 , UP: 38 , RIGHT: 39 , DOWN: 40 , E: 69 , H: 72 , N: 78 , P: 80 , SLASH: 191 } function KeyMap() { this.entries = [] this.keyDownListener = this.keyDowned.bind(this) Object.freeze(this) } KeyMap.prototype.add = function(key, action) { var entry = { action: action } if (typeof(key) === 'object') { entry.keyCode = key.keyCode entry.shift = key.shift } else { entry.keyCode = key entry.shift = false } this.entries.push(entry) } KeyMap.prototype.keyDowned = function(keyEvent) { if (keyEvent.target.tagName === 'SELECT') return this.entries.filter(function(entry) { return !keyEvent.altKey && !keyEvent.ctrlKey && !keyEvent.metaKey && keyEvent.shiftKey === entry.shift && keyEvent.keyCode === entry.keyCode }).forEach(function(entry) { keyEvent.stopImmediatePropagation() entry.action() }) } var BookView = (function() { function newLineBox(doc) { var result = (doc || document).createElement('div') result.style.marginTop = GM_getMarginTop() result.style.lineHeight = GM_getCharHeight() result.style.fontSize = GM_getFontSize() result.style.fontWeight = GM_getFontWeight() result.style.display = 'inline-block' result.style.fontFamily = GM_getFontFamily() return result } function newRoot(lineBox, doc) { var result = (doc || document).createElement('div') result.style.position = 'fixed' result.style.top = '0px' result.style.left = '0px' result.style.width = '100%' result.style.height = '100%' result.style.backgroundColor = GM_getBackgroundColor() result.style.color = GM_getColor() result.style.textAlign = 'center' result.appendChild(lineBox) return result } function newNumElem(doc) { var result = (doc || document).createElement('span') result.textContent = '0' result.style.display = 'inline-block' result.style.width = '3em' return result } function newPageIndexElem(doc) { var result = newNumElem(doc) result.style.textAlign = 'right' return result } function newPageNumElem(doc) { var result = newNumElem(doc) result.style.textAlign = 'left' return result } var newButton = (function() { var preventFocus = function(e) { e.target.blur() } return function(textContent, clickListener, title, doc) { var result = (doc || document).createElement('button') result.textContent = textContent result.addEventListener('click', clickListener, false) result.addEventListener('focus', preventFocus, false) if (title) result.title = title result.style.width = '3em' result.style.padding = '0px' return result } })() function setSpaceBetweenLines(lineBox, spaceBetweenLines) { var sp = spaceBetweenLines || GM_getSpaceBetweenLines() var cn = lineBox.childNodes for (var i = 0, n = cn.length - 1; i < n; i++) { cn[i].style.marginLeft = sp } } function BookView(book, doc) { this.book = book this.doc = doc || document this.lineBox = newLineBox(this.doc) this.root = newRoot(this.lineBox, this.doc) this.pageIndexElem = newPageIndexElem(this.doc) this.pageNumElem = newPageNumElem(this.doc) this.turnPageButton = newButton('<' , book.turnPage.bind(book) , '次のページ' , this.doc) this.returnPageButton = newButton('>' , book.returnPage.bind(book) , '前のページ' , this.doc) this.endButton = newButton('|<' , book.end.bind(book) , '最後のページ' , this.doc) this.beginButton = newButton('>|' , book.begin.bind(book) , '最初のページ' , this.doc) this.loadNextEpisodeButton = newButton('<<' , book.loadNextEpisode.bind(book) , '次の話' , this.doc) this.loadPrevEpisodeButton = newButton('>>' , book.loadPrevEpisode.bind(book) , '前の話' , this.doc) this.listShortcutKeysButton = newButton('?' , this.listShortcutKeys.bind(this) , 'ショートカットキー一覧' , this.doc) this.showConfigDialogButton = newButton('設定' , this.showConfigDialog.bind(this) , this.doc) this.toolbar = this.newToolbar() this.keyDownListener = this.newKeyDownListener() this.wheelListener = this.wheeled.bind(this) this.mouseMoveListener = this.mouseMoved.bind(this) this.shortcutKeyList = null this.configDialog = null this.onhide = null Object.seal(this) } BookView.prototype.newNavigator = function() { var result = this.doc.createElement('span') result.appendChild(this.loadNextEpisodeButton) result.appendChild(this.endButton) result.appendChild(this.turnPageButton) result.appendChild(this.pageIndexElem) result.appendChild(this.doc.createTextNode('/')) result.appendChild(this.pageNumElem) result.appendChild(this.returnPageButton) result.appendChild(this.beginButton) result.appendChild(this.loadPrevEpisodeButton) return result } BookView.prototype.newRightBottomBox = function() { var result = this.doc.createElement('div') result.style.position = 'absolute' result.style.right = '0px' result.style.bottom = '0px' result.appendChild(this.listShortcutKeysButton) result.appendChild(this.showConfigDialogButton) result.appendChild(newButton('X' , this.hide.bind(this) , '閉じる' , this.doc)) return result } BookView.prototype.newToolbar = function() { var result = this.doc.createElement('div') result.style.position = 'absolute' result.style.left = '0px' result.style.bottom = '0px' result.style.width = '100%' result.style.fontFamily = Default.GOTHIC_FONT_FAMILY result.style.fontSize = '16px' result.style.backgroundColor = GM_getBackgroundColor() result.style.display = GM_isToolbarVisible() ? '' : 'none' result.appendChild(this.newNavigator()) result.appendChild(this.newRightBottomBox()) this.root.appendChild(result) return result } BookView.prototype.escKeyDowned = function() { if (this.shortcutKeyList) this.shortcutKeyList.hide() else if (this.configDialog) this.configDialog.hide() else this.hide() } BookView.prototype.newKeyDownListener = function() { var b = this.book var km = new KeyMap() km.add(Key.SPACE, b.turnPage.bind(b)) km.add(Key.LEFT, b.turnPage.bind(b)) km.add(Key.RIGHT, b.returnPage.bind(b)) km.add({ keyCode: Key.SPACE, shift: true }, b.returnPage.bind(b)) km.add(Key.H, b.begin.bind(b)) km.add(Key.HOME, b.begin.bind(b)) km.add(Key.E, b.end.bind(b)) km.add(Key.END, b.end.bind(b)) km.add(Key.N, b.loadNextEpisode.bind(b)) km.add(Key.DOWN, b.loadNextEpisode.bind(b)) km.add(Key.P, b.loadPrevEpisode.bind(b)) km.add(Key.UP, b.loadPrevEpisode.bind(b)) km.add(Key.ESCAPE, this.escKeyDowned.bind(this)) km.add({ keyCode: Key.SLASH, shift: true } , this.listShortcutKeys.bind(this)) return km.keyDownListener } BookView.prototype.addListeners = function() { this.doc.addEventListener('keydown', this.keyDownListener, true) this.doc.addEventListener('mousemove', this.mouseMoveListener, false) this.doc.addEventListener('wheel', this.wheelListener, false) } BookView.prototype.removeListeners = function() { this.doc.removeEventListener('keydown', this.keyDownListener, true) this.doc.removeEventListener('mousemove', this.mouseMoveListener, false) this.doc.removeEventListener('wheel', this.wheelListener, false) } BookView.prototype.show = function() { this.doc.body.appendChild(this.root) this.addListeners() } BookView.prototype.hide = function() { this.removeListeners() this.root.parentNode.removeChild(this.root) if (this.onhide) this.onhide() } BookView.prototype.clear = function() { var b = this.lineBox while (b.hasChildNodes()) b.removeChild(b.firstChild) } BookView.prototype.padLines = function() { var paddingLineNum = this.book.lineNum - this.book.getLines().length for (var i = 0; i < paddingLineNum; i++) { this.lineBox.appendChild(Line.EMPTY.newNode(this.doc)) } } BookView.prototype.writeLines = function() { this.book.getLines().forEach(function(line) { this.lineBox.appendChild(line.newNode(this.doc)) }, this) } BookView.prototype.updateButtonDisabled = function() { var b = this.book this.loadNextEpisodeButton.disabled = !b.episode.nextURL this.loadPrevEpisodeButton.disabled = !b.episode.prevURL this.turnPageButton.disabled = b.isLast() this.endButton.disabled = b.isLast() this.returnPageButton.disabled = b.isTop() this.beginButton.disabled = b.isTop() } BookView.prototype.updatePageIndexAndPageNum = function() { this.pageIndexElem.textContent = this.book.pageIndex + 1 this.pageNumElem.textContent = this.book.pageNum } BookView.prototype.update = function() { this.clear() this.writeLines() if (!this.book.isTop()) this.padLines() setSpaceBetweenLines(this.lineBox) this.updateButtonDisabled() this.updatePageIndexAndPageNum() } BookView.prototype.wheeled = (function() { function convert(e) { if (e.deltaY) return { down: e.deltaY > 0, up: e.deltaY < 0 } return e.wheelDelta ? { down: e.wheelDelta < 0, up: e.wheelDelta > 0 } : null } return function(e) { var wheel = convert(e) if (!wheel) return if (wheel.down) this.book.turnPage() else if (wheel.up) this.book.returnPage() } })() BookView.prototype.showConfigDialog = function() { if (this.configDialog) return this.showConfigDialogButton.disabled = true this.configDialog = new ConfigDialog(this.book, this, this.doc) this.configDialog.onhide = (function() { this.configDialog = null this.showConfigDialogButton.disabled = false }).bind(this) this.configDialog.show(this.root) } BookView.prototype.listShortcutKeys = function() { if (this.shortcutKeyList) return this.listShortcutKeysButton.disabled = true this.shortcutKeyList = new ShortcutKeyList(this.doc) this.shortcutKeyList.onhide = (function() { this.shortcutKeyList = null this.listShortcutKeysButton.disabled = false }).bind(this) this.shortcutKeyList.show(this.root) } BookView.prototype.setFontType = function(fontType) { this.lineBox.style.fontFamily = GM_getFontFamily(fontType) } BookView.prototype.setFontFamily = function(fontFamily) { this.lineBox.style.fontFamily = fontFamily } BookView.prototype.setFontSize = function(fontSize) { this.lineBox.style.fontSize = fontSize } BookView.prototype.setCharHeight = function(charHeight) { this.lineBox.style.lineHeight = charHeight } BookView.prototype.setMarginTop = function(marginTop) { this.lineBox.style.marginTop = marginTop } BookView.prototype.setSpaceBetweenLines = function(spaceBetweenLines) { setSpaceBetweenLines(this.lineBox, spaceBetweenLines) } BookView.prototype.setColor = function(color) { this.root.style.color = color } BookView.prototype.setBackgroundColor = function(backgroundColor) { this.root.style.backgroundColor = backgroundColor this.toolbar.style.backgroundColor = backgroundColor } BookView.prototype.setToolbarVisible = function(visible) { this.toolbar.style.display = visible ? '' : 'none' } BookView.prototype.setFontWeight = function(fontWeight) { this.lineBox.style.fontWeight = fontWeight } BookView.prototype.mouseMoved = (function() { var toolbarVisibleToggleHeight = 50 function onToolbarVisibleArea(clientY) { return clientY >= window.innerHeight - toolbarVisibleToggleHeight } return function(mouseEvent) { if (GM_isToolbarVisible()) return this.toolbar.style.display = onToolbarVisibleArea(mouseEvent.clientY) ? '' : 'none' } })() return BookView })() function Dialog(doc) { this.doc = doc || document this.root = this.newRoot() this.onhide = null } Dialog.prototype.newRoot = function() { var result = this.doc.createElement('div') result.style.position = 'absolute' result.style.fontFamily = Default.GOTHIC_FONT_FAMILY result.style.fontSize = '16px' result.style.backgroundColor = 'white' result.style.color = 'black' result.style.padding = '0px 5px 5px' result.appendChild(this.newTopBar()) return result } Dialog.prototype.newTopBar = function() { var result = this.doc.createElement('div') result.style.textAlign = 'right' result.appendChild(this.newCloseButton()) return result } Dialog.prototype.newCloseButton = function() { var result = this.doc.createElement('button') result.textContent = 'X' result.style.width = '3em' result.addEventListener('click', this.hide.bind(this), false) return result } Dialog.prototype.center = function() { var v = this.doc.defaultView || document.defaultView var cs = v.getComputedStyle(this.root) var h = parseInt(cs.height, 10) var w = parseInt(cs.width, 10) this.root.style.top = ((v.innerHeight - h) / 2) + 'px' this.root.style.left = ((v.innerWidth - w) / 2) + 'px' } Dialog.prototype.show = function(owner) { owner.appendChild(this.root) this.center() } Dialog.prototype.hide = function() { this.root.parentNode.removeChild(this.root) if (this.onhide) this.onhide() } var ShortcutKeyList = (function() { function newList(doc) { var result = (doc || document).createElement('table') result.style.textAlign = 'left' result.style.borderCollapse = 'collapse' result.style.color = 'black' ;[ ['←, スペース', '次のページ'] , ['→, Shift+スペース', '前のページ'] , ['End, e', '最後のページ'] , ['Home, h', '最初のページ'] , ['↓, n', '次の話'] , ['↑, p', '前の話'] , ['?', 'ショートカットキー一覧'] , ['Esc', '閉じる'] ].forEach(function(e) { var row = result.insertRow(-1) insertCell(row, e[0]) insertCell(row, e[1]) }) return result } function ShortcutKeyList(doc) { Dialog.call(this, doc) this.root.appendChild(newList(doc)) Object.seal(this) } ShortcutKeyList.prototype = Object.create(Dialog.prototype) return ShortcutKeyList })() var ConfigDialog = (function() { function newColorCellChild(doc) { var colorBox = (doc || document).createElement('span') colorBox.innerHTML = ' ' colorBox.style.display = 'inline-block' colorBox.style.width = '1em' colorBox.style.height = '100%' colorBox.style.marginRight = '8px' colorBox.style.border = '1px solid' var result = (doc || document).createDocumentFragment() result.appendChild(colorBox) result.appendChild(newTextNode('')) return result } function setColorCell(cell, color) { cell.firstChild.style.backgroundColor = color cell.lastChild.nodeValue = color } function newRadio(value, clickListener, doc) { var result = (doc || document).createElement('input') result.type = 'radio' result.name = 'font-type' result.value = value result.addEventListener('click', clickListener, false) return result } function newLabel(inputElem, text, doc) { var result = (doc || document).createElement('label') result.appendChild(inputElem) result.appendChild(newTextNode(text)) return result } function newButton(clickListener, textContent, doc) { var result = (doc || document).createElement('button') result.textContent = textContent || '変更' result.style.padding = '0px 6px' result.addEventListener('click', clickListener, false) return result } function newCheckbox(clickListener, doc) { var result = (doc || document).createElement('input') result.type = 'checkbox' result.addEventListener('click', clickListener, false) return result } var promptUntilValid = (function() { function isValidCssPropertyValue(propertyName, propertyValue) { var e = document.createElement('span') e.style.setProperty(propertyName, propertyValue, null) return !!e.style.getPropertyValue(propertyName) } function errorMessage(propertyName) { return 'CSS ' + propertyName + ' プロパティに有効な値を入力して下さい\n' } return function(propertyName, message, initValue) { var r = null do { r = window.prompt((r ? errorMessage(propertyName) : '') + message , r ? r : initValue) if (!r) return } while (!isValidCssPropertyValue(propertyName, r)) return r } })() function promptInteger(message, initValue) { var r = parseInt(window.prompt(message, initValue), 10) return isNaN(r) ? null : r } function newDiv(doc) { var result = (doc || document).createElement('div') result.style.textAlign = 'left' result.style.marginTop = '5px' return result } function newOption(text, doc) { var result = (doc || document).createElement('option') result.text = text return result } function ConfigDialog(book, bookView, doc) { Dialog.call(this, doc) this.book = book this.bookView = bookView this.gothicRadio = newRadio('gothic' , this.fontTypeRadioClicked.bind(this) , this.doc) this.minchoRadio = newRadio('mincho' , this.fontTypeRadioClicked.bind(this) , this.doc) this.fontWeightSelect = this.newFontWeightSelect() this.toolbarVisibleCheckbox = this.newToolbarVisibleCheckbox() this.autoVerticalWritingCheckbox = this.newAutoVerticalWritingCheckbox() this.root.appendChild(this.newConfigTable()) this.root.appendChild(this.newToolbarVisibleDiv()) this.root.appendChild(this.newAutoVerticalWritingDiv()) this.root.appendChild(this.newConfigClearDiv()) this.setConfigValues() Object.seal(this) } ConfigDialog.prototype = Object.create(Dialog.prototype) ConfigDialog.prototype.newToolbarVisibleCheckbox = function() { var clickListener = (function(e) { this.bookView.setToolbarVisible(e.target.checked) _setValue('toolbarVisible', e.target.checked) }).bind(this) return newCheckbox(clickListener, this.doc) } ConfigDialog.prototype.newAutoVerticalWritingCheckbox = function() { var clickListener = (function(e) { _setValue('autoVerticalWriting', e.target.checked) }).bind(this) return newCheckbox(clickListener, this.doc) } ConfigDialog.prototype.newToolbarVisibleDiv = function() { var result = newDiv() result.appendChild(newLabel(this.toolbarVisibleCheckbox , 'ツールバーを常に表示する' , this.doc)) return result } ConfigDialog.prototype.newAutoVerticalWritingDiv = function() { var result = newDiv() result.appendChild(newLabel(this.autoVerticalWritingCheckbox , '自動で縦書き表示にする' , this.doc)) return result } ConfigDialog.prototype.newFontWeightSelect = function() { var result = this.doc.createElement('select') result.appendChild(newOption('normal', this.doc)) result.appendChild(newOption('bold', this.doc)) for (var i = 1; i <= 9; i++) { result.appendChild(newOption(i + '00', this.doc)) } result.value = GM_getFontWeight() result.addEventListener('change' , this.fontWeightSelectChanged.bind(this)) return result } ConfigDialog.prototype.fontWeightSelectChanged = function() { var v = this.fontWeightSelect.value _setValue('fontWeight', v) this.bookView.setFontWeight(v) pixelCharSize.clearCache() this.book.update() } ConfigDialog.prototype.insertFontRows = function(table) { var gothicRow = table.insertRow(-1) insertCell(gothicRow, 'フォント').rowSpan = 2 insertCell(gothicRow, newLabel(this.gothicRadio, 'ゴシック', this.doc)) insertCell(gothicRow , newButton(this.gothicFontEditButtonClicked.bind(this) , null , this.doc)) var minchoRow = table.insertRow(-1) insertCell(minchoRow, newLabel(this.minchoRadio, '明朝', this.doc)) insertCell(minchoRow , newButton(this.minchoFontEditButtonClicked.bind(this) , null , this.doc)) } ConfigDialog.prototype.insertRow = function(table , configName , configValueCellName , clickListener , valueCellChild) { var row = table.insertRow(-1) insertCell(row, configName) this[configValueCellName] = insertCell(row, valueCellChild) insertCell(row, newButton(clickListener, null, this.doc)) } ConfigDialog.prototype.insertFontWeightRow = function(table) { var row = table.insertRow(-1) insertCell(row, '文字の太さ') var c = insertCell(row, this.fontWeightSelect) c.colSpan = 2 } ConfigDialog.prototype.newConfigTable = function() { var result = this.doc.createElement('table') result.style.textAlign = 'left' result.style.borderCollapse = 'collapse' result.style.color = 'black' this.insertFontRows(result) this.insertRow(result , 'フォントサイズ' , 'fontSizeCell' , this.fontSizeEditButtonClicked.bind(this)) this.insertRow(result , '文字の高さ' , 'charHeightCell' , this.charHeightEditButtonClicked.bind(this)) this.insertFontWeightRow(result) this.insertRow(result , '行数' , 'lineNumCell' , this.lineNumEditButtonClicked.bind(this)) this.insertRow(result , '一行の文字数' , 'lineCharNumCell' , this.lineCharNumEditButtonClicked.bind(this)) this.insertRow(result , '上余白' , 'marginTopCell' , this.marginTopEditButtonClicked.bind(this)) this.insertRow(result , '行間' , 'spaceBetweenLinesCell' , this.spaceBetweenLinesEditButtonClicked.bind(this)) this.insertRow(result , '文字色' , 'colorCell' , this.colorEditButtonClicked.bind(this) , newColorCellChild()) this.insertRow(result , '背景色' , 'backgroundColorCell' , this.backgroundColorEditButtonClicked.bind(this) , newColorCellChild()) return result } ConfigDialog.prototype.newConfigClearDiv = function() { var result = newDiv() result.appendChild(newButton(this.clearConfigButtonClicked.bind(this) , '初期設定に戻す' , this.doc)) return result } ConfigDialog.prototype.setConfigValues = function() { this.gothicRadio.checked = GM_getFontType() === 'gothic' this.minchoRadio.checked = GM_getFontType() === 'mincho' this.fontSizeCell.textContent = GM_getFontSize() this.charHeightCell.textContent = GM_getCharHeight() this.lineNumCell.textContent = GM_getLineNum() this.lineCharNumCell.textContent = GM_getLineCharNum() this.marginTopCell.textContent = GM_getMarginTop() this.spaceBetweenLinesCell.textContent = GM_getSpaceBetweenLines() setColorCell(this.colorCell, GM_getColor()) setColorCell(this.backgroundColorCell, GM_getBackgroundColor()) this.toolbarVisibleCheckbox.checked = GM_isToolbarVisible() this.autoVerticalWritingCheckbox.checked = GM_isAutoVerticalWriting() this.fontWeightSelect.value = GM_getFontWeight() } ConfigDialog.prototype.notifyConfigCleared = function() { this.book.setLineNum(GM_getLineNum()) this.book.setLineCharNum(GM_getLineCharNum()) this.bookView.setFontType(GM_getFontType()) this.bookView.setFontSize(GM_getFontSize()) this.bookView.setCharHeight(GM_getCharHeight()) this.bookView.setMarginTop(GM_getMarginTop()) this.bookView.setSpaceBetweenLines(GM_getSpaceBetweenLines()) this.bookView.setColor(GM_getColor()) this.bookView.setBackgroundColor(GM_getBackgroundColor()) this.bookView.setToolbarVisible(GM_isToolbarVisible()) this.bookView.setFontWeight(GM_getFontWeight()) } ConfigDialog.prototype.clearConfigButtonClicked = function() { GM_clear() this.setConfigValues() this.notifyConfigCleared() pixelCharSize.clearCache() this.book.update() } ConfigDialog.prototype.fontTypeRadioClicked = function() { ;[this.gothicRadio, this.minchoRadio].forEach(function(e) { if (!e.checked) return this.bookView.setFontType(e.value) _setValue('fontType', e.value) pixelCharSize.clearCache() this.book.update() }, this) } ConfigDialog.prototype.gothicFontEditButtonClicked = function() { var r = promptUntilValid('font-family' , 'ゴシックフォント' , GM_getGothicFontFamily()) if (!r) return _setValue('gothicFontFamily', r) if (GM_getFontType() !== 'gothic') return this.bookView.setFontFamily(r) pixelCharSize.clearCache() this.book.update() } ConfigDialog.prototype.minchoFontEditButtonClicked = function() { var r = promptUntilValid('font-family' , '明朝フォント' , GM_getMinchoFontFamily()) if (!r) return _setValue('minchoFontFamily', r) if (GM_getFontType() !== 'mincho') return this.bookView.setFontFamily(r) pixelCharSize.clearCache() this.book.update() } ConfigDialog.prototype.fontSizeEditButtonClicked = function() { var r = promptUntilValid('font-size', 'フォントサイズ', GM_getFontSize()) if (!r) return this.bookView.setFontSize(r) this.fontSizeCell.textContent = r _setValue('fontSize', r) pixelCharSize.clearCache() this.book.update() } ConfigDialog.prototype.charHeightEditButtonClicked = function() { var r = promptUntilValid('line-height', '文字の高さ', GM_getCharHeight()) if (!r) return this.bookView.setCharHeight(r) this.charHeightCell.textContent = r _setValue('charHeight', r) pixelCharSize.clearCache() this.book.update() } ConfigDialog.prototype.lineNumEditButtonClicked = function() { var r = promptInteger('行数', GM_getLineNum()) if (r === null) return this.book.setLineNum(r) this.lineNumCell.textContent = this.book.lineNum _setValue('lineNum', this.book.lineNum) } ConfigDialog.prototype.lineCharNumEditButtonClicked = function() { var r = promptInteger('一行の文字数', GM_getLineCharNum()) if (r === null) return this.book.setLineCharNum(r) this.lineCharNumCell.textContent = this.book.lineCharNum _setValue('lineCharNum', this.book.lineCharNum) } ConfigDialog.prototype.marginTopEditButtonClicked = function() { var r = promptUntilValid('margin-top', '上余白', GM_getMarginTop()) if (!r) return this.bookView.setMarginTop(r) this.marginTopCell.textContent = r _setValue('marginTop', r) } ConfigDialog.prototype.spaceBetweenLinesEditButtonClicked = function() { var r = promptUntilValid('margin-left' , '行間' , GM_getSpaceBetweenLines()) if (!r) return this.bookView.setSpaceBetweenLines(r) this.spaceBetweenLinesCell.textContent = r _setValue('spaceBetweenLines', r) } ConfigDialog.prototype.colorEditButtonClicked = function() { var r = promptUntilValid('color', '文字色', GM_getColor()) if (!r) return this.bookView.setColor(r) setColorCell(this.colorCell, r) _setValue('color', r) } ConfigDialog.prototype.backgroundColorEditButtonClicked = function() { var r = promptUntilValid('background-color' , '背景色' , GM_getBackgroundColor()) if (!r) return this.bookView.setBackgroundColor(r) setColorCell(this.backgroundColorCell, r) _setValue('backgroundColor', r) } return ConfigDialog })() function main() { ;[ new Narou() , new Arcadia() , new Hameln() , new Pixiv() , new Akatsuki() ].forEach(function(site) { if (!site.is(window.location)) return site.addButton() if (!(GM_isAutoVerticalWriting() || _getValue('auto', false))) return _setValue('auto', false) site.showBookView() }) } main() })()