Using Obsidian’s Templater Plugin

  • Ask for help in the WikiWe 🟡 Group. Please include the step you are stuck on and other relevant info to get better support.

Setup Steps

  1. Getting Started with Obsidian.md if you are new to Obsidian
  2. Install and enable Templater within Community Plugins.
  3. Configure or confirm the “Template folder location” and “Script files location folder” under Templater settings
  4. Copy and add the following templates and scripts to these folders:
  5. Open a markdown document in Obsidian you want to digest via Doc Seal
  6. Run the Templater: Open insert template modal command via the command pallet in Obsidian.
  7. Select Doc Seal 0.9 - Digest & Seal - Current Document. For small files, after a few moments, you should see digest tags added to the end of each tile in your file and a few notice messages.
  8. Confirm that the folder x/ds is automatically created and tiles and blockchain files are created therein. You can modify the templates and scripts to change this folder path. 2024-09-12

Templates

  • Add the following templates to your Templater “Template folder location”

Doc Seal 0.9 - Digest & Seal - Current Document.md

 

Doc Seal 0.9 - Checkhash SHA-256 - Selected Text.md

 

User Scripts

  • Add the following scripts in your Templater “Script files location folder”
  • [!] Use VS Code or another text editor because Obsidian does not handle files with .js extension

computeSHA256.js

// Import the crypto module for cryptographic operations
const crypto = require('crypto');
 
// Function to generate a truncated SHA-256 hash of the input content
function computeSHA256(content, length = 12) {
 
// Record the start time for performance measurement
const startTime = Date.now();
 
// Generate SHA-256 hash, convert to hex, and truncate to specified length
const shash = crypto.createHash('sha256').update(content).digest('hex').substring(0, length);
 
// Log the number of characters hashed and the time taken
const endTime = Date.now();
const hashtime = ((endTime - startTime) / 1000).toFixed(2);
console.log(`Hashed ${content.length} characters in ${hashtime}s into #${shash}`)
 
// Return the truncated hash
return shash;
}
 
// Export the generateHash function for use in other modules
module.exports = computeSHA256;

deformatTile.js

