# Obsidian: Automatic Index/JDex Generation (Area/Category Modes)

**URL:** <https://forum.johnnydecimal.com/t/obsidian-automatic-index-jdex-generation-area-category-modes/2240>\
**Category:** JDHQ\
**Created:** [April 3, 2025, 1:29am UTC](https://forum.johnnydecimal.com/t/obsidian-automatic-index-jdex-generation-area-category-modes/2240 "2025-04-03T01:29:47Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![Ashtrath](https://forum.johnnydecimal.com/letter_avatar_proxy/v4/letter/a/e99b99/32.png) [@Ashtrath](https://forum.johnnydecimal.com/u/Ashtrath)\
**Post date:** [April 3, 2025, 1:29am UTC](https://forum.johnnydecimal.com/t/obsidian-automatic-index-jdex-generation-area-category-modes/2240/1 "2025-04-03T01:29:47Z")

</div>

Hello fellow JohnnyDecimal & Obsidian users!

I wanted to share a Templater user script I’ve made to help automate the process of generating JDex for new notes. If you use the JohnnyDecimal system to organize your vault, you know that finding the next sequential ID in a category can be a manual step. This script aims to solve that!

**What it Does**  
This script, named `JDFormatter`, provides a function you can call from your Obsidian templates to automatically:

1. **Find the Next ID:** Scans a specified JohnnyDecimal category folder for existing files named like `XX.YY ...` (e.g., `12.05 Meeting Notes.md`).
2. **Calculate Increment:** Determines the highest existing `YY` number for that category (`XX`).
3. **Generate New ID:** Calculates the next sequential ID (`maxId + 1`), ensuring it starts from a minimum value (default is `11`).
4. **Two Operating Modes:**
  - **`area` mode (default):** You provide the path to an “Area” folder (e.g., `"10 - 19 Finance"`). The script will then _prompt you_ using Templater’s suggester to choose a specific “Category” folder within that area (e.g., `"11 Banking"`, `"12 Bills"`).
  - **`category` mode:** You provide the _direct path_ to a specific “Category” folder (e.g., `"10 - 19 Finance/12 Bills"`). The script will process this folder directly _without prompting_.

5. **Return Useful Data:** Returns a JavaScript object containing information about the selected category (ID number `XX`, path, name, tag-friendly formatted name) and the newly generated full JDex string (`XX.YY`).

**Requirements**

- **Templater Plugin:** Installed and enabled.
- **User Scripts Enabled:** You need to enable the “User Scripts Functions” option within the Templater plugin settings and specify a folder where your scripts reside.
- **Folder/File Structure:** Your vault needs to follow the expected JohnnyDecimal structure for this script to work correctly:
  - **Category Folders:** Named like `XX Name` (e.g., `01 Quick Note`, `21 Active Projects`). The script uses a regular expression (`/^(\d{2})\s(.+)/`) to find these.
  - **Files within Categories:** Named like `XX.YY Title.md` (e.g., `01.11 Daily Log`, `21.15 Project Alpha Kickoff.md`). The script uses a regular expression (`/^(\d{2})\.(\d{2,})\s.+/`) to find the ID (`YY`) part.

**Installation**

1. **Create Script File:** Copy the code provided below and save it as a `.js` file (e.g., `JDFormatter.js`).
2. **Place in Script Folder:** Move this `.js` file into the folder you’ve designated for Templater User Scripts in the Templater settings.
3. **Enable Scripts:** Double-check that “User Script Functions” is toggled ON in Templater settings and the folder path is correct.

**How to Use**  
You call this function from within a Templater template (`<%* ... %>` block).

**Syntax:**

```javascript
const jdInfo = await tp.user.JDFormatter(tp, BASE_DIR, MODE);

```

- `tp`: The Templater object (always pass this).
- `BASE_DIR`: String containing the path to the Area folder (for `area` mode) or the Category folder (for `category` mode).
- `MODE`: Optional string, either `"area"` (default) or `"category"`. Case-insensitive.

**Example 1: Area Mode (Default - Prompts for Category)**  
Let’s say you have a template to create a new bill record.

```markdown
<%*
// Define the Area folder for finance
const AREA_FOLDER = "10 - 19 Finance";

// Call the script (mode defaults to "area")
// It will prompt you to select a category like "11 Banking", "12 Bills", etc.
const jdInfo = await tp.user.JDFormatter(tp, AREA_FOLDER);

// If user cancelled the category prompt or an error occurred
if (!jdInfo) {
    new Notice("JD ID generation cancelled or failed.", 3000);
    return ""; // Stop template execution
}

// jdInfo now contains:
// jdInfo.category.id -> e.g., "12" (if you selected "12 Bills")
// jdInfo.category.path -> e.g., "10 - 19 Finance/12 Bills"
// jdInfo.category.name -> e.g., "Bills"
// jdInfo.category.formattedName -> e.g., "bills"
// jdInfo.generatedId -> e.g., "12.14" (the next calculated ID)

// Suggest a filename
const suggestedFilename = `${jdInfo.generatedId} New Bill Title`;
const finalFilename = await tp.system.prompt("Enter filename:", suggestedFilename);

if (!finalFilename || !finalFilename.startsWith(jdInfo.generatedId)) {
    new Notice("Filename invalid or cancelled.", 3000);
    return "";
}

// Move the new note (created by Templater) to the correct category and rename it
await tp.file.move(`${jdInfo.category.path}/${finalFilename}`);

// Optional: Display a confirmation
new Notice(`Note created in ${jdInfo.category.name}: ${finalFilename}`, 3000);
%>---
tags: <% jdInfo.category.formattedName %> #finance #<% jdInfo.category.id %>
jd_id: <% jdInfo.generatedId %>
category: <% `${jdInfo.category.id} ${jdInfo.category.name}` %>
status: unprocessed
---

# <% finalFilename.replace(/.md$/,'') %>

- Amount:
- Due Date:
- Paid Date:

```

**Example 2: Category Mode (Direct - No Prompt)**  
If your template _always_ creates notes in one specific category.

```markdown
<%*
// Define the *specific* Category folder
const CATEGORY_FOLDER = "00 - 09 System/01 Quick Note";

// Call the script explicitly in "category" mode
const jdInfo = await tp.user.JDFormatter(tp, CATEGORY_FOLDER, "category");

// Error check (e.g., if folder doesn't exist or has wrong name format)
if (!jdInfo) {
    new Notice("JD ID generation failed. Check category path/name.", 3000);
    return "";
}

// Suggest filename directly
const suggestedFilename = `${jdInfo.generatedId} Quick Note Title`;
const finalFilename = await tp.system.prompt("Enter filename:", suggestedFilename);

if (!finalFilename || !finalFilename.startsWith(jdInfo.generatedId)) {
    new Notice("Filename invalid or cancelled.", 3000);
    return "";
}

// Move and rename
await tp.file.move(`${jdInfo.category.path}/${finalFilename}`);
%>---
tags: <% jdInfo.category.formattedName %> #quicknote
jd_id: <% jdInfo.generatedId %>
---

# <% finalFilename.replace(/.md$/,'') %>

<% tp.date.now("YYYY-MM-DD HH:mm") %>

```

**The Script Code**

```javascript
/**
 * Generates the next sequential JohnnyDecimal ID. Operates in two modes:
 * - "area": Prompts user to select a category within the BASE_DIR (Area folder).
 * - "category": Assumes BASE_DIR is the specific category folder to generate an ID for.
 *
 * @param {object} tp - The Templater instance provided by Templater.
 * @param {string} BASE_DIR - The path to the "Area" directory (mode="area") or "Category" directory (mode="category").
 * @param {string} [MODE="area"] - The operating mode: "area" or "category". Case-insensitive.
 * @returns {Promise<object|null>} A promise that resolves to an object containing category info and the next ID,
 * or null if the process is cancelled or fails.
 * Object structure:
 * {
 * category: {
 * id: string, // e.g., "01"
 * path: string,
 * name: string, // e.g., "Quick Note"
 * formattedName: string // e.g., "quick-note"
 * },
 * generatedId: string // e.g., "01.13"
 * }
 */
async function JDFormatter(tp, BASE_DIR, MODE = "area") {
	// --- Configuration ---
	const STARTING_ID = 11;
	const CATEGORY_REGEX = /^(\d{2})\s(.+)/;
	const FILE_ID_REGEX = /^(\d{2})\.(\d{2,})\s.+/;

	// --- Helper for Name Formatting ---
	function formatCategoryName(name) {
		if (!name) return "";
		return name
			.toLowerCase()
			.replace(/\s+/g, "-")
			.replace(/[^\w-]+/g, "")
			.replace(/^-+|-+$/g, "");
	}

	// --- Input Validation ---
	if (!BASE_DIR) {
		const msg = "JDFormatter: BASE_DIR parameter is required.";
		console.error(msg);
		await new Notice(msg);
		return null;
	}

	const baseAbstractFile = tp.app.vault.getAbstractFileByPath(BASE_DIR);
	if (!baseAbstractFile) {
		const msg = `JDFormatter: BASE_DIR "${BASE_DIR}" not found.`;
		console.error(msg);
		await new Notice(msg);
		return null;
	}

	// --- Mode Handling Variables ---
	let categoryFolder; // The TFolder object for the target category
	let categoryNumber; // The "XX" part
	let categoryName; // The "Name" part
	let categoryPath; // Full path to the category folder

	const effectiveMode = MODE.toLowerCase();

	// --- Determine Category Details Based on Mode ---
	if (effectiveMode === "category") {
		// Mode: BASE_DIR *is* the category folder
		if (baseAbstractFile.children === undefined) {
			const msg = `JDFormatter (Category Mode): BASE_DIR "${BASE_DIR}" is not a valid folder.`;
			console.error(msg);
			await new Notice(msg);
			return null;
		}
		categoryFolder = baseAbstractFile;

		const match = categoryFolder.name.match(CATEGORY_REGEX);
		if (!match) {
			const msg = `JDFormatter (Category Mode): Folder name "${categoryFolder.name}" does not match expected 'XX Name' format.`;
			console.error(msg);
			await new Notice(msg);
			return null;
		}
		categoryNumber = match[1];
		categoryName = match[2];
		categoryPath = categoryFolder.path;
		console.log(`JDFormatter: Running in Category mode for "${categoryPath}"`);
	} else if (effectiveMode === "area") {
		// Mode: BASE_DIR is the area folder, prompt for category
		if (baseAbstractFile.children === undefined) {
			const msg = `JDFormatter (Area Mode): BASE_DIR "${BASE_DIR}" is not a valid folder.`;
			console.error(msg);
			await new Notice(msg);
			return null;
		}
		const areaFolder = baseAbstractFile;

		const categoryFolders = areaFolder.children
			.filter(
				(item) => item.children !== undefined && CATEGORY_REGEX.test(item.name),
			)
			.map((folder) => ({
				name: folder.name,
				path: folder.path,
				display: folder.name,
			}))
			.sort((a, b) => a.name.localeCompare(b.name));

		if (categoryFolders.length === 0) {
			const msg = `JDFormatter (Area Mode): No valid category folders (like "XX Name") found in "${BASE_DIR}".`;
			console.error(msg);
			await tp.templater.warning(msg);
			return null;
		}

		console.log(`JDFormatter: Running in Area mode for "${BASE_DIR}", prompting for category...`);
		const selectedCategory = await tp.system.suggester(
			categoryFolders.map((f) => f.display),
			categoryFolders,
		);

		if (!selectedCategory) {
			console.log("JDFormatter (Area Mode): User cancelled category selection.");
			return null;
		}

		const selectedFolderObject = tp.app.vault.getAbstractFileByPath(selectedCategory.path);
		categoryFolder = selectedFolderObject;

		// Extract details from selected category
		const categoryMatch = selectedCategory.name.match(CATEGORY_REGEX);
		categoryNumber = categoryMatch[1];
		categoryName = categoryMatch[2];
		categoryPath = selectedCategory.path;
		console.log(`JDFormatter: User selected category "${categoryPath}"`);
	} else {
		// Invalid Mode Handling
		const msg = `JDFormatter: Invalid MODE "${MODE}". Use "area" or "category".`;
		console.error(msg);
		await new Notice(msg);
		return null;
	}

	// --- Scan the Determined Category Folder for Existing IDs ---
	if (!categoryFolder || !categoryNumber || !categoryName || !categoryPath) {
		const msg = "JDFormatter: Internal Error - category details were not determined correctly.";
		console.error(msg);
		await new Notice(msg);
		return null;
	}

	let maxId = STARTING_ID - 1; // Initialize below the minimum starting ID

	try {
		const files = categoryFolder.children;
		for (const file of files) {
			// Check if it's a file (doesn't have .children)
			if (file.children === undefined) {
				const match = file.name.match(FILE_ID_REGEX);
				// Check if file belongs to the *correct* category (XX matches) and has an ID
				if (match && match[1] === categoryNumber) {
					const currentId = Number.parseInt(match[2], 10);
					if (!Number.isNaN(currentId)) {
						maxId = Math.max(maxId, currentId);
					}
				}
			}
		}
	} catch (error) {
		console.error(`JDFormatter: Error scanning files in category "${categoryPath}":`, error);
		await new Notice(`Error scanning category folder "${categoryPath}". Check console.`);
		return null;
	}

	// --- Calculate Next ID ---
	const nextId = Math.max(STARTING_ID, maxId + 1);

	return {
		category: {
			id: categoryNumber,
			path: categoryPath,
			name: categoryName,
			formattedName: formatCategoryName(categoryName),
		},
		generatedId: `${categoryNumber}.${nextId.toString().padStart(2, "0")}`,
	};
}

module.exports = JDFormatter;

```

**Configuration / Customization**  
You can tweak the script slightly by editing these constants near the top:

- `STARTING_ID`: Change `11` if you want your IDs to start from a different number (e.g., `1` for `XX.01`).
- `CATEGORY_REGEX`: Modify this regular expression if your _category folders_ have a different naming pattern than `XX Name`.
- `FILE_ID_REGEX`: Modify this regular expression if your _files_ have a different naming pattern for extracting the `XX.YY` ID.  
_ **Warning:** Modifying regex requires understanding regular expressions._

**Important Notes**

- **Naming Convention is Crucial:** This script heavily relies on your folders being named `XX Name` and files `XX.YY ...`. If your convention differs, you _must_ update the `CATEGORY_REGEX` and `FILE_ID_REGEX` constants accordingly.
- **Error Handling:** Errors (e.g., folder not found, invalid name format) will usually display an Obsidian Notice message and log details to the Developer Console (press `Ctrl+Shift+I` or `Cmd+Opt+I` to open it).
- **Performance:** The script reads the list of files in the target category. In extremely large categories (thousands of files), there might be a slight delay, but it should be negligible for most use cases.

Hope this is helpful for other JohnnyDecimal users in Obsidian! Let me know if you found any bugs or have any questions and suggestions. 😁

---

<div class="post-metadata">

**Author:** ![ontologist](https://forum.johnnydecimal.com/user_avatar/forum.johnnydecimal.com/ontologist/32/1495_2.png) [@ontologist](https://forum.johnnydecimal.com/u/ontologist)\
**Post date:** [April 11, 2025, 1:12am UTC](https://forum.johnnydecimal.com/t/obsidian-automatic-index-jdex-generation-area-category-modes/2240/2 "2025-04-11T01:12:11Z")

</div>

Nice! I started with user scripts and the Templater plugin too, which I used alongside the Obsidian extension for Raycast—a fast, extensible macOS launcher. However, since that requires the vault to stay open (triggering Templater events), I re-implemented and enhanced the scripts directly in the Raycast extension (React/TypeScript). Works like a charm now.

However, I hadn’t thought of incrementing the JDex by default—it’s such an obvious idea in hindsight. Thanks for the inspiration!
