Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "longform-paperbell",
"name": "Longform (PaperBell)",
"version": "2.2.0-beta.4",
"version": "2.2.0-beta.6",
"minAppVersion": "1.0",
"description": "Write novels, screenplays, and other long projects in Obsidian.",
"author": "Kevin Barrett",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "longform",
"version": "2.2.0-beta.4",
"version": "2.2.0-beta.6",
"description": "Write novels, screenplays, and other long projects in Obsidian (https://obsidian.md).",
"main": "main.js",
"scripts": {
Expand Down
11 changes: 9 additions & 2 deletions src/commands/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "src/compile";
import { JumpModal } from "./helpers";
import { draftTitle } from "src/model/draft-utils";
import { projectRootPath } from "src/model/project-resources";
import type { Draft } from "src/model/types";

export const compileCurrent: CommandBuilder = (plugin) => ({
Expand Down Expand Up @@ -46,12 +47,17 @@ export const compileCurrent: CommandBuilder = (plugin) => ({
}
}

const projectRoot = projectRootPath(
get(projects)[draft.title] ?? [draft]
);

compile(
plugin.app,
draft,
workflow,
calculatedKinds,
onCompileStatusChange
onCompileStatusChange,
{ projectRoot }
);
},
});
Expand Down Expand Up @@ -162,7 +168,8 @@ export const compileSelection: CommandBuilder = (plugin) => ({
draft,
workflow,
calculatedKinds,
onCompileStatusChange
onCompileStatusChange,
{ projectRoot: projectRootPath(project) }
);
}
).open();
Expand Down
3 changes: 2 additions & 1 deletion src/compile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export async function compile(
workflow: Workflow,
kinds: CompileStepKind[],
statusCallback: (status: CompileStatus) => void,
options?: { suppressOpenAfter?: boolean }
options?: { suppressOpenAfter?: boolean; projectRoot?: string }
): Promise<void> {
let currentInput: any;

Expand Down Expand Up @@ -251,6 +251,7 @@ export async function compile(
normalizePath,
},
suppressOpenAfter: options?.suppressOpenAfter,
projectRoot: options?.projectRoot,
};

