1/* 2 * ui-dialogs.ts 3 * 4 * Copyright (C) 2021 by RStudio, PBC 5 * 6 * Unless you have received this program directly from RStudio pursuant 7 * to the terms of a commercial license agreement with RStudio, then 8 * this program is licensed to you under the terms of version 3 of the 9 * GNU Affero General Public License. This program is distributed WITHOUT 10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, 11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the 12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. 13 * 14 */ 15 16import { LinkTargets, LinkCapabilities, LinkType } from './link'; 17import { ImageDimensions } from './image'; 18import { ListCapabilities, ListType } from './list'; 19import { TableCapabilities } from './table'; 20import { CSL } from './csl'; 21import { CiteField } from './cite'; 22import { kStyleAttrib, attrPartitionKeyvalue, pandocAttrKeyvalueFromText } from './pandoc_attr'; 23 24export interface EditorDialogs { 25 alert: AlertFn; 26 yesNoMessage: YesNoMessageFn; 27 editLink: LinkEditorFn; 28 editImage: ImageEditorFn; 29 editCodeBlock: CodeBlockEditorFn; 30 editList: ListEditorFn; 31 editAttr: AttrEditorFn; 32 editSpan: AttrEditorFn; 33 editDiv: DivAttrEditorFn; 34 editRawInline: RawFormatEditorFn; 35 editRawBlock: RawFormatEditorFn; 36 insertTable: InsertTableFn; 37 insertCite: InsertCiteFn; 38 htmlDialog: EditorHTMLDialogFn; 39} 40 41export type EditorHTMLDialogFn = ( 42 title: string, 43 okText: string | null, 44 create: EditorHTMLDialogCreateFn, 45 focus: VoidFunction, 46 validate: EditorHTMLDialogValidateFn, 47) => Promise<boolean>; 48 49export type EditorHTMLDialogCreateFn = ( 50 containerWidth: number, 51 containerHeight: number, 52 confirm: VoidFunction, 53 cancel: VoidFunction, 54 showProgress: (message: string) => void, 55 hideProgress: VoidFunction, 56) => HTMLElement; 57 58export type EditorHTMLDialogValidateFn = () => string | null; 59 60export const kAlertTypeInfo = 1; 61export const kAlertTypeWarning = 2; 62export const kAlertTypeError = 3; 63 64export type AlertFn = (message: string, title: string, type: number) => Promise<boolean>; 65 66export type YesNoMessageFn = ( 67 message: string, 68 title: string, 69 type: number, 70 yesLabel: string, 71 noLabel: string, 72) => Promise<boolean>; 73 74export type AttrEditorFn = (attr: AttrProps, idHint?: string) => Promise<AttrEditResult | null>; 75 76export type DivAttrEditorFn = (attr: AttrProps, removeEnabled: boolean) => Promise<AttrEditResult | null>; 77 78export type LinkEditorFn = ( 79 link: LinkProps, 80 targets: LinkTargets, 81 capabilities: LinkCapabilities, 82) => Promise<LinkEditResult | null>; 83 84export type ImageEditorFn = ( 85 image: ImageProps, 86 dims: ImageDimensions | null, 87 editAttributes: boolean, 88) => Promise<ImageEditResult | null>; 89 90export type CodeBlockEditorFn = ( 91 codeBlock: CodeBlockProps, 92 attributes: boolean, 93 languages: string[], 94) => Promise<CodeBlockEditResult | null>; 95 96export type ListEditorFn = (list: ListProps, capabilities: ListCapabilities) => Promise<ListEditResult | null>; 97 98export type RawFormatEditorFn = (raw: RawFormatProps, outputFormats: string[]) => Promise<RawFormatResult | null>; 99 100export type InsertTableFn = (capabilities: TableCapabilities) => Promise<InsertTableResult | null>; 101 102export type InsertCiteFn = (props: InsertCiteProps) => Promise<InsertCiteResult | null>; 103 104export interface AttrProps { 105 readonly id?: string; 106 readonly classes?: string[]; 107 readonly keyvalue?: Array<[string, string]>; 108} 109 110export interface AttrEditResult { 111 readonly action: 'edit' | 'remove'; 112 readonly attr: AttrProps; 113} 114 115export interface LinkProps extends AttrProps { 116 readonly type: LinkType; 117 readonly text: string; 118 readonly href: string; 119 readonly heading?: string; 120 readonly title?: string; 121} 122 123export interface LinkEditResult { 124 readonly action: 'edit' | 'remove'; 125 readonly link: LinkProps; 126} 127 128export interface ImageProps extends AttrProps { 129 src: string | null; 130 title?: string; 131 alt?: string; 132 linkTo?: string; 133 width?: number; 134 height?: number; 135 units?: string; 136 lockRatio?: boolean; 137} 138 139export type ImageEditResult = ImageProps; 140 141export interface CodeBlockProps extends AttrProps { 142 lang: string; 143} 144 145export type CodeBlockEditResult = CodeBlockProps; 146 147export interface ListProps { 148 type: ListType; 149 tight: boolean; 150 order: number; 151 number_style: string; 152 number_delim: string; 153} 154 155export type ListEditResult = ListProps; 156 157export interface InsertTableResult { 158 rows: number; 159 cols: number; 160 header: boolean; 161 caption?: string; 162} 163 164export interface InsertCiteProps { 165 doi: string; 166 existingIds: string[]; 167 bibliographyFiles: string[]; 168 provider?: string; 169 csl?: CSL; 170 citeUI?: InsertCiteUI; 171} 172 173export interface InsertCiteUI { 174 suggestedId: string; 175 previewFields: CiteField[]; 176} 177 178export interface InsertCiteResult { 179 id: string; 180 bibliographyFile: string; 181 csl: CSL; 182} 183 184export interface RawFormatProps { 185 content: string; 186 format: string; 187} 188 189export interface RawFormatResult { 190 readonly action: 'edit' | 'remove'; 191 readonly raw: RawFormatProps; 192} 193 194export interface AttrEditInput { 195 id?: string; 196 classes?: string; 197 style?: string; 198 keyvalue?: string; 199} 200 201export function attrInputToProps(attr: AttrEditInput): AttrProps { 202 const classes = attr.classes ? attr.classes.split(/\s+/) : []; 203 let keyvalue: Array<[string, string]> | undefined; 204 if (attr.keyvalue || attr.style) { 205 let text = attr.keyvalue || ''; 206 if (attr.style) { 207 text += `\nstyle=${attr.style}\n`; 208 } 209 keyvalue = pandocAttrKeyvalueFromText(text, '\n'); 210 } 211 return { 212 id: asPandocId(attr.id || ''), 213 classes: classes.map(asPandocClass), 214 keyvalue, 215 }; 216} 217 218function asPandocId(id: string) { 219 return id.replace(/^#/, ''); 220} 221 222function asPandocClass(clz: string) { 223 return clz.replace(/^\./, ''); 224} 225 226export function attrPropsToInput(attr: AttrProps): AttrEditInput { 227 let style: string | undefined; 228 let keyvalue: string | undefined; 229 if (attr.keyvalue) { 230 const partitionedKeyvalue = attrPartitionKeyvalue([kStyleAttrib], attr.keyvalue); 231 if (partitionedKeyvalue.partitioned.length > 0) { 232 style = partitionedKeyvalue.partitioned[0][1]; 233 } 234 keyvalue = attrTextFromKeyvalue(partitionedKeyvalue.base); 235 } 236 237 return { 238 id: asHtmlId(attr.id) || undefined, 239 classes: attr.classes ? attr.classes.map(asHtmlClass).join(' ') : undefined, 240 style, 241 keyvalue, 242 }; 243} 244 245function attrTextFromKeyvalue(keyvalue: Array<[string, string]>) { 246 return keyvalue.map(kv => `${kv[0]}=${kv[1]}`).join('\n'); 247} 248 249function asHtmlId(id: string | undefined) { 250 if (id) { 251 if (id.startsWith('#')) { 252 return id; 253 } else { 254 return '#' + id; 255 } 256 } else { 257 return id; 258 } 259} 260 261function asHtmlClass(clz: string | undefined) { 262 if (clz) { 263 if (clz.startsWith('.')) { 264 return clz; 265 } else { 266 return '.' + clz; 267 } 268 } else { 269 return clz; 270 } 271} 272