changeset 7:a1a934adff8d version-2

Add support for favicons
author Guido Berhoerster <guido+booket@berhoerster.name>
date Sun, 14 Sep 2014 23:12:37 +0200
parents e9ad4c625b7a
children aca8d797d569
files .hgignore Makefile booket.css booket.html booket.js missing-favicon.src.svg
diffstat 6 files changed, 332 insertions(+), 26 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Sun Sep 14 23:12:37 2014 +0200
@@ -0,0 +1,3 @@
+syntax: regexp
+
+(?<!\.src)\.svg$
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Sun Sep 14 23:12:37 2014 +0200
@@ -0,0 +1,44 @@
+#
+# Copyright (C) 2014 Guido Berhoerster <guido+booket@berhoerster.name>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+SCOUR :=	scour
+XMLLINT :=	xmllint
+
+SVGIMAGES :=	missing-favicon.svg
+
+.DEFAULT_TARGET = all
+
+.PHONY: all clean clobber
+
+all: $(SVGIMAGES)
+
+%.svg: %.src.svg
+	$(SCOUR) --quiet -i $< --create-groups --enable-id-stripping \
+	    --enable-comment-stripping --remove-metadata \
+	    --no-renderer-workaround --strip-xml-prolog --enable-viewboxing \
+	    --set-precision=5 | $(XMLLINT) --format --noblanks --output $@ -
+
+clean:
+	rm -f $(SVGIMAGES)
+
+clobber: clean
--- a/booket.css	Wed Sep 10 19:45:23 2014 +0200
+++ b/booket.css	Sun Sep 14 23:12:37 2014 +0200
@@ -63,6 +63,10 @@
     white-space: nowrap;
 }
 
+img {
+    border: none;
+}
+
 h1 {
     font-size: 2em;
     margin: .67em 0
@@ -130,6 +134,11 @@
     margin: 1em 0 0 0;
 }
 
+form.bookmark-editor-form img.bookmark-favicon {
+    display: block;
+    padding: 1px 0;
+}
+
 #bookmarklet,
 #keyboard-shortcuts {
     float: right;
--- a/booket.html	Wed Sep 10 19:45:23 2014 +0200
+++ b/booket.html	Sun Sep 14 23:12:37 2014 +0200
@@ -51,6 +51,9 @@
         name="url" size="60" placeholder="http://example.com/"></input></label>
         <label>Title <input type="text" name="title" size="60"
         placeholder="A Title"></input></label>
+        <label>Favicon <img width="16" height="16" src="missing-favicon.svg"
+        class="bookmark-favicon" alt=""></img><input type="hidden"
+        name="favicon"></input></label>
         <div>
           <ul class="tag-input-list"></ul>
           <button type="button" name="more-tags">Add more tags</button>
@@ -144,7 +147,8 @@
         <template id="bookmark-template">
           <li>
             <details>
-              <summary><p>
+              <summary><p><img width="16" height="16"
+                  class="bookmark-favicon"></img>
                   <a class="bookmark-link" target="_blank"></a>
                   <span class="bookmark-hostname"></span>
                 </p>
--- a/booket.js	Wed Sep 10 19:45:23 2014 +0200
+++ b/booket.js	Sun Sep 14 23:12:37 2014 +0200
@@ -25,15 +25,73 @@
 'use strict';
 
 var BOOKMARKLET_URI =
-    'javascript:(function () {' +
-        '\'use strict\';' +
+    'javascript:(function() {' +
+         '\'use strict\';' +
+     '' +
+        'function displayBookmarkData(bookmarkData) {' +
+            'window.alert(\'Copy the following data and paste it into \' +' +
+                '\'Booket:\\n\\n\' + JSON.stringify(bookmarkData));' +
+        '}' +
+    '' +
+        'var bookmarkData = {' +
+            '\'url\': document.URL,' +
+            '\'title\': document.title,' +
+            '\'favicon\': undefined' +
+        '};' +
+        'var faviconLinkElement;' +
+        'var faviconUrls = [];' +
+        'var aElement;' +
+        'var canvasElement;' +
+        'var canvasCtx;' +
+        'var imgElement;' +
+    '' +
+        'aElement = document.createElement(\'a\');' +
+        'aElement.href = document.URL;' +
+    '' +
+        'faviconUrls.push(aElement.protocol + \'//\' + aElement.host + ' +
+            '\'/favicon.ico\');' +
+    '' +
+        'faviconLinkElement = document.querySelector(' +
+            '\'link[rel~=\\\'icon\\\']\');' +
+        'if (faviconLinkElement !== null) {' +
+            'faviconUrls.push(faviconLinkElement.href);' +
+        '}' +
     '' +
-        'window.alert(\'Copy the following data and paste it into \' +' +
-            '\'Booket:\\n\\n\' + JSON.stringify({' +
-                '\'url\': document.URL,' +
-                '\'title\': document.title' +
-            '}));' +
-    '}) ();';
+        'canvasElement = document.createElement(\'canvas\');' +
+        'canvasCtx = canvasElement.getContext(\'2d\');' +
+    '' +
+        'imgElement = new Image();' +
+        'imgElement.addEventListener(\'load\', function(e) {' +
+            'var faviconUrl;' +
+    '' +
+            'canvasElement.width = 16;' +
+            'canvasElement.height = 16;' +
+            'canvasCtx.clearRect(0, 0, 16, 16);' +
+            'try {' +
+                'canvasCtx.drawImage(this, 0, 0, 16, 16);' +
+                'bookmarkData.favicon = canvasElement.toDataURL();' +
+            '} catch (exception) {' +
+                'faviconUrl = faviconUrls.pop();' +
+            '}' +
+            'if (bookmarkData.favicon !== undefined || ' +
+                    'faviconUrl === undefined) {' +
+                'displayBookmarkData(bookmarkData);' +
+            '} else {' +
+                'imgElement.src = faviconUrl;' +
+            '}' +
+        '});' +
+        'imgElement.addEventListener(\'error\', function(e) {' +
+            'var faviconUrl;' +
+    '' +
+            'faviconUrl = faviconUrls.pop();' +
+            'if (faviconUrl !== undefined) {' +
+                'imgElement.src = faviconUrl;' +
+            '} else {' +
+                'displayBookmarkData(bookmarkData);' +
+            '}' +
+        '});' +
+        'imgElement.src = faviconUrls.pop();' +
+    '})();';
 
 
 /*
@@ -348,7 +406,7 @@
  * model
  */
 
-var Bookmark = function (url, title, tags, ctime, mtime) {
+var Bookmark = function (url, title, favicon, tags, ctime, mtime) {
     var parsedTime;
 
     if (!isString(url)) {
@@ -358,6 +416,12 @@
 
     this.title = (isString(title) && title !== '') ? title : url;
 
+    if (isString(favicon) && favicon.match(/^data:image\/png;base64,/)) {
+        this.favicon = favicon;
+    } else {
+        this.favicon = undefined;
+    }
+
     if (Array.isArray(tags)) {
         // remove duplicates, non-string or empty tags and tags containing
         // commas
@@ -647,7 +711,8 @@
     parsedData.bookmarks.forEach(function (bookmark) {
         if (isString(bookmark.url) && bookmark.url !== '') {
             bookmarks.push(new Bookmark(bookmark.url, bookmark.title,
-                bookmark.tags, bookmark.ctime, bookmark.mtime));
+                bookmark.favicon, bookmark.tags, bookmark.ctime,
+                bookmark.mtime));
         }
     }, this);
 
@@ -822,7 +887,6 @@
     var saveFormElement;
     var loadFormElement;
     var newNode;
-    var editorFormElement;
 
     ObservableMixin.call(this);
 
@@ -839,17 +903,24 @@
     newNode = document.importNode(
         document.querySelector('#bookmark-editor-template').content, true);
 
-    editorFormElement = newNode.querySelector('form.bookmark-editor-form');
-    editorFormElement.querySelector('legend').textContent = 'Add Bookmark';
-    editorFormElement.querySelector('input:not([type="hidden"])').accessKey =
-        'a';
-    editorFormElement.addEventListener('input', this);
-    editorFormElement.addEventListener('click', this);
-    editorFormElement.addEventListener('submit', this);
-    editorFormElement.addEventListener('reset', this);
+    this.editorFormElement = newNode.querySelector('form.bookmark-editor-form');
+    this.editorFormElement.querySelector('legend').textContent = 'Add Bookmark';
+    this.editorFormElement.querySelector(
+        'input:not([type="hidden"])').accessKey = 'a';
+    this.editorFormElement.addEventListener('input', this);
+    this.editorFormElement.addEventListener('click', this);
+    this.editorFormElement.addEventListener('submit', this);
+    this.editorFormElement.addEventListener('reset', this);
+
+    this.faviconImageElement =
+        this.editorFormElement.querySelector('img.bookmark-favicon');
+    this.faviconImageElement.addEventListener('load', this);
+    this.faviconImageElement.addEventListener('error', this);
+
+    this.missingFaviconUri = this.faviconImageElement.src;
 
     this.editTagListElement =
-        editorFormElement.querySelector('ul.tag-input-list');
+        this.editorFormElement.querySelector('ul.tag-input-list');
     this.editTagListElement.appendChild(this.createTagInputElement(''));
 
     saveFormElement.parentNode.insertBefore(newNode,
@@ -876,6 +947,19 @@
     var i;
 
     switch (e.type) {
+    case 'error':
+        if (e.target.classList.contains('bookmark-favicon')) {
+            if (e.target.src !== this.missingFaviconUri) {
+                e.target.src = this.missingFaviconUri;
+            }
+        }
+        break;
+    case 'load':
+        if (e.target.classList.contains('bookmark-favicon')) {
+            this.editorFormElement.favicon.value =
+                (e.target.src !== this.missingFaviconUri) ?  e.target.src : '';
+        }
+        break;
     case 'input':
         if (e.target.name === 'bookmarklet-import') {
             // get rid of any preceding text
@@ -893,6 +977,10 @@
             if (isString(parsedData.title) && parsedData.title !== '') {
                 e.target.form.elements.title.value = parsedData.title;
             }
+            if (isString(parsedData.favicon) &&
+                    parsedData.favicon.match(/^data:image\/png;base64,/)) {
+                this.faviconImageElement.src = parsedData.favicon;
+            }
         }
         break;
     case 'click':
@@ -928,7 +1016,7 @@
             }
 
             this.notify('save-bookmark', e.target.url.value,
-                e.target.title.value, tags);
+                e.target.title.value, e.target.favicon.value, tags);
 
             e.target.reset();
         }
@@ -937,6 +1025,9 @@
         if (e.target.classList.contains('bookmark-editor-form')) {
             e.target.blur();
 
+            e.target.querySelector('img.bookmark-favicon').src =
+                this.missingFaviconUri;
+
             // remove all but one tag input element
             while (this.editTagListElement.firstChild !== null) {
                 this.editTagListElement.removeChild(
@@ -979,6 +1070,7 @@
     this.tagInputTemplate = document.querySelector('#tag-input-template');
 
     this.bookmarkListElement = document.querySelector('ul#bookmark-list');
+    this.bookmarkListElement.addEventListener('input', this);
     this.bookmarkListElement.addEventListener('click', this);
     this.bookmarkListElement.addEventListener('submit', this);
     this.bookmarkListElement.addEventListener('reset', this);
@@ -991,17 +1083,65 @@
 
     this.bookmarkMessageElement = document.querySelector('#bookmark-message');
 
+    this.missingFaviconUri = '';
+
     this.updateBookmarkMessage();
 };
 
 extend(BookmarkView, ObservableMixin);
 
 BookmarkView.prototype.handleEvent = function (e) {
+    var bookmarkletData;
+    var parsedData;
     var i;
     var tags = [];
     var node;
 
     switch (e.type) {
+    case 'error':
+        if (e.target.classList.contains('bookmark-favicon')) {
+            if (e.target.src !== this.missingFaviconUri) {
+                e.target.src = this.missingFaviconUri;
+            }
+        }
+        break;
+    case 'load':
+        if (e.target.classList.contains('bookmark-favicon')) {
+            node = e.target;
+            while ((node = node.parentNode) !== null) {
+                if (node.classList.contains('bookmark-editor-form')) {
+                    node.favicon.value =
+                        (e.target.src !== this.missingFaviconUri) ?
+                        e.target.src : '';
+                    break;
+                }
+            }
+        }
+        break;
+    case 'input':
+        if (e.target.name === 'bookmarklet-import') {
+            // get rid of any preceding text
+            bookmarkletData = e.target.value.replace(/^[^{]*/, '');
+
+            try {
+                parsedData = JSON.parse(bookmarkletData);
+            } catch (exception) {
+                return;
+            }
+
+            if (isString(parsedData.url) && parsedData.url !== '') {
+                e.target.form.elements.url.value = parsedData.url;
+            }
+            if (isString(parsedData.title) && parsedData.title !== '') {
+                e.target.form.elements.title.value = parsedData.title;
+            }
+            if (isString(parsedData.favicon) &&
+                    parsedData.favicon.match(/^data:image\/png;base64,/)) {
+                e.target.form.querySelector('img.bookmark-favicon').src =
+                    parsedData.favicon;
+            }
+        }
+        break;
     case 'click':
         switch (e.target.name) {
         case 'edit-bookmark':
@@ -1040,7 +1180,8 @@
             }
 
             this.notify('save-bookmark', e.target.url.value,
-                e.target.title.value, tags, e.target['original-url'].value);
+                e.target.title.value, e.target.favicon.value, tags,
+                e.target['original-url'].value);
         } else if (e.target.id === 'search-form') {
             // search
             e.preventDefault();
@@ -1083,6 +1224,7 @@
 BookmarkView.prototype.onBookmarkAdded = function (bookmark) {
     var newNode;
     var bookmarkElement;
+    var faviconElement;
     var linkElement;
     var hostnameElement;
     var urlElement;
@@ -1095,6 +1237,11 @@
     bookmarkElement = newNode.querySelector('li');
     bookmarkElement.dataset.bookmarkUrl = bookmark.url;
 
+    faviconElement = bookmarkElement.querySelector('img.bookmark-favicon');
+    faviconElement.src = (bookmark.favicon) ? bookmark.favicon :
+        this.missingFaviconUri;
+    faviconElement.alt = '';
+
     linkElement = bookmarkElement.querySelector('a.bookmark-link');
     linkElement.textContent = linkElement.title = bookmark.title;
     linkElement.href = bookmark.url;
@@ -1211,6 +1358,7 @@
     var bookmarkElement;
     var newNode;
     var formElement;
+    var faviconImageElement;
     var editTagListElement;
 
     bookmarkElement =
@@ -1231,6 +1379,14 @@
     formElement.url.value = bookmark.url;
     formElement.title.value = bookmark.title;
 
+    faviconImageElement = formElement.querySelector('img.bookmark-favicon');
+    faviconImageElement.addEventListener('load', this);
+    faviconImageElement.addEventListener('error', this);
+    this.missingFaviconUri = faviconImageElement.src;
+    if (bookmark.favicon) {
+        faviconImageElement.src = bookmark.favicon;
+    }
+
     editTagListElement = formElement.querySelector('ul.tag-input-list');
     bookmark.tags.forEach(function (tag) {
         editTagListElement.appendChild(this.createTagInputElement(tag));
@@ -1379,8 +1535,8 @@
         this.bookmarkModel.get(bookmarkUrl));
 };
 
-BooketController.prototype.onSaveBookmark = function (url, title, tags,
-        originalUrl) {
+BooketController.prototype.onSaveBookmark = function (url, title,
+        favicon, tags, originalUrl) {
     var ctime;
 
     if (originalUrl === undefined) {
@@ -1403,7 +1559,7 @@
 
         this.bookmarkModel.delete(originalUrl);
     }
-    this.bookmarkModel.add(new Bookmark(url, title, tags, ctime));
+    this.bookmarkModel.add(new Bookmark(url, title, favicon, tags, ctime));
 };
 
 BooketController.prototype.onDeleteBookmark = function (bookmarkUrl) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/missing-favicon.src.svg	Sun Sep 14 23:12:37 2014 +0200
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="16"
+   height="16"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="missing-favicon.svg">
+  <title
+     id="title2985">Missing Favicon</title>
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="43.9375"
+     inkscape:cx="7.4651494"
+     inkscape:cy="8"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:window-width="1542"
+     inkscape:window-height="947"
+     inkscape:window-x="54"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0">
+    <inkscape:grid
+       type="xygrid"
+       id="grid3006"
+       empspacing="5"
+       visible="true"
+       enabled="true"
+       snapvisiblegridlinesonly="true" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title>Missing Favicon</dc:title>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Guido Berhoerster</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:date>2014-09-14</dc:date>
+        <cc:license
+           rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
+      </cc:Work>
+      <cc:License
+         rdf:about="http://creativecommons.org/licenses/publicdomain/">
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Reproduction" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Distribution" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1036.3622)">
+    <rect
+       style="fill:none;stroke:#555753;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.00000029;stroke-opacity:1;stroke-dasharray:1,1;stroke-dashoffset:0.5"
+       id="rect3008"
+       width="15"
+       height="15"
+       x="0.5"
+       y="1036.8622" />
+  </g>
+</svg>