ivnote-web-clipper

Choose your favorite DOM node and its content will be automatically copied to the clipboard in markdown format

  1. // ==UserScript==
  2. // @name ivnote-web-clipper
  3. // @version 0.8.1
  4. // @description Choose your favorite DOM node and its content will be automatically copied to the clipboard in markdown format
  5. // @author Ivan Jiang
  6. // @match http://*/*
  7. // @match https://*/*
  8. // @grant GM_addStyle
  9. // @require https://cdn.bootcss.com/to-markdown/3.0.3/to-markdown.min.js
  10. // @namespace http://ivnote.xyz
  11. // ==/UserScript==
  12.  
  13. /* globals GM_addStyle, toMarkdown */
  14. GM_addStyle(` .ivnote-web-clipper-hover { background: rgba(124, 201, 191, .7); }
  15. .ivnote-web-clipper-icon {
  16. position: fixed; bottom: 15px; right: 15px;
  17. height: 50px; width: 50px; border-radius: 50%;
  18. background: RGBA(124, 201, 191, .6) url('https://github.com/iplus26/ivnote/raw/master/web-clipper/toggle-icon.png') no-repeat center center / 80%;
  19. opacity: .7; cursor: pointer;
  20. transition: all .3s ease-in-out;
  21. z-index: 9999;
  22. }
  23. .ivnote-web-clipper-icon.ivnote-web-clipper-enable {
  24. opacity: 1; transform: rotate(180deg);
  25. right: 50px;
  26. }
  27. .ivnote-web-clipper-icon:hover {
  28. opacity: 1;
  29. transform: rotate(180deg)
  30. }`);
  31.  
  32. (function() {
  33. 'use strict';
  34.  
  35. // Utils =========================================================================
  36. var classCache = {};
  37.  
  38. var _ = {
  39. hasClass: hasClass,
  40. addClass: addClass,
  41. removeClass: removeClass,
  42. };
  43.  
  44. function hasClass(node, name) {
  45. if (!name) {
  46. return false;
  47. }
  48. return classRE(name).test(className(node));
  49. }
  50.  
  51. function addClass(node, name) {
  52. if (!name) {
  53. return node;
  54. }
  55. if (!('className' in node)) {
  56. return;
  57. }
  58. var classList = [];
  59. var cls = className(node); // get node's className
  60. name.split(/\s+/g).forEach(function(klass) {
  61. if (!hasClass(node, klass)) {
  62. classList.push(klass);
  63. }
  64. });
  65. return classList.length && className(node, cls + (cls ? ' ' : '') + classList.join(' '));
  66. }
  67.  
  68. function removeClass(node, name) {
  69. if (!('className' in node)) {
  70. return;
  71. }
  72. if (name === undefined) {
  73. return className(node, '');
  74. }
  75. let cls = className(node);
  76. name.split(/\s+/g)
  77. .forEach((klass) => cls = cls.replace(classRE(klass), ' '));
  78. className(node, cls.trim());
  79. }
  80.  
  81. function classRE(name) {
  82. return name in classCache ?
  83. classCache[name] : (classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)'));
  84. }
  85. // access className property while respecting SVGAnimatedString
  86. function className(el, value) {
  87. var klass = el.className || '',
  88. svg = klass && klass.baseVal !== undefined;
  89.  
  90. // get className
  91. if (value === undefined) {
  92. return svg ? klass.baseVal : klass;
  93. }
  94.  
  95. // set className
  96. return svg ? (klass.baseVal = value) : (el.className = value);
  97. }
  98.  
  99. // Toggle icon =====================================================================
  100. var toggle = document.createElement('div');
  101. toggle.className = "ivnote-web-clipper-icon";
  102. document.body.appendChild(toggle);
  103.  
  104. var enable = false;
  105.  
  106. toggle.addEventListener('click', function(e) {
  107. if (_.hasClass(toggle, 'ivnote-web-clipper-enable')) {
  108. // turn it off
  109. _.removeClass(toggle, 'ivnote-web-clipper-enable');
  110. enable = false;
  111. } else {
  112. // turn it on
  113. _.addClass(toggle, 'ivnote-web-clipper-enable');
  114. enable = true;
  115. }
  116. e.stopPropagation();
  117. });
  118.  
  119. // Manipulate the doms ==============================================================
  120. document.addEventListener('mouseover', function(e) {
  121. if (enable) _.addClass(e.target, 'ivnote-web-clipper-hover');
  122. });
  123.  
  124. document.addEventListener('mouseout', function(e) {
  125. if (enable) {
  126. _.removeClass(e.target, 'ivnote-web-clipper-hover');
  127.  
  128. // To be safe
  129. var select = document.querySelectorAll('.ivnote-web-clipper-hover');
  130. Array.prototype.slice.call(select).forEach(function(el) { _.removeClass(el, 'ivnote-web-clipper-hover'); });
  131. }
  132. });
  133.  
  134. document.addEventListener('click', function(e) {
  135. if (enable) {
  136. var el = e.target;
  137.  
  138. var fRemoveContent = function() { return '' }
  139. var fRemoveInlineTag = function(content) { return content }
  140. var fRemoveBlockTag = function(content) { return '\n\n' + content + '\n\n' }
  141.  
  142. var generalConverters = [
  143. // Originally, toMarkdown keeps the tag name of block level elements.
  144. {
  145. filter: ['article', 'div'],
  146. replacement: fRemoveBlockTag
  147. },
  148. {
  149. filter: ['span'],
  150. replacement: fRemoveInlineTag
  151. },
  152. {
  153. filter: ['style', 'script'],
  154. replacement: fRemoveContent
  155. },
  156. {
  157. filter: function(node) {
  158. return node.nodeName === 'A' && !node.getAttribute('href');
  159. },
  160. replacement: fRemoveInlineTag
  161. },
  162. // More cases to create the TR spec.
  163. {
  164. filter: 'tr',
  165. replacement: function(content, node) {
  166. var borderCells = '';
  167. var alignMap = {
  168. left: ':--',
  169. right: '--:',
  170. center: ':-:'
  171. };
  172.  
  173. var headRow = node.parentNode.nodeName === 'THEAD';
  174.  
  175. // Assume ths in first tr in tbody are header cells. (Issue #89)
  176. headRow = headRow || node.parentNode.nodeName === 'TBODY' && !node.previousSibling && !node.parentNode.previousSibling;
  177.  
  178. if (headRow) {
  179. for (var i = 0; i < node.childNodes.length; i++) {
  180. var align = node.childNodes[i].attributes.align;
  181. var border = '---';
  182.  
  183. if (align) {
  184. border = alignMap[align.value] || border;
  185. }
  186.  
  187. borderCells += cell(border, node.childNodes[i]);
  188. }
  189. }
  190. return '\n' + content + (borderCells ? '\n' + borderCells : '');
  191. }
  192. }
  193. ];
  194.  
  195. var particularWebsitesConverters = [
  196. {
  197. filter: function(node) {
  198. return node.nodeName === 'BR' && /^(.+\.)?zhihu.com$/gi.test(location.host)
  199. },
  200. replacement: function() {
  201. return '\n\n'
  202. }
  203. }
  204. ];
  205.  
  206. var markdown = '*The note is clipped from [here](' + location.href + ').*\n\n' +
  207. (document.title && !el.querySelector('h1') ? '#' + document.title + '\n\n' : '') +
  208. toMarkdown(el.outerHTML, {
  209. gfm: true,
  210. converters: particularWebsitesConverters.concat(generalConverters)
  211. });
  212. copyTextToClipboard(markdown);
  213. }
  214. });
  215.  
  216. // Internal function of to-markdown
  217. function cell (content, node) {
  218. var index = Array.prototype.indexOf.call(node.parentNode.childNodes, node);
  219. var prefix = ' ';
  220. if (index === 0) prefix = '| ';
  221. return prefix + content + ' |';
  222. }
  223.  
  224. // Copy to the clipboard ============================================================
  225. // Copyright (c) Dean Taylor. Original post: http://stackoverflow.com/a/30810322/4158282
  226. function copyTextToClipboard(text) {
  227. var textArea = document.createElement("textarea");
  228. textArea.style.position = 'fixed';
  229. textArea.style.top = 0;
  230. textArea.style.left = 0;
  231. textArea.style.width = '2em';
  232. textArea.style.height = '2em';
  233. textArea.style.padding = 0;
  234. textArea.style.border = 'none';
  235. textArea.style.outline = 'none';
  236. textArea.style.boxShadow = 'none';
  237. textArea.style.background = 'transparent';
  238. textArea.value = text;
  239. document.body.appendChild(textArea);
  240. textArea.select();
  241. try {
  242. var successful = document.execCommand('copy');
  243. var msg = successful ? 'successful' : 'unsuccessful';
  244. console.log('Copying text command was ' + msg);
  245. } catch (err) {
  246. console.log('Oops, unable to copy');
  247. }
  248. document.body.removeChild(textArea);
  249. }
  250.  
  251. })();