git ssb

3+

dust / capsule



Commit bd3e4314469072052f476a8acb6945b96e85b121

added modified snapshooter HTML serialisation

dust committed on 4/4/2016, 5:04:45 AM
Parent: c50a18e69ec34926dd9384c4b51536f9b2133504

Files changed

serializeSelectedHTML.jsadded
serializeSelectedHTML.jsView
@@ -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