diff --git a/scripts/main.js b/scripts/main.js index 7ca0cd3..0e03fab 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -3,7 +3,7 @@ requirejs.config(window.getRequirejsCDN()); - /* jshint -W072 */ + /* jshint -W072 */ // Allow several required modules requirejs([ 'interface/Interface', 'sequence/Parser', diff --git a/scripts/sequence/CodeMirrorMode.js b/scripts/sequence/CodeMirrorMode.js index de41f5b..3aba024 100644 --- a/scripts/sequence/CodeMirrorMode.js +++ b/scripts/sequence/CodeMirrorMode.js @@ -20,10 +20,11 @@ define(['core/ArrayUtilities'], (array) => { const CM_AGENT_LIST_TO_TEXT = makeCMCommaBlock('variable', 'Agent', { ':': {type: 'operator', suggest: true, then: {'': CM_TEXT_TO_END}}, }); - const CM_AGENT_LIST_TO_OPTTEXT = makeCMCommaBlock('variable', 'Agent', { + const CM_AGENT_TO_OPTTEXT = {type: 'variable', suggest: 'Agent', then: { + '': 0, ':': {type: 'operator', suggest: true, then: {'': CM_TEXT_TO_END}}, '\n': CM_END, - }); + }}; const CM_NOTE_SIDE_THEN = { 'of': {type: 'keyword', suggest: true, then: { @@ -35,20 +36,29 @@ define(['core/ArrayUtilities'], (array) => { '': CM_AGENT_LIST_TO_TEXT, }; - const CM_NOTE_LSIDE = { - type: 'keyword', - suggest: ['left of ', 'left: '], - then: CM_NOTE_SIDE_THEN, - }; - - const CM_NOTE_RSIDE = { - type: 'keyword', - suggest: ['right of ', 'right: '], - then: CM_NOTE_SIDE_THEN, - }; + function makeCMSideNote(side) { + return { + type: 'keyword', + suggest: [side + ' of ', side + ': '], + then: CM_NOTE_SIDE_THEN, + }; + } const CM_CONNECT = {type: 'keyword', suggest: true, then: { - '': CM_AGENT_LIST_TO_OPTTEXT, + '+': {type: 'operator', suggest: true, then: {'': CM_AGENT_TO_OPTTEXT}}, + '-': {type: 'operator', suggest: true, then: {'': CM_AGENT_TO_OPTTEXT}}, + '': CM_AGENT_TO_OPTTEXT, + }}; + + const CM_CONNECT_FULL = {type: 'variable', suggest: 'Agent', then: { + '->': CM_CONNECT, + '-->': CM_CONNECT, + '<-': CM_CONNECT, + '<--': CM_CONNECT, + '<->': CM_CONNECT, + '<-->': CM_CONNECT, + ':': {type: 'operator', suggest: true, override: 'Label', then: {}}, + '': 0, }}; const CM_COMMANDS = {type: 'error', then: { @@ -98,8 +108,8 @@ define(['core/ArrayUtilities'], (array) => { 'over': {type: 'keyword', suggest: true, then: { '': CM_AGENT_LIST_TO_TEXT, }}, - 'left': CM_NOTE_LSIDE, - 'right': CM_NOTE_RSIDE, + 'left': makeCMSideNote('left'), + 'right': makeCMSideNote('right'), 'between': {type: 'keyword', suggest: true, then: { '': CM_AGENT_LIST_TO_TEXT, }}, @@ -110,8 +120,8 @@ define(['core/ArrayUtilities'], (array) => { }}, }}, 'text': {type: 'keyword', suggest: true, then: { - 'left': CM_NOTE_LSIDE, - 'right': CM_NOTE_RSIDE, + 'left': makeCMSideNote('left'), + 'right': makeCMSideNote('right'), }}, 'simultaneously': {type: 'keyword', suggest: true, then: { ':': {type: 'operator', suggest: true, then: {}}, @@ -122,31 +132,38 @@ define(['core/ArrayUtilities'], (array) => { }}, }}, }}, - '': {type: 'variable', suggest: 'Agent', then: { - '->': CM_CONNECT, - '-->': CM_CONNECT, - '<-': CM_CONNECT, - '<--': CM_CONNECT, - '<->': CM_CONNECT, - '<-->': CM_CONNECT, - ':': {type: 'operator', suggest: true, override: 'Label', then: {}}, - '': 0, - }}, + '+': {type: 'operator', suggest: true, then: {'': CM_CONNECT_FULL}}, + '-': {type: 'operator', suggest: true, then: {'': CM_CONNECT_FULL}}, + '': CM_CONNECT_FULL, }}; - function cmGetSuggestions(state, token, {suggest, then}) { + function cmCappedToken(token, current) { + if(Object.keys(current.then).length > 0) { + return token + ' '; + } else { + return token + '\n'; + } + } + + function cmGetVarSuggestions(state, previous, current) { + if( + typeof current.suggest !== 'string' || + previous.suggest === current.suggest + ) { + return null; + } + return state['known' + current.suggest]; + } + + function cmGetSuggestions(state, token, previous, current) { if(token === '') { - return state['known' + suggest]; - } else if(suggest === true) { - if(Object.keys(then).length > 0) { - return [token + ' ']; - } else { - return [token + '\n']; - } - } else if(Array.isArray(suggest)) { - return suggest; - } else if(suggest) { - return [suggest]; + return cmGetVarSuggestions(state, previous, current); + } else if(current.suggest === true) { + return [cmCappedToken(token, current)]; + } else if(Array.isArray(current.suggest)) { + return current.suggest; + } else if(current.suggest) { + return [current.suggest]; } else { return null; } @@ -154,13 +171,16 @@ define(['core/ArrayUtilities'], (array) => { function cmMakeCompletions(state, path) { const comp = []; - const {then} = array.last(path); - Object.keys(then).forEach((token) => { - let next = then[token]; + const current = array.last(path); + Object.keys(current.then).forEach((token) => { + let next = current.then[token]; if(typeof next === 'number') { next = path[path.length - next - 1]; } - array.mergeSets(comp, cmGetSuggestions(state, token, next)); + array.mergeSets( + comp, + cmGetSuggestions(state, token, current, next) + ); }); return comp; } @@ -213,6 +233,10 @@ define(['core/ArrayUtilities'], (array) => { return current.type; } + function getInitialValue(block) { + return (block.baseToken || {}).v || ''; + } + return class Mode { constructor(tokenDefinitions) { this.tokenDefinitions = tokenDefinitions; @@ -250,7 +274,7 @@ define(['core/ArrayUtilities'], (array) => { const block = this.tokenDefinitions[i]; if(this._matchPattern(stream, block.start, true)) { state.currentType = i; - state.current = block.prefix || ''; + state.current = getInitialValue(block); return true; } } @@ -271,12 +295,6 @@ define(['core/ArrayUtilities'], (array) => { return 'comment'; } state.line.push(state.current); - if(state.current === '\n') { - // quoted newline is interpreted as a command separator; - // probably not what the writer expected, so highlight it - state.line.length = 0; - return 'warning'; - } return cmCheckToken(state, stream.eol()); } diff --git a/scripts/sequence/Generator.js b/scripts/sequence/Generator.js index e0e67a8..2047973 100644 --- a/scripts/sequence/Generator.js +++ b/scripts/sequence/Generator.js @@ -12,9 +12,9 @@ define(['core/ArrayUtilities'], (array) => { const DEFAULT_AGENT = new AgentState(false); const NOTE_DEFAULT_AGENTS = { - 'note over': ['[', ']'], - 'note left': ['['], - 'note right': [']'], + 'note over': [{name: '['}, {name: ']'}], + 'note left': [{name: '['}], + 'note right': [{name: ']'}], }; return class Generator { @@ -151,16 +151,19 @@ define(['core/ArrayUtilities'], (array) => { } handleAgentDefine({agents}) { - array.mergeSets(this.currentNest.agents, agents); - array.mergeSets(this.agents, agents); + const agentNames = agents.map((agent) => agent.name); + array.mergeSets(this.currentNest.agents, agentNames); + array.mergeSets(this.agents, agentNames); } handleAgentBegin({agents, mode}) { - this.setAgentVis(agents, true, mode, true); + const agentNames = agents.map((agent) => agent.name); + this.setAgentVis(agentNames, true, mode, true); } handleAgentEnd({agents, mode}) { - this.setAgentVis(agents, false, mode, true); + const agentNames = agents.map((agent) => agent.name); + this.setAgentVis(agentNames, false, mode, true); } handleBlockBegin({mode, label}) { @@ -204,11 +207,16 @@ define(['core/ArrayUtilities'], (array) => { handleUnknownStage(stage) { if(stage.agents) { - this.setAgentVis(stage.agents, true, 'box'); - array.mergeSets(this.currentNest.agents, stage.agents); - array.mergeSets(this.agents, stage.agents); + const agentNames = stage.agents.map((agent) => agent.name); + this.setAgentVis(agentNames, true, 'box'); + array.mergeSets(this.currentNest.agents, agentNames); + array.mergeSets(this.agents, agentNames); + this.currentSection.stages.push(Object.assign({}, stage, { + agents: agentNames, + })); + } else { + this.currentSection.stages.push(stage); } - this.currentSection.stages.push(stage); this.currentNest.hasContent = true; } diff --git a/scripts/sequence/Generator_spec.js b/scripts/sequence/Generator_spec.js index ce1106b..a8ede7f 100644 --- a/scripts/sequence/Generator_spec.js +++ b/scripts/sequence/Generator_spec.js @@ -53,9 +53,9 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('returns aggregated agents', () => { const sequence = generator.generate({stages: [ - {type: '->', agents: ['A', 'B']}, - {type: '<-', agents: ['C', 'D']}, - {type: AGENT_BEGIN, agents: ['E'], mode: 'box'}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, + {type: '<-', agents: [{name: 'C'}, {name: 'D'}]}, + {type: AGENT_BEGIN, agents: [{name: 'E'}], mode: 'box'}, ]}); expect(sequence.agents).toEqual( ['[', 'A', 'B', 'C', 'D', 'E', ']'] @@ -64,23 +64,23 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('always puts the implicit right agent on the right', () => { const sequence = generator.generate({stages: [ - {type: '->', agents: [']', 'B']}, + {type: '->', agents: [{name: ']'}, {name: 'B'}]}, ]}); expect(sequence.agents).toEqual(['[', 'B', ']']); }); it('accounts for define calls when ordering agents', () => { const sequence = generator.generate({stages: [ - {type: AGENT_DEFINE, agents: ['B']}, - {type: '->', agents: ['A', 'B']}, + {type: AGENT_DEFINE, agents: [{name: 'B'}]}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, ]}); expect(sequence.agents).toEqual(['[', 'B', 'A', ']']); }); it('creates implicit begin stages for agents when used', () => { const sequence = generator.generate({stages: [ - {type: '->', agents: ['A', 'B']}, - {type: '->', agents: ['B', 'C']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, + {type: '->', agents: [{name: 'B'}, {name: 'C'}]}, ]}); expect(sequence.stages).toEqual([ {type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, @@ -97,7 +97,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { terminators: 'foo', }, stages: [ - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, ], }); expect(sequence.stages).toEqual([ @@ -109,9 +109,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('does not create duplicate begin stages', () => { const sequence = generator.generate({stages: [ - {type: AGENT_BEGIN, agents: ['A', 'B', 'C'], mode: 'box'}, - {type: '->', agents: ['A', 'B']}, - {type: '->', agents: ['B', 'C']}, + {type: AGENT_BEGIN, agents: [ + {name: 'A'}, + {name: 'B'}, + {name: 'C'}, + ], mode: 'box'}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, + {type: '->', agents: [{name: 'B'}, {name: 'C'}]}, ]}); expect(sequence.stages).toEqual([ {type: AGENT_BEGIN, agents: ['A', 'B', 'C'], mode: 'box'}, @@ -123,10 +127,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('redisplays agents if they have been hidden', () => { const sequence = generator.generate({stages: [ - {type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, - {type: '->', agents: ['A', 'B']}, - {type: AGENT_END, agents: ['B'], mode: 'cross'}, - {type: '->', agents: ['A', 'B']}, + {type: AGENT_BEGIN, agents: [ + {name: 'A'}, + {name: 'B'}, + ], mode: 'box'}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, + {type: AGENT_END, agents: [{name: 'B'}], mode: 'cross'}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, ]}); expect(sequence.stages).toEqual([ {type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, @@ -140,10 +147,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('collapses adjacent begin statements', () => { const sequence = generator.generate({stages: [ - {type: '->', agents: ['A', 'B']}, - {type: AGENT_BEGIN, agents: ['D'], mode: 'box'}, - {type: '->', agents: ['B', 'C']}, - {type: '->', agents: ['C', 'D']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, + {type: AGENT_BEGIN, agents: [{name: 'D'}], mode: 'box'}, + {type: '->', agents: [{name: 'B'}, {name: 'C'}]}, + {type: '->', agents: [{name: 'C'}, {name: 'D'}]}, ]}); expect(sequence.stages).toEqual([ {type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, @@ -157,9 +164,16 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('removes superfluous begin statements', () => { const sequence = generator.generate({stages: [ - {type: '->', agents: ['A', 'B']}, - {type: AGENT_BEGIN, agents: ['A', 'C', 'D'], mode: 'box'}, - {type: AGENT_BEGIN, agents: ['C', 'E'], mode: 'box'}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, + {type: AGENT_BEGIN, agents: [ + {name: 'A'}, + {name: 'C'}, + {name: 'D'}, + ], mode: 'box'}, + {type: AGENT_BEGIN, agents: [ + {name: 'C'}, + {name: 'E'}, + ], mode: 'box'}, ]}); expect(sequence.stages).toEqual([ {type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, @@ -173,11 +187,22 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('removes superfluous end statements', () => { const sequence = generator.generate({stages: [ - {type: AGENT_DEFINE, agents: ['E']}, - {type: AGENT_BEGIN, agents: ['C', 'D'], mode: 'box'}, - {type: '->', agents: ['A', 'B']}, - {type: AGENT_END, agents: ['A', 'B', 'C'], mode: 'cross'}, - {type: AGENT_END, agents: ['A', 'D', 'E'], mode: 'cross'}, + {type: AGENT_DEFINE, agents: [{name: 'E'}]}, + {type: AGENT_BEGIN, agents: [ + {name: 'C'}, + {name: 'D'}, + ], mode: 'box'}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, + {type: AGENT_END, agents: [ + {name: 'A'}, + {name: 'B'}, + {name: 'C'}, + ], mode: 'cross'}, + {type: AGENT_END, agents: [ + {name: 'A'}, + {name: 'D'}, + {name: 'E'}, + ], mode: 'cross'}, ]}); expect(sequence.stages).toEqual([ {type: AGENT_BEGIN, agents: ['C', 'D', 'A', 'B'], mode: 'box'}, @@ -188,9 +213,19 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('does not merge different modes of end', () => { const sequence = generator.generate({stages: [ - {type: AGENT_BEGIN, agents: ['C', 'D'], mode: 'box'}, - {type: '->', agents: ['A', 'B']}, - {type: AGENT_END, agents: ['A', 'B', 'C'], mode: 'cross'}, + {type: AGENT_BEGIN, agents: [ + {name: 'C'}, + {name: 'D'}, + ], mode: 'box'}, + {type: '->', agents: [ + {name: 'A'}, + {name: 'B'}, + ]}, + {type: AGENT_END, agents: [ + {name: 'A'}, + {name: 'B'}, + {name: 'C'}, + ], mode: 'cross'}, ]}); expect(sequence.stages).toEqual([ {type: AGENT_BEGIN, agents: ['C', 'D', 'A', 'B'], mode: 'box'}, @@ -203,7 +238,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('creates virtual agents for block statements', () => { const sequence = generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_END}, ]}); @@ -214,14 +249,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('positions virtual block agents near involved agents', () => { const sequence = generator.generate({stages: [ - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, - {type: '->', agents: ['C', 'D']}, + {type: '->', agents: [{name: 'C'}, {name: 'D'}]}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, - {type: '->', agents: ['E', 'F']}, + {type: '->', agents: [{name: 'E'}, {name: 'F'}]}, {type: BLOCK_END}, {type: BLOCK_END}, - {type: '->', agents: ['G', 'H']}, + {type: '->', agents: [{name: 'G'}, {name: 'H'}]}, ]}); expect(sequence.agents).toEqual([ @@ -245,7 +280,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('records virtual block agent names in blocks', () => { const sequence = generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_END}, ]}); @@ -258,9 +293,9 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('records all sections within blocks', () => { const sequence = generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, - {type: '->', agents: ['A', 'C']}, + {type: '->', agents: [{name: 'A'}, {name: 'C'}]}, {type: BLOCK_END}, ]}); @@ -280,10 +315,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('records all involved agents in nested blocks', () => { const sequence = generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, {type: BLOCK_BEGIN, mode: 'if', label: 'def'}, - {type: '->', agents: ['A', 'C']}, + {type: '->', agents: [{name: 'A'}, {name: 'C'}]}, {type: BLOCK_END}, {type: BLOCK_END}, ]}); @@ -312,10 +347,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('preserves block boundaries when agents exist outside', () => { const sequence = generator.generate({stages: [ - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'if', label: 'def'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_END}, {type: BLOCK_END}, ]}); @@ -344,7 +379,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('allows empty block parts after split', () => { const sequence = generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, {type: BLOCK_END}, ]}); @@ -363,7 +398,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { const sequence = generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_END}, ]}); @@ -392,7 +427,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('removes blocks containing only define statements / markers', () => { const sequence = generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, - {type: AGENT_DEFINE, agents: ['A']}, + {type: AGENT_DEFINE, agents: [{name: 'A'}]}, {type: 'mark', name: 'foo'}, {type: BLOCK_END}, ]}); @@ -415,7 +450,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('removes entirely empty nested blocks', () => { const sequence = generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_END}, @@ -435,13 +470,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('rejects unterminated blocks', () => { expect(() => generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, ]})).toThrow(); expect(() => generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'if', label: 'def'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_END}, ]})).toThrow(); }); @@ -453,7 +488,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { expect(() => generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_END}, {type: BLOCK_END}, ]})).toThrow(); @@ -466,7 +501,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { expect(() => generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_END}, {type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, ]})).toThrow(); @@ -476,7 +511,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { expect(() => generator.generate({stages: [ {type: BLOCK_BEGIN, mode: 'repeat', label: 'abc'}, {type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, - {type: '->', agents: ['A', 'B']}, + {type: '->', agents: [{name: 'A'}, {name: 'B'}]}, {type: BLOCK_END}, ]})).toThrow(); }); @@ -486,7 +521,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { {type: 'note right', agents: [], foo: 'bar'}, {type: 'note left', agents: [], foo: 'bar'}, {type: 'note over', agents: [], foo: 'bar'}, - {type: 'note right', agents: ['[']}, + {type: 'note right', agents: [{name: '['}]}, ]}); expect(sequence.stages).toEqual([ {type: 'note right', agents: [']'], foo: 'bar'}, @@ -498,19 +533,19 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('rejects attempts to change implicit agents', () => { expect(() => generator.generate({stages: [ - {type: AGENT_BEGIN, agents: ['['], mode: 'box'}, + {type: AGENT_BEGIN, agents: [{name: '['}], mode: 'box'}, ]})).toThrow(); expect(() => generator.generate({stages: [ - {type: AGENT_BEGIN, agents: [']'], mode: 'box'}, + {type: AGENT_BEGIN, agents: [{name: ']'}], mode: 'box'}, ]})).toThrow(); expect(() => generator.generate({stages: [ - {type: AGENT_END, agents: ['['], mode: 'cross'}, + {type: AGENT_END, agents: [{name: '['}], mode: 'cross'}, ]})).toThrow(); expect(() => generator.generate({stages: [ - {type: AGENT_END, agents: [']'], mode: 'cross'}, + {type: AGENT_END, agents: [{name: ']'}], mode: 'cross'}, ]})).toThrow(); }); }); diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js index affa291..3b3556a 100644 --- a/scripts/sequence/Parser.js +++ b/scripts/sequence/Parser.js @@ -24,13 +24,26 @@ define([ const TOKENS = [ {start: /#/y, end: /(?=\n)|$/y, omit: true}, - {start: /"/y, end: /"/y, escape: /\\(.)/y, escapeWith: unescape}, - {start: /'/y, end: /'/y, escape: /\\(.)/y, escapeWith: unescape}, + { + start: /"/y, + end: /"/y, + escape: /\\(.)/y, + escapeWith: unescape, + baseToken: {q: true}, + }, + { + start: /'/y, + end: /'/y, + escape: /\\(.)/y, + escapeWith: + unescape, + baseToken: {q: true}, + }, {start: /(?=[^ \t\r\n:+\-<>,])/y, end: /(?=[ \t\r\n:+\-<>,])|$/y}, {start: /(?=[+\-<>])/y, end: /(?=[^+\-<>])|$/y}, - {start: /,/y, prefix: ','}, - {start: /:/y, prefix: ':'}, - {start: /\n/y, prefix: '\n'}, + {start: /,/y, baseToken: {v: ','}}, + {start: /:/y, baseToken: {v: ':'}}, + {start: /\n/y, baseToken: {v: '\n'}}, ]; const BLOCK_TYPES = { @@ -95,7 +108,8 @@ define([ return { newBlock: block, end: !block.end, - append: (block.prefix || ''), + appendSpace: '', + appendValue: '', skip: match[0].length, }; } @@ -103,7 +117,8 @@ define([ return { newBlock: null, end: false, - append: '', + appendSpace: src[i], + appendValue: '', skip: 1, }; } @@ -115,7 +130,8 @@ define([ return { newBlock: null, end: false, - append: block.escapeWith(match), + appendSpace: '', + appendValue: block.escapeWith(match), skip: match[0].length, }; } @@ -125,14 +141,16 @@ define([ return { newBlock: null, end: true, - append: '', + appendSpace: '', + appendValue: '', skip: match[0].length, }; } return { newBlock: null, end: false, - append: src[i], + appendSpace: '', + appendValue: src[i], skip: 1, }; } @@ -145,10 +163,28 @@ define([ } } + function joinLabel(line, begin, end) { + if(end <= begin) { + return ''; + } + let result = line[begin].v; + for(let i = begin + 1; i < end; ++ i) { + result += line[i].s + line[i].v; + } + return result; + } + + function debugLine(line) { + return joinLabel(line, 0, line.length); + } + function skipOver(line, start, skip, error = null) { - if(skip.some((token, i) => (line[start + i] !== token))) { + if(skip.some((token, i) => ( + !line[start + i] || + line[start + i].v !== token + ))) { if(error) { - throw new Error(error + ': ' + line.join(' ')); + throw new Error(error + ': ' + debugLine(line)); } else { return start; } @@ -156,53 +192,83 @@ define([ return start + skip.length; } - function parseCommaList(tokens) { + function findToken(line, token, start = 0) { + for(let i = start; i < line.length; ++ i) { + if(line[i].v === token) { + return i; + } + } + return -1; + } + + function parseAgentList(line, start, end) { const list = []; let current = ''; - tokens.forEach((token) => { - if(token === ',') { + let first = true; + for(let i = start; i < end; ++ i) { + if(line[i].v === ',') { if(current) { - list.push(current); + list.push({name: current}); current = ''; + first = true; } } else { - current += (current ? ' ' : '') + token; + if(!first) { + current += line[i].s; + } else { + first = false; + } + current += line[i].v; } - }); + } if(current) { - list.push(current); + list.push({name: current}); } return list; } + function readAgent(line, begin, end) { + if(line[begin].v === '+' || line[begin].v === '-') { + return { + opt: line[begin].v, + name: joinLabel(line, begin + 1, end), + }; + } else { + return { + opt: '', + name: joinLabel(line, begin, end), + }; + } + } + const PARSERS = [ (line, meta) => { // title - if(line[0] !== 'title') { + if(line[0].v !== 'title') { return null; } - meta.title = line.slice(1).join(' '); + meta.title = joinLabel(line, 1, line.length); return true; }, (line, meta) => { // terminators - if(line[0] !== 'terminators') { + if(line[0].v !== 'terminators') { return null; } - if(TERMINATOR_TYPES.indexOf(line[1]) === -1) { - throw new Error('Unknown termination: ' + line.join(' ')); + if(TERMINATOR_TYPES.indexOf(line[1].v) === -1) { + throw new Error('Unknown termination: ' + debugLine(line)); } - meta.terminators = line[1]; + meta.terminators = line[1].v; return true; }, (line) => { // block - if(line[0] === 'end' && line.length === 1) { + if(line[0].v === 'end' && line.length === 1) { return {type: 'block end'}; } - const type = BLOCK_TYPES[line[0]]; + const type = BLOCK_TYPES[line[0].v]; if(!type) { return null; } @@ -214,36 +280,33 @@ define([ return { type: type.type, mode: type.mode, - label: line.slice(skip).join(' '), + label: joinLabel(line, skip, line.length), }; }, (line) => { // agent - const type = AGENT_MANIPULATION_TYPES[line[0]]; - if(!type) { - return null; - } - if(line.length <= 1) { + const type = AGENT_MANIPULATION_TYPES[line[0].v]; + if(!type || line.length <= 1) { return null; } return Object.assign({ - agents: parseCommaList(line.slice(1)), + agents: parseAgentList(line, 1, line.length), }, type); }, (line) => { // async - if(line[0] !== 'simultaneously') { + if(line[0].v !== 'simultaneously') { return null; } - if(array.last(line) !== ':') { + if(array.last(line).v !== ':') { return null; } let target = ''; if(line.length > 2) { - if(line[1] !== 'with') { + if(line[1].v !== 'with') { return null; } - target = line.slice(2, line.length - 1).join(' '); + target = joinLabel(line, 2, line.length - 1); } return { type: 'async', @@ -252,41 +315,44 @@ define([ }, (line) => { // note - const mode = NOTE_TYPES[line[0]]; - const labelSplit = line.indexOf(':'); + const mode = NOTE_TYPES[line[0].v]; + const labelSplit = findToken(line, ':'); if(!mode || labelSplit === -1) { return null; } - const type = mode.types[line[1]]; + const type = mode.types[line[1].v]; if(!type) { return null; } let skip = 2; skip = skipOver(line, skip, type.skip); - const agents = parseCommaList(line.slice(skip, labelSplit)); + const agents = parseAgentList(line, skip, labelSplit); if( agents.length < type.min || (type.max !== null && agents.length > type.max) ) { - throw new Error('Invalid ' + line[0] + ': ' + line.join(' ')); + throw new Error( + 'Invalid ' + mode.mode + + ': ' + debugLine(line) + ); } return { type: type.type, agents, mode: mode.mode, - label: line.slice(labelSplit + 1).join(' '), + label: joinLabel(line, labelSplit + 1, line.length), }; }, (line) => { // connection - let labelSplit = line.indexOf(':'); + let labelSplit = findToken(line, ':'); if(labelSplit === -1) { labelSplit = line.length; } let typeSplit = -1; let options = null; for(let j = 0; j < line.length; ++ j) { - const opts = CONNECTION_TYPES[line[j]]; + const opts = CONNECTION_TYPES[line[j].v]; if(opts) { typeSplit = j; options = opts; @@ -299,20 +365,20 @@ define([ return Object.assign({ type: 'connection', agents: [ - line.slice(0, typeSplit).join(' '), - line.slice(typeSplit + 1, labelSplit).join(' '), + readAgent(line, 0, typeSplit), + readAgent(line, typeSplit + 1, labelSplit), ], - label: line.slice(labelSplit + 1).join(' '), + label: joinLabel(line, labelSplit + 1, line.length), }, options); }, (line) => { // marker - if(line.length < 2 || array.last(line) !== ':') { + if(line.length < 2 || array.last(line).v !== ':') { return null; } return { type: 'mark', - name: line.slice(0, line.length - 1).join(' '), + name: joinLabel(line, 0, line.length - 1), }; }, ]; @@ -326,7 +392,7 @@ define([ } } if(!stage) { - throw new Error('Unrecognised command: ' + line.join(' ')); + throw new Error('Unrecognised command: ' + debugLine(line)); } if(typeof stage === 'object') { stages.push(stage); @@ -337,19 +403,21 @@ define([ tokenise(src) { const tokens = []; let block = null; - let current = ''; + let current = {s: '', v: '', q: false}; for(let i = 0; i <= src.length;) { - const {newBlock, end, append, skip} = tokAdvance(src, i, block); - if(newBlock) { - block = newBlock; - current = ''; + const advance = tokAdvance(src, i, block); + if(advance.newBlock) { + block = advance.newBlock; + Object.assign(current, block.baseToken); } - current += append; - i += skip; - if(end) { + current.s += advance.appendSpace; + current.v += advance.appendValue; + i += advance.skip; + if(advance.end) { if(!block.omit) { tokens.push(current); } + current = {s: '', v: '', q: false}; block = null; } } @@ -371,7 +439,7 @@ define([ const lines = []; let line = []; tokens.forEach((token) => { - if(token === '\n') { + if(token.v === '\n' && !token.q) { if(line.length > 0) { lines.push(line); line = []; diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js index 9e45cae..5f9d0d7 100644 --- a/scripts/sequence/Parser_spec.js +++ b/scripts/sequence/Parser_spec.js @@ -7,55 +7,103 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { it('converts the source into atomic tokens', () => { const input = 'foo bar -> baz'; const tokens = parser.tokenise(input); - expect(tokens).toEqual(['foo', 'bar', '->', 'baz']); + expect(tokens).toEqual([ + {s: '', v: 'foo', q: false}, + {s: ' ', v: 'bar', q: false}, + {s: ' ', v: '->', q: false}, + {s: ' ', v: 'baz', q: false}, + ]); }); it('splits tokens at flexible boundaries', () => { const input = 'foo bar->baz'; const tokens = parser.tokenise(input); - expect(tokens).toEqual(['foo', 'bar', '->', 'baz']); + expect(tokens).toEqual([ + {s: '', v: 'foo', q: false}, + {s: ' ', v: 'bar', q: false}, + {s: '', v: '->', q: false}, + {s: '', v: 'baz', q: false}, + ]); }); it('parses newlines as tokens', () => { const input = 'foo bar\nbaz'; const tokens = parser.tokenise(input); - expect(tokens).toEqual(['foo', 'bar', '\n', 'baz']); + expect(tokens).toEqual([ + {s: '', v: 'foo', q: false}, + {s: ' ', v: 'bar', q: false}, + {s: '', v: '\n', q: false}, + {s: '', v: 'baz', q: false}, + ]); + }); + + it('parses quoted newlines as quoted tokens', () => { + const input = 'foo "\n" baz'; + const tokens = parser.tokenise(input); + expect(tokens).toEqual([ + {s: '', v: 'foo', q: false}, + {s: ' ', v: '\n', q: true}, + {s: ' ', v: 'baz', q: false}, + ]); }); it('removes leading and trailing whitespace', () => { const input = ' foo \t bar\t\n baz'; const tokens = parser.tokenise(input); - expect(tokens).toEqual(['foo', 'bar', '\n', 'baz']); + expect(tokens).toEqual([ + {s: ' ', v: 'foo', q: false}, + {s: ' \t ', v: 'bar', q: false}, + {s: '\t', v: '\n', q: false}, + {s: ' ', v: 'baz', q: false}, + ]); }); it('parses quoted strings as single tokens', () => { const input = 'foo "zig zag" \'abc def\''; const tokens = parser.tokenise(input); - expect(tokens).toEqual(['foo', 'zig zag', 'abc def']); + expect(tokens).toEqual([ + {s: '', v: 'foo', q: false}, + {s: ' ', v: 'zig zag', q: true}, + {s: ' ', v: 'abc def', q: true}, + ]); }); it('ignores comments', () => { const input = 'foo # bar baz\nzig'; const tokens = parser.tokenise(input); - expect(tokens).toEqual(['foo', '\n', 'zig']); + expect(tokens).toEqual([ + {s: '', v: 'foo', q: false}, + {s: '', v: '\n', q: false}, + {s: '', v: 'zig', q: false}, + ]); }); it('ignores quotes within comments', () => { const input = 'foo # bar "\'baz\nzig'; const tokens = parser.tokenise(input); - expect(tokens).toEqual(['foo', '\n', 'zig']); + expect(tokens).toEqual([ + {s: '', v: 'foo', q: false}, + {s: '', v: '\n', q: false}, + {s: '', v: 'zig', q: false}, + ]); }); it('interprets special characters within quoted strings', () => { const input = 'foo "zig\\" zag\\n"'; const tokens = parser.tokenise(input); - expect(tokens).toEqual(['foo', 'zig" zag\n']); + expect(tokens).toEqual([ + {s: '', v: 'foo', q: false}, + {s: ' ', v: 'zig" zag\n', q: true}, + ]); }); it('maintains whitespace and newlines within quoted strings', () => { const input = 'foo " zig\n zag "'; const tokens = parser.tokenise(input); - expect(tokens).toEqual(['foo', ' zig\n zag ']); + expect(tokens).toEqual([ + {s: '', v: 'foo', q: false}, + {s: ' ', v: ' zig\n zag ', q: true}, + ]); }); it('rejects unterminated quoted values', () => { @@ -65,33 +113,70 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { describe('.splitLines', () => { it('combines tokens', () => { - const lines = parser.splitLines(['abc', 'd']); + const lines = parser.splitLines([ + {s: '', v: 'abc', q: false}, + {s: '', v: 'd', q: false}, + ]); expect(lines).toEqual([ - ['abc', 'd'], + [{s: '', v: 'abc', q: false}, {s: '', v: 'd', q: false}], ]); }); it('splits at newlines', () => { - const lines = parser.splitLines(['abc', 'd', '\n', 'e']); + const lines = parser.splitLines([ + {s: '', v: 'abc', q: false}, + {s: '', v: 'd', q: false}, + {s: '', v: '\n', q: false}, + {s: '', v: 'e', q: false}, + ]); expect(lines).toEqual([ - ['abc', 'd'], - ['e'], + [{s: '', v: 'abc', q: false}, {s: '', v: 'd', q: false}], + [{s: '', v: 'e', q: false}], ]); }); it('ignores multiple newlines', () => { - const lines = parser.splitLines(['abc', 'd', '\n', '\n', 'e']); + const lines = parser.splitLines([ + {s: '', v: 'abc', q: false}, + {s: '', v: 'd', q: false}, + {s: '', v: '\n', q: false}, + {s: '', v: '\n', q: false}, + {s: '', v: 'e', q: false}, + ]); expect(lines).toEqual([ - ['abc', 'd'], - ['e'], + [{s: '', v: 'abc', q: false}, {s: '', v: 'd', q: false}], + [{s: '', v: 'e', q: false}], ]); }); it('ignores trailing newlines', () => { - const lines = parser.splitLines(['abc', 'd', '\n', 'e', '\n']); + const lines = parser.splitLines([ + {s: '', v: 'abc', q: false}, + {s: '', v: 'd', q: false}, + {s: '', v: '\n', q: false}, + {s: '', v: 'e', q: false}, + {s: '', v: '\n', q: false}, + ]); expect(lines).toEqual([ - ['abc', 'd'], - ['e'], + [{s: '', v: 'abc', q: false}, {s: '', v: 'd', q: false}], + [{s: '', v: 'e', q: false}], + ]); + }); + + it('handles quoted newlines as regular tokens', () => { + const lines = parser.splitLines([ + {s: '', v: 'abc', q: false}, + {s: '', v: 'd', q: false}, + {s: '', v: '\n', q: true}, + {s: '', v: 'e', q: false}, + ]); + expect(lines).toEqual([ + [ + {s: '', v: 'abc', q: false}, + {s: '', v: 'd', q: false}, + {s: '', v: '\n', q: true}, + {s: '', v: 'e', q: false}, + ], ]); }); @@ -101,13 +186,13 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { }); }); - function connectionStage(agents, label = '') { + function connectionStage(agentNames, label = '') { return { type: 'connection', line: 'solid', left: false, right: true, - agents, + agents: agentNames.map((agent) => ({opt: '', name: agent})), label, }; } @@ -153,6 +238,13 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { ]); }); + it('respects spacing within agent names', () => { + const parsed = parser.parse('A+B -> C D'); + expect(parsed.stages).toEqual([ + connectionStage(['A+B', 'C D']), + ]); + }); + it('parses optional labels', () => { const parsed = parser.parse('A B -> C D: foo bar'); expect(parsed.stages).toEqual([ @@ -191,7 +283,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { line: 'solid', left: false, right: true, - agents: ['A', 'B'], + agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}], label: '', }, { @@ -199,7 +291,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { line: 'solid', left: true, right: false, - agents: ['A', 'B'], + agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}], label: '', }, { @@ -207,7 +299,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { line: 'solid', left: true, right: true, - agents: ['A', 'B'], + agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}], label: '', }, { @@ -215,7 +307,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { line: 'dash', left: false, right: true, - agents: ['A', 'B'], + agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}], label: '', }, { @@ -223,7 +315,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { line: 'dash', left: true, right: false, - agents: ['A', 'B'], + agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}], label: '', }, { @@ -231,7 +323,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { line: 'dash', left: true, right: true, - agents: ['A', 'B'], + agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}], label: '', }, ]); @@ -248,7 +340,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { line: 'solid', left: true, right: false, - agents: ['A', 'B'], + agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}], label: 'B -> A', }, { @@ -256,7 +348,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { line: 'solid', left: false, right: true, - agents: ['A', 'B'], + agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}], label: 'B <- A', }, ]); @@ -266,7 +358,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { const parsed = parser.parse('note over A: hello there'); expect(parsed.stages).toEqual([{ type: 'note over', - agents: ['A'], + agents: [{name: 'A'}], mode: 'note', label: 'hello there', }]); @@ -283,31 +375,31 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { expect(parsed.stages).toEqual([ { type: 'note left', - agents: ['A'], + agents: [{name: 'A'}], mode: 'note', label: 'hello there', }, { type: 'note left', - agents: ['A'], + agents: [{name: 'A'}], mode: 'note', label: 'hello there', }, { type: 'note right', - agents: ['A'], + agents: [{name: 'A'}], mode: 'note', label: 'hello there', }, { type: 'note right', - agents: ['A'], + agents: [{name: 'A'}], mode: 'note', label: 'hello there', }, { type: 'note between', - agents: ['A', 'B'], + agents: [{name: 'A'}, {name: 'B'}], mode: 'note', label: 'hi', }, @@ -318,7 +410,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { const parsed = parser.parse('note over A B, C D: hi'); expect(parsed.stages).toEqual([{ type: 'note over', - agents: ['A B', 'C D'], + agents: [{name: 'A B'}, {name: 'C D'}], mode: 'note', label: 'hi', }]); @@ -332,7 +424,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { const parsed = parser.parse('state over A: doing stuff'); expect(parsed.stages).toEqual([{ type: 'note over', - agents: ['A'], + agents: [{name: 'A'}], mode: 'state', label: 'doing stuff', }]); @@ -346,7 +438,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { const parsed = parser.parse('text right of A: doing stuff'); expect(parsed.stages).toEqual([{ type: 'note right', - agents: ['A'], + agents: [{name: 'A'}], mode: 'text', label: 'doing stuff', }]); @@ -359,9 +451,20 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { 'end A, B\n' ); expect(parsed.stages).toEqual([ - {type: 'agent define', agents: ['A', 'B']}, - {type: 'agent begin', agents: ['A', 'B'], mode: 'box'}, - {type: 'agent end', agents: ['A', 'B'], mode: 'cross'}, + { + type: 'agent define', + agents: [{name: 'A'}, {name: 'B'}], + }, + { + type: 'agent begin', + agents: [{name: 'A'}, {name: 'B'}], + mode: 'box', + }, + { + type: 'agent end', + agents: [{name: 'A'}, {name: 'B'}], + mode: 'cross', + }, ]); }); diff --git a/scripts/sequence/themes/Basic.js b/scripts/sequence/themes/Basic.js index 917e634..e0386b0 100644 --- a/scripts/sequence/themes/Basic.js +++ b/scripts/sequence/themes/Basic.js @@ -103,7 +103,7 @@ define([ top: 0, left: 3, right: 3, - bottom: 0, + bottom: 1, }, maskAttrs: { 'fill': '#FFFFFF', diff --git a/styles/main.css b/styles/main.css index f163091..bbe8236 100644 --- a/styles/main.css +++ b/styles/main.css @@ -24,9 +24,6 @@ html, body { .cm-s-default .cm-string {color: #221111;} .cm-s-default .cm-error {color: #FF0000;} -.cm-s-default .cm-warning { - background: #FFFF00; -} .cm-s-default .cm-trailingspace { background: rgba(255, 0, 0, 0.5); }