blob: d3ba27e3b369e69d50f38b4cb78c0fe6fdc52fdd [file] [log] [blame]
package org.unicode.cldr.draft.keyboard.osx;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Map;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.unicode.cldr.draft.keyboard.CharacterMap;
import org.unicode.cldr.draft.keyboard.IsoLayoutPosition;
import org.unicode.cldr.draft.keyboard.Keyboard;
import org.unicode.cldr.draft.keyboard.KeyboardBuilder;
import org.unicode.cldr.draft.keyboard.KeyboardId;
import org.unicode.cldr.draft.keyboard.KeyboardId.Platform;
import org.unicode.cldr.draft.keyboard.KeyboardIdMap;
import org.unicode.cldr.draft.keyboard.KeycodeMap;
import org.unicode.cldr.draft.keyboard.ModifierKey;
import org.unicode.cldr.draft.keyboard.ModifierKeyCombination;
import org.unicode.cldr.draft.keyboard.ModifierKeyCombinationSet;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import com.google.common.base.CharMatcher;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
public final class KeylayoutParser {
private static final Pattern INVALID_UNICODE_XML = Pattern.compile("&#x00([01][0-9a-fA-F]);");
private static final KeycodeMap KEYCODE_MAP = KeycodeMap.fromResource(KeylayoutParser.class,
"osx-keycodes.csv");
public static final KeyboardIdMap KEYBOARD_ID_MAP = KeyboardIdMap.fromResource(KeylayoutParser.class,
"osx-locales.csv", Platform.OSX);
private final KeyboardBuilder builder;
private final Element keyboardElement;
private final Map<String, ModifierKeyCombinationSet> mapIndexToModifier = Maps.newHashMap();
private KeylayoutParser(Element keyboardElement) {
this.keyboardElement = checkNotNull(keyboardElement);
builder = new KeyboardBuilder();
}
public static ImmutableList<Keyboard> parseLayout(String keylayoutContents) {
String cleansedString = keylayoutContents
.replace("\u007f", "")
.replace("&#x007f;", "")
.replace("&#x007F;", "")
.replace("\"<\"", "\"&lt;\"")
.replace("\">\"", "\"&gt;\"")
.replace("\"\"\"", "\"&quot;\"")
.replace("\"&\"", "\"&amp;\"")
.replace("\"'\"", "\"&apos;\"");
cleansedString = INVALID_UNICODE_XML.matcher(cleansedString).replaceAll("");
Document document;
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
document = builder.parse(new InputSource(new ByteArrayInputStream(
cleansedString.toString().getBytes(Charsets.US_ASCII))));
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
} catch (SAXException e) {
throw new RuntimeException(e);
}
KeylayoutParser parser = new KeylayoutParser(document.getDocumentElement());
return parser.parse();
}
/** Parse the given keylayout contents and populate the keyboard builder. */
private ImmutableList<Keyboard> parse() {
parseId();
Element layout = findLayout();
String modifierId = layout.getAttribute("modifiers");
parseModifiers(modifierId);
String mapSet = layout.getAttribute("mapSet");
parseKeyMaps(mapSet);
return builder.build();
}
private void parseId() {
String name = keyboardElement.getAttribute("name");
checkArgument(!name.isEmpty());
builder.addName(name);
ImmutableCollection<KeyboardId> ids = KEYBOARD_ID_MAP.getKeyboardId(name);
builder.addKeyboardIds(ids);
}
private Element findLayout() {
NodeList layouts = keyboardElement.getElementsByTagName("layout");
for (int i = 0; i < layouts.getLength(); i++) {
Element layout = (Element) layouts.item(i);
if (layout.getAttribute("first").equals("0") || layout.getAttribute("first").equals("1")) {
return layout;
}
}
throw new IllegalArgumentException("No layout element containing first='0'");
}
private void parseModifiers(String modifierId) {
checkArgument(!modifierId.isEmpty());
NodeList modifierMaps = keyboardElement.getElementsByTagName("modifierMap");
Element modifierMap = null;
for (int i = 0; i < modifierMaps.getLength(); i++) {
Element modifierMapElement = (Element) modifierMaps.item(i);
if (modifierMapElement.getAttribute("id").equals(modifierId)) {
modifierMap = modifierMapElement;
}
}
checkNotNull(modifierMap);
NodeList keyMapSelects = modifierMap.getElementsByTagName("keyMapSelect");
for (int i = 0; i < keyMapSelects.getLength(); i++) {
Element keyMapSelect = (Element) keyMapSelects.item(i);
NodeList modifiers = keyMapSelect.getElementsByTagName("modifier");
ImmutableSet.Builder<ModifierKeyCombination> combinations = ImmutableSet.builder();
for (int j = 0; j < modifiers.getLength(); j++) {
Element modifier = (Element) modifiers.item(j);
String keys = modifier.getAttribute("keys");
combinations.add(parseKeys(keys));
}
String mapIndex = keyMapSelect.getAttribute("mapIndex");
checkArgument(!mapIndex.isEmpty());
mapIndexToModifier.put(mapIndex, ModifierKeyCombinationSet.of(combinations.build()));
}
}
private static final Splitter WHITESPACE_SPLITTER = Splitter.on(CharMatcher.WHITESPACE)
.omitEmptyStrings();
private static ModifierKeyCombination parseKeys(String keys) {
ImmutableSet.Builder<ModifierKey> onKeys = ImmutableSet.builder();
ImmutableSet.Builder<ModifierKey> dontCareKeys = ImmutableSet.builder();
for (String key : WHITESPACE_SPLITTER.splitToList(keys)) {
boolean maybe = key.contains("?");
key = maybe ? key.substring(0, key.length() - 1) : key;
ImmutableSet.Builder<ModifierKey> builder = maybe ? dontCareKeys : onKeys;
if (key.equals("command")) {
builder.add(ModifierKey.COMMAND);
} else if (key.equals("caps")) {
builder.add(ModifierKey.CAPSLOCK);
} else if (key.equals("anyShift")) {
builder.add(ModifierKey.SHIFT);
} else if (key.equals("shift")) {
builder.add(ModifierKey.SHIFT_LEFT);
} else if (key.equals("rightShift")) {
builder.add(ModifierKey.SHIFT_RIGHT);
} else if (key.equals("anyOption")) {
builder.add(ModifierKey.OPTION);
} else if (key.equals("option")) {
builder.add(ModifierKey.OPTION_LEFT);
} else if (key.equals("rightOption")) {
builder.add(ModifierKey.OPTION_RIGHT);
} else if (key.equals("anyControl")) {
builder.add(ModifierKey.CONTROL);
} else if (key.equals("control")) {
builder.add(ModifierKey.CONTROL_LEFT);
} else if (key.equals("rightControl")) {
builder.add(ModifierKey.CONTROL_RIGHT);
} else {
throw new IllegalArgumentException(key);
}
}
return ModifierKeyCombination.ofOnAndDontCareKeys(onKeys.build(), dontCareKeys.build());
}
private void parseKeyMaps(String mapSet) {
NodeList keyMapSets = keyboardElement.getElementsByTagName("keyMapSet");
Element keyMapSet = null;
for (int i = 0; i < keyMapSets.getLength(); i++) {
Element keyMapSetElement = (Element) keyMapSets.item(i);
if (keyMapSetElement.getAttribute("id").equals(mapSet)) {
keyMapSet = keyMapSetElement;
}
}
checkNotNull(keyMapSet);
NodeList keyMaps = keyMapSet.getElementsByTagName("keyMap");
for (int i = 0; i < keyMaps.getLength(); i++) {
Element keyMap = (Element) keyMaps.item(i);
String modifierIndex = keyMap.getAttribute("index");
ModifierKeyCombinationSet combinationSet = mapIndexToModifier.get(modifierIndex);
checkNotNull(combinationSet, modifierIndex);
NodeList keys = keyMap.getElementsByTagName("key");
for (int j = 0; j < keys.getLength(); j++) {
Element key = (Element) keys.item(j);
if (!key.getAttribute("code").matches("[0-9]+")) {
throw new IllegalArgumentException(key.getAttribute("code"));
}
Integer code = Integer.valueOf(key.getAttribute("code"));
if (!KEYCODE_MAP.hasIsoLayoutPosition(code)) {
continue;
}
IsoLayoutPosition position = KEYCODE_MAP.getIsoLayoutPosition(code);
CharacterMap characterMap = CharacterMap.of(position, key.getAttribute("output"));
for (ModifierKeyCombination combination : combinationSet.combinations()) {
builder.addCharacterMap(combination, characterMap);
}
}
}
}
}