Skip to content

Instantly share code, notes, and snippets.

@Heziode
Created April 8, 2023 14:47
Show Gist options
  • Select an option

  • Save Heziode/f37d9ddbef0c29a40bf138fc0fc4b058 to your computer and use it in GitHub Desktop.

Select an option

Save Heziode/f37d9ddbef0c29a40bf138fc0fc4b058 to your computer and use it in GitHub Desktop.
Word and Character Count of multiple notes in Obsidian, using dataviewjs.
// Word Count Dashboard (Obsidian dataviewjs snippet)
// by @pseudometa, https://gist.github.com/chrisgrieser/ac16a80cdd9e8e0e84606cc24e35ad99
//Import configuration
const source = dv.current();
const sourceFolder = source.sourceFolder;
let sourceTag = source.sourceTag;
if (!sourceTag.startsWith("#")) sourceTag = "#" + sourceTag;
const charTarget = source.charTarget;
const wordTarget = source.wordTarget;
const includeFootnotes = source.includeFootnotes;
const charactersIncludeSpaces = source.charactersIncludeSpaces;
const excludeComments = source.excludeComments;
let excludeTag = source.excludeTag;
if (!excludeTag.startsWith("#")) excludeTag = "#" + excludeTag;
const includeBibliographyEstimate = source.includeBibliographyEstimate;
const wordsPerCitation = source.wordsPerCitation;
const charsPerCitation = source.charsPerCitation;
const thousandSeperator = source.thousandSeperator;
const naChar = source.naChar;
//const nameOfIndexFile = source.nameOfIndexFile; // leave empty when not using the Longform plugin
// > Functions
function getWordCount(text) {
//Regex from BetterWordCount Plugin
var spaceDelimitedChars = /A-Za-z\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC/
.source;
var nonSpaceDelimitedWords = /[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u4E00-\u9FD5]{1}/
.source;
var pattern = new RegExp([
"(?:[0-9]+(?:(?:,|\\.)[0-9]+)*|[\\-" + spaceDelimitedChars + "])+",
nonSpaceDelimitedWords,
].join("|"), "g");
return (text.match(pattern) || []).length;
}
function getCharacterCount(text) {
if (charactersIncludeSpaces) return text.length;
return text.replaceAll(" ","").length;
}
function insert1000sep (num){
let numText = String(num);
if (num >= 10000) numText =
numText.slice(0, -3) +
thousandSeperator +
numText.slice (-3);
return numText;
}
String.prototype.strong = function () {
if (this == " ") return " ";
return "**" + this + "**";
};
function removeMarkdown (text){
let plaintext = text.replace(/^---\n.*\n---\n/s,"") //YAML header
.replace(/\!?\[(.+)\]\(.+\)/g,"") //URLs & Image Captions
.replace(/\*|_|\[\[|\]\]|\||==|~~|---|#|>|`/ig,""); //markdown syntax
if (!excludeComments) return plaintext;
return plaintext.replace(/<!--.*-->/sg,"") //HTML comments
.replace(/%%.*%%/sg,""); //Obsidian comments
}
function removeFootnotes (text){
return text.replace(/^\[\^\w+\]:.*$/gm,"") //footnote at the end
.replace(/\[\^\w+\]/g,""); //footnote reference inline
}
function countPandocCitations (text){
let citations = text.match(/@\w+(?=[,;\] ])/gi);
if (citations == null) return 0;
let uniqCitations = [...new Set(citations)]; //only unique citations
return uniqCitations.length;
}
function calculateShare (charCount, wordCount){
let countToUse = 0;
let targetToUse = 0;
if (charTarget != 0) {
countToUse = charCount;
targetToUse = charTarget;
}
else if (wordTarget != 0) {
countToUse = wordCount;
targetToUse = wordTarget;
}
else {
return naChar;
}
return (countToUse / targetToUse);
}
function toPercentStr (share){
return (share * 100).toFixed(1).toString() + " %";
}
// > table construction
const getTableContents = () => {
let totalWords = 0;
let totalChars = 0;
let output = [];
let completeText = "";
let cumulativeShare = 0;
// get sections via folder or via tag
let sections = dv.pages('"' + folder + '"');
sections = dv.pages(sourceTag);
// > exclude sections with status "exclude"
sections = sections.where(t => t.status != "exclude");
numExcludeStatus = sections.where(t => t.status == "exclude").length;
// or exclude tag
if (excludeTag != ""){
numExcludedNotes = sections
.filter(t => t.file.tags.includes(excludeTag))
.length;
sections = sections
.filter(t => !t.file.tags.includes(excludeTag));
}
// >> SORT sections
sections = sections.sort(s => s.file.name);
// not yet implemented for Longform plugin
//let indexPath = folder.split("/").slice(0, -2).join("/") + nameOfIndexFile;
//if (indexPath.slice (-3) != ".md") indexPath += ".md";
// > SECTIONS LOOP
let sectionPaths = sections.file.path;
for(let i = 0; i < sectionPaths.length; i++){
// >> read page content via Obsidian API
let content = "";
let page = this.app.vault.getAbstractFileByPath(sectionPaths.values[i]);
if (page.unsafeCachedData != null) content = page.unsafeCachedData;
// >> clean up
content = removeMarkdown (content);
if (!includeFootnotes) content = removeFootnotes (content);
content = content.replace(/(^\s*)|(\s*$)/gi,"") // remove the start and end spaces of the given string
.replace(/ {2,}/gi," "); // reduce multiple spaces to a single space
// >> Table Values: Count & Share
let characterCount = getCharacterCount(content);
let wordCount = getWordCount(content);
cumulativeShare += calculateShare(characterCount, wordCount);
// >> Table Values: name & status
let name = sections[i].file.link;
let status = sections[i].status;
if (status == null) status = " ";
// >> push table values
output.push([
name,
insert1000sep(characterCount),
insert1000sep(wordCount),
toPercentStr(cumulativeShare),
status
]);
//add to totals & bibligraphy calculation
totalChars += characterCount;
totalWords += wordCount;
if (includeBibliographyEstimate) completeText += content;
}
// > OVERALL
// >> Bibliography Estimate
if (includeBibliographyEstimate){
let citationCount = countPandocCitations(completeText);
let wordCount = citationCount * wordsPerCitation;
let characterCount = citationCount * charsPerCitation;
if (!charactersIncludeSpaces) characterCount = citationCount * (charsPerReference - 20);
cumulativeShare += calculateShare(characterCount, wordCount);
output.push([
"Bibliography (Estimate)",
insert1000sep(characterCount),
insert1000sep(wordCount),
toPercentStr(cumulativeShare),
naChar
]);
//add for Totals calculation
totalChars += characterCount;
totalWords += wordCount;
}
// >> Totals calculation
let totalProgress = calculateShare(totalChars, totalWords);
output.push([
"Total".strong(),
insert1000sep(totalChars).strong(),
insert1000sep(totalWords).strong(),
toPercentStr(totalProgress).strong(),
naChar.strong()
]);
// >> Progress Bar
let progressBar =
'&nbsp;&nbsp;&nbsp;&nbsp;'
+ " "
+ '<progress max="100" value="'
+ (totalProgress * 100).toFixed().toString()
+ '"> </progress>';
// >> Target
let charTargetText = naChar;
let wordTargetText = naChar;
if (charTarget != 0) {
charTargetText = insert1000sep(charTarget);
}
if (wordTarget != 0) {
wordTargetText = insert1000sep(wordTarget);
}
output.push([
"Target".strong() + progressBar,
charTargetText.strong(),
wordTargetText.strong(),
naChar.strong(),
naChar.strong()
]);
// >> return array
return output;
};
// >> Print Table
let numExcludedNotes = 0;
let numExcludeStatus = 0;
dv.table(["Section", "Chars", "Words", "Target", "Status"], getTableContents());
// > Append Settings Info
let setting_ft = "Footnotes excluded. ";
let setting_excludeTag = "";
let setting_excludeStatus = "";
let setting_bibliography = "";
let setting_charSpaces = "Character Count includes Spaces. ";
let setting_comments = "Comments included. ";
if (includeFootnotes) setting_ft = "Footnotes included. ";
if (!includeBibliographyEstimate) setting_bibliography = "Bibliography excluded. ";
if (!charactersIncludeSpaces) setting_charSpaces = "Character Count without Spaces. ";
if (excludeComments) setting_comments = "Comments excluded. ";
if (numExcludeStatus != 0) {
let plural = "s";
if (numExcludeStatus == 1) plural = "";
let excludedQuery = "\"status: exclude\" path:(" + folder + ")";
setting_excludeStatus =
"[" + numExcludeStatus.toString() + " Section" + plural + "]"
+ "(obsidian://search?query=" + encodeURIComponent(excludedQuery) + ")"
+ " with the status \"exclude\" omitted. ";
}
if (numExcludedNotes != 0) {
let plural = "s";
if (numExcludedNotes == 1) plural = "";
let excludedQuery = "tag:" + excludeTag + " path:(" + folder + ")";
setting_excludeTag =
"[" + numExcludedNotes.toString() + " Section" + plural + "]"
+ "(obsidian://search?query=" + encodeURIComponent(excludedQuery) + ")"
+ " tagged with " + excludeTag
+ " omitted. ";
}
dv.span("---");
dv.span(
"<small>"
+ "Settings: ".strong()
+ setting_excludeStatus
+ setting_excludeTag
+ setting_ft
+ setting_bibliography
+ setting_comments
+ setting_charSpaces
+ "</small>"
);

