Skip to content

Instantly share code, notes, and snippets.

@farskid
Last active November 15, 2019 09:33
Show Gist options
  • Save farskid/a3c35f31bd3c02a76ba257d25f02c8ec to your computer and use it in GitHub Desktop.
Save farskid/a3c35f31bd3c02a76ba257d25f02c8ec to your computer and use it in GitHub Desktop.
Speaking statecharts
const statechart2 = {
id: "zoneMachine",
initial: "preparing",
context: {
error: undefined,
zone: undefined
},
states: {
preparing: {
invoke: {
src: "detectLocationSupport",
id: "detectLocationSupport",
onDone: {
target: "ready"
},
onError: {
target: "error"
}
}
},
ready: {
initial: "zone_idle",
states: {
zone_idle: {
on: {
DETECT_ZONE: {
target: "zone_pending"
}
}
},
zone_pending: {
invoke: {
id: "detectZone",
src: "detectZone",
onDone: {
target: "zone_available",
actions: ["setZone"]
},
onError: {
target: "zone_error",
actions: ["setError"]
}
}
},
zone_available: {
initial: "not_watching",
on: {
REFRESH: {
target: "zone_pending"
},
LOCATION_CHANGED: {
target: ".history",
actions: ["setZone", "sendNotif"]
}
},
states: {
history: {
type: "history",
history: "deep"
},
not_watching: {
on: {
WATCH_LOCATION: {
target: "watching"
}
}
},
watching: {
invoke: {
id: "watchLocation",
src: "watchLocationService"
}
}
}
},
zone_error: {
on: {
DETECT_ZONE: {
target: "zone_pending"
}
}
}
}
},
error: {
type: "final"
}
}
}
const statechart1 = {
id: "dragging-box",
initial: "released",
context: {
mousePositionInBox: { x: 0, y: 0 },
boxPositionInContainer: { x: 0, y: 0 }
},
states: {
released: {
on: {
GRAB: {
target: "grabbed"
}
}
},
grabbed: {
entry: [
"saveMousePositionInBox",
"saveBoxPositionInContainer",
"prepareBoxStyles",
"moveBox"
],
on: {
MOVE: "dragging"
}
},
dragging: {
entry: [
"saveBoxPositionInContainer",
"moveBox"
],
on: {
MOVE: "dragging",
RELEASE: "released"
}
}
}
}
// language exceptions
var exceptions = {
"are": "were",
"eat": "ate",
"go": "went",
"have": "had",
"inherit": "inherited",
"is": "was",
"run": "ran",
"sit": "sat",
"visit": "visited"
}
// grammatically predictable rules
function getPastTense(verb) {
if (exceptions[verb]) {
return exceptions[verb];
}
if ((/e$/i).test(verb)) {
return verb + 'd';
}
if ((/[aeiou]c/i).test(verb)) {
return verb + 'ked';
}
// for american english only
if ((/el$/i).test(verb)) {
return verb + 'ed';
}
if ((/[aeio][aeiou][dlmnprst]$/).test(verb)) {
return verb + 'ed';
}
if ((/[aeiou][bdglmnprst]$/i).test(verb)) {
return verb.replace(/(.+[aeiou])([bdglmnprst])/, '$1$2$2ed');
}
return verb + 'ed';
}
"use strict";
const rules = [
// [long vowel][consonant]
{
test: /([uao]m[pb]|[oa]wn|ey|elp|[ei]gn|ilm|o[uo]r|[oa]ugh|igh|ki|ff|oubt|ount|awl|o[alo]d|[iu]rl|upt|[oa]y|ight|oid|empt|act|aud|e[ea]d|ound|[aeiou][srcln]t|ept|dd|[eia]n[dk]|[ioa][xk]|[oa]rm|[ue]rn|[ao]ng|uin|eam|ai[mr]|[oea]w|[eaoui][rscl]k|[oa]r[nd]|ear|er|[^aieouyfm]it|[aeiouy]ir|[^aieouyfm]et|ll|en|vil|om|[^rno].mit|rup|bat|val|.[^skxwb][rvmchslwngb]el)$/,
transform: function(vb, to) {
if (to === "VBG")
return vb + 'ing';
return vb;
}
},
// [consonant][y]
{
test: /[^aeiou]y$/,
transform: function(vb, to) {
var base = vb.substr(0, vb.length - 1);
if (to === "VBG")
return vb + 'ing';
return vb;
}
},
// [consonant][e]
{
test: /[^aeiouy]e$/,
transform: function(vb, to) {
var base = vb.substr(0, vb.length - 1);
if (to === "VBG")
return base + 'ing';
return vb;
}
},
// [short vowel][consonant]
{
test: /([^dtaeiuo][aeiuo][ptlgnm]|ir|cur|ug|[hj]ar|[^aouiey]ep|[^aeiuo][oua][db])$/,
transform: function(vb, to) {
if (to === "VBG")
return vb + vb[vb.length - 1] + 'ing';
return vb;
}
},
// [sibilant]
{
test: /([ieao]ss|[aeiouy]zz|[aeiouy]ch|nch|rch|[aeiouy]sh|[iae]tch|ax)$/,
transform: function(vb, to) {
if (to === "VBG")
return vb + 'ing';
return vb;
}
},
// [e][e]
{
test: /(ee)$/,
transform: function(vb, to) {
if (to === "VBG")
return vb + 'ing';
return vb;
}
},
// [i][e]
{
test: /(ie)$/,
transform: function(vb, to) {
if (to === "VBG")
return vb.substr(0, vb.length - 2) + 'ying';
return vb;
}
},
// [u][e]
{
test: /(ue)$/,
transform: function(vb, to) {
if (to === "VBG")
return vb.substr(0, vb.length - 1) + 'ing';
return vb;
}
},
// default (regular)
{
test: /./,
transform: function(vb, to) {
if (to === "VBG")
return vb + 'ing';
return vb;
}
}
];
function solve(input, to) {
for (let i = 0; i < rules.length; i++) {
let rule = rules[i];
if (rule.test.test(input))
return rule.transform(input, to);
}
return "";
}
function speak(txt) {
const synth = window.speechSynthesis;
const utterThis = new SpeechSynthesisUtterance(txt);
utterThis.pitch = 1;
utterThis.rate = 1;
/* utterThis.voice = "Google US English" */
;
synth.speak(utterThis);
}
function getStateEventKeys(sNode) {
if (!sNode.on) return [];
return Object.keys(sNode.on);
}
function getStateEvents(sNode) {
return sNode.on;
}
function getEventName(evt) {
return typeof evt === "string" ? evt : evt.target;
}
function getStateEntryActions(sNode) {
const {
entry
} = sNode;
const entryType = typeof entry;
if (entryType === "string") {
return [entry]
} else if (Array.isArray(entry)) {
return entry
} else {
return []
}
}
function getRecurringStateEvents(sKey, sNode) {
const events = getStateEvents(sNode);
let found = [];
for (const evt in events) {
if (sKey === getEventName(events[evt])) {
found.push(evt)
}
}
return found;
}
function parse(sc) {
const steps = [];
const id = sc.id;
const initialState = sc.initial;
const states = sc.states;
const stateKeys = Object.keys(states).sort((a) => a === initialState ? -1 : 1)
steps.push(`${id} is ${initialState} at first.`, "\n");
stateKeys.forEach(sKey => {
const sNode = states[sKey];
const eventKeys = getStateEventKeys(sNode);
const events = getStateEvents(sNode);
const entryActions = getStateEntryActions(sNode);
const recurringEvents = getRecurringStateEvents(sKey, sNode);
const eventKeysBesidesRecurrings = eventKeys.filter(evt => !recurringEvents.includes(evt));
// announce entry actions
if (sNode.entry !== undefined) {
steps.push(`as soon as it's ${sKey}, we ${entryActions.slice(0,-1).join(", ")} and ${entryActions.slice().pop()}.`)
}
// announce event names
if (eventKeysBesidesRecurrings.length > 1) {
steps.push(`when it's ${sKey}, it can be ${eventKeys.map(s => s.toLowerCase()).map(getPastTense).join(" and ")}.`);
} else if (eventKeys.length === 1) {
steps.push(`when it's ${sKey}, it can only be ${eventKeys.map(s => s.toLowerCase()).map(getPastTense)}.`);
}
// Recurring events
recurringEvents.forEach(evt => {
steps.push(`as long as it's ${solve(evt.toLowerCase(), 'VBG')}, we keep it in ${sKey}`)
if (sNode.entry !== undefined) {
steps.push(`which means continuously ${entryActions.slice(0,-1).join(", ")} and ${entryActions.slice().pop()}.`)
}
});
// Nested states
if (states[sKey].states) {
console.log(`encountered nested states for state: ${sKey}`)
steps.push(parse(states[sKey]))
}
// announce transitions
//eventKeys.forEach((evt, _, self) => {
//steps.push(`on event ${evt}, it will transition to state: ${getEventName(events[evt])}`)
//});
steps.push("\n")
})
return steps;
}
console.log(parse(statechart1).join("\n"))
/* parse(statechart).forEach(speak) */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment