Skip to content
Open
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
4 changes: 2 additions & 2 deletions apps/panmirror/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
*/


import { Editor, UITools } from 'editor';
export default { Editor, UITools };
import { Editor, UITools, InsertCitationDialog } from 'editor';
export default { Editor, UITools, InsertCitationDialog };
32 changes: 14 additions & 18 deletions packages/editor/src/api/bibliography/bibliography-provider_local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Copyright (C) 2022-2026 by Posit Software, PBC
*/
import { Node as ProsemirrorNode, Schema } from 'prosemirror-model';
import { Schema } from 'prosemirror-model';
import { Transaction } from 'prosemirror-state';

import { PandocServer } from '../pandoc';
Expand All @@ -17,7 +17,7 @@ import {
BibliographyCollection,
BibliographySourceWithCollections,
} from './bibliography';
import { ParsedYaml, parseYamlNodes, valueFromYamlText } from '../yaml';
import { ParsedYaml, YamlBlock, parseYamlNodes, valueFromYamlText } from '../yaml';
import { toBibTeX } from './bibDB';
import { CSL } from '../csl';
import { Bibliography } from 'editor-types';
Expand Down Expand Up @@ -47,9 +47,9 @@ export class BibliographyDataProviderLocal implements BibliographyDataProvider {
return Promise.resolve();
}

