You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
541 lines
13 KiB
541 lines
13 KiB
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: ' 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(evt) { |
|
var data = evt.data; |
|
switch (data.action) { |
|
case 'removeText': |
|
cvox.Api.speak(data.text, 0, DELETED_PROP); |
|
changed = true; |
|
break; |
|
case 'insertText': |
|
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() { |
|
ace.require(["ace/ext/chromevox"], function() {}); |
|
})(); |
|
|