bangumi 显示 条目 wiki 版本差异

显示条目信息版本差异, 可以在 https://github.com/trim21/bgm-tv-userscripts/tree/master/scripts/wiki-rev-diff#readme 查看效果图

当前为 2024-09-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name bgm-wiki-rev-diff
  3. // @name:zh bangumi 显示 条目 wiki 版本差异
  4. // @name:zh-CN bangumi 显示 条目 wiki 版本差异
  5. // @description 显示条目信息版本差异, 可以在 https://github.com/trim21/bgm-tv-userscripts/tree/master/scripts/wiki-rev-diff#readme 查看效果图
  6. // @description:zh-CN 显示条目信息版本差异, 可以在 https://github.com/trim21/bgm-tv-userscripts/tree/master/scripts/wiki-rev-diff#readme 查看效果图
  7. // @namespace https://trim21.me/
  8. // @version 0.2.27
  9. // @source https://github.com/trim21/bgm-tv-userscripts
  10. // @supportURL https://github.com/trim21/bgm-tv-userscripts/issues
  11. // @license MIT
  12. // @match https://bgm.tv/subject/*/edit*
  13. // @match https://bangumi.tv/subject/*/edit*
  14. // @match https://chii.in/subject/*/edit*
  15. // @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js
  16. // @require https://cdn.jsdelivr.net/npm/diff2html@3.4.48/bundles/js/diff2html.min.js
  17. // @require https://cdn.jsdelivr.net/npm/diff@7.0.0/dist/diff.min.js
  18. // @require https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js
  19. // @resource diff2html https://cdn.jsdelivr.net/npm/diff2html@3.4.48/bundles/css/diff2html.min.css
  20. // @grant GM.getResourceUrl
  21. // @grant GM.registerMenuCommand
  22. // @grant GM.setValue
  23. // @grant GM.getValue
  24. // @run-at document-end
  25. // @author Trim21 <i@trim21.me>
  26. // ==/UserScript==
  27.  
  28. /******/ (() => { // webpackBootstrap
  29. /******/ "use strict";
  30.  
  31. ;// CONCATENATED MODULE: external "$"
  32. const external_$_namespaceObject = $;
  33. ;// CONCATENATED MODULE: external "_"
  34. const external_namespaceObject = _;
  35. ;// CONCATENATED MODULE: ./scripts/wiki-rev-diff/src/parser.ts
  36.  
  37. function parseRevDetails(html) {
  38. const jq = external_$_namespaceObject(html);
  39. const rawInfo = jq.find('#subject_infobox').val()?.toString() ?? '';
  40. const title = jq.find('input[name="subject_title"]').val()?.toString() ?? '';
  41. const description = jq.find('textarea#subject_summary').val()?.toString() ?? '';
  42. return {
  43. title,
  44. rawInfo,
  45. description
  46. };
  47. }
  48. function parseRevEl(el) {
  49. const date = el.find('a:not(.compare-previous-trim21-cn)').first().html();
  50. const revEL = el.find('a.l:contains("恢复")');
  51. const revCommentEl = el.find('span.comment');
  52. let comment = '';
  53. if (revCommentEl.length > 0) {
  54. comment = revCommentEl.html();
  55. comment = comment.substring(1, comment.length - 1);
  56. }
  57. const revHref = revEL.attr('href');
  58. if (!revHref) {
  59. // this is a merge commit, can't know what's really info
  60. return undefined;
  61. }
  62. const revID = revHref.split('/').pop();
  63. if (!revID) {
  64. throw new Error(`can't parse rev id from ${revHref}`);
  65. }
  66. return {
  67. id: revID,
  68. comment,
  69. date,
  70. url: revHref
  71. };
  72. }
  73. function getRevs() {
  74. const revs = [];
  75. external_$_namespaceObject('#pagehistory li').each(function () {
  76. const rev = parseRevEl(external_$_namespaceObject(this));
  77. if (rev != null) {
  78. revs.push(rev);
  79. }
  80. });
  81. return revs;
  82. }
  83. function getRevInfo(revID) {
  84. for (const rev of getRevs()) {
  85. if (rev.id === revID) {
  86. return rev;
  87. }
  88. }
  89. }
  90. ;// CONCATENATED MODULE: external "Diff2Html"
  91. const external_Diff2Html_namespaceObject = Diff2Html;
  92. ;// CONCATENATED MODULE: ./scripts/wiki-rev-diff/src/config.ts
  93. const configKey = 'view-mode';
  94. ;// CONCATENATED MODULE: external "Diff"
  95. const external_Diff_namespaceObject = Diff;
  96. ;// CONCATENATED MODULE: ./scripts/wiki-rev-diff/src/differ.ts
  97.  
  98. const pattern = /(?![\t\r\n])(\p{Cf}|\p{Cc}|\p{Co})/gu;
  99. function escapeInvisible(s) {
  100. return s.replaceAll(pattern, function (match) {
  101. const u = match.codePointAt(0);
  102. if (u === undefined) {
  103. return '';
  104. }
  105. return '\\u' + u.toString(16).toLowerCase();
  106. });
  107. }
  108. function diff(revOld, revNew, style) {
  109. const options = {
  110. context: 100
  111. };
  112. if (style === 'line-by-line') {
  113. options.context = 4;
  114. }
  115. return [titleDiff(revOld, revNew, options), infoDiff(revOld, revNew, options), descriptionDiff(revOld, revNew, options)].join('\n');
  116. }
  117. function titleDiff(rev1, rev2, options) {
  118. if (rev1.details.title === rev2.details.title) {
  119. return '';
  120. }
  121. return external_Diff_namespaceObject.createPatch('条目名', escapeInvisible(rev1.details.title), escapeInvisible(rev2.details.title), rev1.rev.date, rev2.rev.date, options);
  122. }
  123. function infoDiff(rev1, rev2, options) {
  124. if (rev1.details.rawInfo === rev2.details.rawInfo) {
  125. return '';
  126. }
  127. return external_Diff_namespaceObject.createPatch('相关信息', escapeInvisible(rev1.details.rawInfo), escapeInvisible(rev2.details.rawInfo), rev1.rev.date, rev2.rev.date, options);
  128. }
  129. function descriptionDiff(rev1, rev2, options) {
  130. if (rev1.details.description === rev2.details.description) {
  131. return '';
  132. }
  133. return external_Diff_namespaceObject.createPatch('简介', escapeInvisible(rev1.details.description), escapeInvisible(rev2.details.description), rev1.rev.date, rev2.rev.date, options);
  134. }
  135. ;// CONCATENATED MODULE: ./scripts/wiki-rev-diff/src/utils.ts
  136. function getCookie(name) {
  137. const value = '; ' + document.cookie;
  138. const parts = value.split('; ' + name + '=');
  139. if (parts.length === 2) return parts.pop()?.split(';').shift();
  140. return undefined;
  141. }
  142. ;// CONCATENATED MODULE: ./scripts/wiki-rev-diff/src/ui.ts
  143.  
  144.  
  145.  
  146.  
  147.  
  148. async function render(revOld, revNew) {
  149. let outputFormat = await GM.getValue(configKey);
  150. if (!outputFormat) {
  151. outputFormat = 'line-by-line';
  152. }
  153. const colorScheme = getCookie('chii_theme');
  154. const patch = diff(revOld, revNew, outputFormat);
  155. const html = external_Diff2Html_namespaceObject.html(patch, {
  156. outputFormat,
  157. colorScheme
  158. });
  159. const elID = `show-diff-view-${outputFormat}`;
  160. show('');
  161. external_$_namespaceObject(`#${elID}`).html(html);
  162. document.getElementById(elID)?.scrollIntoView({
  163. behavior: 'smooth'
  164. });
  165. }
  166. function show(html) {
  167. external_$_namespaceObject('#show-diff-info').html(html);
  168. }
  169. function clear() {
  170. external_$_namespaceObject('#show-diff-view-line-by-line').html('');
  171. external_$_namespaceObject('#show-diff-view-side-by-side').html('');
  172. show('');
  173. }
  174. ;// CONCATENATED MODULE: ./scripts/wiki-rev-diff/src/model.ts
  175. class Commit {
  176. constructor(rev, detail) {
  177. this.rev = rev;
  178. this.details = detail;
  179. }
  180. }
  181. ;// CONCATENATED MODULE: ./scripts/wiki-rev-diff/src/compare.ts
  182.  
  183.  
  184.  
  185. function compare(revID1, revID2) {
  186. clear();
  187. show('<h2>loading versions...</h2>');
  188. const rev1 = getRevInfo(revID1);
  189. const rev2 = getRevInfo(revID2);
  190. if (rev1 == null) {
  191. throw new Error(`error finding ${revID1}`);
  192. }
  193. const ps = [fetchRev(rev1), fetchRev(rev2)];
  194. Promise.all(ps).then(async values => {
  195. await render(values[1], values[0]);
  196. }).catch(e => {
  197. console.error(e);
  198. show('<div style="color: red">获取历史修改失败,请刷新页面后重试</div>');
  199. });
  200. }
  201. const _cache = {};
  202. async function fetchRev(rev) {
  203. if (rev == null) {
  204. return new Commit({
  205. id: '0',
  206. comment: '',
  207. date: '',
  208. url: ''
  209. }, {
  210. title: '',
  211. rawInfo: '',
  212. description: ''
  213. });
  214. }
  215. if (!_cache[rev.id]) {
  216. const res = await fetch(rev.url);
  217. _cache[rev.id] = new Commit(rev, parseRevDetails(await res.text()));
  218. }
  219. return _cache[rev.id];
  220. }
  221. ;// CONCATENATED MODULE: ./scripts/wiki-rev-diff/src/index.ts
  222.  
  223.  
  224.  
  225.  
  226.  
  227. async function main() {
  228. console.log('start bgm-wiki-rev-diff UserScript');
  229. await initUI();
  230. }
  231. const style = `
  232. <style>
  233. #show-diff-view-side-by-side {
  234. margin:0 auto;
  235. max-width: 100em;
  236. }
  237.  
  238. .show-version-diff .d2h-code-line, .show-version-diff .d2h-code-side-line {
  239. width: calc(100% - 8em);
  240. padding-right: 0;
  241. }
  242.  
  243. .show-version-diff .d2h-code-line-ctn {
  244. width: calc(100% - 8em);
  245. }
  246.  
  247. #columnInSubjectA .rev-trim21-cn {
  248. margin: 0 0.2em;
  249. }
  250.  
  251. ul#pagehistory > li > * {
  252. vertical-align: middle;
  253. }
  254. </style>
  255. `;
  256. async function initUI() {
  257. GM.registerMenuCommand('切换diff视图', function () {
  258. void (async () => {
  259. let outputFormat = await GM.getValue(configKey);
  260. if (!outputFormat || outputFormat === 'side-by-side') {
  261. outputFormat = 'line-by-line';
  262. } else {
  263. outputFormat = 'side-by-side';
  264. }
  265. await GM.setValue(configKey, outputFormat);
  266. })();
  267. });
  268. external_$_namespaceObject('#headerSubject').after('<div id="show-diff-view-side-by-side" class="show-version-diff"></div>');
  269. external_$_namespaceObject('#columnInSubjectA > hr.board').after(style + '<div id="show-diff-view-line-by-line" class="show-version-diff"></div>');
  270. external_$_namespaceObject('#columnInSubjectA .subtitle').after('<div id="show-diff-info"></div>');
  271. const diff2htmlStyle = await GM.getResourceUrl('diff2html');
  272. external_$_namespaceObject('head').append(style).append(`<link rel='stylesheet' type='text/css' href='${diff2htmlStyle}' />`);
  273. const s = external_$_namespaceObject('#pagehistory li');
  274. const revs = Array.from(s).map(function (e) {
  275. return parseRevEl(external_$_namespaceObject(e))?.id;
  276. });
  277. s.each(function (index) {
  278. const el = external_$_namespaceObject(this);
  279. const id = revs[index];
  280. if (!id) {
  281. el.prepend('<span style="padding-right: 1.4em"> 无法参与比较 </span>');
  282. return;
  283. }
  284. el.prepend(`<input type='radio' class='rev-trim21-cn' name='rev-right' label='select to compare' value='${id}'>`);
  285. el.prepend(`<input type='radio' class='rev-trim21-cn' name='rev-left' label='select to compare' value='${id}'>`);
  286. const previous = external_namespaceObject.find(revs, Boolean, index + 1) ?? '';
  287. el.prepend(`(<a href='#' data-rev='${id}' data-previous='${previous}' class='l compare-previous-trim21-cn'>显示修改</a>) `);
  288. });
  289. const typeRevert = {
  290. 'rev-left': 'rev-right',
  291. 'rev-right': 'rev-left'
  292. };
  293. external_$_namespaceObject('input[type="radio"]').on('change', function (e) {
  294. const name = e.target.getAttribute('name');
  295. if (!name) {
  296. return;
  297. }
  298. const selectName = typeRevert[name];
  299. const rev = e.target.getAttribute('value');
  300. if (rev) {
  301. external_$_namespaceObject(`input[name="${selectName}"][value="${rev}"]`).css('visibility', 'hidden');
  302. external_$_namespaceObject(`input[name="${selectName}"][value!="${rev}"]`).css('visibility', 'visible');
  303. }
  304. });
  305. external_$_namespaceObject('.compare-previous-trim21-cn').on('click', function () {
  306. const el = external_$_namespaceObject(this);
  307. const left = String(el.data('rev'));
  308. const right = String(el.data('previous'));
  309. compare(left, right);
  310. external_$_namespaceObject(`input[name="rev-left"][value="${left}"]`).prop('checked', true);
  311. external_$_namespaceObject(`input[name="rev-left"][value!="${left}"]`).prop('checked', null);
  312. external_$_namespaceObject(`input[name="rev-right"][value="${left}"]`).css('visibility', 'hidden');
  313. external_$_namespaceObject(`input[name="rev-right"][value!="${left}"]`).css('visibility', 'visible');
  314. external_$_namespaceObject('input[name="rev-left"]').css('visibility', 'visible');
  315. external_$_namespaceObject('input[name="rev-right"]').prop('checked', null);
  316. if (right) {
  317. external_$_namespaceObject(`input[name="rev-right"][value="${right}"]`).prop('checked', true);
  318. external_$_namespaceObject(`input[name="rev-left"][value="${right}"]`).css('visibility', 'hidden');
  319. }
  320. });
  321. external_$_namespaceObject('#columnInSubjectA span.text').append('<a href="#" id="compare-trim21-cn" class="l"> > 比较选中的版本</a>');
  322. external_$_namespaceObject('#compare-trim21-cn').on('click', function () {
  323. const selectedRevs = getSelectedVersion();
  324. compare(selectedRevs[0], selectedRevs[1]);
  325. });
  326. }
  327. function getSelectedVersion() {
  328. const selectedVersion = [];
  329. const selectedRev = external_$_namespaceObject('.rev-trim21-cn:checked');
  330. if (selectedRev.length < 2) {
  331. window.alert('请选中两个版本进行比较');
  332. throw new Error();
  333. }
  334. selectedRev.each(function () {
  335. const val = external_$_namespaceObject(this).val();
  336. selectedVersion.push(val);
  337. });
  338. selectedVersion.sort((a, b) => parseInt(b) - parseInt(a));
  339. return selectedVersion;
  340. }
  341. main().catch(console.error);
  342. /******/ })()
  343. ;