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