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
// version 1.5.2
//----------------------------------------------------
// Import configuration
//----------------------------------------------------
const source = dv.current();
const sourceFolder = source.sourceFolder;
const charTarget = source.charTarget;
const wordTarget = source.wordTarget;
const includeFootnotes = source.includeFootnotes;
const charactersIncludeSpaces = source.charactersIncludeSpaces;
const excludeComments = source.excludeComments;
const includeBibliographyEstimate = source.includeBibliographyEstimate;
const wordsPerCitation = source.wordsPerCitation;
const charsPerCitation = source.charsPerCitation;
const thousandSeperator = source.thousandSeperator;
const useThousandSeperator = source.useThousandSeperator;
const naChar = source.naChar;
const subsectionStartChar = source.subsectionStartChar;
const wordsPerPage = source.wordsPerPage;
const pathToIndexFile = source.pathToIndexFile;
let sourceTag = source.sourceTag;
let excludeTag = source.excludeTag;
// prepend hashtags for tags
if (sourceTag) if (!sourceTag.startsWith("#")) sourceTag = "#" + sourceTag;
if (excludeTag) if (!excludeTag.startsWith("#")) excludeTag = "#" + excludeTag;
//----------------------------------------------------
// Functions
//----------------------------------------------------
function getWordCount(text) {
// Regex from BetterWordCount Plugin
const 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;
const nonSpaceDelimitedWords = /[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u4E00-\u9FD5]{1}/
.source;
const 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 (!useThousandSeperator) return numText;
if (num >= 10000) numText = numText.slice(0, -3) + thousandSeperator + numText.slice (-3); // eslint-disable-line no-magic-numbers
return numText;
}
String.prototype.strong = function () {
if (this === " ") return " ";
return "**" + this + "**";
};
function removeMarkdown (text) {
let plaintext = text
.replace(/`\$?=[^`]+`/g, "") // inline dataview
.replace(/^---\n.*?\n---\n/s, "") // YAML Header
.replace(/!?\[(.+)\]\(.+\)/g, "$1") // URLs & Image Captions
.replace(/\*|_|\[\[|\]\]|\||==|~~|---|#|> |`/g, ""); // Markdown Syntax
if (excludeComments) {
plaintext = plaintext
.replace(/<!--.*?-->/sg, "")
.replace(/%%.*?%%/sg, "");
}
else {
plaintext = plaintext
.replace(/%%|<!--|-->/g, ""); // remove only comment syntax
}
return plaintext;
}
function removeFootnotes (text) {
return text
.replace(/^\[\^\w+\]:.*$/gm, "") // footnote at the end
.replace(/\[\^\w+\]/g, ""); // footnote reference inline
}
function countPandocCitations (text) {
const citations = text.match(/@\w+(?=[,;\] ])/gi);
if (!citations) return 0;
const uniqCitations = [...new Set(citations)]; // only unique citations
return uniqCitations.length;
}
function calculateShare (charCount, wordCount) {
if (!wordTarget && !charTarget) return naChar;
if (charTarget !== 0) return charCount / charTarget;
if (wordTarget !== 0) return wordCount / wordTarget;
}
function toPercentStr (share) {
return (share * 100).toFixed(1).toString() + "%";
}
//----------------------------------------------------
// Table Construction
//----------------------------------------------------
async function getTableContents () {
const output = [];
let completeText = "";
let totalWords = 0;
let totalChars = 0;
let cumulativeShare = 0;
let sectionCounter = 0;
let subsectionCounter = 0;
// get sections via folder or via tag
let sections;
if (sourceFolder) sections = dv.pages("\"" + sourceFolder + "\"");
else sections = dv.pages(sourceTag);
// exclude certain notes
numExcludeStatus = sections.filter(t => t.status === "exclude").length;
sections = sections.filter(t => t.status !== "exclude");
if (excludeTag !== "") {
numExcludedNotes = sections.filter(t => t.file.tags.includes(excludeTag)).length;
sections = sections.filter(t => !t.file.tags.includes(excludeTag));
}
// SORT sections
if (pathToIndexFile) {
const draftName = sourceFolder.split("/").pop();
const longformOrder =
dv.page(pathToIndexFile)
.drafts
.filter(d => d.name === draftName)
.scenes;
sections = sections.sort(
s => s.file.name,
"desc",
(a, b) => longformOrder.indexOf(b) - longformOrder.indexOf(a)
);
} else {
sections = sections.sort(s => s.file.name);
}
//-------------------------------------------------
// SECTIONS LOOP
//-------------------------------------------------
for (const section of sections) {
// read page content
let content = await dv.io.load(section.file.path); // eslint-disable-line no-await-in-loop
// clean up
content = removeMarkdown (content);
if (!includeFootnotes) content = removeFootnotes (content);
content = content
.replace(/(^\s*)|(\s*$)/g, "") // remove the start and end spaces of the given string
.replace(/ {2,}/g, " "); // reduce multiple spaces to a single space
// Table Values: Count & Share
const characterCount = getCharacterCount(content);
const wordCount = getWordCount(content);
cumulativeShare += calculateShare(characterCount, wordCount);
// Status
let status = section.status;
if (!status) status = " ";
// Section numbering
const isSubsection = section.file.name.startsWith(subsectionStartChar);
let sectionNumbering;
if (isSubsection) {
subsectionCounter++;
sectionNumbering = sectionCounter.toString() + "." + subsectionCounter.toString();
}
else {
subsectionCounter = 0;
sectionCounter++;
sectionNumbering = sectionCounter.toString().strong();
}
// push table values
output.push([
sectionNumbering,
section.file.link,
insert1000sep(characterCount),
insert1000sep(wordCount),
toPercentStr(cumulativeShare),
status
]);
// add to totals & bibliography calculation
totalChars += characterCount;
totalWords += wordCount;
if (includeBibliographyEstimate) completeText += content;
}
//-------------------------------------------------
// OVERALL
//-------------------------------------------------
// Bibliography Estimate
if (includeBibliographyEstimate) {
const citationCount = countPandocCitations(completeText);
const wordCount = citationCount * wordsPerCitation;
let characterCount = citationCount * charsPerCitation;
if (!charactersIncludeSpaces) characterCount = citationCount * (charsPerCitation - 20); // eslint-disable-line no-magic-numbers
cumulativeShare += calculateShare(characterCount, wordCount);
output.push([
"",
"Bibliography (" + citationCount + " citations)",
insert1000sep(characterCount),
insert1000sep(wordCount),
toPercentStr(cumulativeShare),
naChar
]);
// add for Totals calculation
totalChars += characterCount;
totalWords += wordCount;
}
// Totals calculation
const totalProgress = calculateShare(totalChars, totalWords);
let total = "Total";
if (wordsPerPage) {
const totalPages = (totalWords / wordsPerPage).toFixed(1);
total += "&nbsp;&nbsp;&nbsp;&nbsp;(" + totalPages + " Pages)";
}
output.push([
"",
total.strong(),
insert1000sep(totalChars).strong(),
insert1000sep(totalWords).strong(),
toPercentStr(totalProgress).strong(),
naChar.strong()
]);
// Target & Progress Bar
const progressBar =
"&nbsp;&nbsp;&nbsp;&nbsp;"
+ " <progress max=\"100\" value=\""
+ (totalProgress * 100).toFixed().toString()
+ "\"> </progress>";
let charTargetText = naChar;
let wordTargetText = naChar;
if (charTarget) charTargetText = insert1000sep(charTarget);
if (wordTarget) wordTargetText = insert1000sep(wordTarget);
output.push([
"",
"Target".strong() + progressBar,
charTargetText.strong(),
wordTargetText.strong(),
naChar.strong(),
naChar.strong()
]);
return output;
}
//----------------------------------------------------
// Main
//----------------------------------------------------
// Print Table
let numExcludedNotes = 0;
let numExcludeStatus = 0;
const tcontent = await getTableContents();
dv.table(["⟡", "Section", "Chars", "Words", "Target", "Status"], tcontent);
// Append Settings Info
let settingFt = "Footnotes excluded. ";
let settingExcludeTag = "";
let settingExcludeStatus = "";
let settingBibliography = "";
let settingCharSpaces = "Character Count includes Spaces. ";
let settingComments = "Comments included. ";
let settingPages = "";
if (includeFootnotes) settingFt = "Footnotes included. ";
if (!includeBibliographyEstimate) settingBibliography = "Bibliography excluded. ";
if (!charactersIncludeSpaces) settingCharSpaces = "Character Count without Spaces. ";
if (excludeComments) settingComments = "Comments excluded. ";
if (wordsPerPage) settingPages = "Assuming " + wordsPerPage.toString() + " words per page. ";
if (numExcludeStatus) {
let plural = "s";
if (numExcludeStatus === 1) plural = "";
const excludedQuery = "\"status: exclude\" path:(" + sourceFolder + ")";
settingExcludeStatus =
"[" + numExcludeStatus.toString() + " Section" + plural + "]"
+ "(obsidian://search?query=" + encodeURIComponent(excludedQuery) + ")"
+ " with the status \"exclude\" omitted. ";
}
if (numExcludedNotes) {
let plural = "s";
if (numExcludedNotes === 1) plural = "";
const excludedQuery = "tag:" + excludeTag + " path:(" + sourceFolder + ")";
settingExcludeTag =
"[" + numExcludedNotes.toString() + " Section" + plural + "]"
+ "(obsidian://search?query=" + encodeURIComponent(excludedQuery) + ")"
+ " tagged with " + excludeTag
+ " omitted. ";
}
dv.span("---");
dv.span(
"<small>"
+ "Settings: ".strong()
+ settingExcludeStatus
+ settingExcludeTag
+ settingFt
+ settingBibliography
+ settingComments
+ settingCharSpaces
+ settingPages
+ "</small>"
);

