CF-Html-Viewer

View html immediately

目前為 2020-05-23 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name CF-Html-Viewer
  3. // @version 0.3.1
  4. // @description View html immediately
  5. // @author Iverycool
  6. // @namespace Cyberforum
  7. // @match https://www.cyberforum.ru/*
  8. // @grant none
  9. // ==/UserScript==
  10. (function() {
  11.  
  12. const $ = (sec, node = document) => node.querySelector(sec);
  13. const $$ = (sec, node = document) => node.querySelectorAll(sec);
  14. const size = 1.4;
  15. // const isFF = ~navigator.userAgent.search(/firefox/i);
  16.  
  17. const styles = {
  18. but: 'color: rgb(96, 96, 96); font-weight: normal; margin-left: 5px;',
  19. rightBut: 'color: rgb(96, 96, 96); font-weight: normal; float: right;',
  20. frame: 'display: block; width: 100%;',
  21. textarea: getStyleString({
  22. 'position': 'absolute',
  23. 'margin': 0,
  24. 'padding': 0,
  25. 'border': 'transparent',
  26. 'background': 'transparent',
  27. 'color': 'transparent',
  28. 'font-family': 'Consolas,Monaco,\'Andale Mono\',\'Ubuntu Mono\',monospace',
  29. 'font-size': '1em',
  30. 'line-height': size + ' !important',
  31. 'white-space': 'pre',
  32. 'caret-color': 'black',
  33. 'outline': 'none',
  34. 'resize': 'none'
  35. })
  36. }
  37. const sels = {
  38. codeFrame: 'div.codeframe',
  39. codePre: 'td.de1 > pre.de1',
  40. lnPre: 'td.ln > pre.de1'
  41. }
  42.  
  43. document.addEventListener('DOMContentLoaded', () => {
  44. // debugger;
  45. init().then(() => $$('#posts div[id^="post_message_"]').forEach(register));
  46. });
  47.  
  48.  
  49. function init() {
  50. const prismStyle = createElem('link', document.head);
  51. prismStyle.rel = 'stylesheet';
  52. prismStyle.href = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.20.0/themes/prism.min.css';
  53.  
  54. const myStyle = createElem('style', document.head);
  55. myStyle.innerHTML = 'pre.de1, pre.de1 * { line-height: ' + size + ' !important }' +
  56. ['html5', 'phphtml', 'css', 'javascript'].map(name =>
  57. `table.${name} > tbody {background: #f5f2f0 !important}`
  58. ).join('');
  59.  
  60. const prismScript = createElem('script', document.head);
  61. prismScript.type = 'text/javascript';
  62. prismScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.20.0/prism.min.js';
  63. return new Promise(res => prismScript.onload = res);
  64. }
  65.  
  66.  
  67. function register(post) {
  68. const viewer = new Viewer();
  69. const htmlNodes = $$('table.html5, table.phphtml', post);
  70. const cssNodes = $$('table.css', post);
  71. const jsNodes = $$('table.javascript', post);
  72.  
  73. htmlNodes.forEach(htmlNode => {
  74. makeViewerLink(viewer, htmlNode);
  75. makeEditableLink(viewer, htmlNode, 'html');
  76. stylize(htmlNode, 'html');
  77. });
  78.  
  79. cssNodes.forEach(cssNode => {
  80. makeUseLink(viewer, cssNode, 'style');
  81. makeEditableLink(viewer, cssNode, 'css');
  82. stylize(cssNode, 'css');
  83. });
  84.  
  85. jsNodes.forEach(jsNode => {
  86. createLink({
  87. parentNode: $('.head', jsNode),
  88. style: styles.but,
  89. text: 'Выполнить',
  90. onClick: () => eval($(sels.codePre, jsNode).innerText)
  91. });
  92.  
  93. makeUseLink(viewer, jsNode, 'script');
  94. makeEditableLink(jsNode, 'js');
  95. stylize(jsNode, 'js');
  96. });
  97. }
  98.  
  99.  
  100. class Viewer {
  101. style = [];
  102. script = [];
  103. mounted = false;
  104.  
  105. create(parent, htmlCode, onClose = () => {}) {
  106. this.clean();
  107. this.mounted = true;
  108.  
  109. this.html = htmlCode.replace(/ /g, '');
  110. this.container = createElem('tfoot', parent);
  111. createLink({
  112. parentNode: this.container,
  113. style: styles.but,
  114. text: 'Открыть в новом окне',
  115. onClick: () => this.openInNewWin()
  116. });
  117.  
  118. createLink({
  119. parentNode: this.container,
  120. style: styles.but,
  121. text: 'Скачать страницу',
  122. onClick: () => this.download()
  123. })
  124.  
  125. createLink({
  126. parentNode: this.container,
  127. style: styles.rightBut,
  128. text: 'Закрыть',
  129. onClick: () => (this.clean(), onClose())
  130. });
  131.  
  132. this.iframe = createElem('iframe', this.container);
  133. this.iframe.style = styles.frame;
  134. this.update();
  135. }
  136.  
  137. clean() {
  138. if (this.container) this.container.remove();
  139. this.iframe = null;
  140. this.doc = null;
  141. this.html = '';
  142. this.mounted = false;
  143. }
  144.  
  145. add(id, code, type) {
  146. if (!this.mounted) return false;
  147. const ind = this[type].findIndex(el => el[0] == id);
  148. if (~ind) {
  149. this[type].splice(ind, 1, [id, code.replace(/ /g, '')])
  150. } else {
  151. this[type].push([id, code.replace(/ /g, '')]);
  152. }
  153. return this.update();
  154. }
  155.  
  156. has(id, type) {
  157. return !!(~this[type].findIndex(el => el[0] == id));
  158. }
  159.  
  160. remove(id, type) {
  161. const ind = this[type].findIndex(el => el[0] == id);
  162. const res = (~ind) ? !!this[type].splice(ind, 1) : false;
  163. this.update();
  164. return res;
  165. }
  166.  
  167. update(code) {
  168. if (!this.mounted) return;
  169. if (code) this.html = code;
  170. const str = this._compile();
  171.  
  172. this.iframe.src = 'about:blank';
  173. return new Promise(res => {
  174. this.iframe.onload = () => {
  175. this.doc = this.iframe.contentWindow.document;
  176. this.doc.write(str);
  177. this.doc.close();
  178. this.updateFrameHeight();
  179. res();
  180. }
  181. });
  182. }
  183.  
  184. updateFrameHeight() {
  185. const coords = getCoords(this.doc.body.lastElementChild, this.iframe.contentWindow);
  186. const contentHeight = coords.top + coords.height;
  187. const frameHeight = (contentHeight > 500) ? '500px' : contentHeight + 16 + 'px';
  188. this.iframe.style.height = frameHeight;
  189. }
  190.  
  191. openInNewWin() {
  192. const str = this._compile();
  193. const win = window.open('about:blank');
  194. win.onload = () => {
  195. win.document.write(str);
  196. win.document.close();
  197. }
  198. }
  199.  
  200. download() {
  201. const str = this._compile();
  202. const id = this.container.parentElement.querySelector(sels.codeFrame).id;
  203. const a = createElem('a');
  204. a.download = `doc-${id}.html`;
  205. a.href = 'data:text/html;charset=UTF-8,' + str;
  206. a.click();
  207. a.remove();
  208. }
  209.  
  210. _compile() {
  211. const wrap = (arr, type) =>
  212. arr.map(([,code]) => `<${type}>${code}</${type}>`).join('');
  213. return wrap(this.style, 'style') + wrap(this.script, 'script') + this.html;
  214. }
  215. }
  216.  
  217. function stylize(node, type) {
  218. const pre = $(sels.codePre, node);
  219. const code = Prism.highlight(pre.innerText, Prism.languages[type]);
  220. pre.classList.add(`language-${type}`);
  221. pre.innerHTML = `<code class="language-${type}">${code}</code>`;
  222. updateCodeFrameHeight($(sels.codeFrame, node));
  223. }
  224.  
  225. function updateCodeFrameHeight(div) {
  226. const conHeight = getCoords(div.firstChild).height;
  227. div.style.height = ((conHeight > 570) ? 590 : conHeight + 20) + 'px';
  228. }
  229.  
  230. function makeViewerLink(viewer, htmlNode) {
  231. const makeObj = {
  232. text: 'Показать viewer',
  233. onClick: function() {
  234. viewer.create(htmlNode, $(sels.codePre, htmlNode).innerText, () => {
  235. this.update(makeObj);
  236. });
  237. this.update(unMakeObj);
  238. }
  239. }
  240.  
  241. const unMakeObj = {
  242. text: 'Обновить viewer',
  243. onClick: () => viewer.update($(sels.codePre, htmlNode).innerText)
  244. }
  245.  
  246. createLink({
  247. parentNode: $('.head', htmlNode),
  248. style: styles.but,
  249. ...makeObj
  250. });
  251. }
  252.  
  253. function makeUseLink(viewer, codeNode, type) {
  254. const id = $(sels.codeFrame, codeNode).id;
  255.  
  256. const useObj = {
  257. text: 'Использовать в viewer',
  258. onClick: function() {
  259. if (viewer.add(id, $(sels.codePre, codeNode).innerText, type)) {
  260. this.update(unUseObj);
  261. }
  262. }
  263. }
  264.  
  265. const unUseObj = {
  266. text: 'Отвязать от viewer',
  267. onClick: function() {
  268. viewer.remove(id, type);
  269. this.update(useObj);
  270. }
  271. }
  272.  
  273. createLink({
  274. parentNode: $('.head', codeNode),
  275. style: styles.but,
  276. ...useObj
  277. });
  278. }
  279.  
  280. function makeEditableLink(viewer, node, type) {
  281. const id = $(sels.codeFrame, node).id;
  282. let textarea, autoReloadingLink;
  283. const makeObj = {
  284. text: 'Редактировать',
  285. onClick: function() {
  286. textarea = makeInput($(sels.codePre, node), type);
  287. this.update(unMakeObj);
  288. autoReloadingLink = makeAutoReloadingLink();
  289. }
  290. }
  291. const unMakeObj = {
  292. text: 'Отменить редактирование',
  293. onClick: function() {
  294. textarea.remove();
  295. autoReloadingLink.remove();
  296. this.update(makeObj);
  297. }
  298. }
  299.  
  300. createLink({
  301. parentNode: $('.head', node),
  302. style: styles.but,
  303. ...makeObj
  304. });
  305.  
  306. function makeAutoReloadingLink() {
  307. const makeObj = {
  308. style: styles.but + 'color: rgb(130, 130, 130);',
  309. text: 'AutoReloading - off',
  310. onClick: function() {
  311. textarea.onInput = function(text) {
  312. if (type === 'html') {
  313. return viewer.update(text);
  314. } else {
  315. return viewer.add(id, text, type === 'js' ? 'script' : 'style');
  316. }
  317. }
  318. this.update(unMakeObj);
  319. }
  320. };
  321. const unMakeObj = {
  322. style: styles.but + 'color: green',
  323. text: 'AutoReloading - on',
  324. onClick: function() {
  325. textarea.onInput = null;
  326. this.update(makeObj);
  327. }
  328. };
  329.  
  330. return createLink({
  331. parentNode: $('.head', node),
  332. ...makeObj
  333. });
  334. }
  335. }
  336.  
  337. function makeInput(pre, type, onInput = () => {}) {
  338. const div = pre.closest(sels.codeFrame);
  339. const lnPre = div.querySelector(sels.lnPre);
  340. const textarea = createElem('textarea', pre.parentElement);
  341. const divScroll = {
  342. left: div.scrollLeft,
  343. top: div.scrollTop
  344. }
  345. div.scrollTo(0, 0);
  346.  
  347. const preCoords = getCoords(pre);
  348. const divCoords = getCoords(div);
  349. const paddingLeft = preCoords.left - divCoords.left;
  350. const paddingTop = preCoords.top - divCoords.top;
  351.  
  352. textarea.style = styles.textarea + getStyleString({
  353. 'padding-left': paddingLeft + 'px',
  354. 'padding-top': paddingTop + 'px'
  355. });
  356. updateTextarea();
  357. textarea.spellcheck = false;
  358.  
  359. textarea.value = pre.innerText;
  360. textarea.scrollTo(divScroll.left, divScroll.top);
  361. div.scrollTo(divScroll.left, divScroll.top);
  362.  
  363. textarea.oninput = async () => {
  364. const text = textarea.value;
  365. const textLen = text.split('\n').length;
  366. const code = Prism.highlight(text, Prism.languages[type]);
  367. pre.innerHTML = `<code class="language-${type}">${code}</code>`;
  368. lnPre.innerText = new Array(textLen).fill(1).map((_, i) => i + 1).join('\n');
  369.  
  370. await (textarea.onInput || onInput)(text);
  371. updateCodeFrameHeight(div);
  372. updateTextarea();
  373. }
  374. textarea.onscroll = () => div.scrollTo(textarea.scrollLeft, textarea.scrollTop);
  375.  
  376. function updateTextarea() {
  377. const tStyle = textarea.style;
  378. const divCoords = getCoords(div);
  379.  
  380. tStyle.width = divCoords.width - paddingLeft + 'px';
  381. tStyle.height = divCoords.height - paddingTop + 'px';
  382. tStyle.left = divCoords.left + 'px';
  383. tStyle.top = divCoords.top + 'px';
  384. }
  385.  
  386. return textarea;
  387. }
  388.  
  389. // Utils
  390. function createElem(name, parentNode = document.documentElement) {
  391. const elem = document.createElement(name);
  392. parentNode.appendChild(elem);
  393. return elem;
  394. }
  395.  
  396. function createLink({parentNode = document.documentElement, style, text, onClick = () => {}}) {
  397. const a = createElem('a', parentNode);
  398. a.style = style;
  399. a.href = '#';
  400. a.innerText = text;
  401. a.onclick = function(ev) {
  402. ev.preventDefault();
  403. onClick.call(a, ev);
  404. }
  405.  
  406. a.update = function({style: newStyle, text: newText, onClick: newOnClick}) {
  407. a.style = newStyle || style;
  408. a.innerText = newText || text;
  409. a.onclick = function(ev) {
  410. ev.preventDefault();
  411. (newOnClick || onClick).call(a, ev);
  412. }
  413. }
  414.  
  415. return a;
  416. }
  417.  
  418. function getStyleString(obj) {
  419. return Object.entries(obj).reduce((str, [key, value]) => str + `${key}:${value};`, '');
  420. }
  421.  
  422. function getCoords(elem, win = window) {
  423. const box = elem.getBoundingClientRect();
  424.  
  425. return {
  426. top: box.top + win.pageYOffset,
  427. left: box.left + win.pageXOffset,
  428. width: box.width,
  429. height: box.height
  430. }
  431. }
  432.  
  433. })();