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); - } } }), );