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. 48
      src/main.ts
  3. 69
      src/metadataManager.ts
  4. 8
      src/nostr/eventBuilder.ts
  5. 2
      src/types.ts
  6. 14
      src/ui/metadataModal.ts

13
README.md

@ -86,10 +86,11 @@ author: "Author Name" @@ -86,10 +86,11 @@ author: "Author Name"
summary: "Article summary"
image: "https://example.com/image.jpg"
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`)
Supported event kinds: **30040**, **30041**, **30818**
@ -175,8 +176,8 @@ All tag values are normalized per NKBIP-08 spec (lowercase, hyphens, numbers onl @@ -175,8 +176,8 @@ All tag values are normalized per NKBIP-08 spec (lowercase, hyphens, numbers onl
### Stand-alone vs Nested 30041
- **Stand-alone 30041**: Uses NKBIP-01 tags (d, title, image, summary, published_at, topics)
- **Nested 30041** (under 30040): Uses NKBIP-08 tags
- **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 plus automatically-generated `published_at`
- **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)
- 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 @@ -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.
**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
- `kind` - Event kind (required)
@ -198,7 +201,7 @@ All predefined metadata fields are shown in frontmatter/attributes with placehol @@ -198,7 +201,7 @@ All predefined metadata fields are shown in frontmatter/attributes with placehol
### Kind-Specific Fields
**30023 (Article)**:
- `published_at` - Unix timestamp
- No additional fields beyond common ones
**30040 (Publication Index)**:
- `type` - Publication type (book, illustrated, magazine, documentation, academic, blog)
@ -211,7 +214,7 @@ All predefined metadata fields are shown in frontmatter/attributes with placehol @@ -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
**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
- `collection_id` - Inherited from root 30040 (C tag)
- `title_id` - From root 30040 book title (T tag)

48
src/main.ts

@ -1,4 +1,4 @@ @@ -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 { ScriptoriumSettingTab } from "./ui/settingsTab";
import { NewDocumentModal } from "./ui/newDocumentModal";
@ -65,9 +65,31 @@ export default class ScriptoriumPlugin extends Plugin { @@ -65,9 +65,31 @@ export default class ScriptoriumPlugin extends Plugin {
callback: () => this.handleNewDocument(),
});
// Add ribbon icon for creating new documents
this.addRibbonIcon("file-plus", "New Nostr Document", () => {
this.handleNewDocument();
// Add ribbon icon with menu for Nostr actions
const ribbonIcon = this.addRibbonIcon("zap", "Nostr", () => {
// 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
@ -163,17 +185,15 @@ export default class ScriptoriumPlugin extends Plugin { @@ -163,17 +185,15 @@ export default class ScriptoriumPlugin extends Plugin {
}
// 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 = "";
if (kind === 30040) {
// AsciiDoc document header for 30040
if (kind === 30040 || kind === 30041 || kind === 30818) {
// AsciiDoc files - minimal content, writeMetadata will format properly
content = `= ${title}\n\n`;
} else if (kind === 30023 || kind === 30817 || kind === 30818) {
// Add title as heading for other kinds that require title
if (kind === 30817 || kind === 30818) {
content = `# ${title}\n\n`;
} else {
} else if (kind === 30023 || kind === 30817) {
// Markdown files - add title as heading
content = `# ${title}\n\n`;
}
} else if (kind === 1 || kind === 11) {
// For kind 1 and 11, add a simple placeholder
content = `\n`;
@ -207,8 +227,8 @@ export default class ScriptoriumPlugin extends Plugin { @@ -207,8 +227,8 @@ export default class ScriptoriumPlugin extends Plugin {
// Create metadata with title preset from the filename
const metadata = createDefaultMetadata(kind);
// Always set title if provided (even for kind 1 where it's optional)
if (title && title.trim()) {
// Set title if provided (skip for kind 1 which doesn't have title)
if (kind !== 1 && title && title.trim()) {
(metadata as any).title = title.trim();
}

69
src/metadataManager.ts

@ -14,9 +14,6 @@ interface TagDefinition { @@ -14,9 +14,6 @@ interface TagDefinition {
const TAG_DEFINITIONS: Record<EventKind, TagDefinition[]> = {
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 },
],
11: [
@ -30,7 +27,6 @@ const TAG_DEFINITIONS: Record<EventKind, TagDefinition[]> = { @@ -30,7 +27,6 @@ const TAG_DEFINITIONS: Record<EventKind, TagDefinition[]> = {
{ key: "author", description: "Author name", required: false },
{ key: "summary", description: "Article summary", 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 },
],
30040: [
@ -52,7 +48,6 @@ const TAG_DEFINITIONS: Record<EventKind, TagDefinition[]> = { @@ -52,7 +48,6 @@ const TAG_DEFINITIONS: Record<EventKind, TagDefinition[]> = {
{ key: "title", description: "Chapter/section title (required)", required: true },
{ key: "image", description: "Image URL", 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 },
// 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
@ -214,16 +209,23 @@ function parseAsciiDocAttributes(content: string): { metadata: Record<string, an @@ -214,16 +209,23 @@ function parseAsciiDocAttributes(content: string): { metadata: Record<string, an
}
}
bodyStartIndex = i + 1;
} else if (foundTitle && line === "") {
// Empty line after title/attributes - body starts after this
continue;
}
// Handle blank lines and body start
if (foundTitle) {
if (line === "") {
// Empty line - continue parsing in case there are more attributes after blank line
// (for backwards compatibility), but update body start index
bodyStartIndex = i + 1;
break;
} else if (foundTitle && !line.startsWith(":")) {
// Non-attribute line after title - body starts here
continue;
} else if (!line.startsWith(":")) {
// Non-attribute, non-empty line after title - body starts here
bodyStartIndex = i;
break;
}
}
}
const body = lines.slice(bodyStartIndex).join("\n");
return { metadata, body };
@ -231,6 +233,7 @@ function parseAsciiDocAttributes(content: string): { metadata: Record<string, an @@ -231,6 +233,7 @@ function parseAsciiDocAttributes(content: string): { metadata: Record<string, an
/**
* 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> {
const filtered: Record<string, any> = {};
@ -242,6 +245,11 @@ function filterPlaceholders(metadata: Record<string, any>, kind: EventKind): Rec @@ -242,6 +245,11 @@ function filterPlaceholders(metadata: Record<string, any>, kind: EventKind): Rec
continue;
}
// Remove published_at - it's automatically generated during event creation
if (key === "published_at") {
continue;
}
// Skip placeholder values
if (isPlaceholder(value, key, kind)) {
continue;
@ -356,7 +364,21 @@ export async function writeMetadata( @@ -356,7 +364,21 @@ export async function writeMetadata(
if (isMarkdownFile(file)) {
const { body } = parseMarkdownFrontmatter(currentContent);
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);
} else if (isAsciiDocFile(file)) {
// For AsciiDoc, we need to preserve the title if it exists in the body
@ -391,13 +413,13 @@ export async function writeMetadata( @@ -391,13 +413,13 @@ export async function writeMetadata(
const actualBody = bodyLines.slice(actualBodyStart).join("\n");
// Format new content with title + attributes + body
// Note: No blank line between document header and attributes (AsciiDoc spec)
const lines: string[] = [];
if (titleLine) {
lines.push(titleLine);
} else if (metadata.title) {
lines.push(`= ${metadata.title}`);
}
lines.push("");
// Add all predefined attributes with placeholders or actual values
const kind = metadata.kind;
@ -450,9 +472,30 @@ export async function writeMetadata( @@ -450,9 +472,30 @@ export async function writeMetadata(
}
}
// Add blank line after attributes (before body content)
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);
}
} catch (error) {

8
src/nostr/eventBuilder.ts

@ -67,6 +67,7 @@ export function getNpubFromPrivkey(privkey: string): string { @@ -67,6 +67,7 @@ export function getNpubFromPrivkey(privkey: string): string {
/**
* Build tags array from metadata
* Automatically adds published_at tag with current UNIX timestamp for all replaceable event kinds
*/
export function buildTagsFromMetadata(
metadata: EventMetadata,
@ -75,6 +76,11 @@ export function buildTagsFromMetadata( @@ -75,6 +76,11 @@ export function buildTagsFromMetadata(
): 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) {
case 1:
// No special tags required (title is optional)
@ -108,7 +114,6 @@ export function buildTagsFromMetadata( @@ -108,7 +114,6 @@ export function buildTagsFromMetadata(
if (metadata.title) tags.push(["title", metadata.title]);
if (metadata.image) tags.push(["image", metadata.image]);
if (metadata.summary) tags.push(["summary", metadata.summary]);
if (metadata.published_at) tags.push(["published_at", metadata.published_at]);
if (metadata.topics) {
metadata.topics.forEach((topic) => tags.push(["t", topic]));
}
@ -176,7 +181,6 @@ export function buildTagsFromMetadata( @@ -176,7 +181,6 @@ export function buildTagsFromMetadata(
// Stand-alone 30041 can have same tags as 30023
if (meta30041.image) tags.push(["image", meta30041.image]);
if (meta30041.summary) tags.push(["summary", meta30041.summary]);
if (meta30041.published_at) tags.push(["published_at", meta30041.published_at]);
if (meta30041.topics) {
meta30041.topics.forEach((topic) => tags.push(["t", topic]));
}

2
src/types.ts

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

14
src/ui/metadataModal.ts

@ -34,6 +34,8 @@ export class MetadataModal extends Modal { @@ -34,6 +34,8 @@ export class MetadataModal extends Modal {
});
}
// Author and Summary are not shown for kind 1
if (this.metadata.kind !== 1) {
new Setting(contentEl)
.setName("Author")
.setDesc("Author name")
@ -56,6 +58,7 @@ export class MetadataModal extends Modal { @@ -56,6 +58,7 @@ export class MetadataModal extends Modal {
});
text.inputEl.rows = 3;
});
}
// Kind-specific fields
this.renderKindSpecificFields(contentEl);
@ -119,17 +122,6 @@ export class MetadataModal extends Modal { @@ -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)
.setName("Topics")
.setDesc("Comma-separated topics (t tags)")

Loading…
Cancel
Save