javascript:(function(){setTimeout(function(){ var VER='2025.11.25-d'; var ST='BEGIN_WORDCOUNT_EXCLUSION'; var ET='END_WORDCOUNT_EXCLUSION'; var CONSEL=[ /* Content selectors to try in order */ '.content', /* LessWrong */ 'textarea[aria-label="Markdown value"]', /* gissue */ '.tiptap', /* Substack */ 'textarea.pencraft', /* Substack comment */ '.cm-content', /* Replit */ 'textarea', /* lots of things, eg, eat-the-richtext */ 'body', /* fallback */ /* Nothing below this line ever needed? */ 'textarea.MuiTextarea-textarea.MuiInputBase-input', /* LessWrong title */ 'input[aria-label*="title"]', /* gissue title */ 'textarea[aria-label*="description"]', 'article', 'main', 'textarea.comment-form-textarea', '.PostsPage-postContent', '.PostBody-root', ]; /* AI-generated black magic: - NFC normalize; CRLF -> \n - Unicode spaces to ascii (LSEP/PSEP -> \n) - Drop invisibles (BOM, ZWSP, SHY, bidi controls, isolates) - KEEP emoji machinery: ZWJ (U+200D) and VS16 (U+FE0F) */ function sanitize(s){ s=String(s??''); if(s.normalize)s=s.normalize('NFC'); s=s.replace(/\r\n?/g,'\n'); s=s.replace(/[\u00AD\u200B\u2060\uFEFF\u200E\u200F\u202A-\u202E\u2066-\u2069]/g,''); s=s.replace(/[\u2028\u2029]/g,'\n'); s=s.replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g,' '); s=s.replace(/[ \t\f\v]+/g,' '); s=s.replace(/[ \t\f\v]*\n[ \t\f\v]*/g,'\n'); s=s.replace(/\n{3,}/g,'\n\n'); return s } /* Wordcount algorithm: - Split by whitespace into tokens - Count tokens w/ >=1 meat characters - Meat = letters (\p{L}), numbers (\p{N}), or emoji - Scaffold = apostrophes/hyphens/punctuation */ function tallyho(s){ s=sanitize(s);if(!s)return 0; var tokens=s.split(/\s+/); var pic;try{pic=new RegExp('\\p{Extended_Pictographic}','u')} catch{pic=/[\u2600-\u27BF\u{1F300}-\u{1FAFF}]/u} var meat=/[\p{L}\p{N}]/u; var n=0; for(var i=0;i/g,'>')} function highlightExcluded(s,exs){ var result=escHtml(s); exs.forEach(function(excl){ var pattern=escHtml(excl).replace(/[.*+?^${}()|[\]\\]/g,'\\$&').replace(/\s+/g,'\\s+'); result=result.replace(new RegExp(pattern,'gi'),'$&'); }); return result.replace(/\n\n/g,'
').replace(/\n/g,'
') } function textract(el){ /* textareas/inputs: use .value */ if(el.tagName==='TEXTAREA'||el.tagName==='INPUT')return el.value||''; /* other elements: clone & extract text */ var c=el.cloneNode(true); c.querySelectorAll('script,style,nav,footer,header,iframe').forEach(function(x){x.remove()}); /* preserve line breaks, paragraph breaks */ c.querySelectorAll('br').forEach(function(br){ var tn=document.createTextNode('¶BR¶'); br.parentNode.replaceChild(tn,br) }); c.querySelectorAll('p,div,h1,h2,h3,h4,h5,h6,li,td,th').forEach(function(el){el.insertAdjacentText('afterend','¶PARA¶')}); var txt=(c.innerText||c.textContent); /* placeholders back to newlines */ return txt.replace(/\s*¶BR¶\s*/g,'\n').replace(/\s*¶PARA¶\s*/g,'\n\n') } function findFirstNonEmpty(selector){ var els=document.querySelectorAll(selector); for(var j=0;j0?minusTerms.join(' – '):'0')+' = '+twc.toLocaleString()+" words:"; var words=document.createElement('div'); words.className='mn r3 p8 lh'; words.style.cssText='font-size:11px;color:#444;flex:1 1 auto;min-height:0;overflow:auto;background:#f5f5f5'; words.innerHTML=highlightExcluded(txt,exs); var cb=document.createElement('button'); cb.className='ns mn r3 p8 bld tc'; cb.style.cssText='margin-top:10px;background:#e3f2fd;border:2px dashed #2196F3;font-size:9px;color:#1976D2'; /* Return whatever text in the popup is highlighted */ function getsel(){ var sel=window.getSelection(); if(sel&&sel.rangeCount>0&&sel.toString().trim()){ var range=sel.getRangeAt(0); if(words.contains(range.commonAncestorContainer)){return sel.toString()} } return '' } function buttonup(){ var hi=!!getsel(); cb.style.background=hi?'#e3f2fd':'#f5f5f5'; cb.style.color=hi?'#1976D2':'#999'; cb.style.cursor=hi?'pointer':'default'; cb.textContent=hi?'Copy exclusion tags' :'Highlight text to exclude from wordcount' } buttonup(); document.addEventListener('selectionchange',buttonup); cb.addEventListener('click',function(){ var t=getsel();if(!t)return; navigator.clipboard.writeText([ST,t,ET].join('\n')); cb.style.background='#c8e6c9'; cb.textContent='Copied! Now hit paste at the end of your doc' }); var footer=document.createElement('div'); footer.style.cssText='display:flex;justify-content:space-between;margin-top:8px;font-size:9px;color:#999'; var gh=document.createElement('a'); gh.href='https://github.com/dreeves/tallyglot'; gh.target='_blank'; gh.textContent='Tallyglot v'+VER; var dbg=document.createElement('a'); dbg.href='#'; dbg.textContent='debug link'; dbg.addEventListener('click',function(e){ e.preventDefault(); var w=window.open('','_blank'),h="

Tallyglot Debug Page

We use the first of these elements that's nonempty:

"; function add(label,content){ var wc=" ("+tallyho(content)+" words)"; h+='
'+label+wc+'
'+escHtml(content)+'
' } function buildSel(e){ var s=e.tagName.toLowerCase(); if(e.id)s+='#'+e.id; if(e.className)s+='.'+e.className.split(/\s+/).join('.'); return s } CONSEL.forEach(function(selector){ var r=findFirstNonEmpty(selector); var m=r.el?' → '+buildSel(r.el)+'':''; add(selector+m+(selector===sel?' ✓':''),r.txt) }); h+="

All Input/Textarea/Contenteditable elements:

"; document.querySelectorAll('input[type="text"],input:not([type]),textarea,[contenteditable="true"],[role="textbox"]').forEach(function(e){ add(buildSel(e),textract(e)) }); w.document.documentElement.innerHTML=h }); footer.appendChild(gh); footer.appendChild(dbg); [tg,words,cb,footer].forEach(function(el){m.appendChild(el)}); document.body.appendChild(m); function isTypingKey(e){ if(e.ctrlKey||e.altKey||e.metaKey) return false; /* Shift is allowed */ var k=e.key||''; return k.length===1 /* printable chars, incl space */ || k==='Escape' || k==='Enter' || k==='Tab' || k==='Backspace' || k==='Delete' } function cleanup(){ if(m&&m.parentNode)m.parentNode.removeChild(m); document.removeEventListener('click', clickHandler, true); document.removeEventListener('keydown', keyHandler, true) } function clickHandler(evt){if(!m||!m.contains(evt.target))cleanup()} function keyHandler(e){if(isTypingKey(e))cleanup()} /* Delay to not immediately catch the opening click? */ setTimeout(function(){ document.addEventListener('click', clickHandler, true); document.addEventListener('keydown', keyHandler, true) }, 10) },100) /* end of setTimeout */ })(); /* end of IIFE, end of tallyglot bookmarklet */ /* We're near the bookmarklet length limit (I believe Google Chrome is the most restrictive in this regard). Lengthen this comment to see how much space we have left before Chrome starts truncating it when you paste it in. We're doing a fair bit of ugly compression in the above code, with more possible, ie by actually minifying it. Thinking out loud: It'd be nice to allow concatenation of multiple elements, to include the title in the wordcount. And ideally you (the user) could flip thru the elements on the fly right there in the popup till you found the element w/ the actual content you cared about. Even more ideally, it'd then remember that from then on for that website/domain. */