ace.define("ace/ext/chromevox",["require","exports","module","ace/editor","ace/config"], function(require, exports, module) { var cvoxAce = {}; cvoxAce.SpeechProperty; cvoxAce.Cursor; cvoxAce.Token; cvoxAce.Annotation; var CONSTANT_PROP = { 'rate': 0.8, 'pitch': 0.4, 'volume': 0.9 }; var DEFAULT_PROP = { 'rate': 1, 'pitch': 0.5, 'volume': 0.9 }; var ENTITY_PROP = { 'rate': 0.8, 'pitch': 0.8, 'volume': 0.9 }; var KEYWORD_PROP = { 'rate': 0.8, 'pitch': 0.3, 'volume': 0.9 }; var STORAGE_PROP = { 'rate': 0.8, 'pitch': 0.7, 'volume': 0.9 }; var VARIABLE_PROP = { 'rate': 0.8, 'pitch': 0.8, 'volume': 0.9 }; var DELETED_PROP = { 'punctuationEcho': 'none', 'relativePitch': -0.6 }; var ERROR_EARCON = 'ALERT_NONMODAL'; var MODE_SWITCH_EARCON = 'ALERT_MODAL'; var NO_MATCH_EARCON = 'INVALID_KEYPRESS'; var INSERT_MODE_STATE = 'insertMode'; var COMMAND_MODE_STATE = 'start'; var REPLACE_LIST = [ { substr: ';', newSubstr: ' semicolon ' }, { substr: ':', newSubstr: ' colon ' } ]; var Command = { SPEAK_ANNOT: 'annots', SPEAK_ALL_ANNOTS: 'all_annots', TOGGLE_LOCATION: 'toggle_location', SPEAK_MODE: 'mode', SPEAK_ROW_COL: 'row_col', TOGGLE_DISPLACEMENT: 'toggle_displacement', FOCUS_TEXT: 'focus_text' }; var KEY_PREFIX = 'CONTROL + SHIFT '; cvoxAce.editor = null; var lastCursor = null; var annotTable = {}; var shouldSpeakRowLocation = false; var shouldSpeakDisplacement = false; var changed = false; var vimState = null; var keyCodeToShortcutMap = {}; var cmdToShortcutMap = {}; var getKeyShortcutString = function(keyCode) { return KEY_PREFIX + String.fromCharCode(keyCode); }; var isVimMode = function() { var keyboardHandler = cvoxAce.editor.keyBinding.getKeyboardHandler(); return keyboardHandler.$id === 'ace/keyboard/vim'; }; var getCurrentToken = function(cursor) { return cvoxAce.editor.getSession().getTokenAt(cursor.row, cursor.column + 1); }; var getCurrentLine = function(cursor) { return cvoxAce.editor.getSession().getLine(cursor.row); }; var onRowChange = function(currCursor) { if (annotTable[currCursor.row]) { cvox.Api.playEarcon(ERROR_EARCON); } if (shouldSpeakRowLocation) { cvox.Api.stop(); speakChar(currCursor); speakTokenQueue(getCurrentToken(currCursor)); speakLine(currCursor.row, 1); } else { speakLine(currCursor.row, 0); } }; var isWord = function(cursor) { var line = getCurrentLine(cursor); var lineSuffix = line.substr(cursor.column - 1); if (cursor.column === 0) { lineSuffix = ' ' + line; } var firstWordRegExp = /^\W(\w+)/; var words = firstWordRegExp.exec(lineSuffix); return words !== null; }; var rules = { 'constant': { prop: CONSTANT_PROP }, 'entity': { prop: ENTITY_PROP }, 'keyword': { prop: KEYWORD_PROP }, 'storage': { prop: STORAGE_PROP }, 'variable': { prop: VARIABLE_PROP }, 'meta': { prop: DEFAULT_PROP, replace: [ { substr: '', newSubstr: ' close tag ' }, { substr: '<', newSubstr: ' tag start ' }, { substr: '>', newSubstr: ' tag end ' } ] } }; var DEFAULT_RULE = { prop: DEFAULT_RULE }; var expand = function(value, replaceRules) { var newValue = value; for (var i = 0; i < replaceRules.length; i++) { var replaceRule = replaceRules[i]; var regexp = new RegExp(replaceRule.substr, 'g'); newValue = newValue.replace(regexp, replaceRule.newSubstr); } return newValue; }; var mergeTokens = function(tokens, start, end) { var newToken = {}; newToken.value = ''; newToken.type = tokens[start].type; for (var j = start; j < end; j++) { newToken.value += tokens[j].value; } return newToken; }; var mergeLikeTokens = function(tokens) { if (tokens.length <= 1) { return tokens; } var newTokens = []; var lastLikeIndex = 0; for (var i = 1; i < tokens.length; i++) { var lastLikeToken = tokens[lastLikeIndex]; var currToken = tokens[i]; if (getTokenRule(lastLikeToken) !== getTokenRule(currToken)) { newTokens.push(mergeTokens(tokens, lastLikeIndex, i)); lastLikeIndex = i; } } newTokens.push(mergeTokens(tokens, lastLikeIndex, tokens.length)); return newTokens; }; var isRowWhiteSpace = function(row) { var line = cvoxAce.editor.getSession().getLine(row); var whiteSpaceRegexp = /^\s*$/; return whiteSpaceRegexp.exec(line) !== null; }; var speakLine = function(row, queue) { var tokens = cvoxAce.editor.getSession().getTokens(row); if (tokens.length === 0 || isRowWhiteSpace(row)) { cvox.Api.playEarcon('EDITABLE_TEXT'); return; } tokens = mergeLikeTokens(tokens); var firstToken = tokens[0]; tokens = tokens.filter(function(token) { return token !== firstToken; }); speakToken_(firstToken, queue); tokens.forEach(speakTokenQueue); }; var speakTokenFlush = function(token) { speakToken_(token, 0); }; var speakTokenQueue = function(token) { speakToken_(token, 1); }; var getTokenRule = function(token) { if (!token || !token.type) { return; } var split = token.type.split('.'); if (split.length === 0) { return; } var type = split[0]; var rule = rules[type]; if (!rule) { return DEFAULT_RULE; } return rule; }; var speakToken_ = function(token, queue) { var rule = getTokenRule(token); var value = expand(token.value, REPLACE_LIST); if (rule.replace) { value = expand(value, rule.replace); } cvox.Api.speak(value, queue, rule.prop); }; var speakChar = function(cursor) { var line = getCurrentLine(cursor); cvox.Api.speak(line[cursor.column], 1); }; var speakDisplacement = function(lastCursor, currCursor) { var line = getCurrentLine(currCursor); var displace = line.substring(lastCursor.column, currCursor.column); displace = displace.replace(/ /g, ' space '); cvox.Api.speak(displace); }; var speakCharOrWordOrLine = function(lastCursor, currCursor) { if (Math.abs(lastCursor.column - currCursor.column) !== 1) { var currLineLength = getCurrentLine(currCursor).length; if (currCursor.column === 0 || currCursor.column === currLineLength) { speakLine(currCursor.row, 0); return; } if (isWord(currCursor)) { cvox.Api.stop(); speakTokenQueue(getCurrentToken(currCursor)); return; } } speakChar(currCursor); }; var onColumnChange = function(lastCursor, currCursor) { if (!cvoxAce.editor.selection.isEmpty()) { speakDisplacement(lastCursor, currCursor); cvox.Api.speak('selected', 1); } else if (shouldSpeakDisplacement) { speakDisplacement(lastCursor, currCursor); } else { speakCharOrWordOrLine(lastCursor, currCursor); } }; var onCursorChange = function(evt) { if (changed) { changed = false; return; } var currCursor = cvoxAce.editor.selection.getCursor(); if (currCursor.row !== lastCursor.row) { onRowChange(currCursor); } else { onColumnChange(lastCursor, currCursor); } lastCursor = currCursor; }; var onSelectionChange = function(evt) { if (cvoxAce.editor.selection.isEmpty()) { cvox.Api.speak('unselected'); } }; var onChange = function(delta) { switch (delta.action) { case 'remove': cvox.Api.speak(delta.text, 0, DELETED_PROP); changed = true; break; case 'insert': cvox.Api.speak(delta.text, 0); changed = true; break; } }; var isNewAnnotation = function(annot) { var row = annot.row; var col = annot.column; return !annotTable[row] || !annotTable[row][col]; }; var populateAnnotations = function(annotations) { annotTable = {}; for (var i = 0; i < annotations.length; i++) { var annotation = annotations[i]; var row = annotation.row; var col = annotation.column; if (!annotTable[row]) { annotTable[row] = {}; } annotTable[row][col] = annotation; } }; var onAnnotationChange = function(evt) { var annotations = cvoxAce.editor.getSession().getAnnotations(); var newAnnotations = annotations.filter(isNewAnnotation); if (newAnnotations.length > 0) { cvox.Api.playEarcon(ERROR_EARCON); } populateAnnotations(annotations); }; var speakAnnot = function(annot) { var annotText = annot.type + ' ' + annot.text + ' on ' + rowColToString(annot.row, annot.column); annotText = annotText.replace(';', 'semicolon'); cvox.Api.speak(annotText, 1); }; var speakAnnotsByRow = function(row) { var annots = annotTable[row]; for (var col in annots) { speakAnnot(annots[col]); } }; var rowColToString = function(row, col) { return 'row ' + (row + 1) + ' column ' + (col + 1); }; var speakCurrRowAndCol = function() { cvox.Api.speak(rowColToString(lastCursor.row, lastCursor.column)); }; var speakAllAnnots = function() { for (var row in annotTable) { speakAnnotsByRow(row); } }; var speakMode = function() { if (!isVimMode()) { return; } switch (cvoxAce.editor.keyBinding.$data.state) { case INSERT_MODE_STATE: cvox.Api.speak('Insert mode'); break; case COMMAND_MODE_STATE: cvox.Api.speak('Command mode'); break; } }; var toggleSpeakRowLocation = function() { shouldSpeakRowLocation = !shouldSpeakRowLocation; if (shouldSpeakRowLocation) { cvox.Api.speak('Speak location on row change enabled.'); } else { cvox.Api.speak('Speak location on row change disabled.'); } }; var toggleSpeakDisplacement = function() { shouldSpeakDisplacement = !shouldSpeakDisplacement; if (shouldSpeakDisplacement) { cvox.Api.speak('Speak displacement on column changes.'); } else { cvox.Api.speak('Speak current character or word on column changes.'); } }; var onKeyDown = function(evt) { if (evt.ctrlKey && evt.shiftKey) { var shortcut = keyCodeToShortcutMap[evt.keyCode]; if (shortcut) { shortcut.func(); } } }; var onChangeStatus = function(evt, editor) { if (!isVimMode()) { return; } var state = editor.keyBinding.$data.state; if (state === vimState) { return; } switch (state) { case INSERT_MODE_STATE: cvox.Api.playEarcon(MODE_SWITCH_EARCON); cvox.Api.setKeyEcho(true); break; case COMMAND_MODE_STATE: cvox.Api.playEarcon(MODE_SWITCH_EARCON); cvox.Api.setKeyEcho(false); break; } vimState = state; }; var contextMenuHandler = function(evt) { var cmd = evt.detail['customCommand']; var shortcut = cmdToShortcutMap[cmd]; if (shortcut) { shortcut.func(); cvoxAce.editor.focus(); } }; var initContextMenu = function() { var ACTIONS = SHORTCUTS.map(function(shortcut) { return { desc: shortcut.desc + getKeyShortcutString(shortcut.keyCode), cmd: shortcut.cmd }; }); var body = document.querySelector('body'); body.setAttribute('contextMenuActions', JSON.stringify(ACTIONS)); body.addEventListener('ATCustomEvent', contextMenuHandler, true); }; var onFindSearchbox = function(evt) { if (evt.match) { speakLine(lastCursor.row, 0); } else { cvox.Api.playEarcon(NO_MATCH_EARCON); } }; var focus = function() { cvoxAce.editor.focus(); }; var SHORTCUTS = [ { keyCode: 49, func: function() { speakAnnotsByRow(lastCursor.row); }, cmd: Command.SPEAK_ANNOT, desc: 'Speak annotations on line' }, { keyCode: 50, func: speakAllAnnots, cmd: Command.SPEAK_ALL_ANNOTS, desc: 'Speak all annotations' }, { keyCode: 51, func: speakMode, cmd: Command.SPEAK_MODE, desc: 'Speak Vim mode' }, { keyCode: 52, func: toggleSpeakRowLocation, cmd: Command.TOGGLE_LOCATION, desc: 'Toggle speak row location' }, { keyCode: 53, func: speakCurrRowAndCol, cmd: Command.SPEAK_ROW_COL, desc: 'Speak row and column' }, { keyCode: 54, func: toggleSpeakDisplacement, cmd: Command.TOGGLE_DISPLACEMENT, desc: 'Toggle speak displacement' }, { keyCode: 55, func: focus, cmd: Command.FOCUS_TEXT, desc: 'Focus text' } ]; var onFocus = function(_, editor) { cvoxAce.editor = editor; editor.getSession().selection.on('changeCursor', onCursorChange); editor.getSession().selection.on('changeSelection', onSelectionChange); editor.getSession().on('change', onChange); editor.getSession().on('changeAnnotation', onAnnotationChange); editor.on('changeStatus', onChangeStatus); editor.on('findSearchBox', onFindSearchbox); editor.container.addEventListener('keydown', onKeyDown); lastCursor = editor.selection.getCursor(); }; var init = function(editor) { onFocus(null, editor); SHORTCUTS.forEach(function(shortcut) { keyCodeToShortcutMap[shortcut.keyCode] = shortcut; cmdToShortcutMap[shortcut.cmd] = shortcut; }); editor.on('focus', onFocus); if (isVimMode()) { cvox.Api.setKeyEcho(false); } initContextMenu(); }; function cvoxApiExists() { return (typeof(cvox) !== 'undefined') && cvox && cvox.Api; } var tries = 0; var MAX_TRIES = 15; function watchForCvoxLoad(editor) { if (cvoxApiExists()) { init(editor); } else { tries++; if (tries >= MAX_TRIES) { return; } window.setTimeout(watchForCvoxLoad, 500, editor); } } var Editor = require('../editor').Editor; require('../config').defineOptions(Editor.prototype, 'editor', { enableChromevoxEnhancements: { set: function(val) { if (val) { watchForCvoxLoad(this); } }, value: true // turn it on by default or check for window.cvox } }); }); (function() { ace.require(["ace/ext/chromevox"], function() {}); })();