| 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: ' closing tag ' |
| }, |
| { |
| 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 (data.action) { |
| case 'remove': |
| cvox.Api.speak(data.text, 0, DELETED_PROP); |
| changed = true; |
| break; |
| case 'insert': |
| cvox.Api.speak(data.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() { |
| 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(); |
| 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() { |
| window.require(["ace/ext/chromevox"], function() {}); |
| })(); |
| |