pd-fileutils.parser
Version:
Pd file parser (see pd-fileutils)
342 lines (304 loc) • 15.2 kB
JavaScript
/*
* Copyright (c) 2012-2015 Sébastien Piquemal <sebpiq@gmail.com>
*
* BSD Simplified License.
* For information on usage and redistribution, and for a DISCLAIMER OF ALL
* WARRANTIES, see the file, "LICENSE.txt," in this distribution.
*
* See https://github.com/sebpiq/pd-fileutils for documentation
*
*/
// See http://puredata.info/docs/developer/PdFileFormat for the Pd file format reference
var _ = require('underscore')
, NODES = ['obj', 'floatatom', 'symbolatom', 'msg', 'text']
// Regular expression to split tokens in a message.
, tokensRe = / |\r\n?|\n/
, afterCommaRe = /,(?!\\)/
// Regular expressions to detect escaped special chars.
, escapedDollarVarReGlob = /\\(\$\d+)/g
, escapedCommaVarReGlob = /\\\,/g
, escapedSemicolonVarReGlob = /\\\;/g
// Regular expression for finding valid lines of Pd in a file
, linesRe = /(#((.|\r|\n)*?)[^\\\\])\r{0,1}\n{0,1};\r{0,1}(\n|$)/i
// Helper function to reverse a string
var _reverseString = function(s) { return s.split("").reverse().join("") }
// Parses argument to a string or a number.
var parseArg = exports.parseArg = function(arg) {
var parsed = pdParseFloat(arg)
if (_.isNumber(parsed) && !isNaN(parsed)) return parsed
else if (_.isString(arg)) {
var matched, arg = arg.substr(0)
// Unescape special characters
arg = arg.replace(escapedCommaVarReGlob, ',')
arg = arg.replace(escapedSemicolonVarReGlob, ';')
while (matched = escapedDollarVarReGlob.exec(arg)) {
arg = arg.replace(matched[0], matched[1])
}
return arg
} else throw new Error('couldn\'t parse arg ' + arg)
}
// Parses a float from a .pd file. Returns the parsed float or NaN.
var pdParseFloat = exports.parseFloat = function(data) {
if (_.isNumber(data) && !isNaN(data)) return data
else if (_.isString(data)) return parseFloat(data)
else return NaN
}
// Converts arguments to a javascript array
var parseArgs = exports.parseArgs = function(args) {
// if it's an int, make a single valued array
if (_.isNumber(args) && !isNaN(args)) return [args]
// if it's a string, split the atom
else {
var parts = _.isString(args) ? args.split(tokensRe) : args
, parsed = []
, arg, i, length
for (i = 0, length = parts.length; i < length; i++) {
if ((arg = parts[i]) === '') continue
else parsed.push(parseArg(arg))
}
return parsed
}
}
// Parses a Pd file, creates and returns a graph from it
exports.parse = function(txt) {
return recursParse(txt)[0]
}
var recursParse = function(txt) {
var currentTable = null // last table name to add samples to
, idCounter = -1, nextId = function() { idCounter++; return idCounter }
, patch = {nodes: [], connections: [], layout: undefined, args: []}
, line, firstLine = true
, nextLine = function() { txt = txt.slice(line.index + line[0].length) }
// use our regular expression to match instances of valid Pd lines
linesRe.lastIndex = 0 // reset lastIndex, in case the previous call threw an error
while (line = txt.match(linesRe)) {
// In order to support object width, pd vanilla adds something like ", f 10" at the end
// of the line. So we need to look for non-escaped comma, and get that part after it.
// Doing that is annoying in JS since regexps have no look-behind assertions.
// The hack is to reverse the string, and use a regexp look-forward assertion.
var lineParts = _reverseString(line[1]).split(afterCommaRe).reverse().map(_reverseString)
, lineAfterComma = lineParts[1]
, tokens = lineParts[0].split(tokensRe)
, chunkType = tokens[0]
//================ #N : frameset ================//
if (chunkType === '#N') {
var elementType = tokens[1]
if (elementType === 'canvas') {
// This is a subpatch
if (!firstLine) {
var result = recursParse(txt)
, subpatch = result[0]
, attrs = result[2]
patch.nodes.push(_.extend({
id: nextId(),
subpatch: subpatch
}, attrs))
// The remaining text is what was returned
txt = result[1]
// Else this is the first line of the patch file
} else {
patch.layout = {
x: parseInt(tokens[2], 10), y: parseInt(tokens[3], 10),
width: parseInt(tokens[4], 10), height: parseInt(tokens[5], 10),
openOnLoad: tokens[7]
}
patch.args = [tokens[6]]
nextLine()
}
} else throw new Error('invalid element type for chunk #N : ' + elementType)
//================ #X : patch elements ================//
} else if (chunkType === '#X') {
var elementType = tokens[1]
// ---- restore : ends a canvas definition ---- //
if (elementType === 'restore') {
var layout = {x: parseInt(tokens[2], 10), y: parseInt(tokens[3], 10)}
, canvasType = tokens[4]
, args = []
// add subpatch name
if (canvasType === 'pd') args.push(tokens[5])
// end the current table, pad the data with zeros
if (currentTable) {
var tableSize = currentTable.args[1]
while (currentTable.data.length < tableSize)
currentTable.data.push(0)
currentTable = null
}
// Return `subpatch`, `remaining text`, `attrs`
nextLine()
return [patch, txt, {
proto: canvasType,
args: args,
layout: layout
}]
// ---- NODES : object/control instantiation ---- //
} else if (_.contains(NODES, elementType)) {
var proto // the object name
, args // the construction args for the object
, layout = {x: parseInt(tokens[2], 10), y: parseInt(tokens[3], 10)}
, result
// 2 categories here :
// - elems whose name is `elementType`
// - elems whose name is `token[4]`
if (elementType === 'obj') {
proto = tokens[4]
args = tokens.slice(5)
} else {
proto = elementType
args = tokens.slice(4)
}
if (elementType === 'text') args = [tokens.slice(4).join(' ')]
// Handling controls' creation arguments
result = parseControls(proto, args, layout)
args = result[0]
layout = result[1]
// Handling stuff after the comma
// I have no idea what's the specification for this, so this is really reverse
// engineering on what appears in pd files.
if (lineAfterComma) {
var afterCommaTokens = lineAfterComma.split(tokensRe)
while (afterCommaTokens.length) {
var command = afterCommaTokens.shift()
if (command === 'f')
layout.width = afterCommaTokens.shift()
}
}
// Add the object to the graph
patch.nodes.push({
id: nextId(),
proto: proto,
layout: layout,
args: parseArgs(args)
})
// ---- array : start of an array definition ---- //
} else if (elementType === 'array') {
var arrayName = tokens[2]
, arraySize = parseFloat(tokens[3])
, table = {
id: nextId(),
proto: 'table',
args: [arrayName, arraySize],
data: []
}
patch.nodes.push(table)
// remind the last table for handling correctly
// the table related instructions which might follow.
currentTable = table
// ---- connect : connection between 2 nodes ---- //
} else if (elementType === 'connect') {
var sourceId = parseInt(tokens[2], 10)
, sinkId = parseInt(tokens[4], 10)
, sourceOutlet = parseInt(tokens[3], 10)
, sinkInlet = parseInt(tokens[5], 10)
patch.connections.push({
source: {id: sourceId, port: sourceOutlet},
sink: {id: sinkId, port: sinkInlet}
})
// ---- coords : visual range of framsets ---- //
} else if (elementType === 'coords') { // TODO ?
} else throw new Error('invalid element type for chunk #X : ' + elementType)
nextLine()
//================ #A : array data ================//
} else if (chunkType === '#A') {
// reads in part of an array/table of data, starting at the index specified in this line
// name of the array/table comes from the the '#X array' and '#X restore' matches above
var idx = parseFloat(tokens[1]), t, length, val
if (currentTable) {
for (t = 2, length = tokens.length; t < length; t++, idx++) {
val = parseFloat(tokens[t])
if (_.isNumber(val) && !isNaN(val)) currentTable.data[idx] = val
}
} else {
console.error('got table data outside of a table.')
}
nextLine()
} else throw new Error('invalid chunk : ' + chunkType)
firstLine = false
}
return [patch, '']
}
// This is put here just for readability of the main `parse` function
var parseControls = function(proto, args, layout) {
if (proto === 'floatatom') {
// <width> <lower_limit> <upper_limit> <label_pos> <label> <receive> <send>
layout.width = args[0] ; layout.labelPos = args[3] ; layout.label = args[4]
// <lower_limit> <upper_limit> <receive> <send>
args = [args[1], args[2], args[5], args[6]]
} else if (proto === 'symbolatom') {
// <width> <lower_limit> <upper_limit> <label_pos> <label> <receive> <send>
layout.width = args[0] ; layout.labelPos = args[3] ; layout.label = args[4]
// <lower_limit> <upper_limit> <receive> <send>
args = [args[1], args[2], args[5], args[6]]
} else if (proto === 'bng') {
// <size> <hold> <interrupt> <init> <send> <receive> <label> <x_off> <y_off> <font> <fontsize> <bg_color> <fg_color> <label_color>
layout.size = args[0] ; layout.hold = args[1] ; layout.interrupt = args[2]
layout.label = args[6] ; layout.labelX = args[7] ; layout.labelY = args[8]
layout.labelFont = args[9] ; layout.labelFontSize = args[10] ; layout.bgColor = args[11]
layout.fgColor = args[12] ; layout.labelColor = args[13]
// <init> <send> <receive>
args = [args[3], args[4], args[5]]
} else if (proto === 'tgl') {
// <size> <init> <send> <receive> <label> <x_off> <y_off> <font> <fontsize> <bg_color> <fg_color> <label_color> <init_value> <default_value>
layout.size = args[0] ; layout.label = args[4] ; layout.labelX = args[5]
layout.labelY = args[6] ; layout.labelFont = args[7] ; layout.labelFontSize = args[8]
layout.bgColor = args[9] ; layout.fgColor = args[10] ; layout.labelColor = args[11]
// <init> <send> <receive> <init_value> <default_value>
args = [args[1], args[2], args[3], args[12], args[13]]
} else if (proto === 'nbx') {
// !!! doc is inexact here, logHeight is not at the specified position, and initial value of the nbx was missing.
// <size> <height> <min> <max> <log> <init> <send> <receive> <label> <x_off> <y_off> <font> <fontsize> <bg_color> <fg_color> <label_color> <log_height>
layout.size = args[0] ; layout.height = args[1] ; layout.log = args[4]
layout.label = args[8] ; layout.labelX = args[9] ; layout.labelY = args[10]
layout.labelFont = args[11] ; layout.labelFontSize = args[12] ; layout.bgColor = args[13]
layout.fgColor = args[14] ; layout.labelColor = args[15] ; layout.logHeight = args[17]
// <min> <max> <init> <send> <receive>
args = [args[2], args[3], args[5], args[6], args[7], args[16]]
} else if (proto === 'vsl') {
// <width> <height> <bottom> <top> <log> <init> <send> <receive> <label> <x_off> <y_off> <font> <fontsize> <bg_color> <fg_color> <label_color> <default_value> <steady_on_click>
layout.width = args[0] ; layout.height = args[1] ; layout.log = args[4]
layout.label = args[8] ; layout.labelX = args[9] ; layout.labelY = args[10]
layout.labelFont = args[11] ; layout.labelFontSize = args[12] ; layout.bgColor = args[13]
layout.fgColor = args[14] ; layout.labelColor = args[15] ; layout.steadyOnClick = args[17]
// <bottom> <top> <init> <send> <receive> <default_value>
args = [args[2], args[3], args[5], args[6], args[7], args[2] + (args[3] - args[2]) * args[16] / 12700]
} else if (proto === 'hsl') {
// <width> <height> <bottom> <top> <log> <init> <send> <receive> <label> <x_off> <y_off> <font> <fontsize> <bg_color> <fg_color> <label_color> <default_value> <steady_on_click>
layout.width = args[0] ; layout.height = args[1] ; layout.log = args[4]
layout.label = args[8] ; layout.labelX = args[9] ; layout.labelY = args[10]
layout.labelFont = args[11] ; layout.labelFontSize = args[12] ; layout.bgColor = args[13]
layout.fgColor = args[14] ; layout.labelColor = args[15] ; layout.steadyOnClick = args[17]
// <bottom> <top> <init> <send> <receive> <default_value>
args = [args[2], args[3], args[5], args[6], args[7], args[2] + (args[3] - args[2]) * args[16] / 12700]
} else if (proto === 'vradio') {
// <size> <new_old> <init> <number> <send> <receive> <label> <x_off> <y_off> <font> <fontsize> <bg_color> <fg_color> <label_color> <default_value>
layout.size = args[0] ; layout.label = args[6] ; layout.labelX = args[7]
layout.labelY = args[8] ; layout.labelFont = args[9] ; layout.labelFontSize = args[10]
layout.bgColor = args[11] ; layout.fgColor = args[12] ; layout.labelColor = args[13]
// <new_old> <init> <number> <send> <receive> <default_value>
args = [args[1], args[2], args[3], args[4], args[5], args[14]]
} else if (proto === 'hradio') {
// <size> <new_old> <init> <number> <send> <receive> <label> <x_off> <y_off> <font> <fontsize> <bg_color> <fg_color> <label_color> <default_value>
layout.size = args[0] ; layout.label = args[6] ; layout.labelX = args[7]
layout.labelY = args[8] ; layout.labelFont = args[9] ; layout.labelFontSize = args[10]
layout.bgColor = args[11] ; layout.fgColor = args[12] ; layout.labelColor = args[13]
// <new_old> <init> <number> <send> <receive> <default_value>
args = [args[1], args[2], args[3], args[4], args[5], args[14]]
} else if (proto === 'vu') {
// <width> <height> <receive> <label> <x_off> <y_off> <font> <fontsize> <bg_color> <label_color> <scale> <?>
layout.width = args[0] ; layout.height = args[1] ; layout.label = args[3]
layout.labelX = args[4] ; layout.labelY = args[5] ; layout.labelFont = args[6]
layout.labelFontSize = args[7] ; layout.bgColor = args[8] ; layout.labelColor = args[9]
layout.log = args[10]
// <receive> <?>
args = [args[2], args[11]]
} else if (proto === 'cnv') {
// <size> <width> <height> <send> <receive> <label> <x_off> <y_off> <font> <font_size> <bg_color> <label_color> <?>
layout.size = args[0] ; layout.width = args[1] ; layout.height = args[2]
layout.label = args[5] ; layout.labelX = args[6] ; layout.labelY = args[7]
layout.labelFont = args[8] ; layout.labelFontSize = args[9] ; layout.bgColor = args[10]
layout.labelColor = args[11]
// <send> <receive> <?>
args = [args[3], args[4], args[12]]
}
// Other objects (including msg) all args belong to the graph model
return [args, layout]
}