// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. /// import CodeMirror from 'codemirror'; import { Mode } from '@jupyterlab/codemirror'; function fakeMode(name: string) { return { mode: name, ext: [name], mime: `text/${name}`, name: name.toUpperCase() }; } /** * State names used in simple mode. */ type S = 'start' | 'comment'; /** * Tokens used in simple mode. * */ enum T { AM = 'atom', AT = 'attribute', BE = 'builtin.em', BI = 'builtin', BK = 'bracket', CM = 'comment', DF = 'def', HL = 'header', KW = 'keyword', MT = 'meta', NB = 'number', OP = 'operator', PC = 'punctuation', PR = 'property', SE = 'string.em', SH = 'string.header', SS = 'string.strong', SSE = 'string.strong.em', S2 = 'string-2', ST = 'string', TG = 'tag', V1 = 'variable', V2 = 'variable-2', V3 = 'variable-3' } /** * Simple mode states from CodeMirror demo: * from https://codemirror.net/demo/simplemode.html */ const FAKE_SIMPLE_STATES: CodeMirror.TSimpleTopState = { // The start state contains the rules that are intially used start: [ // The regex matches the token, the token property contains the type { regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: T.ST }, // You can match multiple tokens at once. Note that the captured // groups must span the whole string in this case { regex: /(function)(\s+)([a-z$][\w$]*)/, token: [T.KW, null, T.V2] }, // Rules are matched in the order in which they appear, so there is // no ambiguity between this one and the one above { regex: /(?:function|var|return|if|for|while|else|do|this)\b/, token: T.KW }, { regex: /true|false|null|undefined/, token: T.AT }, { regex: /0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i, token: T.NB }, { regex: /\/\/.*/, token: T.CM }, { regex: /\/(?:[^\\]|\\.)*?\//, token: T.V3 }, // A next property will cause the mode to move to a different state { regex: /\/\*/, token: T.CM, next: T.CM }, { regex: /[-+\/*=<>!]+/, token: T.OP }, // indent and dedent properties guide autoindentation { regex: /[\{\[\(]/, indent: true }, { regex: /[\}\]\)]/, dedent: true }, { regex: /[a-z$][\w$]*/, token: T.V1 }, // You can embed other modes with the mode property. This rule // causes all code between << and >> to be highlighted with the XML // mode. { regex: /<>/ } } ], // The multi-line comment state. comment: [ { regex: /.*?\*\//, token: T.CM, next: 'start' }, { regex: /.*/, token: T.CM } ] }; FAKE_SIMPLE_STATES.meta = { dontIndentStates: ['comment'] }; describe('Mode', () => { describe('#ensure', () => { it('should load a defined spec', async () => { CodeMirror.modeInfo.push(fakeMode('foo')); CodeMirror.defineMode('foo', () => { return {}; }); const spec = (await Mode.ensure('text/foo'))!; expect(spec.name).toBe('FOO'); }); it('should load a bundled spec', async () => { const spec = (await Mode.ensure('application/json'))!; expect(spec.name).toBe('JSON'); }); it('should add a spec loader', async () => { let called = 0; let loaded = 0; Mode.addSpecLoader(async spec => { called++; if (spec.mode !== 'bar') { return false; } loaded++; return true; }, 42); CodeMirror.modeInfo.push(fakeMode('bar')); let spec = await Mode.ensure('bar'); expect(called).toBe(1); expect(loaded).toBe(1); expect(spec!.name).toBe('BAR'); spec = await Mode.ensure('python'); expect(called).toBe(1); expect(loaded).toBe(1); try { spec = await Mode.ensure('APL'); } catch (err) { // apparently one cannot use webpack `require` in jest } expect(called).toBe(2); expect(loaded).toBe(1); }); it('should default to plain text', async () => { const spec = (await Mode.ensure('this is not a mode'))!; expect(spec.name).toBe('Plain Text'); }); it('should create a simple mode', async () => { CodeMirror.modeInfo.push(fakeMode('baz')); CodeMirror.defineSimpleMode('baz', FAKE_SIMPLE_STATES); const spec = (await Mode.ensure('text/baz'))!; expect(spec.name).toBe('BAZ'); }); }); });