From 3c5c1efc9764093520900208d89404a8df557670 Mon Sep 17 00:00:00 2001 From: Kevin Ushey Date: Mon, 14 Sep 2026 19:55:52 -0700 Subject: [PATCH] add standalone insert citation dialog for hosts without an editor The Insert Citation dialog only used the ProseMirror document to read the YAML front matter, and the command applied the result with a transaction. Let the bibliography manager accept YAML blocks directly, split the bibliography writes out of ensureSourcesInBibliography, and export an InsertCitationDialog class (also from the panmirror bundle) that returns the citation ids and any bibliography file the host should declare, so plain-text hosts can offer the dialog. Addresses rstudio/rstudio#7876. --- apps/panmirror/src/index.ts | 4 +- .../bibliography-provider_local.ts | 32 +++--- .../bibliography-provider_zotero.ts | 12 +- .../src/api/bibliography/bibliography.ts | 39 ++++--- packages/editor/src/api/yaml.ts | 22 +++- .../insert_citation/insert_citation.tsx | 20 ++-- ...ert_citation-source-panel-bibliography.tsx | 3 - packages/editor/src/editor/editor-citation.ts | 107 ++++++++++++++++++ packages/editor/src/editor/editor-images.ts | 21 ++++ packages/editor/src/editor/editor-theme.ts | 14 ++- packages/editor/src/editor/editor.ts | 19 +--- packages/editor/src/index.ts | 1 + packages/editor/src/marks/cite/cite.ts | 37 +++++- 13 files changed, 247 insertions(+), 84 deletions(-) create mode 100644 packages/editor/src/editor/editor-citation.ts diff --git a/apps/panmirror/src/index.ts b/apps/panmirror/src/index.ts index 3bcf3d3c..d0fcc074 100644 --- a/apps/panmirror/src/index.ts +++ b/apps/panmirror/src/index.ts @@ -5,5 +5,5 @@ */ -import { Editor, UITools } from 'editor'; -export default { Editor, UITools }; +import { Editor, UITools, InsertCitationDialog } from 'editor'; +export default { Editor, UITools, InsertCitationDialog }; diff --git a/packages/editor/src/api/bibliography/bibliography-provider_local.ts b/packages/editor/src/api/bibliography/bibliography-provider_local.ts index 40a8295d..9084d4d9 100644 --- a/packages/editor/src/api/bibliography/bibliography-provider_local.ts +++ b/packages/editor/src/api/bibliography/bibliography-provider_local.ts @@ -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'; @@ -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'; @@ -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 { + public async load(ui: EditorUI, docPath: string | null, resourcePath: string, yamlBlocks: YamlBlock[]): Promise { // 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; @@ -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 => { @@ -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), @@ -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); @@ -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, ); @@ -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 diff --git a/packages/editor/src/api/bibliography/bibliography-provider_zotero.ts b/packages/editor/src/api/bibliography/bibliography-provider_zotero.ts index 3166fc69..d8399814 100644 --- a/packages/editor/src/api/bibliography/bibliography-provider_zotero.ts +++ b/packages/editor/src/api/bibliography/bibliography-provider_zotero.ts @@ -5,7 +5,7 @@ */ -import { ParsedYaml, valueFromYamlText } from '../yaml'; +import { YamlBlock, valueFromYamlText } from '../yaml'; import { suggestCiteId } from '../cite'; import { @@ -47,7 +47,7 @@ export class BibliographyDataProviderZotero implements BibliographyDataProvider _ui: EditorUI, docPath: string, _resourcePath: string, - yamlBlocks: ParsedYaml[], + yamlBlocks: YamlBlock[], refreshCollectionData: boolean, ): Promise { let hasUpdates = false; @@ -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); diff --git a/packages/editor/src/api/bibliography/bibliography.ts b/packages/editor/src/api/bibliography/bibliography.ts index a286ee82..f1682d39 100644 --- a/packages/editor/src/api/bibliography/bibliography.ts +++ b/packages/editor/src/api/bibliography/bibliography.ts @@ -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'; @@ -72,13 +70,13 @@ export interface BibliographyDataProvider { ui: EditorUI, docPath: string | null, resourcePath: string, - yamlBlocks: ParsedYaml[], + yamlBlocks: YamlBlock[], refreshCollectionData?: boolean, ): Promise; 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; warningMessage(): string | undefined; } @@ -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 { - // read the Yaml blocks from the document - const parsedYamlNodes = parseYamlNodes(doc); + public async load(ui: EditorUI, source: YamlBlockSource, refreshCollectionData?: boolean, localOnly?: boolean): Promise { + // 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(); @@ -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), ), ); @@ -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() { @@ -209,8 +207,8 @@ 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; @@ -218,12 +216,13 @@ export class BibliographyManager { 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); } diff --git a/packages/editor/src/api/yaml.ts b/packages/editor/src/api/yaml.ts index 521b174a..a8445304 100644 --- a/packages/editor/src/api/yaml.ts +++ b/packages/editor/src/api/yaml.ts @@ -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); diff --git a/packages/editor/src/behaviors/insert_citation/insert_citation.tsx b/packages/editor/src/behaviors/insert_citation/insert_citation.tsx index 465126c4..088653f9 100644 --- a/packages/editor/src/behaviors/insert_citation/insert_citation.tsx +++ b/packages/editor/src/behaviors/insert_citation/insert_citation.tsx @@ -7,8 +7,6 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { Node as ProsemirrorNode } from 'prosemirror-model'; - import { BibliographyFile, BibliographyManager, @@ -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'; @@ -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, @@ -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), @@ -117,7 +117,7 @@ export async function showInsertCitationDialog( } return providers; } else { - return [bibliographySourcePanel(doc, ui, bibliographyManager)]; + return [bibliographySourcePanel(ui, bibliographyManager)]; } }; @@ -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: () => { @@ -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), }; }); @@ -212,7 +212,6 @@ export async function showInsertCitationDialog( initiallySelectedNodeKey={initiallySelectedNodeKey} onOk={onOk} onCancel={onCancel} - doc={doc} ui={ui} /> ); @@ -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; diff --git a/packages/editor/src/behaviors/insert_citation/source_panels/insert_citation-source-panel-bibliography.tsx b/packages/editor/src/behaviors/insert_citation/source_panels/insert_citation-source-panel-bibliography.tsx index 51654fba..0b9d11c9 100644 --- a/packages/editor/src/behaviors/insert_citation/source_panels/insert_citation-source-panel-bibliography.tsx +++ b/packages/editor/src/behaviors/insert_citation/source_panels/insert_citation-source-panel-bibliography.tsx @@ -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 { @@ -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 { diff --git a/packages/editor/src/editor/editor-citation.ts b/packages/editor/src/editor/editor-citation.ts new file mode 100644 index 00000000..7e798611 --- /dev/null +++ b/packages/editor/src/editor/editor-citation.ts @@ -0,0 +1,107 @@ +/* + * editor-citation.ts + * + * Copyright (C) 2026 by Posit Software, PBC + */ + +import { EditorServer } from 'editor-types'; + +import { BibliographyManager, BibliographySource } from '../api/bibliography/bibliography'; +import { bibliographyFilesFromYaml } from '../api/bibliography/bibliography-provider_local'; +import { EditorUI } from '../api/ui-types'; +import { parseYamlBlocks } from '../api/yaml'; +import { InsertCitationDialogResult, showInsertCitationDialog } from '../behaviors/insert_citation/insert_citation'; +import { writeSourcesToBibliography } from '../marks/cite/cite'; + +import { editorUIWithDefaultImages } from './editor-images'; +import { EditorTheme, ensureTheme } from './editor-theme'; + +export interface InsertCitationOptions { + // yaml front matter block(s) of the document (with or w/o enclosing ---) + yaml: string[]; + // theme to apply (if not specified the default theme is applied if no theme is active) + theme?: EditorTheme; + // key of the source tree node to select initially (typically the selectionKey + // returned from the previous invocation) + selectionKey?: string; +} + +export interface InsertCitationResult { + // ids of the citations to insert (any new sources have already been written + // to the bibliography) + citationIds: string[]; + // whether the user requested an in-text citation (e.g. @smith) rather than + // a bracketed citation (e.g. [@smith]) + intextCitationStyle: boolean; + // bibliography file which the host should declare in the document's yaml + // front matter (undefined if the document already declares a bibliography + // or the file is a project-level bibliography) + bibliographyFile?: string; + // key of the source tree node that was selected when the dialog was dismissed + selectionKey?: string; +} + +// Hosts the insert citation dialog w/o an editor instance (e.g. for a plain text +// markdown editor). Only ui.dialogs, ui.context, ui.prefs and ui.images are used +// (images fall back to the defaults). Bibliography data is cached across invocations. +export class InsertCitationDialog { + private readonly ui: EditorUI; + private readonly server: EditorServer; + private readonly bibliographyManager: BibliographyManager; + + constructor(ui: EditorUI, server: EditorServer) { + this.ui = editorUIWithDefaultImages(ui); + this.server = server; + this.bibliographyManager = new BibliographyManager(server.pandoc, server.zotero); + } + + // Load bibliography data ahead of showing the dialog (otherwise it is loaded on show) + public prime(yaml: string[]): Promise { + return this.bibliographyManager.prime(this.ui, parseYamlBlocks(yaml)); + } + + // Show the dialog, returning null if it was cancelled + public async show(options: InsertCitationOptions): Promise { + ensureTheme(options.theme); + + const yamlBlocks = parseYamlBlocks(options.yaml); + let result: InsertCitationResult | null = null; + await showInsertCitationDialog( + this.ui, + yamlBlocks, + this.bibliographyManager, + this.server, + async (dialogResult: InsertCitationDialogResult) => { + // Remember whether the citation is intext for the future + this.ui.prefs.setCitationDefaultInText(dialogResult.intextCitationStyle); + + // Write any new sources to the bibliography (the user may decline) + const bibliography = dialogResult.bibliography; + const written = await writeSourcesToBibliography( + dialogResult.bibliographySources, + bibliography, + this.bibliographyManager, + yamlBlocks, + this.ui, + this.server.pandoc, + ); + if (!written) { + return; + } + + // The host needs to declare the bibliography unless the document already + // declares one (or the bibliography is project-level) + const declared = bibliographyFilesFromYaml(yamlBlocks) || []; + const declareBibliography = !bibliography.isProject && declared.length === 0; + result = { + citationIds: dialogResult.bibliographySources.map((source: BibliographySource) => source.id), + intextCitationStyle: dialogResult.intextCitationStyle, + bibliographyFile: declareBibliography ? bibliography.displayPath : undefined, + selectionKey: dialogResult.selectionKey, + }; + }, + options.selectionKey, + ); + return result; + } +} diff --git a/packages/editor/src/editor/editor-images.ts b/packages/editor/src/editor/editor-images.ts index 6709404b..3e6415de 100644 --- a/packages/editor/src/editor/editor-images.ts +++ b/packages/editor/src/editor/editor-images.ts @@ -5,6 +5,7 @@ */ import { EditorUIImages } from '../api/ui-images'; +import { EditorUI } from '../api/ui-types'; import copyImage from './images/copy.png'; import propertiesImage from './images/properties.png'; @@ -310,3 +311,23 @@ export function defaultEditorUIImages(): EditorUIImages { }, }; } + +// Merge the default images into the images provided by the host +export function editorUIWithDefaultImages(ui: EditorUI): EditorUI { + const defaultImages = defaultEditorUIImages(); + return { + ...ui, + images: { + ...defaultImages, + ...ui.images, + omni_insert: { + ...defaultImages.omni_insert, + ...ui.images?.omni_insert, + }, + citations: { + ...defaultImages.citations, + ...ui.images?.citations, + }, + }, + }; +} diff --git a/packages/editor/src/editor/editor-theme.ts b/packages/editor/src/editor/editor-theme.ts index 008b14ae..771e49c6 100644 --- a/packages/editor/src/editor/editor-theme.ts +++ b/packages/editor/src/editor/editor-theme.ts @@ -168,6 +168,18 @@ export function defaultTheme(): EditorTheme { }; } +const kThemeStylesId = 'pm-editor-theme-styles-id'; + +// Apply the theme if one is provided, otherwise ensure that at least the +// default theme has been applied (e.g. for dialogs shown w/o an editor instance) +export function ensureTheme(theme?: EditorTheme) { + if (theme) { + applyTheme(theme); + } else if (!window.document.getElementById(kThemeStylesId)) { + applyTheme(defaultTheme()); + } +} + export function applyTheme(theme: EditorTheme) { // merge w/ defaults const defaults = defaultTheme(); @@ -418,7 +430,7 @@ export function applyTheme(theme: EditorTheme) { `; // set style - setStyleElement('pm-editor-theme-styles-id', themeCss); + setStyleElement(kThemeStylesId, themeCss); } export function applyPadding(padding: string) { diff --git a/packages/editor/src/editor/editor.ts b/packages/editor/src/editor/editor.ts index aae80244..0ef73a9e 100644 --- a/packages/editor/src/editor/editor.ts +++ b/packages/editor/src/editor/editor.ts @@ -130,7 +130,7 @@ import { realtimeSpellingPlugin, invalidateAllWords, invalidateWord, spellingCon import { PandocConverter, PandocLineWrapping } from '../pandoc/pandoc_converter'; -import { defaultEditorUIImages } from './editor-images'; +import { defaultEditorUIImages, editorUIWithDefaultImages } from './editor-images'; import { editorMenus } from './editor-menus'; import { editorSchema } from './editor-schema'; @@ -410,24 +410,9 @@ export class Editor { // provide context defaults - const defaultImages = defaultEditorUIImages(); context = { ...context, - ui: { - ...context.ui, - images: { - ...defaultImages, - ...context.ui.images, - omni_insert: { - ...defaultImages.omni_insert, - ...context.ui.images, - }, - citations: { - ...defaultImages.citations, - ...context.ui.images, - }, - }, - }, + ui: editorUIWithDefaultImages(context.ui), }; // resolve the format diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index df292c7d..a7fd106f 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -29,6 +29,7 @@ export * from './api/codeview'; // main editor module export * from './editor/editor'; +export * from './editor/editor-citation'; diff --git a/packages/editor/src/marks/cite/cite.ts b/packages/editor/src/marks/cite/cite.ts index 89992d56..47ea8da7 100644 --- a/packages/editor/src/marks/cite/cite.ts +++ b/packages/editor/src/marks/cite/cite.ts @@ -27,6 +27,7 @@ import { performCompletionReplacement } from '../../api/completion'; import { FixupContext } from '../../api/fixup'; import { pasteTransaction } from '../../api/clipboard'; import { ensureBibliographyFileForDoc } from '../../api/bibliography/bibliography-provider_local'; +import { YamlBlockSource } from '../../api/yaml'; import { citationCompletionHandler } from './cite-completion'; import { citeHighlightPlugin } from './cite-highlight'; @@ -754,9 +755,39 @@ export async function ensureSourcesInBibliography( view: EditorView, ui: EditorUI, server: PandocServer, +): Promise { + // Write the sources to the bibliography file + const proceedWithInsert = await writeSourcesToBibliography( + sources, + bibliographyFile, + bibManager, + view.state.doc, + ui, + server, + ); + + // Ensure the bibliography file is referenced in the document YAML + if (proceedWithInsert && !bibliographyFile.isProject && sources.some(source => source.id)) { + ensureBibliographyFileForDoc(tr, bibliographyFile.displayPath); + } + + return proceedWithInsert; +} + +// Writes the sources to the specified bibliography file (if they aren't already +// present), confirming with the user if a provider has a warning. Returns false +// if the user elected not to proceed. This doesn't touch the document, so it can +// also be used by hosts that don't have an editor instance. +export async function writeSourcesToBibliography( + sources: BibliographySource[], + bibliographyFile: BibliographyFile, + bibManager: BibliographyManager, + yamlSource: YamlBlockSource, + ui: EditorUI, + server: PandocServer, ): Promise { // Write entry to a bibliography file if it isn't already present - await bibManager.loadLocal(ui, view.state.doc); + await bibManager.loadLocal(ui, yamlSource); // See if there is a warning for the selected provider. If there is, we may need to surface // that to the user. If there is no provider specified, no need to care about warnings. @@ -816,10 +847,6 @@ export async function ensureSourcesInBibliography( ui.context.getDocumentPath() ); } - - if (!bibliographyFile.isProject) { - ensureBibliographyFileForDoc(tr, bibliographyFile.displayPath); - } } }), );