ficUpdate

bulk copy-paste all the chapters from Word on ficbook

当前为 2022-01-31 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ficUpdate
  3. // @namespace ficscript
  4. // @version 3.0.0
  5. // @description bulk copy-paste all the chapters from Word on ficbook
  6. // @author Dimava
  7. // @license MIT
  8. // @match https://ficbook.net/home/myfics*
  9. // @grant none
  10. // @require https://greasyfork.org/scripts/439153-poopjs/code/PoopJs.js?version=1012736
  11. // ==/UserScript==
  12.  
  13. FicUpdate = class FicUpdate {
  14. debug = true;
  15. strings = {
  16. infoRoot: 'ⓘFicUpdate: Для обновления глав перейдите на страницу одного из фанфиков',
  17. infoEditor: 'ⓘFicUpdate: Скопируйте текст сюда',
  18. style: `
  19. .fu-container{display:grid;grid-template-areas: "buttons buttons" "infoEditor infoPrepared" "editor prepared";grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);overflow:hidden;}
  20. .fu-editor{background:white;border:1px dotted gray; grid-area:editor;}
  21. .fu-prepared{background:hsl(0,0%,95%);border:1px dotted gray;grid-area:prepared;}
  22. .fu-infoEditor{grid-area:infoEditor;}
  23. .fu-buttons{grid-area: buttons;}
  24. .fu-summary-buttons{display:inline-block;}
  25. .fu-rotate{animation:anim-fu-rotate 1s linear infinite;display:inline-block;}
  26. @keyframes anim-fu-rotate{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
  27. `,
  28. errNoHeaders: 'ⓘFicUpdate: Текст не содержит заголовков глав! (<H1>)',
  29. sucPrepared: 'ⓘFicUpdate: Текст подготовлен',
  30. infoStep: `ⓘFicUpdate: Преобразование текста`,
  31. infoPasted: `ⓘFicUpdate: Нажмите на эту кнопку чтобы подготовить текст`,
  32. statusTextSame: `ⓘтекст совпадает`,
  33. statusTextFromTo: (a, b) => `${a}ch${b}ch${b > a ? '' : ` (-${a - b})`}`,
  34. chReload: '⟳',
  35. chUpload: '⇑',
  36. chDownload: '⇓',
  37. };
  38. els = {};
  39.  
  40.  
  41. elm(s, ...a) {
  42. return elm(s+'.fic-update', ...a);
  43. }
  44. elmake(key, ...a) {
  45. if (key.startsWith('.fu-')) {
  46. return this.els[key.match(/\w{3,}/)[0]] = elm(key + '.fic-update', ...a);
  47. } else {
  48. return this.els[key] = elm(a[0] + '.fic-update', ...a.slice(1));
  49. }
  50. }
  51. constructor() {
  52. if (!location.pathname.startsWith('/home/myfics')) {
  53. return;
  54. }
  55. __init__;
  56. if (location.pathname == '/home/myfics') {
  57. this.elmake('.fu-infoRoot.btn.btn-info', this.strings.infoRoot);
  58. q('h1').after(this.els.infoRoot);
  59. return;
  60. }
  61. const els = this.els;
  62.  
  63. this.elmake('.fu-container');
  64. q('.myfic').after(els.container);
  65. this.elmake('style', 'style').appendTo('head').innerHTML = this.strings.style;
  66.  
  67.  
  68. this.elmake('.fu-buttons').appendTo(els.container);
  69. this.elmake('.fu-infoEditor.btn.btn-info', this.strings.infoEditor
  70. , click=>this.prepareText()
  71. , paste=>this.onpaste).appendTo(els.container);
  72. this.elmake('.fu-editor').appendTo(els.container);
  73. els.editor.contentEditable = true;
  74. this.elmake('.fu-prepared').appendTo(els.container);
  75. }
  76. onpaste() {
  77. this.els.infoEditor.innerText = this.strings.infoPasted;
  78. }
  79. remove() {
  80. qq('.fic-update').map(e=>e.remove());
  81. }
  82.  
  83.  
  84. prepareTextStep(id, f) {
  85. let before = this.text;
  86. let after = f(before);
  87. this._textSteps[id] = {before, after};
  88. return this.text = after;
  89. }
  90.  
  91. prepareText() {
  92. this.text = this.els.editor.innerHTML;
  93. this._textSteps = {init: this.text};
  94. this.refs = {};
  95.  
  96. if (!this.text.match(/<h1/)) {
  97. this.els.infoEditor.innerText = this.strings.errNoHeaders;
  98. this.els.infoEditor.classList.toggle('btn-warning', true);
  99. return;
  100. }
  101. this.prepareTextStep('extractFootnotes1', t => {
  102. return t.replace(/<a\s+[^>]*name="_ftn[^]*?a>/g, s=>{
  103. let refm = s.match(/_ftn(ref)?(\d+)/);
  104. let refn = +refm[2];
  105. if (refm[1]) {
  106. refs[refn] = {
  107. n: refn,
  108. t: this.tosupnum(refn),
  109. };
  110. }
  111. return refm[1] ? '' : this.tosupnum(refn);
  112. });
  113. });
  114.  
  115. this.prepareTextStep('extractFootnotes2', t => {
  116. return t.replace(/<div id="ftn(\d+)"[^]*?div>/g, (s,n)=>{
  117. this.refs[n].s = this.htmlToText(s).trim();
  118. return '';
  119. });
  120. });
  121.  
  122. this.prepareTextStep('removeBadTags', t => {
  123. return t.replace(/<(?!\/?(h1|br|p|b|s|i|center|right)[\s|>])[^>]*>/g, '');
  124. });
  125. this.prepareTextStep('removeAttributes', t => {
  126. return t.replace(/<(\/?)(h1|br|b|s|i|center|right)(?=[\s|>])[^>]*>/g, '<$1$2>');
  127. });
  128. this.prepareTextStep('split', t => {
  129. return this.parts = t.split(/<h1[^>]*>/).map(e=>e.split('</h1>')).slice(1).map(([name, text])=>{
  130. name = this.htmlToText(name).replace(/\s+/g, ' ').trim()
  131. text = this.tabber(text);
  132. let o = {
  133. name,
  134. text,
  135. com: '',
  136. comp: true,
  137. refs: [],
  138. };
  139. text = text.replace(/\s*\/\*\s*([^]*?)\s*\*\/\s*/, (s,a,i,t)=>{
  140. o.com = a;
  141. o.comp = i > 100;
  142. return '';
  143. });
  144. let supi = 1;
  145. text = text.replace(/[⁰¹²³⁴⁵⁶⁷⁸⁹]+/g, (s,i,t)=>{
  146. let n = fromsupnum(s);
  147. let ref = refs[n];
  148. ref.n = supi;
  149. ref.t = tosupnum(supi);
  150. o.refs.push(ref);
  151. supi++;
  152. return ref.t;
  153. });
  154. o.text = text;
  155. return o;
  156. });
  157. });
  158. this.prepareTextStep('join', t => {
  159. return t
  160. .map(p => {
  161. let t = p.text.replace(/\n/g, '\n<br>');
  162. if (p.com || p.refs.length) {
  163. let com = p.com;
  164. let ref = p.refs.map(r=>r.t + this.htmlToText('&nbsp;') + r.s).join('\n<br>');
  165. let brc = com && ref ? '<br><br>' : '';
  166. t = p.comp ? `${t}<br><br><u>${ref}${brc}${com}</u>` : `<u>${com}${brc}${ref}</u><br><br>${t}`;
  167. }
  168. return `<details class="fu-chapter"><summary class="fu-summary" chapter="${p.name}">\n${p.name}\n</summary>${t}</details>\n`;
  169. })
  170. .join('\n');
  171. });
  172. this.prepareTextStep('display', t => {
  173. return this.els.prepared.innerHTML = t;
  174. });
  175.  
  176. this.prepared = true;
  177. this.els.infoEditor.innerText = this.strings.sucPrepared;
  178. this.els.infoEditor.classList.toggle('btn-info', false);
  179. this.els.infoEditor.classList.toggle('btn-success', true);
  180.  
  181. this.makePreparedButtons();
  182.  
  183. }
  184.  
  185. makePreparedButtons() {
  186. qq('.fu-summary').map(e => {
  187. let name = e.getAttribute('chapter');
  188. let chapter = this.parts.find(e => e.name == name);
  189. let a = qq('.parts .title a').find(e=>e.innerText == name);
  190. chapter.a = a;
  191. chapter.summary = e;
  192. console.log({chapter, a, name});
  193.  
  194. chapter.buttons = elm('.fu-summary-buttons').appendTo(e);
  195. chapter.status = elm('sup.fu-chapter-status').appendTo(chapter.buttons);
  196.  
  197. if (chapter.a) {
  198. elm('button.fu-sync-chapter', this.strings.chReload+this.strings.chDownload, click => {click.preventDefault(); this.syncChapter(chapter)})
  199. .appendTo(chapter.buttons);
  200. } else {
  201. chapter.status.innerText = this.strings.statusTextFromTo(0, chapter.text.length);
  202. elm('button.fu-chapter-make', this.strings.chUpload, click => {click.preventDefault(); this.makeChapter(chapter)})
  203. .appendTo(chapter.buttons);
  204. }
  205. });
  206. }
  207.  
  208. async syncChapter(chapter) {
  209. console.log(chapter);
  210. chapter.summary.q('.fu-sync-chapter').classList.add('fu-rotate');
  211. console.log(window.chap = chapter);
  212.  
  213. if (!chapter.a) {
  214. throw alert('wrong button!');
  215. }
  216.  
  217. chapter.doc = await fetch.doc(chapter.a.href);
  218. chapter.summary.q('.fu-sync-chapter').remove();
  219. chapter.oldText = chapter.doc.q('textarea#content').value;
  220. console.log(chapter);
  221. chapter.isSame = chapter.text == chapter.oldText;
  222.  
  223. chapter.status.innerText =
  224. chapter.isSame ? this.strings.statusTextSame: this.strings.statusTextFromTo(chapter.oldText.length, chapter.text.length);
  225.  
  226. chapter.btnUpdate = elm('button.fu-chapter-update', this.strings.chUpload, click=>this.updateChapter(chapter));
  227. chapter.buttons.append(chapter.status);
  228. if (!chapter.isSame) {
  229. chapter.buttons.append(chapter.btnUpdate);
  230. }
  231. }
  232.  
  233. async updateChapter(chapter) {
  234. console.log('upload', chapter);
  235. chapter.buttons.q('.fu-chapter-update').classList.add('fu-rotate');
  236. chapter.iframe = elm('iframe').appendTo(chapter.buttons);
  237.  
  238. await this.iframeLoad(chapter.iframe, chapter.a.href);
  239.  
  240. let ta = chapter.iframe.contentDocument.querySelector('textarea#content');
  241. ta.scrollIntoView();
  242. console.log('oldText: ', ta.value == chapter.oldText)
  243. if (ta.value != chapter.oldText) {
  244. alert('Error: can\'t update, chapter text has changed');
  245. throw new Error('oldText has changed!');
  246. }
  247. await Promise.frame(30);
  248. ta.value = chapter.text;
  249. console.log('text: ', ta.value == chapter.text);
  250. await Promise.frame(30);
  251.  
  252. let bsave = chapter.iframe.contentDocument.querySelector('#save_part')
  253. bsave.scrollIntoView();
  254. await Promise.frame(30);
  255.  
  256. await this.iframeLoad(chapter.iframe, () => bsave.click())
  257.  
  258. console.log('frame loaded, chapter updated!');
  259.  
  260. ta = chapter.iframe.contentDocument.querySelector('textarea#content');
  261. chapter.newText = ta.value;
  262. if (ta.value != chapter.text) {
  263. alert('Error: failed to update dunno why');
  264. throw new Error('failed to update dunno why!');
  265. }
  266. await Promise.frame(30);
  267.  
  268. chapter.status.innerText = this.strings.statusTextSame;
  269. chapter.buttons.q('.fu-chapter-update').remove();
  270.  
  271. chapter.iframe.remove();
  272. chapter.iframe = null;
  273. }
  274.  
  275. async makeChapter(chapter) {
  276. console.log('upload', window.chap=chapter);
  277. chapter.buttons.q('.fu-chapter-make').classList.add('fu-rotate');
  278. chapter.iframe = elm('iframe').appendTo(chapter.buttons);
  279. let href = q('.add-part a[href*="addpart"]').href;
  280. await this.iframeLoad(chapter.iframe, href);
  281.  
  282. let ta = chapter.iframe.contentDocument.querySelector('textarea#content');
  283. let ti = chapter.iframe.contentDocument.querySelector('#titleInput');
  284. let cb = chapter.iframe.contentDocument.querySelector('#not_published_chb');
  285.  
  286. ti.scrollIntoView();
  287. ti.value = chapter.name;
  288. await Promise.frame(30);
  289.  
  290. ta.scrollIntoView();
  291. await Promise.frame(30);
  292. ta.value = chapter.text;
  293. await Promise.frame(30);
  294.  
  295. cb.scrollIntoView();
  296. cb.checked = true;
  297. await Promise.frame(30);
  298.  
  299. let bsave = chapter.iframe.contentDocument.querySelector('button[type="submit"]')
  300. if (bsave?.innerText != 'Добавить часть') {
  301. alert('Кнопка не найдена, нажмите сами');
  302. }
  303. bsave?.scrollIntoView();
  304. await Promise.frame(30);
  305. await this.iframeLoad(chapter.iframe, () => bsave?.click());
  306. console.log('frame loaded, chapter updated!');
  307. await Promise.frame(30);
  308.  
  309. chapter.status.innerText = this.strings.statusTextSame;
  310. chapter.buttons.q('.fu-chapter-make').remove();
  311.  
  312. chapter.iframe.remove();
  313. chapter.iframe = null;
  314. }
  315.  
  316.  
  317. async iframeLoad(iframe, src='') {
  318. return new Promise(r => {
  319. iframe.addEventListener('load', r);
  320. if (src) {
  321. if (typeof src == 'string') iframe.src = src;
  322. if (typeof src == 'function') src(iframe);
  323. }
  324. });
  325. }
  326.  
  327. htmlToText(h) {
  328. let a = document.createElement('a');
  329. a.innerHTML = h;
  330. return a.innerText;
  331. }
  332. tosupnum(t) {
  333. let num = '⁰¹²³⁴⁵⁶⁷⁸⁹'.split('');
  334. return (t + '').match(/\d/g).map(e=>num[e]).join('');
  335. }
  336. fromsupnum(t) {
  337. let num = '⁰¹²³⁴⁵⁶⁷⁸⁹'.split('');
  338. return +(t + '').match(/[⁰¹²³⁴⁵⁶⁷⁸⁹]/g).map(e=>num.indexOf(e)).join('');
  339. }
  340. tabber(s) {
  341. let hTexts = {};
  342.  
  343. function hText(s) {
  344. if (hTexts[s])
  345. return hTexts[s];
  346. let a = document.createElement('a');
  347. a.innerHTML = s;
  348. return hTexts[s] = a.innerText;
  349. }
  350. const nbsp = '\xa0';
  351. //hText('&nbsp;');
  352. const emsp = '\u2003';
  353. //hText('&emsp;');
  354. const ndash = '\u2013';
  355. //hText('&ndash;');
  356. const replacers = [
  357. [/\n/g, ' '],
  358. [/^\s+|\s+$/gm, '\n\n\n'],
  359. [/&[^;]{2,7};/g, hText],
  360. [/<br>|<.div><div[^>]*>|<.div>|<div[^>]*>/g, '\n'],
  361. [/<p[^>]*(center|right)[^>]*>([^]*?)<\/p>/g, '\n<$1>\n$2\n</$1>\n'],
  362. [/<\/p>\s*<p[^>]*>/g, '\n'],
  363. [/<\/p>\s*|\s*<p[^>]*>/g, '\n'],
  364. [/<script>[^]*?<.script>/, ''],
  365. [/\s*\n{4,}/g, '\n\n\n'], [/(\s*)(<(b|i|s)>)/g, '$2$1'],
  366. [/(^|[^\.])(…|\.{2,4}(?!\.))(?!\n\s)? /gm, '$1… '],
  367. [/(–|—|―)/gm, ' - '],
  368. [/--?(?![\-\wа-яёА-ЯЁ])|([^\-\wа-яёА-ЯЁ])-(?![\->\w])/g, `$1 - `],
  369. [/^((?=.)\s)*/gm, emsp + emsp],
  370. [/((?!\n)\s)+-\s+/gm, ' ' + ndash + nbsp],
  371. [/^\s*–/gm, emsp + nbsp + ndash],
  372. [/\n<center>\n([^]*?)\n<\/center>\n/g, s=>s.replace(/^\s*/gm, '')],
  373. [/\n<right>\n([^]*?)\n<\/right>\n/g, s=>s.replace(/^\s*/gm, '')],
  374. [/\s*<center>\s*([*][\s*]*[*])\s*<\/center>\n*|\n+\s*([*][\s*]*[*])\s*\n+/g, '\n\n\n<center>\n$1$2\n</center>\n\n'],
  375. [/\n(<.?(center|right)>)\n/g, '$1'],
  376. [/(<(b|i|s)>)(\s*)/g, '$3$1'],
  377. [/<(?!\/?(b|i|s|center|right))/g, '&lt;'],
  378. [/^\s*\n|\n\s*$/g, '']
  379. ];
  380. replacers.forEach(rpl=>{
  381. s = s.replace(rpl[0], rpl[1]);
  382. });
  383. return s;
  384. }
  385.  
  386. }
  387.  
  388. window.ficUpdate = new FicUpdate();