public async load(ui: EditorUI, docPath: string | null, resourcePath: string, yamlBlocks: ParsedYaml[]): Promise<boolean> {
public async load(ui: EditorUI, docPath: string | null, resourcePath: string, yamlBlocks: YamlBlock[]): Promise<boolean> {
// Gather the biblography files from the document
const bibliographiesRelative = bibliographyFilesFromDoc(yamlBlocks);
const bibliographiesRelative = bibliographyFilesFromYaml(yamlBlocks);
const bibliographiesAbsolute = bibliographiesRelative?.map(path => {
if (isAbsolute(path, ui.context.isWindowsDesktop())) {
return path;
Expand Down Expand Up @@ -133,7 +133,7 @@ export class BibliographyDataProviderLocal implements BibliographyDataProvider {
return undefined;
}

public bibliographyPaths(doc: ProsemirrorNode, ui: EditorUI): BibliographyFile[] {
public bibliographyPaths(yamlBlocks: YamlBlock[], ui: EditorUI): BibliographyFile[] {
const kPermissableFileExtensions = ['bibtex', 'bib', 'yaml', 'yml', 'json'];
if (this.bibliography?.project_biblios && this.bibliography.project_biblios.length > 0) {
return this.bibliography?.project_biblios.map(projectBiblio => {
Expand All @@ -146,7 +146,7 @@ export class BibliographyDataProviderLocal implements BibliographyDataProvider {
});
}
return (
bibliographyFilesFromDocument(doc)?.map(path => {
bibliographyFilesFromYaml(yamlBlocks)?.map(path => {
return {
displayPath: path,
fullPath: isAbsolute(path, ui.context.isWindowsDesktop()) ? path : joinPaths(ui.context.getDefaultResourceDir(), path),
Expand All @@ -158,17 +158,13 @@ export class BibliographyDataProviderLocal implements BibliographyDataProvider {
}
}

function bibliographyFilesFromDocument(doc: ProsemirrorNode): string[] | undefined {
// Gather the files from the document
return bibliographyFilesFromDoc(parseYamlNodes(doc));
}

function bibliographyFilesFromDoc(parsedYamls: ParsedYaml[]): string[] | undefined {
// The bibliography files declared in the yaml blocks (undefined if there are none)
export function bibliographyFilesFromYaml(yamlBlocks: YamlBlock[]): string[] | undefined {
// Read the values of any yaml blocks that include bibliography headers
// filter out blocks that don't include such headers
const bibliographyValues = parsedYamls
.map(parsedYaml => {
return valueFromYamlText('bibliography', parsedYaml.yamlCode);
const bibliographyValues = yamlBlocks
.map(yamlBlock => {
return valueFromYamlText('bibliography', yamlBlock.yamlCode);
})
.filter(val => val !== null);

Expand All @@ -189,8 +185,8 @@ function bibliographyFilesFromDoc(parsedYamls: ParsedYaml[]): string[] | undefin
return undefined;
}

function referenceBlockFromYaml(parsedYamls: ParsedYaml[]): string {
const refBlockParsedYamls = parsedYamls.filter(
function referenceBlockFromYaml(yamlBlocks: YamlBlock[]): string {
const refBlockParsedYamls = yamlBlocks.filter(
parsedYaml => parsedYaml.yaml !== null && typeof parsedYaml.yaml === 'object' && parsedYaml.yaml.references,
);

Expand Down Expand Up @@ -221,7 +217,7 @@ export function ensureBibliographyFileForDoc(tr: Transaction, bibliographyFile:
const parsedYamlNodes = parseYamlNodes(tr.doc);

// Gather the biblography files from the document
const bibliographiesRelative = bibliographyFilesFromDoc(parsedYamlNodes);
const bibliographiesRelative = bibliographyFilesFromYaml(parsedYamlNodes);
if (bibliographiesRelative && bibliographiesRelative.length > 0) {
// The user selected bibliography is already in the document OR
// There is a bibliography entry, but it doesn't include the user
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/


import { ParsedYaml, valueFromYamlText } from '../yaml';
import { YamlBlock, valueFromYamlText } from '../yaml';
import { suggestCiteId } from '../cite';

import {
Expand Down Expand Up @@ -47,7 +47,7 @@ export class BibliographyDataProviderZotero implements BibliographyDataProvider
_ui: EditorUI,
docPath: string,
_resourcePath: string,
yamlBlocks: ParsedYaml[],
yamlBlocks: YamlBlock[],
refreshCollectionData: boolean,
): Promise<boolean> {
let hasUpdates = false;
Expand Down Expand Up @@ -195,12 +195,12 @@ export class BibliographyDataProviderZotero implements BibliographyDataProvider
//
// By default, zotero integration is enabled. Add this header to disable integration
//
function zoteroConfig(parsedYamls: ParsedYaml[]): boolean | string[] {
function zoteroConfig(yamlBlocks: YamlBlock[]): boolean | string[] {
// Read the values of any yaml blocks that include bibliography headers
// filter out blocks that don't include such headers
const zoteroValues = parsedYamls
.map(parsedYaml => {
return valueFromYamlText('zotero', parsedYaml.yamlCode);
const zoteroValues = yamlBlocks
.map(yamlBlock => {
return valueFromYamlText('zotero', yamlBlock.yamlCode);
})
.filter(val => val !== null);

Expand Down
39 changes: 19 additions & 20 deletions packages/editor/src/api/bibliography/bibliography.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
* Copyright (C) 2022-2026 by Posit Software, PBC
*/

import { Node as ProsemirrorNode } from 'prosemirror-model';

import Fuse from 'fuse.js';
import { PandocServer } from '../pandoc';

import { EditorUI } from '../ui-types';
import { ParsedYaml, parseYamlNodes } from '../yaml';
import { YamlBlock, YamlBlockSource, yamlBlocksFromSource } from '../yaml';
import { CSL } from '../csl';
import { BibliographyDataProviderLocal, kLocalBibliographyProviderKey } from './bibliography-provider_local';
import { BibliographyDataProviderZotero } from './bibliography-provider_zotero';
Expand Down Expand Up @@ -72,13 +70,13 @@ export interface BibliographyDataProvider {
ui: EditorUI,
docPath: string | null,
resourcePath: string,
yamlBlocks: ParsedYaml[],
yamlBlocks: YamlBlock[],
refreshCollectionData?: boolean,
): Promise<boolean>;
collections(): BibliographyCollection[];
items(): BibliographySourceWithCollections[];
itemsForCollection(collectionKey: string): BibliographySourceWithCollections[];
bibliographyPaths(doc: ProsemirrorNode, ui: EditorUI): BibliographyFile[];
bibliographyPaths(yamlBlocks: YamlBlock[], ui: EditorUI): BibliographyFile[];
generateBibTeX(ui: EditorUI, id: string, csl: CSL): Promise<string | undefined>;
warningMessage(): string | undefined;
}
Expand Down Expand Up @@ -135,18 +133,18 @@ export class BibliographyManager {
}
}

public async prime(ui: EditorUI, doc: ProsemirrorNode) {
public async prime(ui: EditorUI, source: YamlBlockSource) {
// Load the bibliography
await this.load(ui, doc, true);
await this.load(ui, source, true);
}

public async loadLocal(ui: EditorUI, doc: ProsemirrorNode) {
await this.load(ui, doc, false, true);
public async loadLocal(ui: EditorUI, source: YamlBlockSource) {
await this.load(ui, source, false, true);
}

public async load(ui: EditorUI, doc: ProsemirrorNode, refreshCollectionData?: boolean, localOnly?: boolean): Promise<void> {
// read the Yaml blocks from the document
const parsedYamlNodes = parseYamlNodes(doc);
public async load(ui: EditorUI, source: YamlBlockSource, refreshCollectionData?: boolean, localOnly?: boolean): Promise<void> {
// read the Yaml blocks from the document (or use the blocks provided by the host)
const yamlBlocks = yamlBlocksFromSource(source);

// Currently edited doc
const docPath = ui.context.getDocumentPath();
Expand All @@ -155,7 +153,7 @@ export class BibliographyManager {
const providers = localOnly ? this.providers.filter(provider => provider.requiresWritable === false) : this.providers;
const providersNeedUpdate = await Promise.all(
providers.map(provider =>
provider.load(ui, docPath, ui.context.getDefaultResourceDir(), parsedYamlNodes, refreshCollectionData),
provider.load(ui, docPath, ui.context.getDefaultResourceDir(), yamlBlocks, refreshCollectionData),
),
);

Expand All @@ -177,7 +175,7 @@ export class BibliographyManager {
}

// Is this a writable bibliography
this.writable = this.isWritable(doc, ui);
this.writable = this.isWritable(yamlBlocks, ui);
}

public hasSources() {
Expand Down Expand Up @@ -209,21 +207,22 @@ export class BibliographyManager {
return this.writable || false;
}

private isWritable(doc: ProsemirrorNode, ui: EditorUI): boolean {
const bibliographyFiles = this.bibliographyFiles(doc, ui);
private isWritable(yamlBlocks: YamlBlock[], ui: EditorUI): boolean {
const bibliographyFiles = this.bibliographyFiles(yamlBlocks, ui);
if (bibliographyFiles.length === 0) {
// Since there are no bibliographies, we can permit writing a fresh one
return true;
}
return bibliographyFiles.filter(bibFile => bibFile.writable).length > 0;
}

public writableBibliographyFiles(doc: ProsemirrorNode, ui: EditorUI) {
return this.bibliographyFiles(doc, ui).filter(bibFile => bibFile.writable);
public writableBibliographyFiles(source: YamlBlockSource, ui: EditorUI) {
return this.bibliographyFiles(source, ui).filter(bibFile => bibFile.writable);
}

public bibliographyFiles(doc: ProsemirrorNode, ui: EditorUI): BibliographyFile[] {
const bibliographyPaths = this.providers.map(provider => provider.bibliographyPaths(doc, ui));
public bibliographyFiles(source: YamlBlockSource, ui: EditorUI): BibliographyFile[] {
const yamlBlocks = yamlBlocksFromSource(source);
const bibliographyPaths = this.providers.map(provider => provider.bibliographyPaths(yamlBlocks, ui));
return ([] as BibliographyFile[]).concat(...bibliographyPaths);
}

Expand Down
22 changes: 21 additions & 1 deletion packages/editor/src/api/yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,33 @@ export function stripYamlDelimeters(yamlCode: string) {
return yamlCode.replace(/^[ \t-]+\n/, '').replace(/\n[ \t-.]+$/, '');
}

export interface ParsedYaml {
// A yaml metadata block (e.g. front matter provided by a host w/o an editor instance)
export interface YamlBlock {
yamlCode: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
yaml: any;
}

// A yaml metadata block read from a document node
export interface ParsedYaml extends YamlBlock {
node: NodeWithPos;
}

// Yaml blocks can be read from a document or provided directly by the host
export type YamlBlockSource = ProsemirrorNode | YamlBlock[];

export function yamlBlocksFromSource(source: YamlBlockSource): YamlBlock[] {
return Array.isArray(source) ? source : parseYamlNodes(source);
}

// parse yaml blocks from their code (with or w/o enclosing ---)
export function parseYamlBlocks(yamlBlocks: string[]): YamlBlock[] {
return yamlBlocks.map(yamlText => {
const yamlCode = stripYamlDelimeters(yamlText);
return { yamlCode, yaml: parseYaml(yamlCode) };
});
}

export function parseYamlNodes(doc: ProsemirrorNode): ParsedYaml[] {
const yamlNodes = yamlMetadataNodes(doc);

Expand Down
20 changes: 9 additions & 11 deletions packages/editor/src/behaviors/insert_citation/insert_citation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import React from 'react';
import { createRoot } from 'react-dom/client';

import { Node as ProsemirrorNode } from 'prosemirror-model';

import {
BibliographyFile,
BibliographyManager,
Expand All @@ -17,6 +15,7 @@ import {
BibliographySource,
} from '../../api/bibliography/bibliography';
import { kInvalidCiteKeyChars } from '../../api/cite';
import { YamlBlockSource } from '../../api/yaml';
import { changeExtension } from '../../api/path';
import { EditorUI } from '../../api/ui-types';
import { WidgetProps } from '../../api/widgets/react';
Expand Down Expand Up @@ -56,10 +55,11 @@ export interface InsertCitationDialogResult {

// Show the insert citation dialog and returns the
// items that should be inserted, the bibliography in which to write them
// and the last selected position in the tree
// and the last selected position in the tree. The yaml source is the document
// (or its yaml blocks, for hosts w/o an editor instance)
export async function showInsertCitationDialog(
ui: EditorUI,
doc: ProsemirrorNode,
yamlSource: YamlBlockSource,
bibliographyManager: BibliographyManager,
server: EditorServer,
performInsertCitations: (result: InsertCitationDialogResult) => Promise<void>,
Expand Down Expand Up @@ -106,7 +106,7 @@ export async function showInsertCitationDialog(
const providersForBibliography = (writable: boolean) => {
if (writable) {
const providers = [
bibliographySourcePanel(doc, ui, bibliographyManager),
bibliographySourcePanel(ui, bibliographyManager),
doiSourcePanel(ui, server.doi, bibliographyManager),
crossrefSourcePanel(ui, server.crossref, server.doi, bibliographyManager),
dataciteSourcePanel(ui, server.datacite, server.doi, bibliographyManager),
Expand All @@ -117,7 +117,7 @@ export async function showInsertCitationDialog(
}
return providers;
} else {
return [bibliographySourcePanel(doc, ui, bibliographyManager)];
return [bibliographySourcePanel(ui, bibliographyManager)];
}
};

Expand All @@ -127,7 +127,7 @@ export async function showInsertCitationDialog(
const configurationStream: InsertCitationPanelConfigurationStream = {
current: {
providers: providersForBibliography(bibliographyManager.allowsWrites()),
bibliographyFiles: bibliographyManager.bibliographyFiles(doc, ui),
bibliographyFiles: bibliographyManager.bibliographyFiles(yamlSource, ui),
existingIds: bibliographyManager.localSources().map(source => source.id),
},
stream: () => {
Expand All @@ -136,10 +136,10 @@ export async function showInsertCitationDialog(
};

// Load the bibliography and then update the configuration
bibliographyManager.load(ui, doc, true).then(() => {
bibliographyManager.load(ui, yamlSource, true).then(() => {
updatedConfiguration = {
providers: providersForBibliography(bibliographyManager.allowsWrites()),
bibliographyFiles: bibliographyManager.bibliographyFiles(doc, ui),
bibliographyFiles: bibliographyManager.bibliographyFiles(yamlSource, ui),
existingIds: bibliographyManager.localSources().map(source => source.id),
};
});
Expand Down Expand Up @@ -212,7 +212,6 @@ export async function showInsertCitationDialog(
initiallySelectedNodeKey={initiallySelectedNodeKey}
onOk={onOk}
onCancel={onCancel}
doc={doc}
ui={ui}
/>
);
Expand Down Expand Up @@ -251,7 +250,6 @@ interface InsertCitationPanelConfigurationStream {
// to be added to a document.
interface InsertCitationPanelProps extends WidgetProps {
ui: EditorUI;
doc: ProsemirrorNode;
height: number;
width: number;
themed: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import React from 'react';
import uniqby from 'lodash.uniqby';

import { Node as ProsemirrorNode } from 'prosemirror-model';

import { EditorUI } from '../../../api/ui-types';
import { NavigationTreeNode } from '../../../api/widgets/navigation-tree';
import {
Expand All @@ -34,7 +32,6 @@ import './insert_citation-source-panel-bibliography.css';
const kAllLocalSourcesRootNodeType = 'All Local Sources';

export function bibliographySourcePanel(
_doc: ProsemirrorNode,
ui: EditorUI,
bibliographyManager: BibliographyManager,
): CitationSourcePanelProvider {
Expand Down
Loading
Loading