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. this.update();
  154. return true;
  155. }
  156.  
  157. has(id, type) {
  158. return !!(~this[type].findIndex(el => el[0] == id));
  159. }
  160.  
  161. remove(id, type) {
  162. const ind = this[type].findIndex(el => el[0] == id);
  163. const res = (~ind) ? !!this[type].splice(ind, 1) : false;
  164. this.update();
  165. return res;
  166. }
  167.  
  168. update(code) {
  169. if (!this.mounted) return;
  170. if (code) this.html = code;
  171. const str = this._compile();
  172.  
  173. this.iframe.src = 'about:blank';
  174. this.iframe.onload = () => {
  175. this.doc = this.iframe.contentWindow.document;
  176. this.doc.write(str);
  177. this.doc.close();
  178. this.updateFrameHeight();
  179. }
  180. }
  181.  
  182. updateFrameHeight() {
  183. const coords = getCoords(this.doc.body.lastElementChild, this.iframe.contentWindow);
  184. const contentHeight = coords.top + coords.height;
  185. const frameHeight = (contentHeight > 500) ? '500px' : contentHeight + 16 + 'px';
  186. this.iframe.style.height = frameHeight;
  187. }
  188.  
  189. openInNewWin() {
  190. const str = this._compile();
  191. const win = window.open('about:blank');
  192. win.onload = () => {
  193. win.document.write(str);
  194. win.document.close();
  195. }
  196. }
  197.  
  198. download() {
  199. const str = this._compile();
  200. const id = this.container.parentElement.querySelector(sels.codeFrame).id;
  201. const a = createElem('a');
  202. a.download = `doc-${id}.html`;
  203. a.href = 'data:text/html;charset=UTF-8,' + str;
  204. a.click();
  205. a.remove();
  206. }
  207.  
  208. _compile() {
  209. const wrap = (arr, type) =>
  210. arr.map(([,code]) => `<${type}>${code}</${type}>`).join('');
  211. return wrap(this.style, 'style') + wrap(this.script, 'script') + this.html;
  212. }
  213. }
  214.  
  215. function stylize(node, type) {
  216. const pre = $(sels.codePre, node);
  217. const code = Prism.highlight(pre.innerText, Prism.languages[type]);
  218. pre.classList.add(`language-${type}`);
  219. pre.innerHTML = `<code class="language-${type}">${code}</code>`;
  220. updateCodeFrameHeight($(sels.codeFrame, node));
  221. }
  222.  
  223. function updateCodeFrameHeight(div) {
  224. const conHeight = getCoords(div.firstChild).height;
  225. div.style.height = ((conHeight > 570) ? 590 : conHeight + 20) + 'px';
  226. }
  227.  
  228. function makeViewerLink(viewer, htmlNode) {
  229. const makeObj = {
  230. text: 'Показать viewer',
  231. onClick: function() {
  232. viewer.create(htmlNode, $(sels.codePre, htmlNode).innerText, () => {
  233. this.update(makeObj);
  234. });
  235. this.update(unMakeObj);
  236. }
  237. }
  238.  
  239. const unMakeObj = {
  240. text: 'Обновить viewer',
  241. onClick: () => viewer.update($(sels.codePre, htmlNode).innerText)
  242. }
  243.  
  244. createLink({
  245. parentNode: $('.head', htmlNode),
  246. style: styles.but,
  247. ...makeObj
  248. });
  249. }
  250.  
  251. function makeUseLink(viewer, codeNode, type) {
  252. const id = $(sels.codeFrame, codeNode).id;
  253.  
  254. const useObj = {
  255. text: 'Использовать в viewer',
  256. onClick: function() {
  257. if (viewer.add(id, $(sels.codePre, codeNode).innerText, type)) {
  258. this.update(unUseObj);
  259. }
  260. }
  261. }
  262.  
  263. const unUseObj = {
  264. text: 'Отвязать от viewer',
  265. onClick: function() {
  266. viewer.remove(id, type);
  267. this.update(useObj);
  268. }
  269. }
  270.  
  271. createLink({
  272. parentNode: $('.head', codeNode),
  273. style: styles.but,
  274. ...useObj
  275. });
  276. }
  277.  
  278. function makeEditableLink(viewer, node, type) {
  279. const id = $(sels.codeFrame, node).id;
  280. let textarea, autoReloadingLink;
  281. const makeObj = {
  282. text: 'Редактировать',
  283. onClick: function() {
  284. textarea = makeInput($(sels.codePre, node), type);
  285. this.update(unMakeObj);
  286. autoReloadingLink = makeAutoReloadingLink();
  287. }
  288. }
  289. const unMakeObj = {
  290. text: 'Отменить редактирование',
  291. onClick: function() {
  292. textarea.remove();
  293. autoReloadingLink.remove();
  294. this.update(makeObj);
  295. }
  296. }
  297.  
  298. createLink({
  299. parentNode: $('.head', node),
  300. style: styles.but,
  301. ...makeObj
  302. });
  303.  
  304. function makeAutoReloadingLink() {
  305. const makeObj = {
  306. style: styles.but + 'color: rgb(130, 130, 130);',
  307. text: 'AutoReloading - off',
  308. onClick: function() {
  309. textarea.onInput = function(text) {
  310. if (type === 'html') {
  311. viewer.update(text);
  312. } else {
  313. viewer.add(id, text, type === 'js' ? 'script' : 'style');
  314. }
  315. }
  316. this.update(unMakeObj);
  317. }
  318. };
  319. const unMakeObj = {
  320. style: styles.but + 'color: green',
  321. text: 'AutoReloading - on',
  322. onClick: function() {
  323. textarea.onInput = null;
  324. this.update(makeObj);
  325. }
  326. };
  327.  
  328. return createLink({
  329. parentNode: $('.head', node),
  330. ...makeObj
  331. });
  332. }
  333. }
  334.  
  335. function makeInput(pre, type, onInput = () => {}) {
  336. const div = pre.closest(sels.codeFrame);
  337. const lnPre = div.querySelector(sels.lnPre);
  338. const textarea = createElem('textarea', pre.parentElement);
  339. const divScroll = {
  340. left: div.scrollLeft,
  341. top: div.scrollTop
  342. }
  343. div.scrollTo(0, 0);
  344.  
  345. const preCoords = getCoords(pre);
  346. const divCoords = getCoords(div);
  347. const paddingLeft = preCoords.left - divCoords.left;
  348. const paddingTop = preCoords.top - divCoords.top;
  349.  
  350. textarea.style = styles.textarea + getStyleString({
  351. 'padding-left': paddingLeft + 'px',
  352. 'padding-top': paddingTop + 'px'
  353. });
  354. updateTextarea();
  355. textarea.spellcheck = false;
  356.  
  357. textarea.value = pre.innerText;
  358. textarea.scrollTo(divScroll.left, divScroll.top);
  359. div.scrollTo(divScroll.left, divScroll.top);
  360.  
  361. textarea.oninput = () => {
  362. const text = textarea.value;
  363. const textLen = text.split('\n').length;
  364. const code = Prism.highlight(text, Prism.languages[type]);
  365. pre.innerHTML = `<code class="language-${type}">${code}</code>`;
  366. lnPre.innerText = new Array(textLen).fill(1).map((_, i) => i + 1).join('\n');
  367.  
  368. (textarea.onInput || onInput)(text);
  369. updateCodeFrameHeight(div);
  370. updateTextarea();
  371. }
  372. textarea.onscroll = () => div.scrollTo(textarea.scrollLeft, textarea.scrollTop);
  373.  
  374. function updateTextarea() {
  375. const tStyle = textarea.style;
  376. const divCoords = getCoords(div);
  377.  
  378. tStyle.width = divCoords.width - paddingLeft + 'px';
  379. tStyle.height = divCoords.height - paddingTop + 'px';
  380. tStyle.left = divCoords.left + 'px';
  381. tStyle.top = divCoords.top + 'px';
  382. }
  383.  
  384. return textarea;
  385. }
  386.  
  387. // Utils
  388. function createElem(name, parentNode = document.documentElement) {
  389. const elem = document.createElement(name);
  390. parentNode.appendChild(elem);
  391. return elem;
  392. }
  393.  
  394. function createLink({parentNode = document.documentElement, style, text, onClick = () => {}}) {
  395. const a = createElem('a', parentNode);
  396. a.style = style;
  397. a.href = '#';
  398. a.innerText = text;
  399. a.onclick = function(ev) {
  400. ev.preventDefault();
  401. onClick.call(a, ev);
  402. }
  403.  
  404. a.update = function({style: newStyle, text: newText, onClick: newOnClick}) {
  405. a.style = newStyle || style;
  406. a.innerText = newText || text;
  407. a.onclick = function(ev) {
  408. ev.preventDefault();
  409. (newOnClick || onClick).call(a, ev);
  410. }
  411. }
  412.  
  413. return a;
  414. }
  415.  
  416. function getStyleString(obj) {
  417. return Object.entries(obj).reduce((str, [key, value]) => str + `${key}:${value};`, '');
  418. }
  419.  
  420. function getCoords(elem, win = window) {
  421. const box = elem.getBoundingClientRect();
  422.  
  423. return {
  424. top: box.top + win.pageYOffset,
  425. left: box.left + win.pageXOffset,
  426. width: box.width,
  427. height: box.height
  428. }
  429. }
  430.  
  431. })();