1 | import { PromiseDelegate } from '@lumino/coreutils';
|
2 | import { Signal } from '@lumino/signaling';
|
3 | import { Menu } from '@lumino/widgets';
|
4 | import { Stylist } from './stylist';
|
5 | import { PACKAGE_NAME, CSS, CMD, TEXT_LABELS, TextKind, ROOT, TEXT_OPTIONS, FontFormat } from '.';
|
6 | import { dataURISrc } from './util';
|
7 | const ALL_PALETTE = 'Fonts';
|
8 | const PALETTE = {
|
9 | code: 'Fonts (Code)',
|
10 | content: 'Fonts (Content)'
|
11 | };
|
12 | export class FontManager {
|
13 | constructor(commands, palette, notebooks) {
|
14 | this.licensePaneRequested = new Signal(this);
|
15 | this._fontFamilyMenus = new Map();
|
16 | this._fontSizeMenus = new Map();
|
17 | this._lineHeightMenus = new Map();
|
18 | this._ready = new PromiseDelegate();
|
19 | this._stylist = new Stylist();
|
20 | this._stylist.cacheUpdated.connect(this.settingsUpdate, this);
|
21 | this._commands = commands;
|
22 | this._palette = palette;
|
23 | this._notebooks = notebooks;
|
24 | this._notebooks.currentChanged.connect(this._onNotebooksChanged, this);
|
25 | this.makeMenus(commands);
|
26 | this.makeCommands();
|
27 | this.hack();
|
28 | }
|
29 | get ready() {
|
30 | return this._ready.promise;
|
31 | }
|
32 | get fonts() {
|
33 | return this._stylist.fonts;
|
34 | }
|
35 | get enabled() {
|
36 | if (!this.settings) {
|
37 | return false;
|
38 | }
|
39 | const enabled = !!this._settings.get('enabled').composite;
|
40 | return enabled;
|
41 | }
|
42 | set enabled(enabled) {
|
43 | if (!this.settings) {
|
44 | return;
|
45 | }
|
46 | this._settings
|
47 | .set('enabled', enabled)
|
48 | .then()
|
49 | .catch(console.warn);
|
50 | }
|
51 | get settings() {
|
52 | return this._settings;
|
53 | }
|
54 | get menu() {
|
55 | return this._menu;
|
56 | }
|
57 | get stylesheets() {
|
58 | return this._stylist.stylesheets;
|
59 | }
|
60 | set settings(settings) {
|
61 | if (this._settings) {
|
62 | this._settings.changed.disconnect(this.settingsUpdate, this);
|
63 | }
|
64 | this._settings = settings;
|
65 | if (settings) {
|
66 | settings.changed.connect(this.settingsUpdate, this);
|
67 | }
|
68 | this.settingsUpdate();
|
69 | }
|
70 | async dataURISrc(url, format = FontFormat.woff2) {
|
71 | return await dataURISrc(url, format);
|
72 | }
|
73 | registerFontFace(options) {
|
74 | this._stylist.fonts.set(options.name, options);
|
75 | this.registerFontCommands(options);
|
76 | }
|
77 | getVarName(property, { kind }) {
|
78 | if (kind == null) {
|
79 | return null;
|
80 | }
|
81 | return CSS[kind][property];
|
82 | }
|
83 | getTextStyle(property, { kind, notebook }) {
|
84 | var _a;
|
85 | if (!notebook && !this.settings) {
|
86 | return null;
|
87 | }
|
88 | try {
|
89 | const styles = ((_a = notebook) === null || _a === void 0 ? void 0 : _a.model) ? notebook.model.metadata.get(PACKAGE_NAME).styles
|
90 | : this._settings.get('styles').composite;
|
91 | let varName = this.getVarName(property, { kind });
|
92 | if (styles != null) {
|
93 | const rootStyle = styles[ROOT];
|
94 | if (rootStyle == null) {
|
95 | return null;
|
96 | }
|
97 | return rootStyle[varName];
|
98 | }
|
99 | }
|
100 | catch (err) {
|
101 |
|
102 | }
|
103 | return null;
|
104 | }
|
105 | async setTextStyle(property, value, { kind, notebook }) {
|
106 | var _a, _b, _c;
|
107 | if (!notebook && !this.settings) {
|
108 | return;
|
109 | }
|
110 | let oldStyles = {};
|
111 | if ((_a = notebook) === null || _a === void 0 ? void 0 : _a.model) {
|
112 | try {
|
113 | oldStyles = notebook
|
114 | ? notebook.model.metadata.get(PACKAGE_NAME).styles
|
115 | : this._settings.get('styles').composite;
|
116 | }
|
117 | catch (err) {
|
118 |
|
119 | }
|
120 | }
|
121 | let styles = JSON.parse(JSON.stringify(oldStyles || {}));
|
122 | let root = (styles[ROOT] = styles[ROOT] ? styles[ROOT] : {});
|
123 | let varName = this.getVarName(property, { kind });
|
124 | if (root) {
|
125 | if (value == null) {
|
126 | delete root[varName];
|
127 | }
|
128 | else {
|
129 | root[varName] = value;
|
130 | }
|
131 | }
|
132 | if (notebook) {
|
133 | let metadata = (((_b = notebook.model) === null || _b === void 0 ? void 0 : _b.metadata.get(PACKAGE_NAME)) ||
|
134 | {});
|
135 | metadata = JSON.parse(JSON.stringify(metadata));
|
136 | metadata.styles = styles;
|
137 | switch (property) {
|
138 | case 'font-family':
|
139 | if (value != null) {
|
140 | await this.embedFont(value, metadata);
|
141 | }
|
142 | break;
|
143 | default:
|
144 | break;
|
145 | }
|
146 | this.cleanMetadata(metadata);
|
147 | (_c = notebook.model) === null || _c === void 0 ? void 0 : _c.metadata.set(PACKAGE_NAME, metadata);
|
148 | }
|
149 | else {
|
150 | if (!Object.keys(styles[ROOT] || {}).length) {
|
151 | delete styles[ROOT];
|
152 | }
|
153 | try {
|
154 | await this._settings.set('styles', styles);
|
155 | }
|
156 | catch (err) {
|
157 | console.warn(err);
|
158 | }
|
159 | }
|
160 | }
|
161 | cleanMetadata(metadata) {
|
162 | const rawStyle = JSON.stringify(metadata.styles, null, 2);
|
163 | const oldFonts = Object.keys(metadata.fonts || {});
|
164 | for (let fontFamily of oldFonts) {
|
165 | let pattern = `'${fontFamily}'`;
|
166 | if (rawStyle.indexOf(pattern) === -1) {
|
167 | if (metadata.fonts) {
|
168 | delete metadata.fonts[fontFamily];
|
169 | }
|
170 | if (metadata.fontLicenses) {
|
171 | delete metadata.fontLicenses[fontFamily];
|
172 | }
|
173 | }
|
174 | }
|
175 | }
|
176 | async embedFont(fontFamily, metadata) {
|
177 | if (fontFamily == null) {
|
178 | return;
|
179 | }
|
180 | const unquoted = fontFamily.replace(/(['"]?)(.*)\1/, '$2');
|
181 | const registered = this._stylist.fonts.get(unquoted);
|
182 | if (!registered) {
|
183 | return;
|
184 | }
|
185 | try {
|
186 | const faces = await registered.faces();
|
187 | const oldFaces = (metadata.fonts || {});
|
188 | const oldLicenses = (metadata.fontLicenses ||
|
189 | {});
|
190 | oldFaces[unquoted] = faces;
|
191 | oldLicenses[unquoted] = {
|
192 | spdx: registered.license.spdx,
|
193 | name: registered.license.name,
|
194 | text: await registered.license.text(),
|
195 | holders: registered.license.holders
|
196 | };
|
197 | metadata.fonts = oldFaces;
|
198 | metadata.fontLicenses = oldLicenses;
|
199 | }
|
200 | catch (err) {
|
201 | console.warn('error embedding font');
|
202 | console.warn(err);
|
203 | }
|
204 | }
|
205 | _onNotebooksChanged() {
|
206 | let styled = this._stylist.notebooks();
|
207 | this._notebooks.forEach(notebook => {
|
208 | if (styled.indexOf(notebook) === -1) {
|
209 | this._registerNotebook(notebook);
|
210 | }
|
211 | });
|
212 | }
|
213 | _registerNotebook(notebook) {
|
214 | var _a;
|
215 | this._stylist.registerNotebook(notebook, true);
|
216 | let watcher = this._notebookMetaWatcher(notebook);
|
217 | if ((_a = notebook) === null || _a === void 0 ? void 0 : _a.model) {
|
218 | notebook.model.metadata.changed.connect(watcher);
|
219 | }
|
220 | notebook.disposed.connect(this._onNotebookDisposed);
|
221 | watcher();
|
222 | this.hack();
|
223 | }
|
224 | _onNotebookDisposed(notebook) {
|
225 | this._stylist.registerNotebook(notebook, false);
|
226 | }
|
227 | _notebookMetaWatcher(_notebook) {
|
228 | return () => {
|
229 | this._notebooks.forEach(notebook => {
|
230 | if (notebook.id !== notebook.id || !notebook.model) {
|
231 | return;
|
232 | }
|
233 | const meta = notebook.model.metadata.get(PACKAGE_NAME);
|
234 | if (meta) {
|
235 | this._stylist.stylesheet(meta, notebook);
|
236 | }
|
237 | });
|
238 | };
|
239 | }
|
240 | fontSizeOptions() {
|
241 | return Array.from(Array(25).keys()).map(i => `${i + 8}px`);
|
242 | }
|
243 | fontSizeCommands(prefix) {
|
244 | return this.fontSizeOptions().map(px => `${prefix}:${px}`);
|
245 | }
|
246 | makeCommands() {
|
247 | [TextKind.code, TextKind.content].map(kind => {
|
248 | ['Increase', 'Decrease'].map((label, i) => {
|
249 | var _a;
|
250 | let command = `${CMD[kind].fontSize}:${label.toLowerCase()}`;
|
251 | this._commands.addCommand(command, {
|
252 | label: `${label} Code Font Size`,
|
253 | execute: async () => {
|
254 | let oldSize = this.getTextStyle('font-size', { kind });
|
255 | let cfs = parseInt((oldSize || '0').replace(/px$/, ''), 10) || 13;
|
256 | try {
|
257 | await this.setTextStyle('font-size', `${cfs + (i ? -1 : 1)}px`, {
|
258 | kind
|
259 | });
|
260 | }
|
261 | catch (err) {
|
262 | console.warn(err);
|
263 | }
|
264 | },
|
265 | isVisible: () => this.enabled
|
266 | });
|
267 | (_a = this._fontSizeMenus.get(kind)) === null || _a === void 0 ? void 0 : _a.addItem({ command });
|
268 | this._palette.addItem({ command, category: PALETTE[kind], rank: 0 });
|
269 | });
|
270 | ['line-height', 'font-size', 'font-family'].forEach((prop) => {
|
271 | const command = `${kind}-${prop}:-reset`;
|
272 | this._commands.addCommand(command, {
|
273 | label: `Default ${kind[0].toUpperCase()}${kind.slice(1)} ${TEXT_LABELS[prop]}`,
|
274 | execute: () => this.setTextStyle(prop, null, { kind }),
|
275 | isVisible: () => this.enabled,
|
276 | isToggled: () => this.getTextStyle(prop, { kind }) == null
|
277 | });
|
278 | });
|
279 | TEXT_OPTIONS['line-height'](this).map(lineHeight => {
|
280 | var _a;
|
281 | const command = `${CMD[kind].lineHeight}:${lineHeight}`;
|
282 | this._commands.addCommand(command, {
|
283 | label: `${lineHeight}`,
|
284 | isToggled: () => this.getTextStyle('line-height', { kind }) === lineHeight,
|
285 | isVisible: () => this.enabled,
|
286 | execute: () => this.setTextStyle('line-height', lineHeight, { kind })
|
287 | });
|
288 | (_a = this._lineHeightMenus.get(kind)) === null || _a === void 0 ? void 0 : _a.addItem({ command });
|
289 | });
|
290 | TEXT_OPTIONS['font-size'](this).map(px => {
|
291 | var _a;
|
292 | const command = `${CMD[kind].fontSize}:${px}`;
|
293 | this._commands.addCommand(command, {
|
294 | label: `${px}`,
|
295 | isToggled: () => this.getTextStyle('font-size', { kind }) === px,
|
296 | isVisible: () => this.enabled,
|
297 | execute: () => this.setTextStyle('font-size', px, { kind })
|
298 | });
|
299 | (_a = this._fontSizeMenus.get(kind)) === null || _a === void 0 ? void 0 : _a.addItem({ command });
|
300 | });
|
301 | });
|
302 | ['Enable', 'Disable'].map((label, i) => {
|
303 | const command = `custom-fonts:${label.toLowerCase()}`;
|
304 | this._commands.addCommand(command, {
|
305 | label: `${label} Custom Fonts`,
|
306 | isVisible: () => this.enabled === !!i,
|
307 | execute: async () => {
|
308 | if (!this._settings) {
|
309 | return;
|
310 | }
|
311 | try {
|
312 | await this._settings.set('enabled', !i);
|
313 | }
|
314 | catch (err) {
|
315 | console.warn(err);
|
316 | }
|
317 | }
|
318 | });
|
319 | this._palette.addItem({ command, category: ALL_PALETTE });
|
320 | });
|
321 | }
|
322 | fontPropMenu(parent, kind, property) {
|
323 | let menu = new Menu({ commands: parent.commands });
|
324 | menu.title.label = TEXT_LABELS[property];
|
325 | menu.addItem({
|
326 | command: `${kind}-${property}:-reset`
|
327 | });
|
328 | menu.addItem({
|
329 | type: 'separator'
|
330 | });
|
331 | parent.addItem({ type: 'submenu', submenu: menu });
|
332 | return menu;
|
333 | }
|
334 | makeMenus(commands) {
|
335 | this._menu = new Menu({ commands });
|
336 | this._menu.title.label = 'Fonts';
|
337 | [TextKind.code, TextKind.content].map(kind => {
|
338 | const submenu = new Menu({ commands });
|
339 | submenu.title.label = kind[0].toUpperCase() + kind.slice(1);
|
340 | this._menu.addItem({ type: 'submenu', submenu });
|
341 | const family = this.fontPropMenu(submenu, kind, 'font-family');
|
342 | const height = this.fontPropMenu(submenu, kind, 'line-height');
|
343 | const size = this.fontPropMenu(submenu, kind, 'font-size');
|
344 | this._fontFamilyMenus.set(kind, family);
|
345 | this._lineHeightMenus.set(kind, height);
|
346 | this._fontSizeMenus.set(kind, size);
|
347 | });
|
348 | this._menu.addItem({
|
349 | command: CMD.editFonts,
|
350 | args: { global: true }
|
351 | });
|
352 | this._menu.addItem({
|
353 | command: CMD.customFonts.enable
|
354 | });
|
355 | this._menu.addItem({
|
356 | command: CMD.customFonts.disable
|
357 | });
|
358 | }
|
359 | settingsUpdate() {
|
360 | let meta = {
|
361 | styles: this._settings.get('styles').composite
|
362 | };
|
363 | if (this.enabled) {
|
364 | this._stylist.stylesheet(meta, void 0, true);
|
365 | }
|
366 | else {
|
367 | this._stylist.hack(false);
|
368 | }
|
369 | }
|
370 | registerFontCommands(options) {
|
371 | [TextKind.code, TextKind.content].forEach(kind => {
|
372 | var _a;
|
373 | const slug = options.name.replace(/[^a-z\d]/gi, '-').toLowerCase();
|
374 | let command = `${CMD[kind].fontFamily}:${slug}`;
|
375 | this._commands.addCommand(command, {
|
376 | label: options.name,
|
377 | isToggled: () => {
|
378 | let cff = this.getTextStyle('font-family', { kind });
|
379 | return `${cff}`.indexOf(`'${options.name}'`) > -1;
|
380 | },
|
381 | isVisible: () => this.enabled,
|
382 | execute: async () => {
|
383 | try {
|
384 | await this.setTextStyle('font-family', `'${options.name}'`, {
|
385 | kind
|
386 | });
|
387 | }
|
388 | catch (err) {
|
389 | console.warn(err);
|
390 | }
|
391 | }
|
392 | });
|
393 | (_a = this._fontFamilyMenus.get(kind)) === null || _a === void 0 ? void 0 : _a.addItem({ command });
|
394 | this._palette.addItem({ command, category: PALETTE[kind] });
|
395 | });
|
396 | }
|
397 | requestLicensePane(font) {
|
398 | this.licensePaneRequested.emit(font);
|
399 | }
|
400 | hack() {
|
401 | this._stylist.hack();
|
402 | this._ready.resolve(void 0);
|
403 | }
|
404 | }
|
405 |
|
\ | No newline at end of file |