Commit bd3e4314469072052f476a8acb6945b96e85b121
added modified snapshooter HTML serialisation
dust committed on 4/4/2016, 5:04:45 AMParent: c50a18e69ec34926dd9384c4b51536f9b2133504
Files changed
serializeSelectedHTML.js | added |
serializeSelectedHTML.js | ||
---|---|---|
@@ -1,0 +1,289 @@ | ||
1 | +/** | |
2 | + * Snapshooter is responsible for returning HTML and computed CSS of all nodes from selected DOM subtree. | |
3 | + * | |
4 | + * @param HTMLElement root Root node for the subtree that will be processed | |
5 | + * @returns {*} object with HTML as a string and CSS as an array of arrays of css properties | |
6 | + */ | |
7 | +function Snapshooter(root) { | |
8 | + "use strict"; | |
9 | + | |
10 | + // list of shorthand properties based on CSSShorthands.in from the Chromium | |
11 | + // code (https://code.google.com/p/chromium/codesearch) | |
12 | + // TODO this list should not be hardcoded here | |
13 | + var shorthandProperties = { | |
14 | + 'animation': 'animation', | |
15 | + 'background': 'background', | |
16 | + 'border': 'border', | |
17 | + 'border-top': 'borderTop', | |
18 | + 'border-right': 'borderRight', | |
19 | + 'border-bottom': 'borderBottom', | |
20 | + 'border-left': 'borderLeft', | |
21 | + 'border-width': 'borderWidth', | |
22 | + 'border-color': 'borderColor', | |
23 | + 'border-style': 'borderStyle', | |
24 | + 'border-radius': 'borderRadius', | |
25 | + 'border-image': 'borderImage', | |
26 | + 'border-spacing': 'borderSpacing', | |
27 | + 'flex': 'flex', | |
28 | + 'flex-flow': 'flexFlow', | |
29 | + 'font': 'font', | |
30 | + 'grid-area': 'gridArea', | |
31 | + 'grid-column': 'gridColumn', | |
32 | + 'grid-row': 'gridRow', | |
33 | + 'list-style': 'listStyle', | |
34 | + 'margin': 'margin', | |
35 | + 'marker': 'marker', | |
36 | + 'outline': 'outline', | |
37 | + 'overflow': 'overflow', | |
38 | + 'padding': 'padding', | |
39 | + 'text-decoration': 'textDecoration', | |
40 | + 'transition': 'transition', | |
41 | + '-webkit-border-after': 'webkitBorderAfter', | |
42 | + '-webkit-border-before': 'webkitBorderBefore', | |
43 | + '-webkit-border-end': 'webkitBorderEnd', | |
44 | + '-webkit-border-start': 'webkitBorderStart', | |
45 | + '-webkit-columns': 'webkitBorderColumns', | |
46 | + '-webkit-column-rule': 'webkitBorderColumnRule', | |
47 | + '-webkit-margin-collapse': 'webkitMarginCollapse', | |
48 | + '-webkit-mask': 'webkitMask', | |
49 | + '-webkit-mask-position': 'webkitMaskPosition', | |
50 | + '-webkit-mask-repeat': 'webkitMaskRepeat', | |
51 | + '-webkit-text-emphasis': 'webkitTextEmphasis', | |
52 | + '-webkit-transition': 'webkitTransition', | |
53 | + '-webkit-transform-origin': 'webkitTransformOrigin' | |
54 | + }, | |
55 | + idCounter = 1; | |
56 | + | |
57 | + /** | |
58 | + * Changes CSSStyleDeclaration to simple Object removing unwanted properties | |
59 | + * ('1','2','parentRule','cssText' etc.) in the process. | |
60 | + * | |
61 | + * @param CSSStyleDeclaration style | |
62 | + * @returns {} | |
63 | + */ | |
64 | + function styleDeclarationToSimpleObject(style) { | |
65 | + var i, l, cssName, camelCaseName, | |
66 | + output = {}; | |
67 | + | |
68 | + for (i = 0, l = style.length; i < l; i++) { | |
69 | + output[style[i]] = style[style[i]]; | |
70 | + } | |
71 | + | |
72 | + // Work around http://crbug.com/313670 (the "content" property is not | |
73 | + // present as a computed style indexed property value). | |
74 | + output.content = fixContentProperty(style.content); | |
75 | + | |
76 | + // Since shorthand properties are not available in the indexed array, copy | |
77 | + // them from named properties | |
78 | + for (cssName in shorthandProperties) { | |
79 | + if (shorthandProperties.hasOwnProperty(cssName)) { | |
80 | + camelCaseName = shorthandProperties[cssName]; | |
81 | + output[cssName] = style[camelCaseName]; | |
82 | + } | |
83 | + } | |
84 | + | |
85 | + return output; | |
86 | + } | |
87 | + | |
88 | + // Partial workaround for http://crbug.com/315028 (single words in the | |
89 | + // "content" property are not wrapped with quotes) | |
90 | + function fixContentProperty(content) { | |
91 | + var values, output, value, i, l; | |
92 | + | |
93 | + output = []; | |
94 | + | |
95 | + if (content) { | |
96 | + //content property can take multiple values - we need to split them up | |
97 | + //FIXME this won't work for '\'' | |
98 | + values = content.match(/(?:[^\s']+|'[^']*')+/g); | |
99 | + | |
100 | + for (i = 0, l = values.length; i < l; i++) { | |
101 | + value = values[i]; | |
102 | + | |
103 | + if (value.match(/^(url\()|(attr\()|normal|none|open-quote|close-quote|no-open-quote|no-close-quote|chapter_counter|'/g)) { | |
104 | + output.push(value); | |
105 | + } else { | |
106 | + output.push("'" + value + "'"); | |
107 | + } | |
108 | + } | |
109 | + } | |
110 | + | |
111 | + return output.join(' '); | |
112 | + } | |
113 | + | |
114 | + function createID(node) { | |
115 | + //":snappysnippet_prefix:" is a prefix placeholder | |
116 | + return ':snappysnippet_prefix:' + node.tagName + '_' + idCounter++; | |
117 | + } | |
118 | + | |
119 | + function dumpCSS(node, pseudoElement) { | |
120 | + var styles; | |
121 | + | |
122 | + styles = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElement); | |
123 | + | |
124 | + if (pseudoElement) { | |
125 | + //if we are dealing with pseudoelement, check if 'content' property isn't empty | |
126 | + //if it is, then we can ignore the whole element | |
127 | + if (!styles.getPropertyValue('content')) { | |
128 | + return null; | |
129 | + } | |
130 | + } | |
131 | + | |
132 | + return styleDeclarationToSimpleObject(styles); | |
133 | + } | |
134 | + | |
135 | + function cssObjectForElement(element, omitPseudoElements) { | |
136 | + return { | |
137 | + id: createID(element), | |
138 | + tagName: element.tagName, | |
139 | + node: dumpCSS(element, null), | |
140 | + before: omitPseudoElements ? null : dumpCSS(element, ':before'), | |
141 | + after: omitPseudoElements ? null : dumpCSS(element, ':after') | |
142 | + }; | |
143 | + } | |
144 | + | |
145 | + function ancestorTagHTML(element, closingTag) { | |
146 | + var i, attr, value, idSeen, | |
147 | + result, attributes; | |
148 | + | |
149 | + if (closingTag) { | |
150 | + return '</' + element.tagName + '>'; | |
151 | + } | |
152 | + | |
153 | + result = '<' + element.tagName; | |
154 | + attributes = element.attributes; | |
155 | + | |
156 | + for (i = 0; i < attributes.length; ++i) { | |
157 | + attr = attributes[i]; | |
158 | + | |
159 | + if (attr.name.toLowerCase() === 'id') { | |
160 | + value = createID(element); | |
161 | + idSeen = true; | |
162 | + } else { | |
163 | + value = attr.value; | |
164 | + } | |
165 | + | |
166 | + result += ' ' + attributes[i].name + '="' + value + '"'; | |
167 | + } | |
168 | + | |
169 | + if (!idSeen) { | |
170 | + result += ' id="' + createID(element) + '"'; | |
171 | + } | |
172 | + | |
173 | + result += '>'; | |
174 | + | |
175 | + return result; | |
176 | + } | |
177 | + | |
178 | + /** | |
179 | + * Replaces all relative URLs (in images, links etc.) with absolute URLs | |
180 | + * @param element | |
181 | + */ | |
182 | + function relativeURLsToAbsoluteURLs(element) { | |
183 | + switch (element.nodeName) { | |
184 | + case 'A': | |
185 | + case 'AREA': | |
186 | + case 'LINK': | |
187 | + case 'BASE': | |
188 | + if (element.hasAttribute('href')) { | |
189 | + element.setAttribute('href', element.href); | |
190 | + } | |
191 | + break; | |
192 | + case 'IMG': | |
193 | + case 'IFRAME': | |
194 | + case 'INPUT': | |
195 | + case 'FRAME': | |
196 | + case 'SCRIPT': | |
197 | + if (element.hasAttribute('src')) { | |
198 | + element.setAttribute('src', element.src); | |
199 | + } | |
200 | + break; | |
201 | + case 'FORM': | |
202 | + if (element.hasAttribute('action')) { | |
203 | + element.setAttribute('action', element.action); | |
204 | + } | |
205 | + break; | |
206 | + } | |
207 | + } | |
208 | + | |
209 | + function init() { | |
210 | + var css = [], | |
211 | + ancestorCss = [], | |
212 | + descendants, | |
213 | + descendant, | |
214 | + htmlSegments, | |
215 | + leadingAncestorHtml, | |
216 | + trailingAncestorHtml, | |
217 | + reverseAncestors = [], | |
218 | + i, l, | |
219 | + parent, | |
220 | + clone; | |
221 | + | |
222 | + descendants = root.getElementsByTagName('*'); | |
223 | + | |
224 | + parent = root.parentElement; | |
225 | + while (parent && parent !== document.body) { | |
226 | + reverseAncestors.push(parent); | |
227 | + parent = parent.parentElement; | |
228 | + } | |
229 | + | |
230 | + // First we go through all nodes and dump all CSS | |
231 | + css.push(cssObjectForElement(root)); | |
232 | + | |
233 | + for (i = 0, l = descendants.length; i < l; i++) { | |
234 | + css.push(cssObjectForElement(descendants[i])); | |
235 | + } | |
236 | + | |
237 | + for (i = reverseAncestors.length - 1; i >= 0; i--) { | |
238 | + ancestorCss.push(cssObjectForElement(reverseAncestors[i], true)); | |
239 | + } | |
240 | + | |
241 | + // Next we dump all HTML and update IDs | |
242 | + // Since we don't want to touch original DOM and we want to change IDs, we clone the original DOM subtree | |
243 | + clone = root.cloneNode(true); | |
244 | + descendants = clone.getElementsByTagName('*'); | |
245 | + idCounter = 1; | |
246 | + | |
247 | + clone.setAttribute('id', createID(clone)); | |
248 | + | |
249 | + for (i = 0, l = descendants.length; i < l; i++) { | |
250 | + descendant = descendants[i]; | |
251 | + descendant.setAttribute('id', createID(descendant)); | |
252 | + relativeURLsToAbsoluteURLs(descendant); | |
253 | + } | |
254 | + | |
255 | + // Build leading and trailing HTML for ancestors | |
256 | + htmlSegments = []; | |
257 | + for (i = reverseAncestors.length - 1; i >= 0; i--) { | |
258 | + htmlSegments.push(ancestorTagHTML(reverseAncestors[i])); | |
259 | + } | |
260 | + leadingAncestorHtml = htmlSegments.join(''); | |
261 | + | |
262 | + htmlSegments = []; | |
263 | + for (i = 0, l = reverseAncestors.length; i < l; i++) { | |
264 | + htmlSegments.push(ancestorTagHTML(reverseAncestors[i], true)); | |
265 | + } | |
266 | + trailingAncestorHtml = htmlSegments.join(''); | |
267 | + | |
268 | + return JSON.stringify({ | |
269 | + html: clone.outerHTML, | |
270 | + leadingAncestorHtml: leadingAncestorHtml, | |
271 | + trailingAncestorHtml: trailingAncestorHtml, | |
272 | + css: css, | |
273 | + ancestorCss: ancestorCss | |
274 | + }); | |
275 | + } | |
276 | + | |
277 | + return init(); | |
278 | +} | |
279 | + | |
280 | +// silliness with divs to get a single element to give to snapshooter | |
281 | +var selectionDiv = document.createElement('div') | |
282 | +var selection = window.getSelection().getRangeAt(0) | |
283 | +var startNode = selection.startContainer.parentNode.cloneNode(true) | |
284 | +var endNode = selection.endContainer.parentNode.cloneNode(true) | |
285 | + | |
286 | +selectionDiv.appendChild(startNode) | |
287 | +selectionDiv.appendChild(endNode) | |
288 | + | |
289 | +Snapshooter(selectionDiv) |
Built with git-ssb-web