Browse Source

auto-generated published_at field

remove blank lines between doc header and attributes in asciidoc
add default text in the files
add event-creation and publishing menu items
master
Silberengel 7 days ago
parent
commit
262bc8421a
  1. 13
      README.md
  2. 50
      src/main.ts
  3. 75
      src/metadataManager.ts
  4. 8
      src/nostr/eventBuilder.ts
  5. 2
      src/types.ts
  6. 56
      src/ui/metadataModal.ts

13
README.md

@ -86,10 +86,11 @@ author: "Author Name"
summary: "Article summary" summary: "Article summary"
image: "https://example.com/image.jpg" image: "https://example.com/image.jpg"
topics: "bitcoin, nostr" topics: "bitcoin, nostr"
published_at: "1234567890"
--- ---
``` ```
**Note**: The `published_at` tag is automatically generated with the current UNIX timestamp during event creation for all replaceable event kinds. It should not be included in metadata and will be ignored if present.
### AsciiDoc Files (`.adoc`) ### AsciiDoc Files (`.adoc`)
Supported event kinds: **30040**, **30041**, **30818** Supported event kinds: **30040**, **30041**, **30818**
@ -175,8 +176,8 @@ All tag values are normalized per NKBIP-08 spec (lowercase, hyphens, numbers onl
### Stand-alone vs Nested 30041 ### Stand-alone vs Nested 30041
- **Stand-alone 30041**: Uses NKBIP-01 tags (d, title, image, summary, published_at, topics) - **Stand-alone 30041**: Uses NKBIP-01 tags (d, title, image, summary, topics) plus automatically-generated `published_at`
- **Nested 30041** (under 30040): Uses NKBIP-08 tags - **Nested 30041** (under 30040): Uses NKBIP-08 tags plus automatically-generated `published_at`
- **Two-level structure** (book + chapters): 30041 events are chapters (c tag from chapter title, no s tag) - **Two-level structure** (book + chapters): 30041 events are chapters (c tag from chapter title, no s tag)
- **Three-level structure** (book + chapters + sections): 30041 events are sections (c tag from parent chapter, s tag from section title) - **Three-level structure** (book + chapters + sections): 30041 events are sections (c tag from parent chapter, s tag from section title)
- All nested 30041 events inherit C tag (collection_id) and v tag (version_tag) from root 30040 - All nested 30041 events inherit C tag (collection_id) and v tag (version_tag) from root 30040
@ -186,6 +187,8 @@ All tag values are normalized per NKBIP-08 spec (lowercase, hyphens, numbers onl
All predefined metadata fields are shown in frontmatter/attributes with placeholder descriptions. Remove or update placeholders you don't need. Placeholder values are automatically skipped when creating events. All predefined metadata fields are shown in frontmatter/attributes with placeholder descriptions. Remove or update placeholders you don't need. Placeholder values are automatically skipped when creating events.
**Important**: The `published_at` tag is automatically generated with the current UNIX timestamp during event creation for all replaceable event kinds (all event kinds supported by this plugin). Do not include `published_at` in your metadata - it will be automatically added and any existing `published_at` values in metadata will be ignored.
### Common Fields ### Common Fields
- `kind` - Event kind (required) - `kind` - Event kind (required)
@ -198,7 +201,7 @@ All predefined metadata fields are shown in frontmatter/attributes with placehol
### Kind-Specific Fields ### Kind-Specific Fields
**30023 (Article)**: **30023 (Article)**:
- `published_at` - Unix timestamp - No additional fields beyond common ones
**30040 (Publication Index)**: **30040 (Publication Index)**:
- `type` - Publication type (book, illustrated, magazine, documentation, academic, blog) - `type` - Publication type (book, illustrated, magazine, documentation, academic, blog)
@ -211,7 +214,7 @@ All predefined metadata fields are shown in frontmatter/attributes with placehol
- `version_tag` - NKBIP-08 version identifier (v tag) - If set in root 30040, inherited by all events in the hierarchy - `version_tag` - NKBIP-08 version identifier (v tag) - If set in root 30040, inherited by all events in the hierarchy
**30041 (Publication Content)**: **30041 (Publication Content)**:
- **Stand-alone**: Same as 30023 (image, summary, published_at, topics) - **Stand-alone**: Same as 30023 (image, summary, topics)
- **Nested** (under 30040): NKBIP-08 tags - **Nested** (under 30040): NKBIP-08 tags
- `collection_id` - Inherited from root 30040 (C tag) - `collection_id` - Inherited from root 30040 (C tag)
- `title_id` - From root 30040 book title (T tag) - `title_id` - From root 30040 book title (T tag)

50
src/main.ts

@ -1,4 +1,4 @@
import { Plugin, TFile, Notice } from "obsidian"; import { Plugin, TFile, Notice, Menu } from "obsidian";
import { ScriptoriumSettings, EventKind, DEFAULT_SETTINGS } from "./types"; import { ScriptoriumSettings, EventKind, DEFAULT_SETTINGS } from "./types";
import { ScriptoriumSettingTab } from "./ui/settingsTab"; import { ScriptoriumSettingTab } from "./ui/settingsTab";
import { NewDocumentModal } from "./ui/newDocumentModal"; import { NewDocumentModal } from "./ui/newDocumentModal";
@ -65,9 +65,31 @@ export default class ScriptoriumPlugin extends Plugin {
callback: () => this.handleNewDocument(), callback: () => this.handleNewDocument(),
}); });
// Add ribbon icon for creating new documents // Add ribbon icon with menu for Nostr actions
this.addRibbonIcon("file-plus", "New Nostr Document", () => { const ribbonIcon = this.addRibbonIcon("zap", "Nostr", () => {
this.handleNewDocument(); // Create and show menu
const menu = new Menu();
menu.addItem((item) => {
item.setTitle("Write Nostr note")
.setIcon("file-plus")
.onClick(() => this.handleNewDocument());
});
menu.addItem((item) => {
item.setTitle("Create Nostr events")
.setIcon("file-check")
.onClick(() => this.handleCreateEvents());
});
menu.addItem((item) => {
item.setTitle("Publish events to relays")
.setIcon("upload")
.onClick(() => this.handlePublishEvents());
});
// Show menu at the ribbon icon position
if (ribbonIcon) {
const rect = ribbonIcon.getBoundingClientRect();
menu.showAtPosition({ x: rect.left, y: rect.bottom + 5 });
}
}); });
// Status bar // Status bar
@ -163,17 +185,15 @@ export default class ScriptoriumPlugin extends Plugin {
} }
// Create default content based on kind // Create default content based on kind
// Note: This content will be replaced by writeMetadata() which formats
// the file properly with metadata. We just need minimal content here.
let content = ""; let content = "";
if (kind === 30040) { if (kind === 30040 || kind === 30041 || kind === 30818) {
// AsciiDoc document header for 30040 // AsciiDoc files - minimal content, writeMetadata will format properly
content = `= ${title}\n\n`; content = `= ${title}\n\n`;
} else if (kind === 30023 || kind === 30817 || kind === 30818) { } else if (kind === 30023 || kind === 30817) {
// Add title as heading for other kinds that require title // Markdown files - add title as heading
if (kind === 30817 || kind === 30818) { content = `# ${title}\n\n`;
content = `# ${title}\n\n`;
} else {
content = `# ${title}\n\n`;
}
} else if (kind === 1 || kind === 11) { } else if (kind === 1 || kind === 11) {
// For kind 1 and 11, add a simple placeholder // For kind 1 and 11, add a simple placeholder
content = `\n`; content = `\n`;
@ -207,8 +227,8 @@ export default class ScriptoriumPlugin extends Plugin {
// Create metadata with title preset from the filename // Create metadata with title preset from the filename
const metadata = createDefaultMetadata(kind); const metadata = createDefaultMetadata(kind);
// Always set title if provided (even for kind 1 where it's optional) // Set title if provided (skip for kind 1 which doesn't have title)
if (title && title.trim()) { if (kind !== 1 && title && title.trim()) {
(metadata as any).title = title.trim(); (metadata as any).title = title.trim();
} }

75
src/metadataManager.ts

@ -14,9 +14,6 @@ interface TagDefinition {
const TAG_DEFINITIONS: Record<EventKind, TagDefinition[]> = { const TAG_DEFINITIONS: Record<EventKind, TagDefinition[]> = {
1: [ 1: [
{ key: "title", description: "Note title (optional)", required: false },
{ key: "author", description: "Author name", required: false },
{ key: "summary", description: "Brief summary", required: false },
{ key: "topics", description: "Comma-separated topics (e.g., 'bitcoin, nostr')", required: false }, { key: "topics", description: "Comma-separated topics (e.g., 'bitcoin, nostr')", required: false },
], ],
11: [ 11: [
@ -30,7 +27,6 @@ const TAG_DEFINITIONS: Record<EventKind, TagDefinition[]> = {
{ key: "author", description: "Author name", required: false }, { key: "author", description: "Author name", required: false },
{ key: "summary", description: "Article summary", required: false }, { key: "summary", description: "Article summary", required: false },
{ key: "image", description: "Image URL", required: false }, { key: "image", description: "Image URL", required: false },
{ key: "published_at", description: "Unix timestamp of first publication", required: false },
{ key: "topics", description: "Comma-separated topics (e.g., 'bitcoin, nostr')", required: false }, { key: "topics", description: "Comma-separated topics (e.g., 'bitcoin, nostr')", required: false },
], ],
30040: [ 30040: [
@ -52,7 +48,6 @@ const TAG_DEFINITIONS: Record<EventKind, TagDefinition[]> = {
{ key: "title", description: "Chapter/section title (required)", required: true }, { key: "title", description: "Chapter/section title (required)", required: true },
{ key: "image", description: "Image URL", required: false }, { key: "image", description: "Image URL", required: false },
{ key: "summary", description: "Article summary", required: false }, { key: "summary", description: "Article summary", required: false },
{ key: "published_at", description: "Unix timestamp of first publication", required: false },
{ key: "topics", description: "Comma-separated topics (e.g., 'bitcoin, nostr')", required: false }, { key: "topics", description: "Comma-separated topics (e.g., 'bitcoin, nostr')", required: false },
// Note: NKBIP-08 tags (collection_id, title_id, chapter_id, section_id, version_tag) // Note: NKBIP-08 tags (collection_id, title_id, chapter_id, section_id, version_tag)
// are only used when 30041 is nested under 30040, not for stand-alone 30041 events // are only used when 30041 is nested under 30040, not for stand-alone 30041 events
@ -214,14 +209,21 @@ function parseAsciiDocAttributes(content: string): { metadata: Record<string, an
} }
} }
bodyStartIndex = i + 1; bodyStartIndex = i + 1;
} else if (foundTitle && line === "") { continue;
// Empty line after title/attributes - body starts after this }
bodyStartIndex = i + 1;
break; // Handle blank lines and body start
} else if (foundTitle && !line.startsWith(":")) { if (foundTitle) {
// Non-attribute line after title - body starts here if (line === "") {
bodyStartIndex = i; // Empty line - continue parsing in case there are more attributes after blank line
break; // (for backwards compatibility), but update body start index
bodyStartIndex = i + 1;
continue;
} else if (!line.startsWith(":")) {
// Non-attribute, non-empty line after title - body starts here
bodyStartIndex = i;
break;
}
} }
} }
@ -231,6 +233,7 @@ function parseAsciiDocAttributes(content: string): { metadata: Record<string, an
/** /**
* Filter out placeholder values from metadata * Filter out placeholder values from metadata
* Also removes published_at as it's automatically generated during event creation
*/ */
function filterPlaceholders(metadata: Record<string, any>, kind: EventKind): Record<string, any> { function filterPlaceholders(metadata: Record<string, any>, kind: EventKind): Record<string, any> {
const filtered: Record<string, any> = {}; const filtered: Record<string, any> = {};
@ -242,6 +245,11 @@ function filterPlaceholders(metadata: Record<string, any>, kind: EventKind): Rec
continue; continue;
} }
// Remove published_at - it's automatically generated during event creation
if (key === "published_at") {
continue;
}
// Skip placeholder values // Skip placeholder values
if (isPlaceholder(value, key, kind)) { if (isPlaceholder(value, key, kind)) {
continue; continue;
@ -356,7 +364,21 @@ export async function writeMetadata(
if (isMarkdownFile(file)) { if (isMarkdownFile(file)) {
const { body } = parseMarkdownFrontmatter(currentContent); const { body } = parseMarkdownFrontmatter(currentContent);
const frontmatter = formatMarkdownFrontmatter(metadata); const frontmatter = formatMarkdownFrontmatter(metadata);
const newContent = frontmatter ? `---\n${frontmatter}---\n${body}` : body;
// If body is empty or only whitespace, add default content
const trimmedBody = body.trim();
let finalBody = body;
if (!trimmedBody || trimmedBody.length === 0) {
// For kind 1, just add placeholder text (no header)
if (metadata.kind === 1) {
finalBody = `place your content here\n\n---\n\n**How to use this app:**\n1. Edit your content above\n2. Click the Nostr menu button (lightning bolt icon ⚡) in the left sidebar\n3. Select "Create Nostr events" to create and sign events\n4. Select "Publish events to relays" to publish to relays`;
} else {
// For other kinds, add level-one header (#) with default text
finalBody = `# This is the first header in this document\n\nplace your content here\n\n---\n\n**How to use this app:**\n1. Edit your content above\n2. Click the Nostr menu button (lightning bolt icon ⚡) in the left sidebar\n3. Select "Create Nostr events" to create and sign events\n4. Select "Publish events to relays" to publish to relays`;
}
}
const newContent = frontmatter ? `---\n${frontmatter}---\n${finalBody}` : finalBody;
await app.vault.modify(file, newContent); await app.vault.modify(file, newContent);
} else if (isAsciiDocFile(file)) { } else if (isAsciiDocFile(file)) {
// For AsciiDoc, we need to preserve the title if it exists in the body // For AsciiDoc, we need to preserve the title if it exists in the body
@ -391,13 +413,13 @@ export async function writeMetadata(
const actualBody = bodyLines.slice(actualBodyStart).join("\n"); const actualBody = bodyLines.slice(actualBodyStart).join("\n");
// Format new content with title + attributes + body // Format new content with title + attributes + body
// Note: No blank line between document header and attributes (AsciiDoc spec)
const lines: string[] = []; const lines: string[] = [];
if (titleLine) { if (titleLine) {
lines.push(titleLine); lines.push(titleLine);
} else if (metadata.title) { } else if (metadata.title) {
lines.push(`= ${metadata.title}`); lines.push(`= ${metadata.title}`);
} }
lines.push("");
// Add all predefined attributes with placeholders or actual values // Add all predefined attributes with placeholders or actual values
const kind = metadata.kind; const kind = metadata.kind;
@ -450,9 +472,30 @@ export async function writeMetadata(
} }
} }
// Add blank line after attributes (before body content)
lines.push(""); lines.push("");
const newContent = lines.join("\n") + actualBody; // If body is empty or only whitespace, add default content with level-one header
const trimmedBody = actualBody.trim();
if (!trimmedBody || trimmedBody.length === 0) {
// Add level-one header (==) with default text (title is already in doc header)
lines.push(`== This is the first header in this document`);
lines.push("");
lines.push("place your content here");
lines.push("");
lines.push("---");
lines.push("");
lines.push("**How to use this app:**");
lines.push("1. Edit your content above");
lines.push("2. Click the Nostr menu button (lightning bolt icon ⚡) in the left sidebar");
lines.push("3. Select \"Create Nostr events\" to create and sign events");
lines.push("4. Select \"Publish events to relays\" to publish to relays");
} else {
// Use existing body content
lines.push(actualBody);
}
const newContent = lines.join("\n");
await app.vault.modify(file, newContent); await app.vault.modify(file, newContent);
} }
} catch (error) { } catch (error) {

8
src/nostr/eventBuilder.ts

@ -67,6 +67,7 @@ export function getNpubFromPrivkey(privkey: string): string {
/** /**
* Build tags array from metadata * Build tags array from metadata
* Automatically adds published_at tag with current UNIX timestamp for all replaceable event kinds
*/ */
export function buildTagsFromMetadata( export function buildTagsFromMetadata(
metadata: EventMetadata, metadata: EventMetadata,
@ -75,6 +76,11 @@ export function buildTagsFromMetadata(
): string[][] { ): string[][] {
const tags: string[][] = []; const tags: string[][] = [];
// All event kinds in this plugin are replaceable (0-9999 range)
// Add published_at tag automatically with current UNIX timestamp
const publishedAt = Math.floor(Date.now() / 1000).toString();
tags.push(["published_at", publishedAt]);
switch (metadata.kind) { switch (metadata.kind) {
case 1: case 1:
// No special tags required (title is optional) // No special tags required (title is optional)
@ -108,7 +114,6 @@ export function buildTagsFromMetadata(
if (metadata.title) tags.push(["title", metadata.title]); if (metadata.title) tags.push(["title", metadata.title]);
if (metadata.image) tags.push(["image", metadata.image]); if (metadata.image) tags.push(["image", metadata.image]);
if (metadata.summary) tags.push(["summary", metadata.summary]); if (metadata.summary) tags.push(["summary", metadata.summary]);
if (metadata.published_at) tags.push(["published_at", metadata.published_at]);
if (metadata.topics) { if (metadata.topics) {
metadata.topics.forEach((topic) => tags.push(["t", topic])); metadata.topics.forEach((topic) => tags.push(["t", topic]));
} }
@ -176,7 +181,6 @@ export function buildTagsFromMetadata(
// Stand-alone 30041 can have same tags as 30023 // Stand-alone 30041 can have same tags as 30023
if (meta30041.image) tags.push(["image", meta30041.image]); if (meta30041.image) tags.push(["image", meta30041.image]);
if (meta30041.summary) tags.push(["summary", meta30041.summary]); if (meta30041.summary) tags.push(["summary", meta30041.summary]);
if (meta30041.published_at) tags.push(["published_at", meta30041.published_at]);
if (meta30041.topics) { if (meta30041.topics) {
meta30041.topics.forEach((topic) => tags.push(["t", topic])); meta30041.topics.forEach((topic) => tags.push(["t", topic]));
} }

2
src/types.ts

@ -55,7 +55,6 @@ export interface Kind30023Metadata extends BaseMetadata {
kind: 30023; kind: 30023;
title: string; // mandatory title: string; // mandatory
image?: string; image?: string;
published_at?: string;
topics?: string[]; // t tags topics?: string[]; // t tags
} }
@ -92,7 +91,6 @@ export interface Kind30041Metadata extends BaseMetadata {
title: string; // mandatory title: string; // mandatory
// Stand-alone 30041 can have same tags as 30023 // Stand-alone 30041 can have same tags as 30023
image?: string; image?: string;
published_at?: string;
topics?: string[]; // t tags topics?: string[]; // t tags
// NKBIP-08 tags (only for nested 30041 under 30040) // NKBIP-08 tags (only for nested 30041 under 30040)
collection_id?: string; // C tag (inherited from root 30040) collection_id?: string; // C tag (inherited from root 30040)

56
src/ui/metadataModal.ts

@ -34,28 +34,31 @@ export class MetadataModal extends Modal {
}); });
} }
new Setting(contentEl) // Author and Summary are not shown for kind 1
.setName("Author") if (this.metadata.kind !== 1) {
.setDesc("Author name") new Setting(contentEl)
.addText((text) => { .setName("Author")
text.setValue(this.metadata.author || "") .setDesc("Author name")
.setPlaceholder("Enter author") .addText((text) => {
.onChange((value) => { text.setValue(this.metadata.author || "")
this.metadata.author = value; .setPlaceholder("Enter author")
}); .onChange((value) => {
}); this.metadata.author = value;
});
});
new Setting(contentEl) new Setting(contentEl)
.setName("Summary") .setName("Summary")
.setDesc("Brief summary or description") .setDesc("Brief summary or description")
.addTextArea((text) => { .addTextArea((text) => {
text.setValue(this.metadata.summary || "") text.setValue(this.metadata.summary || "")
.setPlaceholder("Enter summary") .setPlaceholder("Enter summary")
.onChange((value) => { .onChange((value) => {
this.metadata.summary = value; this.metadata.summary = value;
}); });
text.inputEl.rows = 3; text.inputEl.rows = 3;
}); });
}
// Kind-specific fields // Kind-specific fields
this.renderKindSpecificFields(contentEl); this.renderKindSpecificFields(contentEl);
@ -119,17 +122,6 @@ export class MetadataModal extends Modal {
}); });
}); });
new Setting(container)
.setName("Published At")
.setDesc("Unix timestamp of first publication")
.addText((text) => {
text.setValue(meta.published_at || "")
.setPlaceholder("Unix timestamp")
.onChange((value) => {
meta.published_at = value;
});
});
new Setting(container) new Setting(container)
.setName("Topics") .setName("Topics")
.setDesc("Comma-separated topics (t tags)") .setDesc("Comma-separated topics (t tags)")

Loading…
Cancel
Save