contra-renderer/external/libfigure/define-figure.js

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)