Wordcount Dashboard for Obsidian

image

Setup

  1. Install dataview
  2. Install the CSS file as CSS snippet.
  3. Create a note with the markdown note template.
  4. Insert the dataviewjs-script into the dataviewjs-codeblock
  5. Enter the configuration values in the note template. (The surrounding %% %% ensure that they are treated as comments, so the configuration will not be displayed in Preview Mode.)
  6. The status column will be populated with the value of the YAML-key status of every document, i.e., you have to use the add the following YAML-Header to every note.
---
status: 
---
/* used to properly align the numbers of the dataviewjs wordcount snippet
https://gist.github.com/chrisgrieser/ac16a80cdd9e8e0e84606cc24e35ad99 */
.wordcountTable table.dataview.table-view-table td,
.wordcountTable table.dataview.table-view-table th {
border-left: 2px solid;
}
.wordcountTable table.dataview.table-view-table td:first-child,
.wordcountTable table.dataview.table-view-table th:first-child {
border-left: none;
}
.wordcountTable table.dataview.table-view-table td:nth-child(2),
.wordcountTable table.dataview.table-view-table td:nth-child(3),
.wordcountTable table.dataview.table-view-table td:nth-child(4),
.wordcountTable table.dataview.table-view-table td:nth-child(5){
text-align: end;
}
.wordcountTable table.dataview.table-view-table td:last-child{
text-align: center;
}
/* full-width in Preview Mode*/
.wordcountTable .markdown-preview-section {
max-width: 100% !important;
}
.wordcountTable table.dataview.table-view-table th{
text-align: center;
}
---
cssclass: wordcountTable
---
%%
## Configuration
**Gets either notes in a folder or notes with a certain tag. Leave one of them empty.**
sourceFolder:: Writing/Interdependence & Innovation/Drafts/Entwurf
sourceTag:: #stories
**set to 0 to ignore**
charTarget:: 60000
wordTarget:: 0
includeFootnotes:: true
charactersIncludeSpaces:: true
excludeComments:: true
**Notes to exclude**
Leave empty to disable.
Notes with the yaml-key "status" and value "exclude" for that key are also excluded
excludeTag:: #exclude
**Bibliography estimate for Pandoc Citations**
includeBibliographyEstimate:: true
wordsPerCitation:: 22
charsPerCitation:: 155
**purely visual**
thousandSeperator:: .
naChar:: —
%%
```dataviewjs
<!-- put the above code in a codeblock like this-->
```
@jhilker98
Copy link
Copy Markdown

I've been unable to set the path to the index file - I've set the sourceFolder above to /03 Conworlds/031 Broken Thrones/0311 The Ashes Chronicles/01 Ashes (in my specific instance), and then the pathToIndexFile I had set to /ashes.md - however, I get a cannot read properties of undefined (reading drafts). I've only got the initial draft for nanowrimo - how do I fix this?

@Heziode
Copy link
Copy Markdown
Author

Heziode commented Nov 4, 2023

Hum that strange.

I am not the original author of this gist. The original author is @chrisgrieser. I think I have cloned this gist (without editing it) and since it as deleted his gist, I am considered as the new "owner" of this gist.

I do not use this script anymore. Unfortunately, I've gone back to something more archaic for posting statistics: Excel.

I cannot really assist you with your issue…
In my case I only put the note that contains this script at the root of the folders I want to process, without editing variables.

@jhilker98
Copy link
Copy Markdown

jhilker98 commented Nov 4, 2023

No worries, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment