601 lines
17 KiB
JavaScript
601 lines
17 KiB
JavaScript
// ____ _____ _____ ___ _ _ _____ _____ ___ ____ _ _ ____ _____
|
|
// | _ \| ____| ___|_ _| \ | | ____| | ___|_ _/ ___| | | | _ \| ____|
|
|
// | | | | _| | |_ | || \| | _| _____| |_ | | | _| | | | |_) | _|
|
|
// | |_| | |___| _| | || |\ | |__|_____| _| | | |_| | |_| | _ <| |___
|
|
// |____/|_____|_| |___|_| \_|_____| |_| |___\____|\___/|_| \_\_____|
|
|
//
|
|
// language construct for defining dance moves
|
|
// and related support functions for dealing with figures
|
|
|
|
import { formalParamIsDancers } from "./param.js"
|
|
import {
|
|
PUNCTUATION_CHARSET_STRING,
|
|
libfigureObjectCopy,
|
|
longestFirstSortFn,
|
|
parseHeyLength,
|
|
regExpEscape,
|
|
textInDialect,
|
|
throw_up
|
|
} from "./util.js"
|
|
import {
|
|
FLATTEN_FORMAT_HTML,
|
|
FLATTEN_FORMAT_SAFE_TEXT,
|
|
FLATTEN_FORMAT_UNSAFE_TEXT,
|
|
Words,
|
|
lingoLineWords,
|
|
words,
|
|
} from "./words.js";
|
|
|
|
// always freshly allocated
|
|
function newFigure(optional_progression) {
|
|
var m = { move: "stand still", parameter_values: [8] }
|
|
if (optional_progression) {
|
|
m.progression = 1
|
|
}
|
|
return m
|
|
}
|
|
|
|
export function figureBeats(f) {
|
|
var defaultBeats = 8
|
|
if (!f.move) return defaultBeats
|
|
var idx = find_parameter_index_by_name("beats", parameters(f.move))
|
|
return idx < 0 ? defaultBeats : f.parameter_values[idx]
|
|
}
|
|
|
|
function sumBeats(figures, optional_limit) {
|
|
var acc = 0
|
|
var n = Number.isInteger(optional_limit) ? optional_limit : figures.length
|
|
for (var i = 0; i < n; i++) {
|
|
acc += figureBeats(figures[i])
|
|
}
|
|
return acc
|
|
}
|
|
|
|
export const figureToHtml = (f, dialect) => {
|
|
return figureFlatten(f, dialect, FLATTEN_FORMAT_HTML)
|
|
}
|
|
|
|
function figureToUnsafeText(f, dialect) {
|
|
return figureFlatten(f, dialect, FLATTEN_FORMAT_UNSAFE_TEXT)
|
|
}
|
|
|
|
function figureToSafeText(f, dialect) {
|
|
return figureFlatten(f, dialect, FLATTEN_FORMAT_SAFE_TEXT)
|
|
}
|
|
|
|
function figureFlatten(f, dialect, flatten_format) {
|
|
var fig_def = defined_events[f.move]
|
|
if (fig_def) {
|
|
var func = fig_def.props.words || figureGenericWords
|
|
var main = func(alias(f), f.parameter_values, dialect)
|
|
var note = f.note
|
|
var pilcrow = f.progression ? "⁋" : false
|
|
if (note && note.trim()) {
|
|
var fancy_note = lingoLineWords(stringInDialect(note, dialect), dialect)
|
|
return words(main, fancy_note, pilcrow).flatten(flatten_format)
|
|
} else {
|
|
return words(main, pilcrow).flatten(flatten_format)
|
|
}
|
|
} else if (f.move) {
|
|
return "undefined figure '" + words(f.move).flatten(flatten_format) + "'!"
|
|
} else {
|
|
return "empty figure"
|
|
}
|
|
}
|
|
|
|
// Called if they don't specify a Words function in the figure definition:
|
|
function figureGenericWords(move, parameter_values, dialect) {
|
|
var ps = parameters(move)
|
|
var pwords = parameter_words(move, parameter_values, dialect)
|
|
var acc = []
|
|
var subject_index = find_parameter_index_by_name("who", ps)
|
|
var balance_index = find_parameter_index_by_name("bal", ps)
|
|
var beats_index = find_parameter_index_by_name("beats", ps)
|
|
if (subject_index >= 0) {
|
|
acc.push(pwords[subject_index])
|
|
}
|
|
if (balance_index >= 0) {
|
|
acc.push(pwords[balance_index])
|
|
}
|
|
acc.push(moveSubstitution(move, dialect))
|
|
ps.length == parameter_values.length ||
|
|
throw_up(
|
|
"parameter type mismatch. " +
|
|
ps.length +
|
|
" formals and " +
|
|
parameter_values.length +
|
|
" values"
|
|
)
|
|
for (var i = 0; i < parameter_values.length; i++) {
|
|
if (i != subject_index && i != balance_index && i != beats_index) {
|
|
acc.push(pwords[i])
|
|
}
|
|
}
|
|
return new Words(acc)
|
|
}
|
|
|
|
function find_parameter_index_by_name(name, parameters) {
|
|
var match_name_fn = function(p) {
|
|
return p.name === name
|
|
}
|
|
return parameters.findIndex(match_name_fn, parameters)
|
|
}
|
|
|
|
// ================
|
|
|
|
export function parameter_strings(move, parameter_values, dialect) {
|
|
return parameter_strings_or_words(move, parameter_values, dialect, false)
|
|
}
|
|
|
|
export function parameter_words(move, parameter_values, dialect) {
|
|
return parameter_strings_or_words(move, parameter_values, dialect, true)
|
|
}
|
|
|
|
function parameter_strings_or_words(move, parameter_values, dialect, words_ok) {
|
|
var formal_parameters = parameters(move)
|
|
var acc = []
|
|
for (var i = 0; i < parameter_values.length; i++) {
|
|
var pvi = parameter_values[i]
|
|
var term
|
|
if (pvi === undefined || pvi === null) {
|
|
term = "____"
|
|
} else if (formal_parameters[i].words && words_ok) {
|
|
// caller wants special html-enabled return type, and we support it, e.g. Custom
|
|
term = formal_parameters[i].words(pvi, move, dialect)
|
|
} else if (formal_parameters[i].string) {
|
|
term = formal_parameters[i].string(pvi, move, dialect)
|
|
} else {
|
|
term = String(pvi)
|
|
}
|
|
acc.push(parameterSubstitution(formal_parameters[i], term, dialect))
|
|
}
|
|
return acc
|
|
}
|
|
|
|
// called when we don't know if the parameter is a dancer
|
|
function parameterSubstitution(formal_parameter, actual_parameter, dialect) {
|
|
var term = actual_parameter
|
|
return (
|
|
(formalParamIsDancers(formal_parameter) && dialect.dancers[term]) ||
|
|
actual_parameter
|
|
)
|
|
}
|
|
|
|
// called when we do know the parameter is a dancer
|
|
export function dancerSubstitution(dancer_term, dialect) {
|
|
return dialect.dancers[dancer_term] || dancer_term
|
|
}
|
|
|
|
export const dancerMenuLabel = function(dancer_term, dialect) {
|
|
if (dancer_term) {
|
|
return dancerSubstitution(dancer_term, dialect)
|
|
} else {
|
|
return "unspecified"
|
|
}
|
|
}
|
|
|
|
function heyLengthSubstitution(hey_length, dialect) {
|
|
var hey_arr = parseHeyLength(hey_length)
|
|
var hey0 = hey_arr[0]
|
|
if (hey0 === "full" || hey0 === "half") {
|
|
return hey0
|
|
} else {
|
|
hey_arr[1] === 1 ||
|
|
hey_arr[1] === 2 ||
|
|
throw_up("parseHeyLength()s second value is not 1 or 2: " + hey_arr[1])
|
|
var nth_time = hey_arr[1] === 2 ? " 2nd time" : ""
|
|
return dancerSubstitution(hey0, dialect) + " meet" + nth_time
|
|
}
|
|
}
|
|
|
|
var moveSubstitutionPercentSRegexp = / *%S */g
|
|
|
|
export function moveSubstitution(move_term, dialect) {
|
|
var sub = moveSubstitutionWithEscape(move_term, dialect)
|
|
return sub.replace(moveSubstitutionPercentSRegexp, " ").trim()
|
|
}
|
|
|
|
export function moveSubstitutionWithEscape(move_term, dialect) {
|
|
return dialect.moves[move_term] || move_term
|
|
}
|
|
|
|
// The basic applicaiton is a user substitution from 'form an ocean
|
|
// wave' to 'form a short wave' and makes it possible to extract the phrases
|
|
// 'a short wave' and 'short wave'.
|
|
//
|
|
// This takes a substitution that might be 'form a blahblah' and
|
|
// returns either 'a blahblah' or 'blahblah', depending on the
|
|
// optional add_article argument.
|
|
//
|
|
// Oh hey, the word 'form' and the word 'a' are both entered by the
|
|
// user, and so are optional.
|
|
// Check the specs for lots of examples.
|
|
export function moveSubstitutionWithoutForm(
|
|
move_term,
|
|
dialect,
|
|
add_article,
|
|
adjectives
|
|
) {
|
|
if (undefined === add_article) {
|
|
add_article = false
|
|
}
|
|
if (undefined === adjectives) {
|
|
adjectives = false
|
|
}
|
|
var subst = moveSubstitution(move_term, dialect)
|
|
var match = subst.match(/(?:form )?(?:(an?) )?(.*)/i)
|
|
var root = match[2]
|
|
var adjectives_and_root = words(adjectives, root)
|
|
if (add_article) {
|
|
var article = /[aeiou]/.test(adjectives_and_root.peek()) ? "an" : "a"
|
|
return words(article, adjectives, root)
|
|
} else {
|
|
return adjectives_and_root
|
|
}
|
|
}
|
|
|
|
// === Related Moves =============
|
|
// Note that a lot of these are 'is composed of' relationships, and as such they
|
|
// might be moved to another representation later.
|
|
|
|
var _relatedMoves = {}
|
|
|
|
function defineRelatedMove1Way(from, to) {
|
|
_relatedMoves[from] = _relatedMoves[from] || []
|
|
_relatedMoves[from].push(to)
|
|
}
|
|
|
|
export const defineRelatedMove2Way = (from, to) => {
|
|
defineRelatedMove1Way(from, to)
|
|
defineRelatedMove1Way(to, from)
|
|
}
|
|
|
|
function relatedMoves(move) {
|
|
return _relatedMoves[move] || []
|
|
}
|
|
|
|
var defined_events = {}
|
|
|
|
////////////////////////////////////////////////
|
|
// defineFigure //
|
|
////////////////////////////////////////////////
|
|
|
|
export const defineFigure = (name, parameters, props) => {
|
|
var props2 = libfigureObjectCopy(props || {})
|
|
if (!props2.goodBeats) {
|
|
var beats_index = find_parameter_index_by_name("beats", parameters)
|
|
var beats_default = beats_index >= 0 && parameters[beats_index].value
|
|
if (beats_default || beats_default === 0) {
|
|
props2.goodBeats = goodBeatsEqualFn(beats_default)
|
|
}
|
|
}
|
|
defined_events[name] = { name: name, parameters: parameters, props: props2 }
|
|
}
|
|
|
|
export const defineFigureAlias = (
|
|
alias_name,
|
|
canonical_name,
|
|
parameter_defaults
|
|
) => {
|
|
"string" == typeof alias_name || throw_up("first argument isn't a string")
|
|
"string" == typeof canonical_name ||
|
|
throw_up("second argument isn't a string")
|
|
Array.isArray(parameter_defaults) ||
|
|
throw_up("third argument isn't an array aliasing " + alias_name)
|
|
var target =
|
|
defined_events[canonical_name] ||
|
|
throw_up(
|
|
"undefined figure alias '" + alias_name + "' to '" + canonical_name + "'"
|
|
)
|
|
if (target.parameters.length !== parameter_defaults.length) {
|
|
throw_up("wrong number of parameters to " + alias_name)
|
|
}
|
|
// defensively copy parameter_defaults[...]{...} into params
|
|
var params = new Array(target.parameters.length)
|
|
for (var i = 0; i < target.parameters.length; i++) {
|
|
params[i] = parameter_defaults[i] || target.parameters[i]
|
|
}
|
|
defined_events[alias_name] = {
|
|
name: canonical_name,
|
|
parameters: params,
|
|
alias_parameters: parameter_defaults,
|
|
props: target.props,
|
|
}
|
|
}
|
|
|
|
export const deAliasMove = function(move) {
|
|
return defined_events[move].name
|
|
}
|
|
|
|
export const isAlias = function(move_string) {
|
|
return defined_events[move_string].name !== move_string
|
|
}
|
|
|
|
// you can also use 'figure.move' in js - this is a late addition because we need a function -dm 04-20-2018
|
|
function move(figure) {
|
|
return figure.move
|
|
}
|
|
|
|
function alias(figure) {
|
|
var fn = moveProp(figure.move, "alias", move)
|
|
return fn(figure)
|
|
}
|
|
|
|
// does not include itself
|
|
function aliases(move) {
|
|
// loop through defined_events, returning all keys where value.name == move
|
|
var acc = []
|
|
Object.keys(defined_events).forEach(function(key) {
|
|
var value = defined_events[key]
|
|
if (value.name == move && key != move) {
|
|
acc.push(key)
|
|
}
|
|
})
|
|
return acc
|
|
}
|
|
|
|
export const aliasFilter = function(move_alias_string) {
|
|
if (move_alias_string === deAliasMove(move_alias_string)) {
|
|
throw_up(
|
|
"aliasFilter(someDeAliasedMove) would produce weirdly overly specific filters if we weren't raising this error - it's only defined for move aliases"
|
|
)
|
|
}
|
|
return aliasParameters(move_alias_string).map(function(param) {
|
|
return param ? param.value : "*"
|
|
})
|
|
}
|
|
|
|
// List all the moves known to contradb.
|
|
// See also: moveTermsAndSubstitutions
|
|
export const moves = () => {
|
|
return Object.keys(defined_events)
|
|
}
|
|
|
|
function moveTermsAndSubstitutions(dialect) {
|
|
if (!dialect) {
|
|
throw_up("must specify dialect to moveTermsAndSubstitutions")
|
|
}
|
|
var terms = Object.keys(defined_events)
|
|
var ms = terms.map(function(term) {
|
|
return { term: term, substitution: moveSubstitution(term, dialect) }
|
|
})
|
|
ms = ms.sort(function(a, b) {
|
|
var aa = a.substitution.toLowerCase()
|
|
var bb = b.substitution.toLowerCase()
|
|
if (aa < bb) {
|
|
return -1
|
|
} else if (aa > bb) {
|
|
return 1
|
|
} else {
|
|
return 0
|
|
}
|
|
})
|
|
return ms
|
|
}
|
|
|
|
export const moveTermsAndSubstitutionsForSelectMenu = dialect => {
|
|
if (!dialect) {
|
|
throw_up("must specify dialect to moveTermsAndSubstitutionsForSelectMenu")
|
|
}
|
|
var mtas = moveTermsAndSubstitutions(dialect)
|
|
var swing_index = mtas.findIndex(function(e) {
|
|
return "swing" === e.term
|
|
})
|
|
if (swing_index >= 5) {
|
|
mtas.unshift(mtas[swing_index]) // copy swing to front of the list
|
|
}
|
|
return mtas
|
|
}
|
|
|
|
function isMove(string) {
|
|
return !!defined_events[string]
|
|
}
|
|
|
|
export const parameterLabel = (move, index) => {
|
|
var fig_def = defined_events[move]
|
|
var ps = parameters(move)
|
|
return (
|
|
(fig_def &&
|
|
fig_def.props &&
|
|
fig_def.props.labels &&
|
|
fig_def.props.labels[index]) ||
|
|
(ps[index] && ps[index].name)
|
|
)
|
|
}
|
|
|
|
var issued_parameter_warning = false
|
|
|
|
// TODO: complete renaming to formalParameters
|
|
export const parameters = move => {
|
|
var fig = defined_events[move]
|
|
if (fig) {
|
|
return fig.parameters
|
|
}
|
|
if (move && !issued_parameter_warning) {
|
|
issued_parameter_warning = true
|
|
throw_up("could not find a figure definition for '" + move + "'. ")
|
|
}
|
|
return []
|
|
}
|
|
|
|
export const formalParameters = parameters
|
|
|
|
function aliasParameters(move) {
|
|
var fig = defined_events[move]
|
|
if (fig && fig.alias_parameters) {
|
|
return fig.alias_parameters
|
|
} else {
|
|
throw_up(
|
|
"call to aliasParameters('" +
|
|
move +
|
|
"') on a thing that doesn't seem to be an alias"
|
|
)
|
|
}
|
|
}
|
|
|
|
function moveProp(move_or_nil, property_name, default_value) {
|
|
if (move_or_nil) {
|
|
var fig_def = defined_events[move_or_nil]
|
|
return (fig_def && fig_def.props[property_name]) || default_value
|
|
} else {
|
|
return default_value
|
|
}
|
|
}
|
|
|
|
export const stringInDialect = (str, dialect) => {
|
|
if (textInDialect(dialect)) {
|
|
return str
|
|
} else {
|
|
// Since this is called a lot, performance might be helped by memoizing dialectRegExp(dialect)
|
|
return str.replace(dialectRegExp(dialect), function(
|
|
_whole_match,
|
|
left_whitespace,
|
|
match
|
|
) {
|
|
// conceptually this is a pretty straightforward lookup in one hash or another, and usually that's all it takes...
|
|
var substitution = dialect.moves[match] || dialect.dancers[match]
|
|
if (substitution) {
|
|
return stringInDialectHelper(
|
|
match,
|
|
substitution,
|
|
left_whitespace,
|
|
match
|
|
)
|
|
}
|
|
// ...but sometimes they've uppercased their text, so we've gotta look again in the hash (in lower case)...
|
|
var match_lower = match.toLowerCase()
|
|
substitution = dialect.moves[match_lower] || dialect.dancers[match_lower]
|
|
if (substitution) {
|
|
return stringInDialectHelper(
|
|
match_lower,
|
|
substitution,
|
|
left_whitespace,
|
|
match
|
|
)
|
|
}
|
|
// ...and sometimes the term itself is in mixed case, e.g. 'California twirl' or "Rory O'More"...
|
|
var term
|
|
for (term in dialect.moves) {
|
|
if (term.toLowerCase() === match_lower) {
|
|
return stringInDialectHelper(
|
|
term,
|
|
dialect.moves[term],
|
|
left_whitespace,
|
|
match
|
|
)
|
|
}
|
|
}
|
|
for (term in dialect.dancers) {
|
|
if (term.toLowerCase() === match_lower) {
|
|
return stringInDialectHelper(
|
|
term,
|
|
dialect.dancers[term],
|
|
left_whitespace,
|
|
match
|
|
)
|
|
}
|
|
}
|
|
/// ... I guess it's possible none of this worked, which is weird enough to explode.
|
|
throw_up("failed to look up " + match)
|
|
})
|
|
}
|
|
}
|
|
|
|
// polish the case and remove %S in the final substitution
|
|
function stringInDialectHelper(term, substitution, left_whitespace, match) {
|
|
var s
|
|
if (hasUpperCase(substitution)) {
|
|
s = substitution
|
|
} else if (moreCapitalizedThan(match, term)) {
|
|
s = capitalize(substitution)
|
|
} else {
|
|
s = substitution
|
|
}
|
|
return left_whitespace + s.replace(/%S/g, "")
|
|
}
|
|
|
|
function hasUpperCase(x) {
|
|
return x !== x.toLowerCase()
|
|
}
|
|
|
|
function moreCapitalizedThan(left, right) {
|
|
return hasUpperCase(left) && !hasUpperCase(right)
|
|
}
|
|
|
|
// Return a new string with the first lower case letter (if any) capitalized
|
|
function capitalize(s) {
|
|
// ugh, unicode is hard in JS
|
|
for (var i = 0; i < s.length; i++) {
|
|
var c = s[i]
|
|
var c_big = c.toUpperCase()
|
|
if (c_big !== c) {
|
|
var x = s.split("")
|
|
x.splice(i, 1, c_big)
|
|
return x.join("")
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
function dialectRegExp(dialect) {
|
|
var move_strings = Object.keys(dialect.moves)
|
|
var dance_strings = Object.keys(dialect.dancers)
|
|
var term_strings = move_strings.concat(dance_strings).sort(longestFirstSortFn)
|
|
var big_re_string_center = term_strings.map(regExpEscape).join("|")
|
|
var big_re_string
|
|
if (big_re_string_center) {
|
|
big_re_string =
|
|
"(\\s|" +
|
|
PUNCTUATION_CHARSET_STRING +
|
|
"|^)(" +
|
|
big_re_string_center +
|
|
")(?=\\s|" +
|
|
PUNCTUATION_CHARSET_STRING +
|
|
"|$)"
|
|
} else {
|
|
big_re_string = "^[]" // unmatchable regexp - https://stackoverflow.com/a/25315586/5158597
|
|
}
|
|
return new RegExp(big_re_string, "gi")
|
|
}
|
|
|
|
////
|
|
|
|
function goodBeatsWithContext(figures, index) {
|
|
var figure = figures[index]
|
|
if (
|
|
0 !==
|
|
sumBeats(figures, index + 1) % moveProp(figure.move, "alignBeatsEnd", 1)
|
|
) {
|
|
return false
|
|
} else {
|
|
return goodBeats(figure)
|
|
}
|
|
}
|
|
|
|
function goodBeats(figure) {
|
|
var fn = moveProp(figure.move, "goodBeats", defaultGoodBeats)
|
|
return fn(figure)
|
|
}
|
|
|
|
export const goodBeatsMinMaxFn = (min, max) => {
|
|
return function(figure) {
|
|
var beats = figureBeats(figure)
|
|
return min <= beats && beats <= max
|
|
}
|
|
}
|
|
|
|
export const goodBeatsMinFn = min => {
|
|
return function(figure) {
|
|
var beats = figureBeats(figure)
|
|
return min <= beats
|
|
}
|
|
}
|
|
|
|
export const goodBeatsEqualFn = beats => {
|
|
return function(figure) {
|
|
return beats === figureBeats(figure)
|
|
}
|
|
}
|
|
|
|
export const defaultGoodBeats = goodBeatsEqualFn(8)
|