function deformatTile(tile) {
    if (typeof tile !== 'string') {
        throw new Error('Deformat received non-string content');
    }
    
    const originalLength = tile.length;
    
    // Remove Tile IDs at the end of lines
    tile = tile.replace(/\s*\^[a-zA-Z0-9-]+$/gm, '');
    
    // Remove Digest Tags at the very end of the tile
    tile = tile.replace(/\s*#ds\/[a-f0-9]+\/\d{4}-\d{2}-\d{2}$/, '');
    
    // Remove markdown headers
    tile = tile.replace(/^#{1,6}\s+/gm, '');
    
    // Replace ordered list markers with markdown list marker
    tile = tile.replace(/^\s*\d+\.\s+/gm, '- ');
    
    // Remove markdown comments
    tile = tile.replace(//g, '');
 
    // Collapse multiple newlines and trim
    tile = tile.replace(/\n+/g, '\n').trim();
    
    const charactersRemoved = originalLength - tile.length;
    
    return { deformattedContent: tile, charactersRemoved };
}
 
module.exports = deformatTile;

ensureDirectoryExists.js

async function ensureDirectoryExists(path) {
    // Resolve the path, expanding ~ to the user's home directory if necessary
    let resolvedPath = path.startsWith('~') 
        ? path.replace('~', app.vault.adapter.basePath)
        : path;
 
    // Split the path into individual directory names
    const dirs = resolvedPath.split('/').filter(p => p.length);
 
    // Start with the root directory
    let currentPath = '';
 
    // Iterate through each directory in the path
    for (const dir of dirs) {
        currentPath += dir + '/';
        if (!(await app.vault.adapter.exists(currentPath))) {
            try {
                await app.vault.createFolder(currentPath);
                console.log(`Created directory: ${currentPath}`);
            } catch (error) {
                console.error(`Error creating directory ${currentPath}:`, error);
                throw error; // Re-throw the error to be caught by the calling function
            }
        }
    }
 
    console.log(`Ensured existence of directory: ${resolvedPath}`);
}
 
module.exports = ensureDirectoryExists;

getPreviousBlockchainEntryDigest.js

async function getPreviousBlockchainEntryDigest(app, blockchainDirectoryPath, computeSHA256) {
    // Get the current date
    const currentDate = new Date();
    const currentYear = currentDate.getFullYear();
    const currentMonth = currentDate.getMonth() + 1; // JavaScript months are 0-based
  
    // Format the current year and month as YY-MM
    const currentYYMM = `${String(currentYear).slice(-2)}-${String(currentMonth).padStart(2, '0')}`;
  
    let noticeMessage = `Searching for PreviousBlockchainEntryDigest:\n`;
    noticeMessage += `Blockchain Directory: ${blockchainDirectoryPath}\n\n`;
  
    // Function to get the last block from a file
    async function getLastBlockFromFile(filePath) {
        try {
            const fileContent = await app.vault.adapter.read(filePath);
            const blockMatches = [...fileContent.matchAll(/#ds\/block\/([a-f0-9]+)/g)];
            if (blockMatches.length > 0) {
                const lastMatch = blockMatches[blockMatches.length - 1];
                return lastMatch[1];
            }
            return null;
        } catch (error) {
            console.error(`Error reading file ${filePath}:`, error);
            return null;
        }
    }
  
    try {
        // Get all files in the directory
        const { files } = await app.vault.adapter.list(blockchainDirectoryPath);
        
        // Log all files in the directory
        console.log(`Contents of ${blockchainDirectoryPath}:`);
        console.log('Files:', files);
        console.log(`Total files found: ${files.length}`);
  
        noticeMessage += `Total files found in directory: ${files.length}\n`;
        noticeMessage += `Files found:\n${files.map(file => `- ${file}`).join('\n')}\n\n`;
  
        // Sort files in reverse chronological order
        const sortedFiles = files.sort((a, b) => b.localeCompare(a));
  
        console.log(`Sorted files:`, sortedFiles);
        noticeMessage += `Searching files in reverse chronological order:\n`;
  
        let blocksFound = false;
  
        // Iterate through sorted files to find the latest block
        for (const file of sortedFiles) {
            console.log(`Checking file: ${file}`);
            noticeMessage += `- Checking file: ${file}\n`;
            
            const lastBlock = await getLastBlockFromFile(file);
            if (lastBlock) {
                blocksFound = true;
                console.log(`Found last block in file ${file}: ${lastBlock}`);
                noticeMessage += `  Found last block: ${lastBlock}\n\n`;
                noticeMessage += `PreviousBlockchainEntryDigest chosen: ${lastBlock}\n`;
                noticeMessage += `Reason: This is the most recent block found in the latest file (${file}).`;
                new Notice(noticeMessage, 20000);  // 20 seconds duration
                return lastBlock;
            }
            console.log(`No blocks found in file ${file}.`);
            noticeMessage += `  No blocks found in this file.\n`;
        }
  
        if (!blocksFound) {
            console.log('No blocks were found in any file.');
            noticeMessage += '\nNo blocks were found in any file.\n';
        }
  
        // If no blocks found in any file, use the hash of the current YY-MM
        console.log(`Using hash of current YY-MM (${currentYYMM}) as previousBlockchainEntryDigest`);
        const fallbackHash = computeSHA256(currentYYMM);
        noticeMessage += `\nPreviousBlockchainEntryDigest chosen: ${fallbackHash}\n`;
        noticeMessage += `Reason: No existing blocks found. Using hash of current YY-MM (${currentYYMM}) as fallback.`;
        new Notice(noticeMessage, 20000);  // 20 seconds duration
        return fallbackHash;
  
    } catch (error) {
        console.error(`Error accessing blockchain directory ${blockchainDirectoryPath}:`, error);
        noticeMessage += `\nError accessing blockchain directory: ${error.message}\n`;
        noticeMessage += 'Using fallback method to generate PreviousBlockchainEntryDigest.';
        
        const fallbackHash = computeSHA256(currentYYMM);
        noticeMessage += `\nPreviousBlockchainEntryDigest (fallback): ${fallbackHash}`;
        new Notice(noticeMessage, 20000);  // 20 seconds duration
        return fallbackHash;
    }
}
 
module.exports = getPreviousBlockchainEntryDigest;

splitIntoTiles.js

function splitIntoTiles(content) {
    // Extract frontmatter if present
    const frontmatterMatch = content.match(/^\s*---\n[\s\S]*?\n---\s*$/m);
    const frontmatter = frontmatterMatch ? frontmatterMatch[0] : '';
    // Remove frontmatter from the main content
    const contentWithoutFrontmatter = content.replace(/^\s*---\n[\s\S]*?\n---\s*\n/m, '');
 
    // Regex to match both Markdown (### Header) and HTML (<h3>Header</h3>) headers
    const headerRegex = /^(?:#{1,3}\s+.*|(?:<h[1-3]>.*?<\/h[1-3]>))$/gm;
    const tiles = [];
    let lastIndex = 0;
    let match;
 
    // Check if there's content before the first header
    const firstHeaderMatch = contentWithoutFrontmatter.match(headerRegex);
    if (firstHeaderMatch && firstHeaderMatch.index > 0) {
        // Add content before first header as a tile
        tiles.push(contentWithoutFrontmatter.slice(0, firstHeaderMatch.index).trim());
        lastIndex = firstHeaderMatch.index;
    }
 
    // Iterate through all header matches
    while ((match = headerRegex.exec(contentWithoutFrontmatter)) !== null) {
        if (lastIndex !== match.index) {
            // Add content between headers as a tile
            tiles.push(contentWithoutFrontmatter.slice(lastIndex, match.index).trim());
        }
        lastIndex = match.index;
    }
 
    // Add any remaining content after the last header
    if (lastIndex < contentWithoutFrontmatter.length) {
        tiles.push(contentWithoutFrontmatter.slice(lastIndex).trim());
    }
 
    // Count frontmatter properties
    const frontmatterProperties = frontmatter
        .split('\n')
        .filter(line => line.includes(':'))
        .length;
 
    // Log information about the splitting process
    console.log(`Splitting content into ${tiles.length} tiles, and preserving ${frontmatterProperties} frontmatter properties`);
 
    // Return an object containing the frontmatter and the array of tiles
    return { frontmatter, tiles };
}
 
// Export the function for use in other modules
module.exports = splitIntoTiles;

updateFrontMatter.js

async function updateFrontMatter(file, updates) {
    try {
        await app.fileManager.processFrontMatter(file, (frontmatter) => {
            Object.entries(updates).forEach(([key, value]) => {
                if (Array.isArray(value)) {
                    if (!frontmatter[key]) {
                        frontmatter[key] = [];
                    }
                    value.forEach(item => {
                        if (!frontmatter[key].includes(item)) {
                            frontmatter[key].push(item);
                        }
                    });
                } else {
                    frontmatter[key] = value;
                }
            });
        });
    } catch (error) {
        console.error(`Error updating frontmatter for ${file.path}:`, error);
        throw error;
    }
}
 
module.exports = updateFrontMatter;

upsertToFile.js

async function ensureDirectoryExists(path) {
    // Extract the directory path by removing the last part of the path (assumed to be a file)
    const dirs = path.split('/').slice(0, -1).join('/');
    
    // Check if the directory exists
    if (!(await app.vault.adapter.exists(dirs))) {
        // If the directory does not exist, create it
        await app.vault.createFolder(dirs);
        
        // Log a message to the console indicating that the directory was created
        console.log(`Directory created: ${dirs}`);
    }
}
 
async function upsertToFile(filePath, header, content) {
    try {
        // Ensure the directory for the file path exists
        await ensureDirectoryExists(filePath);
 
        // Attempt to retrieve the file from the vault
        let file = app.vault.getAbstractFileByPath(filePath);
        let fileContent = '';
 
        // If the file exists, read its content
        if (file) {
            fileContent = await app.vault.read(file);
        }
 
        // Escape special characters in the header for use in a regular expression
        const escapedHeader = header.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        // Create a regex to find the section of the file starting with the header
        const regex = new RegExp(`${escapedHeader}[\\s\\S]*?(?=######|$)`, 'g');
 
        // Check if the content already exists in the file under the specified header
        if (regex.test(fileContent)) {
            // If the content is already present, log and return unchanged status
            if (fileContent.includes(content.trim())) {
                console.log(`File content already up to date: ${filePath}`);
                return { status: 'unchanged', file: file };
            }
            // Special handling for blockchain entries
            if (header.startsWith('### [[') && header.includes('🔒 #ds/seal/')) {
                if (fileContent.includes(header)) {
                    console.log(`Blockchain entry already exists: ${filePath}`);
                    new Notice(`Blockchain entry already exists: ${filePath}`, 5000);
                    return { status: 'unchanged', file: file };
                }
            }
            // Append new content if the header exists but content is missing
            fileContent += `\n\n${content}\n`;
        } else {
            // Append content if the header does not exist
            fileContent += `${content}\n\n`;
        }
 
        // Update the existing file or create a new one with the updated content
        if (file) {
            await app.vault.modify(file, fileContent);
        } else {
            file = await app.vault.create(filePath, fileContent);
        }
 
        console.log(`File upserted successfully: ${filePath}`);
        return { status: 'updated', file: file };
    } catch (error) {
        // Log and throw any errors encountered during the process
        console.error(`Error upserting file ${filePath}:`, error);
        throw error;
    }
}
 
module.exports = upsertToFile;