console.log(
Expand Down
7 changes: 7 additions & 0 deletions src/compile/steps/abstract-compile-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ export type CompileContext = {
* Set during batch ("Compile All Drafts") runs to avoid spawning a pane per draft.
*/
suppressOpenAfter?: boolean;
/**
* The project root: the lowest common ancestor folder of the project's drafts.
* Steps that resolve shared resources (e.g. metadata.json) search from the
* draft's folder up to this root, inclusive. When absent, only the draft's own
* folder (`projectPath`) is searched.
*/
projectRoot?: string;
};

/**
Expand Down
12 changes: 7 additions & 5 deletions src/compile/steps/add-zenodo-frontmatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
buildPandocYaml,
type ZenodoMetadata,
} from "./add-zenodo-frontmatter-utils";
import { projectResourceCandidatePaths } from "src/model/project-resources";

export const AddZenodoFrontmatterStep = makeBuiltinStep({
id: "add-zenodo-frontmatter",
Expand All @@ -22,7 +23,7 @@ export const AddZenodoFrontmatterStep = makeBuiltinStep({
id: "metadata-file",
name: "Metadata file",
description:
"Filename of the Zenodo deposition metadata JSON in your project folder (or its 'source/' subfolder). Trailing '.json' is optional.",
"Filename of the Zenodo deposition metadata JSON. Searched for in the draft's folder (or its 'source/' subfolder) and any parent folder up to the project root, so multiple drafts can share one file. Trailing '.json' is optional.",
type: CompileStepOptionType.Text,
default: "metadata.json",
},
Expand Down Expand Up @@ -55,10 +56,11 @@ export const AddZenodoFrontmatterStep = makeBuiltinStep({
? metaFileName
: `${metaFileName}.json`;

const candidatePaths = [
`${context.projectPath}/${baseName}`,
`${context.projectPath}/source/${baseName}`,
];
const candidatePaths = projectResourceCandidatePaths(
context.projectPath,
context.projectRoot ?? context.projectPath,
baseName
);

let file: TFile | null = null;
let foundPath = "";
Expand Down
12 changes: 7 additions & 5 deletions src/compile/steps/replace-json-placeholders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
buildPlaceholderRegex,
getByPath,
} from "./replace-json-placeholders-utils";
import { projectResourceCandidatePaths } from "src/model/project-resources";

export const ReplaceJsonPlaceholdersStep = makeBuiltinStep({
id: "replace-json-placeholders",
Expand All @@ -22,7 +23,7 @@ export const ReplaceJsonPlaceholdersStep = makeBuiltinStep({
id: "json-file",
name: "JSON file",
description:
"Filename of the JSON data file in your project folder (or its 'source/' subfolder). Trailing '.json' is optional.",
"Filename of the JSON data file. Searched for in the draft's folder (or its 'source/' subfolder) and any parent folder up to the project root. Trailing '.json' is optional.",
type: CompileStepOptionType.Text,
default: "results.json",
},
Expand Down Expand Up @@ -69,10 +70,11 @@ export const ReplaceJsonPlaceholdersStep = makeBuiltinStep({
? jsonFileName
: `${jsonFileName}.json`;

const candidatePaths = [
`${context.projectPath}/${baseName}`,
`${context.projectPath}/source/${baseName}`,
];
const candidatePaths = projectResourceCandidatePaths(
context.projectPath,
context.projectRoot ?? context.projectPath,
baseName
);

let file: TFile | null = null;
let foundPath = "";
Expand Down
26 changes: 26 additions & 0 deletions src/compile/steps/write-to-note-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Draft } from "src/model/types";

/**
* The per-draft name used by the `$2` placeholder in the Save-as-Note output path.
* Uses the draft's explicit `draftTitle` when set, otherwise falls back to the
* index file's basename (without the `.md` extension) so the name is always
* distinct across the drafts of a project.
*/
export function draftOutputName(draft: Draft): string {
const indexBasename = (draft.vaultPath.split("/").pop() ?? "").replace(
/\.md$/,
""
);
return draft.draftTitle ?? indexBasename;
}

/**
* Substitutes the Save-as-Note output-path placeholders for a given draft:
* - `$1` → the project title (shared across a project's drafts)
* - `$2` → this draft's name (see {@link draftOutputName})
* All occurrences of each token are replaced.
*/
export function applyTargetPlaceholders(target: string, draft: Draft): string {
const draftName = draftOutputName(draft);
return target.split("$2").join(draftName).split("$1").join(draft.title);
}
16 changes: 5 additions & 11 deletions src/compile/steps/write-to-note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CompileStepOptionType,
makeBuiltinStep,
} from "./abstract-compile-step";
import { applyTargetPlaceholders } from "./write-to-note-utils";

