UNPKG

15 kBJavaScriptView Raw
1import { PromiseDelegate } from '@lumino/coreutils';
2import { Signal } from '@lumino/signaling';
3import { Menu } from '@lumino/widgets';
4import { Stylist } from './stylist';
5import { PACKAGE_NAME, CSS, CMD, TEXT_LABELS, TextKind, ROOT, TEXT_OPTIONS, FontFormat } from '.';
6import { dataURISrc } from './util';
7const ALL_PALETTE = 'Fonts';
8const PALETTE = {
9 code: 'Fonts (Code)',
10 content: 'Fonts (Content)'
11};
12export 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//# sourceMappingURL=manager.js.map
\No newline at end of file