Wordcount Dashboard for Obsidian Dataview

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.
  7. When also using the Longform Plugin, you can name the index file in your settings and the Dashboard will automatically change order based on the order of scenes in your Draft.

Wordcount_Dashboard

---
status: 
---

Known Issues

It seems that this dahsboard fails when amount of words / notes becomes too high. As far as I can tell, this is due to limitations by dataview/Obsidian, and one could only be tackled by a dedicated plugin for Word Counts. If you really want multi-note word counts, please request a plugin like that in the forum!

/* used to properly align the numbers of the dataviewjs wordcount snippet
https://gist.github.com/chrisgrieser/ac16a80cdd9e8e0e84606cc24e35ad99 */
.wordcountTable table.dataview.table-view-table td:first-child,
.wordcountTable table.dataview.table-view-table th:first-child {
padding: 4px 7px;
border-left: none;
text-align: center;
}
.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),
.wordcountTable table.dataview.table-view-table td:nth-child(6) {
text-align: end;
}
.wordcountTable table.dataview.table-view-table td:last-child {
text-align: center;
}
.wordcountTable .markdown-preview-section {
max-width: 100% !important;
}
.wordcountTable table.dataview.table-view-table th {
text-align: center;
}
---
cssclass: wordcountTable
---
%%
### Configuration
__Notes to display__
*Gets either notes in a folder or notes with a certain tag. Leave one of them empty.*
sourceFolder:: Writing/Interdependence & Innovation/Drafts/Showcase
sourceTag::
__Notes to exclude__
*Leave empty to disable.*
excludeTag:: #exclude
*(Notes with the yaml-key "status" and value "exclude" for that key are also excluded.)*
__Counting Settings__
*set to zero to ignore*
charTarget:: 60000
wordTarget:: 0
wordsPerPage:: 0
includeFootnotes:: false
charactersIncludeSpaces:: true
excludeComments:: true
__Bibliography Estimate for Pandoc Citations__
includeBibliographyEstimate:: true
wordsPerCitation:: 22
charsPerCitation:: 155
__Longform Plugin__
*Leave empty to sort alphabethically. Enter the path to the index file (not the parent folder) of a longform project to order sections by their order in the longform side bar. (The `sourceFolder::` further above has to be a Longform Drafts folder.)*
pathToIndexFile::
*begin a filename with this character and it will be treated as subsection*
subsectionStartChar:: _
__Purely visual__
useThousandSeperator:: true
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