export const WriteToNoteStep = makeBuiltinStep({
id: "write-to-note",
Expand Down Expand Up @@ -37,17 +38,10 @@ export const WriteToNoteStep = makeBuiltinStep({
if (context.kind !== CompileStepKind.Manuscript) {
throw new Error("Cannot write non-manuscript as note.");
} else {
const indexBasename = context.draft.vaultPath
.split("/")
.last()
.replace(/\.md$/, "");
const draftName = context.draft.draftTitle ?? indexBasename;
let target = context.optionValues["target"] as string;
target = target
.split("$2")
.join(draftName)
.split("$1")
.join(context.draft.title);
const target = applyTargetPlaceholders(
context.optionValues["target"] as string,
context.draft
);

const openAfter = context.optionValues["open-after"] as boolean;
if (!target || target.length == 0) {
Expand Down
65 changes: 65 additions & 0 deletions src/model/project-resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { Draft } from "./types";

/** The folder containing an index note, derived from its vault path. */
export function draftParentFolder(vaultPath: string): string {
return vaultPath.split("/").slice(0, -1).join("/");
}

/**
* The lowest common ancestor folder shared by a set of folder paths, computed
* segment-wise. Returns "" (the vault root) when there is no shared prefix.
*/
export function lowestCommonAncestorFolder(folders: string[]): string {
if (folders.length === 0) return "";
const split = folders.map((f) => f.split("/").filter((s) => s.length > 0));
let common = split[0];
for (const segs of split.slice(1)) {
let i = 0;
while (i < common.length && i < segs.length && common[i] === segs[i]) {
i++;
}
common = common.slice(0, i);
}
return common.join("/");
}

/**
* The "project root" for a set of drafts: the lowest common ancestor of every
* draft's folder. Shared resources (e.g. metadata.json) are searched for between
* a draft's own folder and this root, inclusive.
*/
export function projectRootPath(projectDrafts: Draft[]): string {
return lowestCommonAncestorFolder(
projectDrafts.map((d) => draftParentFolder(d.vaultPath))
);
}

/**
* Ordered candidate paths for a named resource, searched from `startDir` upward
* to `rootDir` (inclusive). At each level both `<dir>/<baseName>` and
* `<dir>/source/<baseName>` are produced. When `rootDir` is not an ancestor of
* (or equal to) `startDir`, only `startDir` is searched — so callers that don't
* know a project root degrade to the original single-folder behavior.
*/
export function projectResourceCandidatePaths(
startDir: string,
rootDir: string,
baseName: string
): string[] {
const startSegs = startDir.split("/").filter((s) => s.length > 0);
const rootSegs = (rootDir ?? "").split("/").filter((s) => s.length > 0);

const rootIsAncestor =
rootSegs.length <= startSegs.length &&
rootSegs.every((s, i) => s === startSegs[i]);
const minLen = rootIsAncestor ? rootSegs.length : startSegs.length;

const candidates: string[] = [];
for (let len = startSegs.length; len >= minLen; len--) {
const dir = startSegs.slice(0, len).join("/");
const prefix = dir.length > 0 ? `${dir}/` : "";
candidates.push(`${prefix}${baseName}`);
candidates.push(`${prefix}source/${baseName}`);
}
return candidates;
}
10 changes: 8 additions & 2 deletions src/view/compile/CompileView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
selectedProjectHasMultipleDrafts,
} from "src/model/stores";
import { draftTitle } from "src/model/draft-utils";
import { projectRootPath } from "src/model/project-resources";
import CompileStepView from "./CompileStepView.svelte";
import SortableList from "../sortable/SortableList.svelte";
import AutoTextArea from "../components/AutoTextArea.svelte";
Expand Down Expand Up @@ -245,17 +246,19 @@
workflow: Workflow,
kinds: CompileStepKind[],
statusCallback: (status: CompileStatus) => void,
options?: { suppressOpenAfter?: boolean }
options?: { suppressOpenAfter?: boolean; projectRoot?: string }
) => Promise<void> = getContext("compile");

let isCompiling = false;

function doCompile() {
const projectRoot = projectRootPath($selectedProject ?? [$selectedDraft]);
compile(
$selectedDraft,
$currentWorkflow,
calculatedKinds,
onCompileStatusChange
onCompileStatusChange,
{ projectRoot }
);
}

Expand All @@ -265,6 +268,8 @@
return;
}

const projectRoot = projectRootPath(projectDrafts);

isCompiling = true;
let compiledCount = 0;

Expand Down Expand Up @@ -304,6 +309,7 @@
try {
await compile(draft, workflow, kinds, wrappedStatus, {
suppressOpenAfter: true,
projectRoot,
});
compiledCount++;
} catch (error) {
Expand Down
13 changes: 8 additions & 5 deletions src/view/explorer/ExplorerPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import ExplorerView from "./ExplorerView.svelte";
import { scenePath } from "src/model/scene-navigation";
import { migrate } from "src/model/migration";
import { get } from "svelte/store";
import { drafts, pluginSettings, selectedDraft } from "src/model/stores";
import {
drafts,
pluginSettings,
selectedDraft,
selectedProject,
} from "src/model/stores";
import { projectRootPath } from "src/model/project-resources";
import { insertScene } from "src/model/draft-utils";
import NewDraftModal from "src/view/project-lifecycle/new-draft-modal";
import MetadataModal from "src/view/metadata-modal";
Expand Down Expand Up @@ -326,10 +332,7 @@ export class ExplorerPane extends ItemView {
context.set("showMetadataModal", () => {
const draft = get(selectedDraft);
if (!draft) return;
const projectPath = draft.vaultPath
.split("/")
.slice(0, -1)
.join("/");
const projectPath = projectRootPath(get(selectedProject) ?? [draft]);
new MetadataModal(this.app, projectPath, draft.title).open();
});

Expand Down
Loading
Loading