├── .gitignore ├── README.md ├── bookmarklet.py ├── build.sh ├── pinboard-particular.js └── pushgist.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | build 3 | node_modules 4 | *.sublime-project 5 | *.sublime-workspace -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # particular pinboard 2 | 3 | A modification of the [pinboard bookmarklet][pinboard] with enhancements that appeal to my fiddly nature. 4 | 5 | - Title is cleaned of extraneous "SEO-junk" through heuristics based on page structure 6 | - Description field is populated with the selected text, or pulled from description tags in page header 7 | - Tags are added according to keyword rules setup by the user. 8 | 9 | This bookmarklet is based on an existing modification to the pinboard bookmarklet by [Ben Ward][benward]. The build file makes variants for "Read Later" and for users of [Pinswift][pinswift] on iOS. 10 | 11 | ## Bookmarklets 12 | 13 | - [Regular](https://gist.github.com/cfd44a7a72442bb734eb) 14 | - [Read Later](https://gist.github.com/98d36e72c48d1795553a) 15 | - [Pinswift iOS app](https://gist.github.com/ff3d0c2e84def744a53d) 16 | 17 | # Building 18 | 19 | Run `build.sh` to compile the source file into bookmarklet form. You must have [UglifyJS][uglify] and [node][node] installed. To install node I recommend [homebrew][homebrew]: 20 | 21 | brew install node 22 | 23 | To install UglifyJS: 24 | 25 | npm install uglify-js 26 | 27 | Please note the dash between "uglify" and "js". It is possible to install an older version without using the dash that does include the command line tool. 28 | 29 | # Customization 30 | 31 | You can enable or disable various features by modifying the constants at the top of the source file. These should be well documented in source. Of particular interest for users are keyword rules which determine the tags based on the text of the document. 32 | 33 | # License 34 | 35 | This code is public domain. Fork it as the basis for your own particular pinboard and let me know what you discover. 36 | 37 | [pinboard]:http://pinboard.in/howto/ 38 | [benward]:https://gist.github.com/BenWard/801657 39 | [pinswift]:http://pinswiftapp.com/ 40 | [homebrew]:http://mxcl.github.com/homebrew/ 41 | [node]:http://nodejs.org/ 42 | [uglify]:https://github.com/mishoo/UglifyJS -------------------------------------------------------------------------------- /bookmarklet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # this just wraps minified javascript code in a function and prepends javascript: 4 | 5 | import sys 6 | text = sys.stdin.read() 7 | sys.stdout.write("javascript:%s" % text) -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SRC=pinboard-particular.js 4 | OUT=build 5 | MINIFY=$(which uglifyjs) 6 | BACKUP_UGLIFY="./node_modules/uglify-js/bin/uglifyjs" 7 | if [ -z "$MINIFY" ]; then 8 | if [ -x "$BACKUP_UGLIFY" ]; then 9 | MINIFY="$BACKUP_UGLIFY" 10 | else 11 | printf "No uglifyjs found. Bailing out\n" 12 | exit 1 13 | fi 14 | fi 15 | MINIFY="${MINIFY} -mt" 16 | 17 | if [ ! -d $OUT ]; then 18 | mkdir $OUT 19 | fi 20 | 21 | printf "javascript:%s" "$($MINIFY 2>/dev/null < $SRC)" > $OUT/bookmark.js 22 | printf "javascript:%s" > $OUT/readlater.js 23 | sed 's/readlater = false/readlater = true/' $SRC | 24 | $MINIFY 2>/dev/null >> $OUT/readlater.js 25 | printf "javascript:%s" > $OUT/pinswift.js 26 | sed 's/appUrl = null/appUrl = "pinswift:\/\/x-callback-url\/add?"/' $SRC | 27 | $MINIFY 2>/dev/null >> $OUT/pinswift.js 28 | -------------------------------------------------------------------------------- /pinboard-particular.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | /******************* begin configuration options ***************************/ 4 | 5 | // Change `read` to true to invoke the promptless, self-closing version of the 6 | // bookmarklet. 7 | var readlater = false; 8 | var appUrl = null; 9 | 10 | // When set to true, selected text is quoted using
. 11 | // Note that Markdown is not supported in link descriptions because of an XSS 12 | // vulnerability: https://twitter.com/Pinboard/status/22436355472625664 13 | var quoteSelection = false; 14 | 15 | // When this text appears in title or description, they are added as tags. 16 | var tagKeywords = { 17 | javascript:'javascript', 18 | js:'javascript', 19 | python:'python', 20 | ios:'ios', 21 | youtube:'video', 22 | vimeo:'video', 23 | video:'video', 24 | books:'book', 25 | book:'book' 26 | }; 27 | 28 | // this matches domain names to special selectors for the title 29 | var titleTweaks = { 30 | "github.com":".entry-title .js-current-repository" 31 | }; 32 | 33 | // this matches domain names to special selectors for the title 34 | var descriptionTweaks = { 35 | "www.kickstarter.com":".short-blurb" 36 | }; 37 | 38 | // limit long titles and descriptions, mostly to avoid 'HTTP/1.0 414 Request URI too long' 39 | var textLengthLimit = 1000; 40 | 41 | /********************* begin code ********************************************/ 42 | 43 | // reduce a string to some canonical representation 44 | // right now this just picks a case but could get really complicated if need be 45 | // see: http://stackoverflow.com/questions/227950/programatic-accent-reduction-in-javascript-aka-text-normalization-or-unaccentin 46 | // some people like stack overflow straighten their curly quotes 47 | var normalize = function(string) { 48 | return string.toLowerCase(); 49 | }; 50 | 51 | var elementText = function(el) { 52 | return el ? el.textContent.trim().replace(/\s+/g,' ') : null; 53 | }; 54 | 55 | var normalizedDocumentTitle = normalize(document.title); 56 | 57 | // used as tes 58 | var isSubtitle = function(string) { 59 | if(string) { 60 | return normalizedDocumentTitle.indexOf(normalize(string)) !== -1; 61 | } 62 | else { 63 | return false; 64 | } 65 | }; 66 | 67 | // loops over a node list and applies a function 68 | // returning the first value that is non-null 69 | var selectFromNodeList = function(nodeList,func,thisObj) { 70 | thisObj = thisObj || window; 71 | var l = nodeList.length; 72 | var result; 73 | for(var i=0;i headerTitle.length)) { 130 | headerTitle = h_text; 131 | } 132 | return null; 133 | }); 134 | } 135 | if(headerTitle) { 136 | return headerTitle; 137 | } 138 | 139 | // method 3 - just return the title 140 | return documentTitle; 141 | }; 142 | 143 | var getTags = function(text) { 144 | text = normalize(text); 145 | var tags = []; 146 | var re; 147 | for(var keyword in tagKeywords) { 148 | re = keyword instanceof RegExp ? keyword : new RegExp("\\b"+keyword+"\\b","i"); 149 | if(re.test(text)) { 150 | tags.push(tagKeywords[keyword]); 151 | } 152 | } 153 | return tags; 154 | }; 155 | 156 | var getMetaDescription = function() { 157 | var e; 158 | e = document.querySelector("meta[name='description']"); 159 | if(e) { 160 | return e.content.trim().replace(/\s+/g,' '); 161 | } 162 | e = document.querySelector("meta[property='og:description']"); 163 | if(e) { 164 | return e.content.trim().replace(/\s+/g,' '); 165 | } 166 | return ""; 167 | }; 168 | 169 | var getDescription = function() { 170 | var text; 171 | // Grab the text selection (if any) and quote it 172 | if('' !== (text = String(document.getSelection()))) { 173 | if(quoteSelection) { 174 | text = text.trim().split("\n").map(function(s) {return "
"+s+"
";}).join("\n"); 175 | } 176 | } 177 | 178 | var host = location.hostname; 179 | var e; 180 | if(host in descriptionTweaks) { 181 | e = document.querySelector(descriptionTweaks[host]); 182 | if(e) { 183 | return elementText(e); 184 | } 185 | } 186 | 187 | if(!text) { 188 | text = getMetaDescription(); 189 | } 190 | return text; 191 | }; 192 | 193 | // Assembles default form pre-fill arguments. 194 | var url = location.href; 195 | var title = getTitle(); 196 | var description = getDescription(); 197 | // remove if title is trailing or leading 198 | var ix = description.indexOf(title); 199 | if(ix === 0) { 200 | description = description.substring(title.length).trim(); 201 | } 202 | else if(ix === description.length-title.length) { 203 | description = description.substring(0,ix).trim(); 204 | } 205 | 206 | var tags = getTags(document.title+" "+description+" "+getMetaDescription()); 207 | 208 | if(textLengthLimit > 0) { 209 | title = title.substring(0, textLengthLimit); 210 | description = description.substring(0, textLengthLimit); 211 | } 212 | 213 | var args = [ 214 | 'url=', encodeURIComponent(url), 215 | '&title=', encodeURIComponent(title), 216 | '&description=', encodeURIComponent(description), 217 | '&tags=', encodeURIComponent(tags.join(" ")) 218 | ]; 219 | 220 | // If readlater mode, add the auto-close parameter and read-later flag: 221 | if(readlater) { 222 | args = args.concat([ 223 | '&later=', 'yes', 224 | '&jump=', 'close' 225 | ]); 226 | } 227 | if(appUrl) { 228 | args = args.concat([ 229 | '&x-source=Safari', 230 | '&x-success=',encodeURIComponent(location.href), 231 | '&x-cancel=',encodeURIComponent(location.href) 232 | ]); 233 | window.location = appUrl+args.join(''); 234 | } 235 | else { 236 | var pin = open('http://pinboard.in/add?'+args.join(''), 'Pinboard', 'toolbar=no,width=610,height=350'); 237 | 238 | // Send the window to the background if readlater mode. 239 | if(readlater) { 240 | pin.blur(); 241 | } 242 | } 243 | 244 | })(); 245 | -------------------------------------------------------------------------------- /pushgist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUT=build 4 | 5 | ./build.sh 6 | jist $OUT/bookmark.js -u cfd44a7a72442bb734eb 7 | jist $OUT/readlater.js -u 98d36e72c48d1795553a 8 | jist $OUT/pinswift.js -u ff3d0c2e84def744a53d 9 | --------------------------------------------------------------------------------