diff --git a/examples/advanced/custom_rules.browse b/examples/advanced/custom_rules.browse index 789ca12..70dcbc6 100644 --- a/examples/advanced/custom_rules.browse +++ b/examples/advanced/custom_rules.browse @@ -24,7 +24,7 @@ rule while { # return is unncescessary since the result from the last rule in a RuleSet # is implicitly returned while $cond $body - } else { return null } + } else { return nil } } set i 0 diff --git a/packages/core/lib/std.js b/packages/core/lib/std.js index df462f2..eb1f0fe 100644 --- a/packages/core/lib/std.js +++ b/packages/core/lib/std.js @@ -680,6 +680,7 @@ const defRule = (evalRuleSet) => (scope) => (_opts) => (name, body) => { /** * @rule { bind } * @scope { rule } + * @parent { std } * @desc { * **Only used within a {@link rule\} body** * 'bind' lets the rule accept arguments. Strings passed to bind are used to @@ -725,6 +726,7 @@ const defRule = (evalRuleSet) => (scope) => (_opts) => (name, body) => { /** * @rule { return } * @scope { rule } + * @parent { std } * @desc { * **Only used within a {@link rule\} body** * 'return' is often used to make the return value for a rule explicit. It's often diff --git a/packages/core/stdlib/datetime/main.browse b/packages/core/stdlib/datetime/main.browse index 590164e..c06253e 100644 --- a/packages/core/stdlib/datetime/main.browse +++ b/packages/core/stdlib/datetime/main.browse @@ -1,14 +1,29 @@ +#* @name { DateTime } +# @scope { DateTime utilities } import math import "./native.js" -set SECOND 1000 -set MINUTE $SECOND * 60 -set HOUR $MINUTE * 60 -set DAY $HOUR * 24 -set WEEK $DAY * 7 +#* Number of seconds in a minute +set SECONDS_PER_MINUTE 60 +#* Number of minutes in an hour +set MINUTES_PER_HOUR 60 +#* Number of hours in a day +set HOURS_PER_DAY 24 +#* Number of days in a week set DAYS_PER_WEEK 7 +#* Number of milliseconds in a second +set SECOND 1000 +#* Number of milliseconds in a minute +set MINUTE $SECOND * $SECONDS_PER_MINUTE +#* Number of milliseconds in an hour +set HOUR $MINUTE * $MINUTES_PER_HOUR +#* Number of milliseconds in a day +set DAY $HOUR * $HOURS_PER_DAY +#* Number of milliseconds in a week +set WEEK $DAY * $DAYS_PER_WEEK + # A date looks like this: # dict { _ __type__ 'Date' ; _ value number } # `value` is in epoch ms @@ -21,50 +36,78 @@ rule Date { bind a b c d e f g; return (native:Date $a $b $c $d $e $f $g) } #################################### ### The Native JS Date functions ### #################################### +#* Returns the day of the month (1–31) for the specified date according to local time. rule getDate { bind date; return (native:date_fn $date getDate) } +#* Returns the day of the week (0–6) for the specified date according to local time. rule getDay { bind date; return (native:date_fn $date getDay) } +#* Returns the day of the week (0–6) for the specified date according to local time. rule getFullYear { bind date; return (native:date_fn $date getFullYear) } +#* Returns the hour (0–23) in the specified date according to local time. rule getHours { bind date; return (native:date_fn $date getHours) } +#* Returns the milliseconds (0–999) in the specified date according to local time. rule getMilliseconds { bind date; return (native:date_fn $date getMilliseconds) } +#* Returns the minutes (0–59) in the specified date according to local time. rule getMinutes { bind date; return (native:date_fn $date getMinutes) } +#* Returns the month (0–11) in the specified date according to local time. rule getMonth { bind date; return (native:date_fn $date getMonth) } +#* Returns the seconds (0–59) in the specified date according to local time. rule getSeconds { bind date; return (native:date_fn $date getSeconds) } +#* Returns the numeric value of the specified date as the number of milliseconds since January 1, 1970, 00:00:00 UTC. (Negative values are returned for prior times.) rule getTime { bind date; return (native:date_fn $date getTime) } +#* Returns the time-zone offset in minutes for the current locale. rule getTimezoneOffset { bind date; return (native:date_fn $date getTimezoneOffset) } +#* Returns the day (date) of the month (1–31) in the specified date according to universal time. rule getUTCDate { bind date; return (native:date_fn $date getUTCDate) } +#* Returns the day of the week (0–6) in the specified date according to universal time. rule getUTCDay { bind date; return (native:date_fn $date getUTCDay) } +#* Returns the year (4 digits for 4-digit years) in the specified date according to universal time. rule getUTCFullYear { bind date; return (native:date_fn $date getUTCFullYear) } +#* Returns the hours (0–23) in the specified date according to universal time. +rule getUTCHours { bind date; return (native:date_fn $date getUTCHours) } +#* Returns the milliseconds (0–999) in the specified date according to universal time. rule getUTCMilliseconds { bind date; return (native:date_fn $date getUTCMilliseconds) } +#* Returns the minutes (0–59) in the specified date according to universal time. rule getUTCMinutes { bind date; return (native:date_fn $date getUTCMinutes) } +#* Returns the month (0–11) in the specified date according to universal time. rule getUTCMonth { bind date; return (native:date_fn $date getUTCMonth) } +#* Returns the seconds (0–59) in the specified date according to universal time. rule getUTCSeconds { bind date; return (native:date_fn $date getUTCSeconds) } +#* Returns the "date" portion of the Date as a human-readable string like 'Thu Apr 12 2018'. rule toDateString { bind date; return (native:date_fn $date toDateString) } +#* Converts a date to a string following the ISO 8601 Extended Format. rule toISOString { bind date; return (native:date_fn $date toISOString) } +#* Returns a string with a locality sensitive representation of the date portion of this date based on system settings. rule toLocaleDateString { bind date tz; return (native:date_fn $date toLocaleDateString $tz) } +#* Returns a string with a locality-sensitive representation of this date rule toLocaleString { bind date tz; return (native:date_fn $date toLocaleString $tz) } +#* Returns a string with a locality-sensitive representation of the time portion of this date, based on system settings. rule toLocaleTimeString { bind date tz; return (native:date_fn $date toLocaleTimeString $tz) } +#* Returns a string representing the specified Date object rule toString { bind date; return (native:date_fn $date toString) } +#* Returns the "time" portion of the Date as a human-readable string. rule toTimeString { bind date; return (native:date_fn $date toTimeString) } +#* Converts a date to a string using the UTC timezone. rule toUTCString { bind date; return (native:date_fn $date toUTCString) } +#* Returns the primitive value of a Date object rule valueOf { bind date; return (native:date_fn $date valueOf) } ##################################### ### Porting the golib/deno stdlib ### ##################################### -# Get number of the day in the year +#* Get number of the day in the year # @return Number of the day in year rule dayOfYear { bind date diff --git a/packages/core/stdlib/math/main.browse b/packages/core/stdlib/math/main.browse index a027536..b60cc54 100644 --- a/packages/core/stdlib/math/main.browse +++ b/packages/core/stdlib/math/main.browse @@ -1,12 +1,29 @@ +#* @scope { Standard Math functions } +# @name { Math } import "./native.js" +#* Euler's constant and the base of natural logarithms; approximately 2.718. set E 2.718281828459045 + +#* Natural logarithm of 2; approximately 0.693. set LN10 2.302585092994046 + +#* Natural logarithm of 10; approximately 2.303. set LN2 0.6931471805599453 + +#* Base-2 logarithm of E; approximately 1.443. set LOG10E 0.4342944819032518 + +#* Base-10 logarithm of E; approximately 0.434. set LOG2E 1.4426950408889634 + +#* Ratio of the a circle's circumference to its diameter; approximately 3.14159. set PI 3.141592653589793 + +#* Square root of ½ (or equivalently, 1/√2); approximately 0.707. set SQRT1_2 0.7071067811865476 + +#* Square root of 2; approximately 1.414. set SQRT2 1.4142135623730951 # built in number functions @@ -22,41 +39,76 @@ rule toPrecision { } # Math.* fns +#* Returns the absolute value of x. rule abs { bind x; return (native:fn abs $x) } +#* Returns the arccosine of x. rule acos { bind x; return (native:fn acos $x) } +#* Returns the hyperbolic arccosine of x. rule acosh { bind x; return (native:fn acosh $x) } +#* Returns the arcsine of x. rule asin { bind x; return (native:fn asin $x) } +#* Returns the hyperbolic arcsine of a number. rule asinh { bind x; return (native:fn asinh $x) } +#* Returns the arctangent of x. rule atan { bind x; return (native:fn atan $x) } +#* Returns the hyperbolic arctangent of x. rule atanh { bind x; return (native:fn atanh $x) } +#* Returns the arctangent of the quotient of its arguments. rule atan2 { bind y x; return (native:fn atan2 $y $x) } -rule cbrt { bind x; return (native:fn cbrt $x) } +#* Returns the cube root of x. +rule cbrt { bind x; return (native:fn cbrt $x) }a +#* Returns the smallest integer greater than or equal to x. rule ceil { bind x; return (native:fn ceil $x) } +#* Returns the number of leading zeroes of the 32-bit integer x. rule clz32 { bind x; return (native:fn clz32 $x) } +#* Returns the cosine of x. rule cos { bind x; return (native:fn cos $x) } +#* Returns the hyperbolic cosine of x. rule cosh { bind x; return (native:fn cosh $x) } +#* Returns E^x, where x is the argument, and E is Euler's constant (2.718…, the base of the natural logarithm). rule exp { bind x; return (native:fn exp $x) } +#* Returns subtracting 1 from exp(x). rule expm1 { bind x; return (native:fn expm1 $x) } +#* Returns the largest integer less than or equal to x. rule floor { bind x; return (native:fn floor $x) } +#* Returns the nearest single precision float representation of x. rule fround { bind x; return (native:fn fround $x) } +#* Returns the square root of the sum of squares of both arguments. # TODO: support more than 2 arguments, like in the JS native version rule hypot { bind x y; return (native:fn hypot $x $y) } +#* Returns the result of the 32-bit integer multiplication of x and y. rule imul { bind x y; return (native:fn imul $x $y) } +#* Returns the natural logarithm (㏒e; also, ㏑) of x. rule log { bind x; return (native:fn log $x) } +#* Returns the natural logarithm (㏒e; also ㏑) of 1 + x for the number x. rule log1p { bind x; return (native:fn log1p $x) } +#* Returns the base-10 logarithm of x. rule log10 { bind x; return (native:fn log10 $x) } +#* Returns the base-2 logarithm of x. rule log2 { bind x; return (native:fn log2 $x) } # TODO: support more than 2 arguments, like in the JS native version +#* Returns the largest of x and y numbers. rule max { bind x y; return (native:fn max $x $y) } # TODO: support more than 2 arguments, like in the JS native version +#* Returns the smallest of x and y. rule min { bind x y; return (native:fn min $x $y) } +#* Returns base x to the exponent power y (that is, xy). rule pow { bind x y; return (native:fn pow $x $y) } +#* Returns a pseudo-random number between 0 and 1. rule random { return (native:fn random) } +#* Returns the value of the number x rounded to the nearest integer. rule round { bind x; return (native:fn round $x) } +#* Returns the sign of the x, indicating whether x is positive, negative, or zero. rule sign { bind x; return (native:fn sign $x) } +#* Returns the sine of x. rule sin { bind x; return (native:fn sin $x) } +#* Returns the hyperbolic sine of x. rule sinh { bind x; return (native:fn sinh $x) } +#* Returns the positive square root of x. rule sqrt { bind x; return (native:fn sqrt $x) } +#* Returns the tangent of x. rule tan { bind x; return (native:fn tan $x) } +#* Returns the hyperbolic tangent of x. rule tanh { bind x; return (native:fn tanh $x) } +#* Returns the integer portion of x, removing any fractional digits. rule trunc { bind x; return (native:fn trunc $x) } diff --git a/packages/docs/index.js b/packages/docs/index.js index 7d6eb39..d736f37 100644 --- a/packages/docs/index.js +++ b/packages/docs/index.js @@ -44,6 +44,7 @@ const main = async () => { ), ]; } else if (ext === ".browse") { + console.log(`Parsing ${directory}/${file}`); const stem = path.basename(file, ".browse"); return [ stem, @@ -65,7 +66,9 @@ const main = async () => { const outputs = await Promise.all( pages.map(([stem, doc]) => - markdownPlugin(doc, path.join(outPath, stem + ".md")) + Object.keys(doc).map((scope) => + markdownPlugin(doc, path.join(outPath, scope + ".md")) + ) ) ); diff --git a/packages/docs/out/std.md b/packages/docs/out/std.md new file mode 100644 index 0000000..61e223f --- /dev/null +++ b/packages/docs/out/std.md @@ -0,0 +1,363 @@ +> This was generated using BrowseDoc which is still very much a work in progress + +# Table of Contents + +- [Scope: std](#scope-std) + - [`help`](#help) + - [`scope`](#scope) + - [`id value`](#id-value) + - [`get key`](#get-key) + - [`arr_get index array`](#arr_get-index-array) + - [`dict_get key dict`](#dict_get-key-dict) + - [`set key value`](#set-key-value) + - [`arr_set index value array`](#arr_set-index-value-array) + - [`dict_set key value dict`](#dict_set-key-value-dict) + - [`unset key`](#unset-key) + - [`dict_unset key dict`](#dict_unset-key-dict) + - [`update key value`](#update-key-value) + - [`push value dest`](#push-value-dest) + - [`pop dest`](#pop-dest) + - [`rule name body`](#rule-name-body) + - [`sleep ms`](#sleep-ms) + - [`print`](#print) + - [`if condition then thenRuleSet else elseRuleSet`](#if-condition-then-thenRuleSet-else-elseRuleSet) + - [`for iterator body`](#for-iterator-body) + - [`eval ruleset inject`](#eval-ruleset-inject) + - [`arr ruleset`](#arr-ruleset) + - [`dict ruleset`](#dict-ruleset) + - [`import`](#import) + - [`string value`](#string-value) + - [`len value`](#len-value) +- [Scope: rule](#scope-rule) + - [`bind`](#bind) + - [`return value`](#return-value) + +## Scope `std` + +This scope is available to every program and consists of all the core rules to write useful browse programs + +## Rules + +### `help` + +Run `help` in a repl, or add it to your code during debugging, to learn about all the rules you can use in a scope + +### `scope` + +Internal: this dumps the current JS scope to stdout for debugging + +### `id value` + +- `value` \<**T**\> Any value +- Returns: \<**T**\> The value passed in, unchanged + +Returns whatever value is passed in. This is the _identity_ rule + +### `get key` + +- `key` \<**string**\> An identifer +- Returns: \<**any**\> The value of `key` + +Resolves to the value of the variable `key` + +> The shorthand for this rule is `$`. So, `$someVar` is the +> same as `(get someVar)`. The shorthand syntax is the preferred way to +> read a value. + +### `arr_get index array` + +- `index` \<**number**\> A valid 0-indexed position in the `array` + +- `array` \<**arr\**\> The array to lookup +- Returns: \<**T**\> The element at `index` in the `array` + +Get the element at `index` in the `array` + +### `dict_get key dict` + +- `key` \<**K**\> A valid key in the dictionary + +- `dict` \<**dict\**\> The dictionary to lookup +- Returns: \<**V**\> The value of `key` in the `dict` dictionary + +Get the value of `key` in the `dict` dictionary + +### `set key value` + +- `key` \<**string**\> An identifer (a.k.a variable name) + +- `value` \<**T**\> The value to set the variable to +- Returns: \<**T**\> value + +sets to the value of the variable `key` to `value` + +> 'set' always creates/updates the variable in the immediate/local scope. +> If a variable with the same name exists in a higher scope, it will be +> 'shadowed', not updated. To update a variable instead of creating a +> new one, use the [update](#update) rule. + +### `arr_set index value array` + +- `index` \<**number**\> A valid 0-indexed position in the `array` + +- `value` \<**T**\> The value to set in the array + +- `array` \<**arr\**\> The array to write to +- Returns: \<**T**\> The value + +Set the element at `index` in the `array` to `value` + +> To increase the size of the array, see [push](#push) or use the `array` library + +### `dict_set key value dict` + +- `key` \<**K**\> The key in the dictionary to set + +- `value` \<**V**\> The value to set `key` to in the dictionary + +- `dict` \<**dict\**\> The dictionary to write to +- Returns: \<**V**\> The value + +Set the value of `key` in the `dict` dictionary + +### `unset key` + +- `key` \<**string**\> An identifer +- Returns: \<**any**\> The value stored in the variable key + +Unset the variable 'key' + +### `dict_unset key dict` + +- `key` \<**K**\> A valid key in dict + +- `dict` \<**dict\**\> The dictionary to update +- Returns: \<**V**\> The value from the deleted pair + +Delete the key-value record matching `key` from the dictionary `dict` + +### `update key value` + +- `key` \<**string**\> An identifer (a.k.a variable name) + +- `value` \<**V**\> The value to set the variable to +- Returns: \<**V**\> value + +Updates the variable 'key' to the value 'value' + +> 'update' updates the value for the variable `key` in the closest ancestor scope. +> If a variable with the name `key` already exists in the current scope, then +> `update` throws an error. You should use [set](#set) instead for such cases. + +### `push value dest` + +- `value` \<**T**\> The value to push + +- `dest` \<**arr\**\> The array to push to +- Returns: \<**number**\> The number of elements in the array after pushing to it + +Push an element to the back of an array + +### `pop dest` + +- `dest` \<**arr\**\> The array to remove an element from +- Returns: \<**T**\> The value of the element removed + +Remove the element at the back of the array and return it + +### `rule name body` + +- `name` \<**string**\> An identifer to name the rule + +- `body` \<**RuleSet**\> The behavior that should be executed when rule is called with arguments +- Returns: \<**Rule**\> TODO: This value cannot be used by browse and is only understood by the runtime. Provide a better value + +Define a new rule 'name'. The 'body' has access to two additional rules, [bind](#bind) and [return](#return) used to take arguments and return a value + +### `sleep ms` + +- `ms` \<**number**\> The number of milliseconds to sleep for +- Returns: \<**number**\> ms + +Sleep for 'ms' milliseconds + +> This is a blocking rule + +### `print` + +- Returns: \<**any**\> The value of the last argument passed to print + +Print values to stdout + +``` +# Hello World +print Hello World + +# Since 'print' evaluates to the last argument passed in, it makes +# it easy to compose `print` when debuggin complicated expressions +rule fact { + bind x + if $x <= 1 then { return $x } else { + return (print $x + '! =' $x * (fact $x - 1)) + } +} +fact 4 + +# output = +# 2! = 2 +# 3! = 6 +# 4! = 24 +``` + +### `if condition then thenRuleSet else elseRuleSet` + +- `condition` \<**any**\> The condition to test + +- `then` \<**"then"**\> The string "then" + +- `thenRuleSet` \<**RuleSet**\> The ruleset that will be executed if condition evaluates to true + ? +- `else` \<**"else"**\> The string "else" + ? +- `elseRuleSet` \<**RuleSet**\> The ruleset that will be executed if condition evaluates to false +- Returns: \<**any**\> The result of the RuleSet that was evaluated code. `nil` is no `else` claus is provided + +If 'condition' is truthy, evaluate the 'then' RuleSet, else evaluate the 'else' rule set + +> If `else` and `elseRuleSet` are not provided, then nothing is evaluated if the `condition` +> is falsy. The entire `if` rule will evaluate to `nil` in this case + +``` +if ($grade > 60) then { print pass +``` + +### `for iterator body` + +- `iterator` \<**RuleSet**\> The iteration criteria + +- `body` \<**RuleSet**\> The body of the loop +- Returns: \<**nil**\> nil (TODO: Should return the value of the last evaluated statement, or the number of iterations?) + +Execute the `body` while the `test` expressions in the `interator` do not fail + +> The contents of the iterator is split into multiple parts: +> +> - The very first rule is evaluated once, at the beginning, to setup the loop. +> Usually used to set a iteration variable +> - The remaining rules, except the last rule, are evaulated at the start of each +> rule. A `test` rule is available here that causes the loop to end if the first +> argument passed to `test` is falsy +> - The last rule is run at the end of each loop, i.e. affter the `body` is evaluated, +> but before the `test` rules (previous point) are evaluated again. Usually use to +> increment the iteration variable defined in point 1 + +``` +for { set i 2; test $i < 5; set i $i + 1 } { print loop $i } +``` + +### `eval ruleset inject` + +- `ruleset` \<**RuleSet**\> The RuleSet to evaluate + ? +- `inject` \<**RuleSet**\> A RuleSet that is evaluated in the scope before the ruleset is evaluated +- Returns: \<**any**\> The result of evaluating the ruleset + +Evaluate a RuleSet. Optionally, inject variables and additional rules into the evaluation context/scope + +> inject is used to add additional variables and rules that can be used by the Ruleset +> This is the "explicit" form of scope injection that's used to make a pleasant experience +> for someone using a given library. See `examples/advanced/custom_rules.browse` in the browse +> repo to see some good examples for this + +``` +# See https://github.com/windsorio/browse/blob/master/examples/advanced/custom_rules.browse +``` + +### `arr ruleset` + +- `ruleset` \<**RuleSet**\> The RuleSet used to instantiate the array +- Returns: \<**arr\**\> The array + +Create an Array from a RuleSet + +> `arr` creates a new array, and then evaluates the RuleSet +> A rule called `el` is available inside this RuleSet. It takes one argument +> Each `el` call adds that element to the array before returning the final +> array. +> +> `e` and `_` are aliases for `el` + +``` +set a1 (arr { _ 1; _ 2; _ 3 }) + +# nested arrays +set a2 (arr { + _ (arr { + _ 1 + }) +}) +``` + +### `dict ruleset` + +- `ruleset` \<**RuleSet**\> The RuleSet used to instantiate the dictionary +- Returns: \<**dict\**\> The dictionary + +Create a Dictionary from a RuleSet + +> `dict` creates a new dictionary, and then evaluates the RuleSet +> A rule called `record` is available inside this RuleSet. It takes two arguments, +> a `key` and `value`. Each `record` call adds a new record to the dictionary +> mapping the `key` to the `value`. The final dictionary is `returned`. +> +> `r` and `_` are aliases for `record` + +``` +set o1 (dict { _ k1 v1; _ k2 v2 }) + +# nested dictionaries +set o2 (dict { + _ k1 (dict { + _ k2 v2 + }) +}) +``` + +### `import` + +Import a module. Read the [Browse Modules](#) guide for more info (TODO) + +### `string value` + +- `value` \<**any**\> Any value + +Serialize any value as a string + +### `len value` + +- `value` \<**string | array\**\> A string or array + +Get the length of the string or number of elements in an array + +## Scope `rule` + +## Rules + +### `bind` + +- Returns: \<**any**\> nil + +'bind' lets the rule accept arguments. Strings passed to bind are used to assign variables that track the incoming values + +``` +# take 2 arguments and return the sum rule add { bind x y; return $x + $y } # accept options rule add2 { bind(print) x y set z $x + $y if $print then { print $z } else { return $z } } +``` + +### `return value` + +- `value` \<**T**\> The value to return +- Returns: \<**T**\> The value passed in, unchanged + +'return' is often used to make the return value for a rule explicit. It's often unnecessary however since every rule uses the last evaluated value in its body as the return value anyway. + +> The return rule doesn't work like `return` in other languages. `return` is just an alias for [id](#id) since the last value in a RuleSet is the implicit return value of the RuleSet. For example `rule f { return foo return bar }` In browse, this is valid and the return value is "bar". `return foo` is the same as `id foo` Which basically does nothing (a.k.a it's a no-op). and the last rule in the body evaluates to "bar" diff --git a/packages/docs/parsers/browse.js b/packages/docs/parsers/browse.js index 0468bb3..d76cc4f 100644 --- a/packages/docs/parsers/browse.js +++ b/packages/docs/parsers/browse.js @@ -1,4 +1,41 @@ const parser = require("@browselang/parser"); +const util = require("util"); +const { + pullTags, + parseRtn, + parseParams, + processVar, + processRule, +} = require("./common"); + +const show = (obj) => + console.log(util.inspect(obj, false, null, true /* enable colors */)); + +const getChildren = (type) => + ({ + Program: ["rules"], + Rule: ["fn", "args"], + RuleSet: ["rules"], + Paren: ["expr"], + UnaryExpr: ["expr"], + BinExpr: ["left", "right"], + RuleExpr: ["expr"], + InitRule: ["module", "name"], + Word: [], + Literal: [], + Ident: [], + }[type]); + +const cleanComment = (comment) => + comment.startsWith("*") + ? comment + .slice(1) + .split("\n") + .map((line) => line.split("#")[1] || line.split("#")[0]) + .filter(Boolean) + .join("") + .trim() + : null; const parseComments = (ast) => { const commentBlocks = ast.comments.reduce((p, c) => { @@ -15,14 +52,227 @@ const parseComments = (ast) => { return p; }, []); - console.log(commentBlocks); + return commentBlocks; +}; + +const dfsTraverse = (node, fn) => { + fn(node); + const children = getChildren(node.type); + if (children) { + children.forEach((child) => { + if (node[child]) { + if (Array.isArray(node[child])) { + node[child].map((child) => dfsTraverse(child, fn)); + } else { + dfsTraverse(node[child], fn); + } + } else if (node[child] === undefined) { + //TODO: module should not be undefined. Null or empty object is better + if (child !== "module") { + show(node); + throw new Error( + `Node did not have the ${child} child indicated by 'getChildren'` + ); + } + } + }); + } else if (child === undefined) { + show(node); + throw new Error(`Unknown Node type for getChildren ${node.type}`); + } +}; + +const bfsTraverse = (node, fn) => { + const queue = []; + queue.push(node); + while (queue.length) { + const curr = queue.shift(); + fn(curr); + const children = getChildren(curr.type); + + if (children) { + children.forEach((child) => { + if (curr[child]) { + if (Array.isArray(curr[child])) { + queue.push(...curr[child]); + } else { + queue.push(curr[child]); + } + } else if (curr[child] === undefined) { + //TODO: module should not be undefined. Null or empty object is better + if (child !== "module") { + show(curr); + throw new Error( + `Node did not have the child ${child}indicated by 'getChildren'` + ); + } + } + }); + } else if (children === undefined) { + show(curr); + throw new Error(`Unknown Node type for getChildren ${curr.type}`); + } + } +}; + +const assignLeadingComment = (ast, comments) => { + /* + * We're going to walk through the AST and look for leading from the list + */ + + const sortedTree = []; + + let sortedTreeIdx = 0; + + //Bfs walks us through the tree in a sorted manner + bfsTraverse(ast, (node) => sortedTree.push(node)); + + for (i in comments) { + const comment = comments[i]; + const commentEnd = comment.source.endIdx; + while (sortedTreeIdx < sortedTree.length) { + const node = sortedTree[sortedTreeIdx]; + //If the node starts before the comment ends, we don't want it + if (node.source.startIdx < commentEnd) { + sortedTreeIdx++; + } + //Since everything is sorted, the first node we find is the one the comment belongs to + else { + if (node.leadingComments) node.leadingComments.push(comment); + else node.leadingComments = [comment]; + break; + } + } + } +}; + +//Trim source for nicer debugging +const trimSource = (ast) => { + dfsTraverse(ast, (node) => { + node.source = { + ...node.source, + sourceString: node.source.sourceString.slice( + node.source.startIdx, + node.source.endIdx + ), + }; + }); }; module.exports = (code, fileName) => { const rtn = {}; const ast = parser.parse(code); - parseComments(ast); + assignLeadingComment(ast, parseComments(ast)); + + trimSource(ast); + + let scope = null; + + const rules = []; + const vars = []; + + bfsTraverse(ast, (node) => { + if (node.leadingComments !== undefined) { + const tags = pullTags(node.leadingComments); + if (tags["@scope"] !== undefined) { + scope = {}; + //If we find the scope tag set the description + scope.desc = tags["@scope"]; + if (tags["@name"] !== undefined) { + //If we find the name, set the name + scope.name = tags["@name"].trim(); + } else { + //Else set the name to be the file name + scope.name = fileName; + } + } + + //All of the rule declarations + if (node.type === "Rule" && node.fn.name.name === "rule") { + rules.push(node); + } + + if (node.type === "Rule" && node.fn.name.name === "set") { + vars.push(node); + } + + //All of the variable declarations + } + }); + + if (scope) { + const scopeName = scope ? scope.name : fileName; + rtn[scopeName] = { + description: scope ? scope.desc : "", + rules: {}, + vars: {}, + }; + + const processedVars = vars.forEach((varNode) => { + varNode.leadingComments = varNode.leadingComments.filter((comment) => + comment.value.startsWith("*") + ); + if (varNode.leadingComments.length) { + //Grab the tags + const tags = pullTags(varNode.leadingComments); + const varName = tags["@name"] || varNode.args[0].value; + + const processedVar = processVar( + varNode.leadingComments.map((comment) => ({ + ...comment, + value: cleanComment(comment.value), + })) + ); + rtn[scopeName].vars[varName] = { + ...processedVar, + }; + } + }); + + const processedRules = rules.forEach((ruleNode) => { + //Make sure at least one of the comments starts with a * + ruleNode.leadingComments = ruleNode.leadingComments.filter((comment) => + comment.value.startsWith("*") + ); + if (ruleNode.leadingComments.length) { + //First we grab the tags from the comments + const tags = pullTags(ruleNode.leadingComments); + const ruleName = tags["@rule"] || ruleNode.args[0].value; + + const processedRule = processRule( + ruleNode.leadingComments.map((comment) => { + const cleanText = cleanComment(comment.value); + return { + ...comment, + value: cleanText, + }; + }) + ); + + const params = {}; + //If we can't find a parameters tag, we try to autoparse the parameters + tags["@params"] || + (ruleNode.args[1].rules && + [] + .concat( + ...ruleNode.args[1].rules + .filter((rule) => rule.fn.name.name === "bind") + .map((rule) => rule.args.map((arg) => arg.value)) + ) + .forEach((arg) => { + params[arg] = {}; + })); + + if (Object.keys(params).length) + processedRule["params"] = processedRule["params"] || params; + //In the absenes of an @rule tag, we use the name of the rule below + rtn[scopeName].rules[ruleName] = { + ...processedRule, + }; + } + }); + } return rtn; }; diff --git a/packages/docs/parsers/common.js b/packages/docs/parsers/common.js new file mode 100644 index 0000000..0448ecf --- /dev/null +++ b/packages/docs/parsers/common.js @@ -0,0 +1,186 @@ +const safeMergeObjs = (o1, o2) => { + const rtn = { ...o2 }; + Object.keys(o1).forEach((key) => { + if (rtn[key] !== undefined) { + const throwStr = `Cannot define key ${key} multiple times on the same structure`; + throw new Error(throwStr); + } + rtn[key] = o1[key]; + }); + return rtn; +}; + +const pullTags = (comment) => { + const rtn = {}; + const annotationMatch = /(@\w+) {(((?:\\})|[^}])*)}/g; + let matches; + + while ((matches = annotationMatch.exec(comment)) !== null) { + const tag = matches[1]; + + let val = matches[2].replace(/^\s*\*/gm, ""); + val = val.replace(/\\}/g, "}"); + + const [leadingWhitespace] = /^\s*/.exec(val); + val = val.replace( + new RegExp(`^[ \\t]{${leadingWhitespace.length}}`, "gm"), + "" + ); + if (val.endsWith("\n")) val = val.slice(0, -1); + rtn[tag] = val.trim(); + } + return rtn; +}; + +//TODO: Doesn't seem to work if the @tag isn't the first thing in the comment +const pullAllTags = (comments) => + comments.map((comment) => pullTags(comment.value)).reduce(safeMergeObjs, {}); + +const parseParams = (paramString) => { + const rtn = {}; + const paramMatch = /\[(?:(\w*)(?::\s(.+))?)\]\s*:?\s*([^\[]*)/g; + let matches; + while ((matches = paramMatch.exec(paramString)) !== null) { + const name = matches[1]; + const type = matches[2]; + const description = matches[3]; + rtn[name] = { + type, + description, + }; + } + return rtn; +}; + +const parseRtn = (rtnString) => { + const matches = /(\[(.*)\])?\s*:?\s*(.+)/g.exec(rtnString); + const type = matches[2] || "any"; + const description = matches[3]; + return { + type: type.trim(), + description: description.trim(), + }; +}; + +/* + * Grabs the text that's not part of the autodoc format + */ +const getPlaintext = (comment) => { + const annotationMatch = /(@\w+) {(((?:\\})|[^}])*)}/g; + + let lastMatchedIndex = 0; + const nonMatched = []; + while ((matches = annotationMatch.exec(comment)) !== null) { + nonMatched.push(comment.slice(lastMatchedIndex, matches.index)); + lastMatchedIndex = matches.index + matches[0].length; + } + nonMatched.push(comment.slice(lastMatchedIndex)); + + return nonMatched; +}; + +/* + * Process a single annotated variable + */ +const processVar = (variableComments) => { + const rtn = {}; + + const tags = pullAllTags(variableComments); + /* Parse the help tag */ + + if (tags["@help"] === undefined && tags["@desc"] === undefined) { + //If the help and desc tags have no data we grab all of the text + rtn.help = variableComments + .map((comment) => getPlaintext(comment.value)) + .join("\n"); + } else { + //else we just extract data from @help tags + rtn.help = tags["@help"] || tags["@desc"]; + } + + /* Parse the desc tag */ + if (tags["@desc"] === undefined && tags["@help"] === undefined) { + //If the help and desc tags have no data we grab all of the text + rtn.help = variableComments + .map((comment) => getPlaintext(comment.value)) + .join("\n"); + } else { + //else we just extract data from @help tags + rtn.help = tags["@desc"] || tags["@help"]; + } + + if (tags["@type"] !== undefined) { + rtn.type = tags["@type"]; + } + + /* Parse the example tag */ + if (tags["@example"] !== undefined) { + rtn.example = tags["@example"]; + } + + /* Parse the example tag */ + if (tags["@notes"] !== undefined) { + rtn.notes = tags["@notes"]; + } + return rtn; +}; + +/* + * Process a single annotated rule + */ +const processRule = (ruleComments) => { + const rtn = {}; + + const tags = pullAllTags(ruleComments); + /* Parse the help tag */ + + if (tags["@help"] === undefined && tags["@desc"] === undefined) { + //If the help and desc tags have no data we grab all of the text + rtn.help = ruleComments + .map((comment) => getPlaintext(comment.value)) + .join("\n"); + } else { + //else we just extract data from @help tags + rtn.help = tags["@help"] || tags["@desc"]; + } + + /* Parse the desc tag */ + if (tags["@desc"] === undefined && tags["@help"] === undefined) { + //If the help and desc tags have no data we grab all of the text + rtn.help = ruleComments + .map((comment) => getPlaintext(comment.value)) + .join("\n"); + } else { + //else we just extract data from @help tags + rtn.help = tags["@desc"] || tags["@help"]; + } + + /* Parse the params tag */ + if (tags["@params"] !== undefined) { + rtn.params = parseParams(tags["@params"]); + } + + /* Parse the returns tag */ + if (tags["@return"] !== undefined) { + rtn.rtn = parseRtn(tags["@return"]); + } + + /* Parse the example tag */ + if (tags["@example"] !== undefined) { + rtn.example = tags["@example"]; + } + + /* Parse the example tag */ + if (tags["@notes"] !== undefined) { + rtn.notes = tags["@notes"]; + } + return rtn; +}; + +module.exports = { + pullTags: pullAllTags, + parseRtn, + parseParams, + processRule, + processVar, +}; diff --git a/packages/docs/parsers/js.js b/packages/docs/parsers/js.js index e4d1b0e..c17325c 100644 --- a/packages/docs/parsers/js.js +++ b/packages/docs/parsers/js.js @@ -1,6 +1,7 @@ const traverse = require("@babel/traverse").default; const parser = require("@babel/parser"); const assert = require("assert"); +const { pullTags, parseRtn, parseParams, processRule } = require("./common"); const split = (arr, n) => { const rtn = []; @@ -11,72 +12,16 @@ const split = (arr, n) => { return rtn; }; -const safeMergeObjs = (o1, o2) => { - const rtn = { ...o2 }; - Object.keys(o1).forEach((key) => { - if (rtn[key] !== undefined) { - const throwStr = `Cannot define key ${key} multiple times on the same structure`; - throw new Error(throwStr); - } - rtn[key] = o1[key]; - }); - return rtn; -}; - //Removes a bunch of extra stuff from block comments such as the newlines and the *'s const cleanComment = (comment) => - comment - .split("\n") - .map((line) => line.split("*")[1]) - .filter(Boolean) - .join("") - .trim(); - -const pullTags = (comment) => { - const rtn = {}; - const annotationMatch = /(@\w+) {(((?:\\})|[^}])*)}/g; - let matches; - - if (comment.startsWith("*")) { - while ((matches = annotationMatch.exec(comment)) !== null) { - const tag = matches[1]; - - let val = matches[2].replace(/^\s*\*/gm, ""); - val = val.replace(/\\}/g, "}"); - - const [leadingWhitespace] = /^\s*/.exec(val); - val = val.replace( - new RegExp(`^[ \\t]{${leadingWhitespace.length}}`, "gm"), - "" - ); - if (val.endsWith("\n")) val = val.slice(0, -1); - rtn[tag] = val; - } - } - return rtn; -}; - -/* - * TODO: Only pull from comments that start with * - */ -const pullAllTags = (comments) => - comments.map((comment) => pullTags(comment.value)).reduce(safeMergeObjs, {}); - -const parseParams = (paramString) => { - const rtn = {}; - const paramMatch = /\[(?:(\w*)(?::\s(.+))?)\]\s*([^\[]*)/g; - let matches; - while ((matches = paramMatch.exec(paramString)) !== null) { - const name = matches[1]; - const type = matches[2]; - const description = matches[3]; - rtn[name] = { - type, - description, - }; - } - return rtn; -}; + comment.startsWith("*") + ? comment + .split("\n") + .map((line) => line.split("*")[1]) + .filter(Boolean) + .join("") + .trim() + : ""; const parseConfig = (configString) => { const rtn = {}; @@ -97,68 +42,6 @@ const parseConfig = (configString) => { return rtn; }; -const parseRtn = (rtnString) => { - const matches = /(\[(.*)\])?\s*(.+)/g.exec(rtnString); - const type = matches[2] || "any"; - const description = matches[3]; - return { - type: type.trim(), - description: description.trim(), - }; -}; - -/* - * Process a single annotated rule - */ -const processRule = (rule) => { - const rtn = {}; - const tags = pullAllTags(rule.leadingComments); - /* Parse the help tag */ - if (tags["@help"] === undefined && tags["@desc"] === undefined) { - //If the help and desc tags have no data we grab all of the text - //TODO: Return just the comments not within a tag - rtn.help = rule.leadingComments - .map((node) => cleanComment(node.value)) - .join("\n"); - } else { - //else we just extract data from @help tags - rtn.help = tags["@help"] || tags["@desc"]; - } - - /* Parse the desc tag */ - if (tags["@desc"] === undefined && tags["@help"] === undefined) { - //If the help and desc tags have no data we grab all of the text - //TODO: Return just the comments not within a tag - rtn.help = rule.leadingComments - .map((node) => cleanComment(node.value)) - .join("\n"); - } else { - //else we just extract data from @help tags - rtn.help = tags["@desc"] || tags["@help"]; - } - - /* Parse the params tag */ - if (tags["@params"] !== undefined) { - rtn.params = parseParams(tags["@params"]); - } - - /* Parse the returns tag */ - if (tags["@return"] !== undefined) { - rtn.rtn = parseRtn(tags["@return"]); - } - - /* Parse the example tag */ - if (tags["@example"] !== undefined) { - rtn.example = tags["@example"]; - } - - /* Parse the example tag */ - if (tags["@notes"] !== undefined) { - rtn.notes = tags["@notes"]; - } - return rtn; -}; - /* * Process multiple rules inside an object * @@ -188,7 +71,7 @@ const processRules = (rules) => { commentedRules.forEach((rule) => { const ruleName = rule.key.name || rule.key.value; rtn[ruleName] = {}; - const tags = pullAllTags(rule.leadingComments); + const tags = pullTags(rule.leadingComments); /* Parse the help tag */ if (tags["@help"] === undefined && tags["@desc"] === undefined) { //If the help and desc tags have no data we grab all of the text @@ -247,7 +130,7 @@ const processConfig = (config) => { //TODO: We should try to grab the name, type, and init value const propertyName = property.key.name; - const tags = pullAllTags(property.leadingComments || []); + const tags = pullTags(property.leadingComments || []); if (tags["@config"] === undefined) { rtn[propertyName] = (property.leadingComments || []) @@ -319,7 +202,7 @@ module.exports = (code, fileName) => { */ if (path.node.leadingComments !== undefined) { //Find the scope tag - const tags = pullAllTags(path.node.leadingComments); + const tags = pullTags(path.node.leadingComments); // In the case of just a scope declaration. if (tags["@scope"] && tags["@rule"] === undefined) { @@ -340,19 +223,45 @@ module.exports = (code, fileName) => { //In the case of a rule definition which has been tagged with a scope else if (tags["@scope"] !== undefined && tags["@rule"] !== undefined) { const scopeName = (tags["@scope"] || scope || fileName).trim(); - - if (!rtn[scopeName]) { - console.warn( - `WARNING:: Scope ${scopeName} does not exist. Creating new scope definition.` - ); - rtn[scopeName] = {}; + let scopeObj = rtn; + //In the case that this scope doesn't exist, we check for an @parent tag. If that doesn't exist we create a new scope + if (!scopeObj[scopeName]) { + //TODO: inference parent + if (tags["@parent"]) { + //TODO: Support arbitrarily nested scopes (currently supports one level of nesting) + //If the parent exists We create a child scope + if (!scopeObj[tags["@parent"]]) { + console.warn( + `WARNING:: Parent Scope '${ + scopeObj[tags["@parent"]] + }' for scope ${scopeName} does not exist. Creating new scope definition.` + ); + scopeObj[tags["@parent"]] = {}; + } + if (!scopeObj[tags["@parent"]]["children"]) { + scopeObj[tags["@parent"]]["children"] = {}; + } + scopeObj = scopeObj[tags["@parent"]]["children"]; + //Since this is a child scope, we don't need to print a warning + if (!scopeObj[scopeName]) scopeObj[scopeName] = {}; + } else { + console.warn( + `WARNING:: Scope '${scopeName}' does not exist. Creating new scope definition.` + ); + scopeObj[scopeName] = {}; + } } /* Deal with the Rule annotations */ //Rules - if (!rtn[scopeName]["rules"]) rtn[scopeName]["rules"] = {}; - - rtn[scopeName]["rules"][tags["@rule"]] = processRule(path.node); + if (!scopeObj[scopeName]["rules"]) scopeObj[scopeName]["rules"] = {}; + + scopeObj[scopeName]["rules"][tags["@rule"]] = processRule( + path.node.leadingComments.map((comment) => ({ + ...comment, + value: cleanComment(comment.value), + })) + ); } // In the case of a config definition diff --git a/packages/docs/plugins/markdownGen.js b/packages/docs/plugins/markdownGen.js index c7a86af..6be78ec 100644 --- a/packages/docs/plugins/markdownGen.js +++ b/packages/docs/plugins/markdownGen.js @@ -26,15 +26,17 @@ const subLinks = (str, map) => link(rule.trim(), `#${map[rule.trim()] || rule.trim()}`) ); -module.exports = async (docTree, file) => { - const readmeLines = [ - quote( - "This was generated using BrowseDoc which is still very much a work in progress" - ), - line, - h1("Table of Contents"), - line, - ]; +const startingLines = [ + quote( + "This was generated using BrowseDoc which is still very much a work in progress" + ), + line, + h1("Table of Contents"), + line, +]; + +const getDirectory = (docTree) => { + const readmeLines = []; // mapping of a rulename to a link slug const ruleMap = {}; @@ -43,7 +45,6 @@ module.exports = async (docTree, file) => { readmeLines.push( bullet( Object.keys(docTree).map((scope) => { - // TODO: Will break if multiple rules have the same name in different scopes const rules = Object.keys(docTree[scope].rules).map((rule) => { let text = rule.trim(); let slug = rule.trim(); @@ -60,11 +61,14 @@ module.exports = async (docTree, file) => { return link(shortcode(text), `#${slug}`); }); + const vars = Object.keys(docTree[scope].vars || {}).map((variable) => { + return link(shortcode(variable), `#${variable}`); + }); const configVars = Object.keys( docTree[scope].config || {} ).map((configVar) => link(`Config: ${configVar}`, `#${configVar}`)); - const entries = bullet([...configVars, ...rules], 1); + const entries = bullet([...vars, ...configVars, ...rules], 1); return `${link( `Scope: ${scope.trim()}`, `#scope-${scope.trim()}` @@ -72,7 +76,20 @@ module.exports = async (docTree, file) => { }) ) ); + const childrenLines = Object.keys(docTree) + .map((scope) => { + if (docTree[scope]["children"]) { + return getDirectory(docTree[scope]["children"]); + } + }) + .filter(Boolean); + return [...readmeLines, ...childrenLines]; +}; +const getReadme = (docTree, file, directory = null) => { + const ruleMap = {}; + const varMap = {}; + const readmeLines = []; //Build actual documentation Object.keys(docTree).map((scope) => { readmeLines.push(line); @@ -81,9 +98,19 @@ module.exports = async (docTree, file) => { readmeLines.push(subLinks(docTree[scope].description || "", ruleMap)); readmeLines.push(line); - const { rules, config } = docTree[scope]; + const { vars, rules, config } = docTree[scope]; + if (vars && Object.keys(vars).length) { + readmeLines.push(h2("Variables")); + const varLines = Object.keys(vars).map((variable) => { + const { help, desc, type } = vars[variable]; + return `${h3(shortcode(variable))}${type ? "\n" + italics(type) : ""}${ + desc ? "\n" + desc : help ? "\n" + help : "" + }`; + }); + readmeLines.push(...varLines); + } if (rules && Object.keys(rules).length) { - readmeLines.push(h3("Rules")); + readmeLines.push(h2("Rules")); const ruleLines = Object.keys(rules).map((rule) => { const { help, desc, params, rtn, example, notes } = rules[rule]; @@ -111,13 +138,15 @@ module.exports = async (docTree, file) => { (param) => (out.header += " " + param.trim()) ); out.params = bullet( - Object.keys(params).map( - (param) => - `${shortcode(param)} ${type(params[param].type)} ${subLinks( - params[param].description || "", - ruleMap - )}` - ), + Object.keys(params) + .map( + (param) => + Object.keys(params[param]).length && + `${shortcode(param)} ${ + params[param].type ? type(params[param].type) : "" + } ${subLinks(params[param].description || "", ruleMap)}` + ) + .filter(Boolean), 1 ); } @@ -129,7 +158,7 @@ module.exports = async (docTree, file) => { readmeLines.push(...ruleLines); } if (config && Object.keys(config).length) { - readmeLines.push(h3("Config")); + readmeLines.push(h2("Config")); const configLines = Object.keys(config).map((configVar) => { return `${h4(configVar)}\n( ${italics(config[configVar].type)} ) ${ config[configVar].description @@ -137,10 +166,25 @@ module.exports = async (docTree, file) => { }); readmeLines.push(bullet(configLines)); } + + //Also write the children readme's below this one + //TODO: Could indent, or have some special rendering for parents + const childrenLines = docTree[scope]["children"] + ? getReadme(docTree[scope]["children"], file) + : []; + readmeLines.push(...childrenLines); }); + return readmeLines; +}; + +module.exports = async (docTree, file) => { + const directory = getDirectory(docTree); + const readmeContents = getReadme(docTree, file); if (typeof file === "string") { - await fs.promises.writeFile(file, readmeLines.join("\n")); + await fs.promises.writeFile( + file, + [...startingLines, ...directory, ...readmeContents].join("\n") + ); } - return readmeLines.join("\n"); }; diff --git a/packages/format/lib/language/printer.js b/packages/format/lib/language/printer.js index db34b1a..3366a39 100644 --- a/packages/format/lib/language/printer.js +++ b/packages/format/lib/language/printer.js @@ -125,7 +125,9 @@ function genericPrint(path, options, print) { return "opt"; } case "Literal": { - if (typeof n.value === "string") { + if (n.value === null) { + return "nil"; + } else if (typeof n.value === "string") { return concat([n.quoteType, n.value, n.quoteType]); } else { return String(n.value);