diff --git a/.gitignore b/.gitignore index 411f204..8cf4124 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ ### Build dist/ +build/ coverage/ *.pyc diff --git a/.npmignore b/.npmignore index 3a887e9..28e6fbf 100644 --- a/.npmignore +++ b/.npmignore @@ -2,6 +2,8 @@ coverage/ codegen/ scripts/ test/ +docs/ +docsgen/ jest.config.json .travis.yml tslint.json diff --git a/.travis.yml b/.travis.yml index 4009ee4..28a2b9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,3 +8,12 @@ script: - yarn verify notifications: email: false +deploy: + provider: pages + skip-cleanup: true + github-token: $GITHUB_TOKEN + keep-history: true + local-dir: docs/build + committer-from-gh: true + on: + branch: master diff --git a/docs/builtin.md b/docs/builtin.md new file mode 100644 index 0000000..877a741 --- /dev/null +++ b/docs/builtin.md @@ -0,0 +1,71 @@ +# Built-in Units + +While users can define their own unit system (see [Defining Quantities](defining-quantities.html)), Safe Units also comes with a large collection of built-in quantities and units based on the SI unit system. + +All quantities provided are generic, of the form `type Quantity` so all will default to using `number` as the numeric type but may be passed another numeric type. + +## Base Units + +The built-in units include the standard set of SI base dimension and corresponding units: + +* `Length` / `meters` +* `Mass` / `kilograms` +* `Time` `seconds` +* `ElectricCurrent` / `amperes` +* `Temperature` / `kelvin` +* `AmountOfSubstance` / `moles` +* `LuminousIntensity` / `candelas` + +In addition, several extra dimensions are defined: + +* `PlaneAngle` / `radians` +* `SolidAngle` / `steradians` +* `Memory` / `bits` + +While plane and solid angles are defined as dimensionless in the SI specification, they are defined here in case users want to be more rigorous with their angles. If not, then these can be safely ignored. + +## Generic Base Units + +The base units are only provided for `number` measures, but users can create the base units for a given measure type through: + +```ts +type SpecialNumber = /* ... */; +const numericOps: INumericOperations = {/* ... */}; +const SpecialMeasure = createMeasureType(numericOps); +const { + meters, + kilograms, + seconds, + amperes, + kelvin, + moles, + candelas, + radians, + steradians, + bits, +} = createBaseUnits(SpecialMeasure); +``` + +These versions of `meters`, `kilograms`, etc. will all operator on measures whose numeric types are `SpecialNumber`. + +## Provided Units + +A large number of built in quantities (e.g. distance, velocity, force and magnetic flux density) are provided. See [here](https://github.com/jscheiny/safe-units/blob/master/src/unit/quantities.ts) for a full list. + +Many units are provided from the metric, Imperial and U.S. customary unit systems. See [here](https://github.com/jscheiny/safe-units/tree/master/src/unit) for all of the units. + +Prefix functions are provided for both standard [SI prefixes](https://github.com/jscheiny/safe-units/blob/master/src/unit/metric.ts) and for [memory prefixes](https://github.com/jscheiny/safe-units/blob/master/src/unit/memory.ts). + +Trigonometric functions are provided in the `Trig` namespace for converting between plane angles and dimensionless values, the signatures are as follows: + +```ts +namespace Trig { + function cos(x: PlaneAngle): Dimensionless; + function sin(x: PlaneAngle): Dimensionless; + function tan(x: PlaneAngle): Dimensionless; + function acos(x: Dimensionless): PlaneAngle; + function asin(x: Dimensionless): PlaneAngle; + function atan(x: Dimensionless): PlaneAngle; + function atan2(y: Length, x: Length): PlaneAngle; +} +``` diff --git a/docs/defining-quantities.md b/docs/defining-quantities.md new file mode 100644 index 0000000..bc32e6a --- /dev/null +++ b/docs/defining-quantities.md @@ -0,0 +1,109 @@ +# Defining Quantities + +We define a **quantity** as the type of a measurement. This includes things like length, time, force, velocity, and pressure. Safe Units contains many built in quantities so we can write typesafe code simply: + +```ts +import { Acceleration, Time, Velocity } from "safe-units"; + +function computeAcceleration(speed: Velocity, time: Time): Acceleration { + return speed.over(time); +} +``` + +## Dimensions and Base Units + +A **dimension** is the basis of a unit system. Units are composed of multiplying together multiple dimensions. For example, in the SI unit system there are the following dimensions, each corresponding to a base unit of the system + +- Length - meters (m) +- Mass - kilograms (kg) +- Time - seconds (s) +- Electric Current - amperes (A) +- Temperature - kelvin (K) +- Amount of substance - moles (mol) +- Luminous intensity - candelas (cd) + +We can use the `Measure.dimension` static method to define base units for a given dimension. Here's how Safe Units defines the dimensions and base units for length and time: + +```ts +const meters = Measure.dimension("length", "m"); +const Length = meters; +type Length = typeof meters; + +const seconds = Measure.dimension("time", "s"); +const Time = seconds; +type Time = typeof seconds; +``` + +Let's break this down: + +```ts +const meters = Measure.dimension("length", "m"); +``` +The first argument should be a unique string literal that defines the name of the dimension that we are creating. Note that if two calls to `Measure.dimension` are made with the same first argument, they will be considered the same dimension. The second argument is the symbol for the base unit of that dimension. This returns a `Measure` that represents 1 base unit (in the example above 1 meter and 1 second). + +```ts +type Length = typeof meters; +``` +Next, we create a type for the quantity that this unit represents. This type is useful for specifying the types of values we use later on (e.g. `const l: Length = Measure.of(30, feet)`). As noted above, a quantity is just the type of a measure so we define just that way. + +```ts +const Length = meters; +``` +Finally, we create a value for the quantity. This is very useful for expressively deriving more quantities from this base unit as can be seen below. + +## Derived Quantities + +Now that we have a set of base units and dimensions, we can derive more interesting quantities by using the quantity values we defined just above. Let's create velocity and acceleration quantities: + +```ts +const Velocity = Length.over(Time); +type Velocity = typeof Velocity; + +const Acceleration = Velocity.over(Time); +type Acceleration = typeof Acceleration; +``` + +Now we can see how the values for `Length` and `Time` came in handy for writing this nicely. If we didn't have values for this we would need to define velocity as: + +```ts +const Velocity = meters.over(seconds); +``` + +While not technically wrong, `Length.over(Time)` is a much cleaner definition of velocity. + +It's worth noting that since our quantities defined thus far are instances of `Measure`, we can perform all the same operations on `Measures` on quantities. For example, here is how Safe Units defines the quantity of magnetic flux density: + +```ts +const MagneticFluxDensity = Voltage.times(Time).over(Area); +``` + +This can then mirror how this measure might be computed: + +```ts +function computeMagneticFluxDensity( + voltage: Voltage, + time: Time, + area: Area, +): MagneticFluxDensity { + return voltage.times(time).over(area); +} +``` + +## Generic Quantities + +The examples in here are for creating dimensions and quantities for the built in `Measure` type. However, measures can be made for any numeric type (see [Generic Measures](generic-measures.html)). As such we may want to make our quantities generic as well. This can be easily done with the generic `LiftMeasure` type which takes a measure of a given unit and changes its numeric type. + +For example, suppose we have a `WrappedNumber` type, we can change a `Velocity` quantity from operating on number measures to `WrappedNumber` measures as follows: + +```ts +type WrappedVelocity = LiftMeasure +``` + +We could also just define the `Velocity` type to be generic in the first place. The recommended way of doing this is as follows: + +```ts +const Velocity = Length.over(Time); +type Velocity = LiftMeasure; +``` + +This way, we can use `Velocity` to refer to the quantity on numbers, but also use `Velocity` to operate on `WrappedMeasure`s. \ No newline at end of file diff --git a/docs/generic-measures.md b/docs/generic-measures.md new file mode 100644 index 0000000..8227c9f --- /dev/null +++ b/docs/generic-measures.md @@ -0,0 +1,79 @@ +# Generic Measures + +The default Safe Units `Measure` class is specifically designed around using measures where the underlying values are represented by the JavaScript `number` type. However, it wouldn't be unreasonable to want measures where the values have different requirements. For example, one might want measures with arbitrary precision or measures on the rational numbers. Creating such a measure class can be accomplished by using the `createMeasureType` function. + +## Example + +Suppose we have our own number type: + +```ts +class WrappedNumber { + constructor(public readonly value: number) {} + + foo(): WrappedNumber { ... } +} + +function wrap(value: number) { + return new WrappedNumber(value); +} +``` + +Now we're going to create our own measure type that operates on `WrappedNumber`s, let's call it `WrappedMeasure`: + +```ts +import { createMeasureType, GenericMeasure, Unit } from "safe-units"; + +type WrappedMeasure = IGenericMeasure; +const WrappedMeasure = createMeasureType({ + one: () => wrap(1), + neg: x => wrap(-x), + add: (x, y) => wrap(x.value + y.value), + sub: (x, y) => wrap(x.value - y.value), + mult: (x, y) => wrap(x.value * y.value), + div: (x, y) => wrap(x.value / y.value), + pow: (x, y) => wrap(x.value ** y), + compare: (x, y) => x - y, + format: x => `${x}`, +}); +``` + +We can then use this class just as we would use `Measure`, except anywhere we'd expect a `number` we now expect a `WrappedNumber`. + +## Breakdown + +Let's deconstruct this example to explain what's going on. First we start with this type definition: + +```ts +type WrappedMeasure = IGenericMeasure; +``` + +This line isn't strictly necessary, but it is often useful to have our `WrappedMeasure` available as a type. The next line `const WrappedMeasure = ...` creates a value for wrapped measures. Having a type for the measure is useful for writing generic functions on wrapped measures. All this line does is bind the numeric type of `GenericMeasure`. Similarly, the `Measure` type has the following definition: + +```ts +type Measure = IGenericMeasure; +``` + +After we've defined the type of `WrappedMeasure` we now define the class itself by calling `createMeasureType`. This function takes an object which let's the generic measure type know how to perform operations on our numeric type. Note that for this simple example, we generally just unwrap the value, perform the arithmetic operation and then wrap it back up. Most of these operations should be self-explanatory, however some require some further explanation: + +- `one`: A function with no arguments that simply returns the 1 value or multiplicative identity of your number system. This is used to construct base units whose values are implicitly one. +- `pow`: This function is slightly different from the rest of the arithmetic operations in that it doesn't take to values of type `N`, instead its signature is: `pow: (base: N, power: Exponent) => N` where `Exponent` is the union of `-5 | -4 | ... | 4 | 5`. This is due to the computational limitations of the library that we need to be specific in the kinds of exponents we can handle. +- `compare`: A function that returns a negative `number` if its first argument is less than its second, a positive `number` if its first argument is greater than its second, and `0` if the arguments are equal. + +## Usage + +The returned `WrappedMeasure` now behaves just like `Measure` does except in the domain of wrapped numbers. This means we can call `WrappedMeasure.dimension` or `WrappedMeasure.of` just as expected. It's important to note that the provided quantities and units (`Length`, `Time`, `meters`, `seconds`, etc) will need to be redefined for other measure types as these types are all specific to `number`s. + +## Static Methods + +By default, generic measures come with a set of static methods that can be applied to measures of all numeric types. However, certain static methods may only make sense for a given numeric type. For example the `Measure.trunc` method exists because `Math.trunc` applies to `number`s. To add static methods to a generic measure type, simple pass an object as a second argument to `createMeasureType`: + +```ts +declare function foo(value: WrappedNumber): WrappedNumber; +declare const mass: WrappedMeasure<{ mass: 1 }>; + +const WrappedMeasure = createMeasureType({ ... }, { + foo: wrapUnaryFn(foo), +}); + +WrappedMeasure.foo(mass); +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..409af24 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,37 @@ +# Safe Units + +Safe Units is a library for using units of measurement in TypeScript in a type safe manner. Check it out on [github](https://github.com/jscheiny/safe-units). Safe Units provides an implementation of an SI based unit system but is flexible enough to allow users to create their own unit systems which can be independent or can interoperate with the built-in units. Users can also make unit systems for any numeric type they'd like not just the JavaScript `number` type. This library makes heavy use of conditional types and thus requires TypeScript 2.9+. + +```ts +import { Length, Measure, meters, seconds, Time, Velocity } from "safe-units"; + +const length: Length = Measure.of(30, meters); +const time: Time = Measure.of(15, seconds); +const velocity: Velocity = length.over(time); + +console.log(length.toString()); // 30 m +console.log(time.toString()); // 15 s +console.log(velocity.toString()); // 2 m * s^-1 + +const error: Velocity = length.times(time); // Error: A measure of m*s isn't assignable to a measure of m/s. +``` + +## Prerequisites + +Safe units is written and TypeScript and should be consumed by TypeScript users to take full advantage of what it provides. In addition you will need the following: + +- TypeScript 2.9 or later +- Strict null checks enabled for your project + +## Installation + +``` +npm install safe-units +``` + +or + +``` +yarn add safe-units +``` + diff --git a/docs/limitations.md b/docs/limitations.md new file mode 100644 index 0000000..2d146c3 --- /dev/null +++ b/docs/limitations.md @@ -0,0 +1,34 @@ +# Limitations + +Since Safe Units is typesafe, it must compute units at compile time. Due to some technical limitations of what you can do with types in TypeScript, there is one major limitation to this library: The exponents for dimensions of your units are limited to integers between -5 and 5 (inclusive). This means that you can not represent a value of 30 m6 in this library (though, why would you?). + +In the research I've conducted for this library I cannot find any instances in which it would be useful to use units with such extreme exponents. If you're aware of any such use cases, please [file an issue](https://github.com/jscheiny/safe-units/issues/new) to discuss it. + +The result of this limitation is that certain operations may fail somewhat unexpectedly. With any luck, these should never occur in practice, but just in case, here's a list of what can fail: + +## Multiplication / Division + +If two units will multiply or divide together to create an exponent out of range a compile error will occur. This will fail at compile time by marking the argument as invalid. + +```ts +const a = Measure.of(1, meters.cubed()); +const b = Measure.of(1, meters.toThe(-3)); +const product = a.times(a); +// ~ Error: The result would have unit m^6 +const quotient = a.over(b); +// ~ Error: The result would have unit m^-6 +``` + +## Exponentiation + +The `squared` and `cubed` methods of measure will fail when applied to measures that would create invalid results. This will fail at compile time because for measures which cannot be squared or cubed these methods will have type `never`. The `toThe` method of measure will fail at compile time by marking the argument as invalid. + +```ts +const m = Measure.of(30, meters.cubed()); +const a = m.squared(); +// ~~~~~~~~~~~ Error: squared has type never +const b = m.cubed(); +// ~~~~~~~~~ Error: cubed has type never +const c = m.toThe(3); +// ~ Error: m cannot be cubed so 3 is an invalid argument +``` diff --git a/docs/measures.md b/docs/measures.md new file mode 100644 index 0000000..910ca14 --- /dev/null +++ b/docs/measures.md @@ -0,0 +1,410 @@ +# Measures + +The `Measure` class provides the core of the functionality of Safe Units. A `Measure` is a value associated with a unit (e.g. 5 meters, 10 seconds). A `Measure` may be given a symbol to indicate that it itself represent some unit (for example, 0.3048 meters might be given the symbol `ft`). Measures are immutable and any operation on a measure returns a new measure. + +**Note:** The `Measure` class provides functionality for manipulating units with values represented by the JavaScript `number` type. For different numeric types see [Generic Measures](generic-measures.html). + +## Construction + +### `Measure.of` +```ts +Measure.of(value: number, quantity: Measure, symbol?: string): Measure +``` + +Creates a measure that is a scalar multiple of a given measure. An optional symbol maybe provided to name the resulting measure. + +*Examples:* + +```ts +const d1 = Measure.of(30, meters); +const feet = Measure.of(0.3048, meters, "ft"); +const d2 = Measure.of(10, feet); +const minutes = Measure.of(60, seconds, "min"); +``` + +### `Measure.dimensionless` + +```ts +Measure.dimensionless(value: number): Measure<{}> +``` + +Creates a dimensionless measure value. + +*Examples:* + +```ts +const scalar = Measure.dimensionless(2); +const distance = Measure.of(20, meters); +const doubled = distance.times(scalar); // 40 m +``` + +### `Measure.dimension` + +```ts +Measure.dimension(dim: Dim, symbol?: string): Measure<...> +``` + +Constructs a dimension of a unit system along with a base unit for that dimension. For more information see [Defining Quantities](defining-quantities.html). + +### `Measure.isMeasure` + +```ts +Measure.isMeasure(value: any): value is Measure +``` + +Since `Measure` isn't technically a class, you can't check that a value is a measure by using `instanceof`. Instead, use this method to determine if a given value is a `Measure`. + +### `Measure.prefix` + +```ts +Measure.prefix(prefix: string, multiplier: number): PrefixFn +``` + +Creates a function which when given a measure applies a prefix to that measure's symbol and multiplies it by a given dimensionless value. + +*Examples:* + +```ts +const kilo = Measure.prefix("k", 1000); +const km = kilo(meters); // 1000 m + +const distance = Measure.of(20, km); // 20000 m +distance.in(km) // 20 km +``` + +## Operations + +### Negation + +```ts +Measure.negate(): Measure +``` + +Returns a new measure containing the negative value of the original. + +*Examples:* + +```ts +const pos = Measure.of(30, meters); +const neg = pos.negate(); // -30 m +``` + +### Addition +```ts +Measure.plus(other: Measure): Measure +Measure.add(left: Measure, right: Measure): Measure +Measure.sum(first: Measure, ...rest: Array>): Measure +``` + +Measures may only be added if they have the same unit. This will produce a new measure with the same unit. The `Measure.add` static function is an alias for `a.plus(b)`. The `Measure.sum` method must be given a list of one or more units, since we can't infer the unit for an empty list. + +*Examples:* + +```ts +const d1 = Measure.of(30, meters); +const d2 = Measure.of(10, meters); +const d3 = Measure.of(100, feet); +const t1 = Measure.of(2, minutes); + +const sum1 = d1.plus(d2); // 40 m +const sum2 = Measure.add(d1, d2); // 40 m +const sum3 = Measure.dim(d1, d2, d3); // 140m + +const good = d1.plus(d3); // Fine because both are lengths +const bad = d1.plus(t1); // ERROR: Cannot add a distance to a time unit +``` + +### Subtraction + +```ts +Measure.minus(other: Measure): Measure +Measure.subtract(left: Measure, right: Measure): Measure +``` + +Measures may only be subtracted if they have the same unit. This will produce a new measure with the same unit. All of these functions behave the same. + +```ts +const d1 = Measure.of(30, meters); +const d2 = Measure.of(10, meters); +const t1 = Measure.of(2, minutes); + +const diff1 = d1.minus(d2); // -20 m +const diff2 = Measure.subtract(d2, d1) // 20 m + +const bad = t1.minus(d1); // ERROR: Cannot subtract a distance from a time unit +``` + +### Multiplication + +```ts +Measure.times(other: Measure): Measure> +Measure.multiply(left: Measure, right: Measure): Measure> +``` + +Multiplies two measures together and returns a new measure. The resulting unit is computed at compile time to be the result of multiplying the arguments' units together. + +```ts +const mass = Measure.of(10, kilograms); +const acceleration = Measure.of(9.8, metersPerSecondsSquared); + +// Works! The result of mass times acceleration is force +const force: Force = mass.times(acceleration); // 98 N + +// ERROR: A force quantity cannot be assigned to a pressure quantity +const bad: Pressure = Measure.multiply(mass, acceleration); +``` + +**Note:** There are limitations on what measures you may multiply and divide. See [Limitations](limitations.html). + +### Division +```ts +Measure.div(other: Measure): Measure> +Measure.over(other: Measure): Measure> +Measure.per(other: Measure): Measure> +Measure.divide(left: Measure, right: Measure): Measure> +``` + +Divides two measures together and returns a new measure. The resulting unit is computed at compile time to be the result of dividing the arguments' units together. All of these functions behave the same and a provided to make writing readable units easier. + +```ts +const distance = Measure.of(30, meters); +const time = Measure.of(10, seconds); + +// Works! The result of distance over time is velocity +const velocity: Velocity = distance.over(time); // 300 m*s + +// ERROR: A velocity quantity cannot be assigned to an acceleration quantity +const bad: Acceleration = Measure.divide(distance, time); +``` + +### Scalar Multiplication + +```ts +Measure.scale(value: number): Measure +``` + +A convenience method for multiplying a measure by a dimensionless value. + +```ts +const t = Measure.of(10, seconds); +const doubledShort = t.scale(2); // 20 s +const doubledLong = t.times(Measure.dimensionless(2)); +``` + +### Exponentiation + +```ts +Measure.toThe(exponent: E): Measure> +Measure.pow(measure: Measure, exponent: E): Measure> + +Measure.squared(): Measure> +Measure.cubed(): Measure> +``` + +The first two methods raise a given measures value and unit to a given exponent (within a limited range). The last two methods, `squared` and `cubed` convenience methods for `measure.toThe(2)` and `measure.toThe(3)` respectively. + +*Examples:* + +```ts +const side = Measure.of(10, meters); + +const area: Area = side.squared(); // 100 m^2 +const volume: Volume = side.cubed(); // 1000 m^3 + +const s: Length = volume.toThe(-3); // 10 m +``` + +**Note:** There are limitations on what measures you may exponentiate. See [Limitations](Limitations). + +### Reciprocals + +```ts +Measure.inverse(): Measure> +Measure.reciprocal(): Measure> +``` + +Computes the reciprocal of the value and unit of the measure. Both methods are identical and equivalent to `measure.toThe(-1)`. + +*Examples:* + +```ts +const freq: Frequency = Measure.of(30, hertz); // 30 1/s + +const cycle: Time = freq.inverse(); // 1/30 s +``` + +### Static Math + +The `Measure` class contains a number of static methods from the JavaScript `Math` object wrapped to work on measures. Many of these functions take in a single measure and return a single measure without changing its unit: + +* `Measure.abs` +* `Measure.ceil` +* `Measure.floor` +* `Measure.fround` +* `Measure.round` +* `Measure.trunc` + +There is also a wrapper for `hypot` that takes in one or more measures all with the same unit and returns a single measure of that unit. + +Lastly, there are wrappers for `sqrt` and `cbrt`. However, not all units have valid roots and these functions will produce errors when given units that don't produce roots with integer exponents. + +*Examples:* + +```ts +const distance = Measure.of(-9.8, meters); + +Measure.abs(distance); // 9.8 m +Measure.trunc(distance); // -9 m + +const width = Measure.of(3, meters); +const height = Measure.of(4, meters); + +Measure.hypot(width, height); // 5 m +``` + +### Roots + +The `Measure` class also provides wrappers for `sqrt` and `cbrt` functions. However, not all units have valid roots and these functions will produce errors when given units that don't produce roots with integer exponents. + +```ts +const area: Area = Measure.of(64, meters.squared()); +const volume: Volume = Measure.of(125, meters.cubed()); + +Measure.sqrt(area); // 8 m +Measure.cbrt(volume); // 5 m + +Measure.cbrt(area); // ERROR: Cannot take the cube root of a unit of the form m^2 +Measure.sqrt(volume); // ERROR: Cannot take the square root of a unit of the form m^3 +``` + +### Comparisons + +```ts +Measure.lt(other: Measure): boolean; +Measure.lte(other: Measure): boolean; +Measure.eq(other: Measure): boolean; +Measure.neq(other: Measure): boolean; +Measure.gte(other: Measure): boolean; +Measure.gt(other: Measure): boolean; +``` + +Comparison methods between two measures. Measures are only comparable if they are of the same unit. + +*Examples:* + +```ts +const t1 = Measure.of(30, minutes); +const t2 = Measure.of(0.25, hours); +const d1 = Measure.of(10, meters); + +t1.gt(t2); // true +t1.eq(d1) // ERROR: Cannot compare temporal and distance values +``` + +### Symbols + +```ts +Measure.withSymbol(symbol: string): Measure; +``` + +Duplicates the current measure and gives the new measure a symbol. Symbols are specific to an instance of a measure, performing operations on that measure will not forward along any symbols to the resulting measures. Calling `m.withSymbol(s)` is equivalent to calling `Measure.of(1, m, s)`. The symbol of a measure can be seen by accessing the readonly `symbol` field. + +```ts +const squareMeters = meters.squared().withSymbol("sq. m"); + +squareMeters.symbol; // "sq. m" + +// All of the following lose the symbol from squareMeters: +const r1 = squareMeters.scale(2); +const r2 = Measure.of(10, squareMeters); +const r3 = Measure.divide(squareMeters, meters); +``` + +Symbols are used in the converting other measures into strings as can be seen below. + +### Formatting + +```ts +Measure.toString(): string; +Measure.in(unit: Measure): string; +``` + +The first method, `toString`, creates a string version of the unit, ignoring any [symbol](#symbols) information on that measure. The second method, `in`, will format a given unit as if its given in units of the second measure, assuming the second measure has a symbol. If not, this is equivalent to calling `toString()`. + +*Examples:* + +```ts +const kilometers = Measure.of(1000, meters, "km"); +// Could be written as: Measure.of(1000, meters).withSymbol("km") + +const distance = Measure.of(5, kilometers); +console.log(distance.toString()); // 5000 m +console.log(distance.in(kilometers)); // 5 km +console.log(kilometers.in(kilometers)); // 1 km + +const force = Measure.of(30, newtons); +console.log(force.toString()); // 30 kg * m * s^-2 +console.log(force.in(newtons)); // 30 N +``` + +### Unsafe Transformations + +```ts +Measure.unsafeMap(valueMap: (value: number) => number): Measure; +Measure.unsafeMap( + valueMap: (value: number) => number, + unitMap: (unit: UnitWithSymbols) => UnitWithSymbols, +): Measure; +``` + +If only one argument is passed, performs a mapping on the value of a measure without affecting the unit of the measure. If both arguments are passed maps both the unit and value of a measure. This is generally used for internal purposes and should be avoided when possible. Instead consider using a [function wrapper](#function-wrappers). + +## Representation + +A `Measure` represents a value in terms its most basic units. For example, consider we build up a unit system for currency as follows: + +```ts +const dollars = Measure.dimension("currency", "$"); +const Currency = dollars; +type Currency = typeof dollars; + +const CashFlowRate = Currency.over(Time); +type CashFlowRate = typeof CashFlowRate; + +const pounds: Currency = Measure.of(1.31, dollars, "£"); +``` + +Then when deriving values from `pounds`, the underlying values are still represented as dollars: + +```ts +const profit: Currency = Measure.of(60, pounds); // 78.6 dollars +const elapsed: Time = Measure.of(1, minutes); // 60 seconds +const rate: CashFlowRate = profit.per(elapsed); // 1.31 dollars / second +``` + +In this way, we never actually have to perform unit conversions since, under the hood, all measures with the same dimension are always represented with the same units. We can format these values using any unit we want with the `in` method: + +```ts +const poundsPerSecond: CashFlowRate = pounds.per(seconds).withSymbol("£/s"); + +profit.in(pounds); // 60 £ +elapsed.in(minutes); // 1 min +rate.in(poundsPerSecond); // 1 £/s +``` + +## Function Wrappers + +It is often desirable to convert operations on numbers into operations on measures. Frequently these functions make no difference on the unit of a value. For example, suppose we want to make an absolute value function that operates on measures. We'd expect the function perserve the unit of the given measure. We can simply wrap an existing absolute value function using `wrapUnaryFn`: + +```ts +const measureAbs = wrapUnaryFn(Math.abs); +const time = Measure.of(-30, seconds); +measureAbs(time); // 30 s +``` + +The following function wrappers are provided: + +* `wrapUnaryFn` - Wraps a function of a single number. +* `wrapBinaryFn` - Wraps a function of two numbers, returning a function that expects two measures of the same unit. +* `wrapSpreadFn` - Wraps a function that takes any number of numbers and returns a function that takes one or more measures of the same unit. +* `wrapRootFn` - Wraps a function that takes the nth root of a given number for a specific n and returns a function that takes nth root of a measure's value and unit. \ No newline at end of file diff --git a/docs/order.txt b/docs/order.txt new file mode 100644 index 0000000..2dcc1f3 --- /dev/null +++ b/docs/order.txt @@ -0,0 +1,4 @@ +measures +generic-measures +defining-quantities +limitations \ No newline at end of file diff --git a/docsgen/index.tsx b/docsgen/index.tsx new file mode 100644 index 0000000..ff0471d --- /dev/null +++ b/docsgen/index.tsx @@ -0,0 +1,86 @@ +import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs"; +import { basename, extname, join } from "path"; +import * as React from "react"; +import * as ReactDOMServer from "react-dom/server"; +import { getStyles } from "typestyle"; +import { Page } from "./page"; +import { PageModel } from "./pageModel"; + +const renderPageHtml = (title: string, body: string, inlineStyles: string, linkedStyles: string) => ` + + + + + ${title} + + + ${linkedStyles} + + + ${body} +`; + +const buildDir = join("docs", "build"); +if (!existsSync(buildDir)) { + mkdirSync(buildDir); +} + +const stylesDir = join(buildDir, "styles"); +if (!existsSync(stylesDir)) { + mkdirSync(stylesDir); +} + +const pages = readdirSync("docs") + .filter(path => extname(path) === ".md") + .map(path => join("docs", path)) + .map(PageModel.from); + +const orderPath = join("docs", "order.txt"); +let order: string[] = []; +if (existsSync(orderPath) && !statSync(orderPath).isDirectory()) { + order = readFileSync(orderPath, "UTF8").split("\n"); +} + +function orderBy(order: string[]): (a: PageModel, b: PageModel) => number { + const getIndex = (page: PageModel) => { + if (page.name === "index") { + return -Infinity; + } + let index = order.indexOf(page.name); + if (index === -1) { + index = order.indexOf("*"); + } + return index === -1 ? Infinity : index; + }; + + return (a, b) => { + const aIndex = getIndex(a); + const bIndex = getIndex(b); + if (aIndex < bIndex) { + return -1; + } else if (aIndex > bIndex) { + return 1; + } else { + return a.title.toLowerCase() < b.title.toLowerCase() ? -1 : 1; + } + }; +} + +function buildLinkedStyles(inPath: string): string { + const name = basename(inPath); + const outPath = join(stylesDir, name); + copyFileSync(inPath, outPath); + return ``; +} + +const linkedStyles = ["node_modules/highlight.js/styles/monokai.css", "node_modules/normalize.css/normalize.css"] + .map(buildLinkedStyles) + .join("\n"); + +pages.sort(orderBy(order)); +pages.forEach((page, index) => { + const body = ReactDOMServer.renderToString(); + const title = page.name === "index" ? page.title : `${page.title} | Safe Units`; + const html = renderPageHtml(title, body, getStyles(), linkedStyles); + writeFileSync(join(buildDir, page.path), html); +}); diff --git a/docsgen/markdown.tsx b/docsgen/markdown.tsx new file mode 100644 index 0000000..fe9a7e0 --- /dev/null +++ b/docsgen/markdown.tsx @@ -0,0 +1,156 @@ +import { Node } from "commonmark"; +import { highlight } from "highlight.js"; +import * as React from "react"; +import { createNodeId } from "./markdownUtils"; +import { component } from "./style"; + +interface IMarkdownProps { + root: Node; +} + +type MarkdownComponent = React.FunctionComponent; + +export const Markdown: MarkdownComponent = ({ root }) => { + switch (root.type) { + case "document": + return ; + case "paragraph": + const children = ; + const grandparent = root.parent ? root.parent.parent : null; + if (grandparent !== null && grandparent.type === "list" && grandparent.listTight) { + return children; + } else { + return

{children}

; + } + case "text": + return <>{root.literal}; + case "heading": + return ; + case "linebreak": + return
; + case "html_inline": + return ; + case "html_block": + return ; + case "link": + return ( + + + + ); + case "image": + return {root.title; + case "thematic_break": + return
; + case "code_block": + const highlightedCode = highlight("typescript", root.literal || "", true).value; + return ( +
+                    
+                
+ ); + case "code": + return {root.literal}; + case "softbreak": + return
; + case "custom_inline": + // TODO Handle this + return null; + case "custom_block": + // TODO Handle this + return null; + default: + const tag = getTag(root.type, root); + return React.createElement(tag, {}, ); + } +}; + +export const MarkdownChildren: MarkdownComponent = ({ root }) => { + const children: JSX.Element[] = []; + let index = 0; + let child = root.firstChild; + while (child !== null) { + children.push(); + index++; + child = child.next; + } + return <>{children}; +}; + +const MarkdownHeading: MarkdownComponent = ({ root }) => { + const tag = getHeadingTag(root.level); + const id = createNodeId(root); + return React.createElement(tag, { id }, ); +}; + +type StrictExclude = T extends U ? never : T; +type TranslatedNodeTypes = StrictExclude< + Node["type"], + | "document" + | "paragraph" + | "text" + | "softbreak" + | "linebreak" + | "html_inline" + | "html_block" + | "link" + | "image" + | "thematic_break" + | "code_block" + | "code" + | "custom_inline" + | "custom_block" + | "heading" +>; + +function getTag(type: TranslatedNodeTypes, node: Node): keyof JSX.IntrinsicElements { + switch (type) { + case "emph": + return "em"; + case "strong": + return "strong"; + case "block_quote": + return "blockquote"; + case "item": + return "li"; + case "list": + return node.listType === "bullet" ? "ul" : "ol"; + } +} + +function getHeadingTag(level: number): keyof JSX.IntrinsicElements { + switch (level) { + case 1: + return "h1"; + case 2: + return "h2"; + case 3: + return "h3"; + case 4: + return "h4"; + case 5: + return "h5"; + case 6: + return "h6"; + default: + return "h6"; + } +} + +const CodeBlock = component("code-block", "code", { + borderRadius: 3, +}); + +const CodeInline = component("code-line", "code", { + color: "#00998C", +}); + +export const Link = component("link", "a", { + color: "#00B3A4", + textDecoration: "none", + $nest: { + "&:hover": { + textDecoration: "underline", + }, + }, +}); diff --git a/docsgen/markdownUtils.ts b/docsgen/markdownUtils.ts new file mode 100644 index 0000000..fd868bf --- /dev/null +++ b/docsgen/markdownUtils.ts @@ -0,0 +1,40 @@ +import { Node, NodeWalker, NodeWalkingStep } from "commonmark"; + +export function getNodeText(root: Node): string { + let text = ""; + walkMarkdown(root, node => { + if (node.literal) { + text += node.literal; + } + }); + return text; +} + +export function walkMarkdown(root: Node, callback: (node: Node) => void): void { + const walker = root.walker(); + + while (true) { + const event = nextStep(walker); + if (event === null) { + break; + } + const { node } = event; + if (event.entering) { + callback(node); + } + } +} + +export function createNodeId(node: Node): string { + return encodeURIComponent( + getNodeText(node) + .toLowerCase() + .split(/[^A-Za-z0-9]+/g) + .filter(x => x !== "") + .join("-"), + ); +} + +export function nextStep(walker: NodeWalker): NodeWalkingStep | null { + return walker.next() as NodeWalkingStep | null; +} diff --git a/docsgen/page.tsx b/docsgen/page.tsx new file mode 100644 index 0000000..bdac87a --- /dev/null +++ b/docsgen/page.tsx @@ -0,0 +1,73 @@ +import * as React from "react"; +import { Link, Markdown } from "./markdown"; +import { PageModel } from "./pageModel"; +import { Sidebar } from "./sidebar"; +import { component } from "./style"; + +interface IPageProps { + pages: PageModel[]; + pageIndex: number; +} + +export const Page: React.FunctionComponent = ({ pages, pageIndex }) => { + const page = pages[pageIndex]; + const year = new Date().getFullYear(); + return ( + + + + +
+ + + Safe Units is developed by Jonah + Scheinerman. Please contact me if you have + questions or concerns. + + Safe Units is distributed under the{" "} + MIT open source license. +
+
+ Copyright © {year} by Jonah Scheinerman +
+
+ + + + ); +}; + +const Container = component("page", "div", { + display: "flex", + flexDirection: "row", + height: "100vh", + width: "100vw", +}); + +const Contents = component("contents", "div", { + overflow: "auto", + flex: "1 1 auto", + position: "relative", + background: "#EBF1F5", + boxShadow: "inset 15px 0 20px -20px #182026", +}); + +const Body = component("body", "div", { + maxWidth: 800, + margin: "0 25px 25px 45px", +}); + +const EndMatter = component("end-matter", "div", { + borderTop: "1px solid #BFCCD6", + marginTop: 20, + paddingTop: 20, + fontSize: 14, +}); + +const License = component("license", "div", { + marginTop: 20, + textAlign: "center", + color: "#738694", + textShadow: "0 1px 0 white", + fontSize: 12, +}); diff --git a/docsgen/pageModel.ts b/docsgen/pageModel.ts new file mode 100644 index 0000000..0cc6098 --- /dev/null +++ b/docsgen/pageModel.ts @@ -0,0 +1,67 @@ +import { Node, Parser } from "commonmark"; +import { readFileSync } from "fs"; +import { basename } from "path"; +import { createNodeId, getNodeText, nextStep, walkMarkdown } from "./markdownUtils"; + +const parser = new Parser(); + +export interface IPageSection { + level: number; + node: Node; + id: string; +} + +export class PageModel { + public static from(path: string): PageModel { + const name = basename(path, ".md"); + const source = readFileSync(path, "UTF8"); + const root = parser.parse(source); + return new PageModel(name, root, getTitle(root), getSections(root)); + } + + constructor( + public readonly name: string, + public readonly root: Node, + public readonly title: string, + public readonly sections: ReadonlyArray, + ) {} + + public get path(): string { + return `${this.name}.html`; + } +} + +function getTitle(root: Node): string { + const fallback = "Untitled"; + const walker = root.walker(); + + const maybeDocument = nextStep(walker); + if (maybeDocument === null || maybeDocument.node.type !== "document") { + return fallback; + } + + const maybeHeading = nextStep(walker); + if (maybeHeading === null || maybeHeading.node.type !== "heading" || maybeHeading.node.level !== 1) { + return fallback; + } + + return getNodeText(maybeHeading.node); +} + +function getSections(root: Node): IPageSection[] { + let sections: IPageSection[] = []; + walkMarkdown(root, node => { + if (node.type === "heading" && node.level !== 1) { + sections.push(getSection(node)); + } + }); + return sections; +} + +function getSection(node: Node): IPageSection { + return { + node, + level: node.level, + id: createNodeId(node), + }; +} diff --git a/docsgen/sidebar.tsx b/docsgen/sidebar.tsx new file mode 100644 index 0000000..6821d05 --- /dev/null +++ b/docsgen/sidebar.tsx @@ -0,0 +1,123 @@ +import * as React from "react"; +import { classes, style } from "typestyle"; +import { MarkdownChildren } from "./markdown"; +import { PageModel } from "./pageModel"; +import { component, styles } from "./style"; + +interface ISidebarProps { + pages: PageModel[]; + selectedIndex: number; +} + +export const Sidebar: React.FunctionComponent = ({ pages, selectedIndex }) => { + const pageSections = pages[selectedIndex].sections.filter(({ level }) => level <= 3).map(({ id, node, level }) => { + const className = classes(level === 2 && section, level === 3 && subsection); + return ( + + + + ); + }); + + const links = pages.map((page, index) => { + const isSelected = index === selectedIndex; + const className = classes(index === 0 ? homeLink : pageLink, isSelected && selectedPage); + const hash = isSelected ? "#top" : ""; + return ( + + + {page.title} + + {isSelected ? pageSections : null} + + ); + }); + + return ( + + {links} + + View on github + + + ); +}; + +const HOME_COLOR = "#008075"; +const LINK_COLOR = "#293742"; + +const Container = component("sidebar", "div", { + minWidth: 250, + overflow: "auto", + fontSize: 14, +}); + +const link = styles({ + display: "block", + color: "#293742", + textDecoration: "none", +}); + +const PageLink = component("page-link", "a", { + ...link, + margin: "5px 0", + padding: "5px 10px", + background: "#E1E8ED", + fontWeight: "bold", + $nest: { + "&:hover": { + background: "#BFCCD6", + }, + }, +}); + +const homeLink = style({ + color: HOME_COLOR, +}); + +const pageLink = style({ + color: LINK_COLOR, +}); + +const selectedPage = style({ + color: "#F5F8FA", + background: HOME_COLOR, + $nest: { + "&:hover": { + background: HOME_COLOR, + color: "#CED9E0", + }, + }, +}); + +const SectionLink = component("section-link", "a", { + ...link, + color: LINK_COLOR, + $nest: { + "&:hover": { + background: "#E1E8ED", + }, + }, +}); + +const section = style({ + $debugName: "section", + padding: "3px 20px", +}); + +const subsection = style({ + $debugName: "subsection", + padding: "3px 40px", + color: "#5C7080", +}); + +const LinkSection = component("links", "div", { + $nest: { + "&:last-child": { + marginBottom: 20, + }, + "&:first-child": { + marginTop: 20, + }, + }, +}); diff --git a/docsgen/style.ts b/docsgen/style.ts new file mode 100644 index 0000000..357b3f0 --- /dev/null +++ b/docsgen/style.ts @@ -0,0 +1,48 @@ +import * as React from "react"; +import { classes, cssRule, style, types } from "typestyle"; + +type Omit = Pick>; +type CSSProps = Omit; + +export function styles(s: CSSProps): CSSProps { + return s; +} + +export function component( + $debugName: string, + tag: Tag, + styles: CSSProps, +): React.FunctionComponent { + const styleClassName = style({ $debugName }, styles); + return ({ className, ...props }: any) => { + return React.createElement(tag, { + className: classes(className, styleClassName), + ...props, + }); + }; +} + +cssRule("body", { + fontFamily: "'Helvetica Neue', HelveticaNeue, Helvetica, Arial, sans-serif", + fontSize: 16, +}); + +cssRule("body pre, body code", { + fontFamily: "'Ubuntu Mono', monospace", +}); + +cssRule("h1, h2, h3, h4, h5, h6", { + fontFamily: "'Open Sans', sans-serif", +}); + +cssRule("h2, h3", { + marginLeft: -20, +}); + +cssRule("body h1", { + color: "#008075", + margin: 0, + paddingBottom: 20, + paddingTop: 25, + marginLeft: -20, +}); diff --git a/docsgen/tsconfig.json b/docsgen/tsconfig.json new file mode 100644 index 0000000..cb2550e --- /dev/null +++ b/docsgen/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "jsx": "react", + "outDir": "./dist", + "lib": [ + "dom", + "esnext", + "es6" + ] + } +} \ No newline at end of file diff --git a/package.json b/package.json index a4b6a54..52fb584 100644 --- a/package.json +++ b/package.json @@ -21,27 +21,39 @@ "typings": "dist/index.d.ts", "scripts": { "build": "npm-run-all -s codegen compile:src", - "clean": "rimraf dist", + "clean": "rimraf dist codegen/dist docsgen/dist docs/build", "codegen": "npm-run-all -s compile:codegen node:codegen", "compile:codegen": "tsc -p codegen", + "compile:docs": "tsc -p docsgen", "compile:src": "tsc -p src", + "docs": "npm-run-all -s compile:docs node:docs", "lint:codegen": "tslint -p codegen/tsconfig.json -c tslint.json", + "lint:docs": "tslint -p docsgen/tsconfig.json -c tslint.json", "lint:src": "tslint -p src/tsconfig.json -c tslint.json", "lint:dist": "./scripts/check-typings.sh", "lint:types": "dtslint test/types", - "lint": "npm-run-all -p lint:codegen lint:src lint:dist lint:types", + "lint": "npm-run-all -p lint:codegen lint:docs lint:src lint:dist lint:types", "node:codegen": "node codegen/dist/emit", + "node:docs": "node docsgen/dist/index", "test": "jest --config jest.config.json", "prepack": "yarn clean && yarn build", - "verify": "npm-run-all -s build lint test" + "verify": "npm-run-all -s build lint test docs" }, "devDependencies": { + "@types/commonmark": "^0.27.1", + "@types/highlight.js": "^9.12.3", "@types/jest": "^22.2.3", "@types/node": "^8.10.16", + "@types/react-dom": "^16.0.11", + "commonmark": "^0.28.1", "dtslint": "^0.3.0", + "highlight.js": "^9.14.2", "jest": "^22.4.3", + "normalize.css": "^8.0.1", "npm-run-all": "^4.1.3", "prettier": "^1.12.1", + "react": "^16.7.0", + "react-dom": "^16.7.0", "rimraf": "^2.6.3", "ts-jest": "^22.4.6", "ts-node": "^6.0.3", @@ -50,6 +62,7 @@ "tslint-config-standard": "^7.0.0", "tslint-no-circular-imports": "^0.4.0", "tslint-plugin-prettier": "^1.3.0", - "typescript": "~2.9.1" + "typescript": "~2.9.1", + "typestyle": "^2.0.1" } } diff --git a/tslint.json b/tslint.json index 8311e67..211a617 100644 --- a/tslint.json +++ b/tslint.json @@ -21,6 +21,7 @@ "order": "statics-first" } ], + "no-use-before-declare": false, "ordered-imports": true, "typedef": [ true, diff --git a/yarn.lock b/yarn.lock index ad48479..d51b100 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,6 +16,16 @@ esutils "^2.0.2" js-tokens "^3.0.0" +"@types/commonmark@^0.27.1": + version "0.27.1" + resolved "https://registry.yarnpkg.com/@types/commonmark/-/commonmark-0.27.1.tgz#d40637518c557ceaa5006149b6a0a044a3dffe29" + integrity sha512-3K97drCi/zJB1nExp2fx0L50FVyOUXqAlrcUwfXr2pCbR6S67iPv2bPplVaN21osWZcVBPHU1dIk5XVC4JNRqQ== + +"@types/highlight.js@^9.12.3": + version "9.12.3" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.3.tgz#b672cfaac25cbbc634a0fd92c515f66faa18dbca" + integrity sha512-pGF/zvYOACZ/gLGWdQH8zSwteQS1epp68yRcVLJMgUck/MjEn/FBYmPub9pXT8C1e4a8YZfHo1CKyV8q1vKUnQ== + "@types/jest@^22.2.3": version "22.2.3" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.2.3.tgz#0157c0316dc3722c43a7b71de3fdf3acbccef10d" @@ -29,6 +39,26 @@ resolved "https://registry.yarnpkg.com/@types/parsimmon/-/parsimmon-1.10.0.tgz#ffb81cb023ff435a41d4710a29ab23c561dc9fdf" integrity sha512-bsTIJFVQv7jnvNiC42ld2pQW2KRI+pAG243L+iATvqzy3X6+NH1obz2itRKDZZ8VVhN3wjwYax/VBGCcXzgTqQ== +"@types/prop-types@*": + version "15.5.8" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.8.tgz#8ae4e0ea205fe95c3901a5a1df7f66495e3a56ce" + integrity sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw== + +"@types/react-dom@^16.0.11": + version "16.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.11.tgz#bd10ccb0d9260343f4b9a49d4f7a8330a5c1f081" + integrity sha512-x6zUx9/42B5Kl2Vl9HlopV8JF64wLpX3c+Pst9kc1HgzrsH+mkehe/zmHMQTplIrR48H2gpU7ZqurQolYu8XBA== + dependencies: + "@types/react" "*" + +"@types/react@*": + version "16.7.22" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.22.tgz#5bc6d166d5ac34b835756f0b736c7b1af0043e81" + integrity sha512-j/3tVoY09kHcTfbia4l67ofQn9xvktUvlC/4QN0KuBHAXlbU/wuGKMb8WfEb/vIcWxsOxHv559uYprkFDFfP8Q== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + abab@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -625,6 +655,16 @@ commander@^2.12.1: version "2.15.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" +commonmark@^0.28.1: + version "0.28.1" + resolved "https://registry.yarnpkg.com/commonmark/-/commonmark-0.28.1.tgz#06eab8d52338b839fa1a2d75af0085eed1b1beae" + integrity sha1-Buq41SM4uDn6Gi11rwCF7tGxvq4= + dependencies: + entities "~ 1.1.1" + mdurl "~ 1.0.1" + minimist "~ 1.2.0" + string.prototype.repeat "^0.2.0" + compare-versions@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.1.0.tgz#43310256a5c555aaed4193c04d8f154cf9c6efd5" @@ -707,6 +747,11 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": dependencies: cssom "0.3.x" +csstype@^2.2.0, csstype@^2.4.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.2.tgz#3043d5e065454579afc7478a18de41909c8a2f01" + integrity sha512-Rl7PvTae0pflc1YtxtKbiSqq20Ts6vpIYOD5WBafl4y123DyHUeLrRdQP66sQW8/6gmX8jrYJLXwNeMqYVJcow== + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -848,6 +893,11 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +"entities@~ 1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" @@ -1128,6 +1178,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +free-style@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/free-style/-/free-style-2.5.1.tgz#5e3f684c470d3bba7e4dbb43524e0a08917e873c" + integrity sha512-X7dtUSTrlS1KRQBtiQ618NWIRDdRgD91IeajKCSh0fgTqArSixv+n3ea6F/OSvrvg14tPLR+yCq2s+O602+pRw== + from@~0: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" @@ -1345,6 +1400,11 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" +highlight.js@^9.14.2: + version "9.14.2" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.14.2.tgz#efbfb22dc701406e4da406056ef8c2b70ebe5b26" + integrity sha512-Nc6YNECYpxyJABGYJAyw7dBAYbXEuIzwzkqoJnwbc1nIpCiN+3ioYf0XrBnLiyyG0JLuJhpPtt2iTSbXiKLoyA== + hoek@4.x.x: version "4.2.1" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" @@ -1969,6 +2029,11 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-yaml@^3.7.0: version "3.11.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" @@ -2156,6 +2221,13 @@ loose-envify@^1.0.0: dependencies: js-tokens "^3.0.0" +loose-envify@^1.1.0, loose-envify@^1.3.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + lru-cache@^4.0.1: version "4.1.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" @@ -2191,6 +2263,11 @@ math-random@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac" +"mdurl@~ 1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= + mem@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" @@ -2271,7 +2348,7 @@ minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0: +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, "minimist@~ 1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -2405,6 +2482,11 @@ normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" +normalize.css@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3" + integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg== + npm-bundled@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308" @@ -2457,7 +2539,7 @@ oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" -object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -2706,6 +2788,14 @@ process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" +prop-types@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" + integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== + dependencies: + loose-envify "^1.3.1" + object-assign "^4.1.1" + ps-tree@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014" @@ -2745,6 +2835,26 @@ rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-dom@^16.7.0: + version "16.7.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.7.0.tgz#a17b2a7ca89ee7390bc1ed5eb81783c7461748b8" + integrity sha512-D0Ufv1ExCAmF38P2Uh1lwpminZFRXEINJe53zRAbm4KPwSyd6DY/uDoS0Blj9jvPpn1+wivKpZYc8aAAN/nAkg== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.12.0" + +react@^16.7.0: + version "16.7.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.7.0.tgz#b674ec396b0a5715873b350446f7ea0802ab6381" + integrity sha512-StCz3QY8lxTb5cl2HJxjwLFOXPIFQp+p+hxQfc8WE0QiLfCtIlKj8/+5tjjKm8uSTlAW+fCPaavGFS06V9Ar3A== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.12.0" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -2963,6 +3073,14 @@ sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" +scheduler@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.12.0.tgz#8ab17699939c0aedc5a196a657743c496538647b" + integrity sha512-t7MBR28Akcp4Jm+QoR63XgAi9YgCUmgvDHqf5otgAj4QvdoBE4ImCX0ffehefePPG+aitiYHp0g/mW6s4Tp+dw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" @@ -3208,6 +3326,11 @@ string.prototype.padend@^3.0.0: es-abstract "^1.4.3" function-bind "^1.0.2" +string.prototype.repeat@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz#aba36de08dcee6a5a337d49b2ea1da1b28fc0ecf" + integrity sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8= + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -3504,6 +3627,14 @@ typescript@~2.9.1: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w== +typestyle@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/typestyle/-/typestyle-2.0.1.tgz#9593e6029ce5f23f1a5728ddd3ed6bf1ffa781f8" + integrity sha512-3Mv5ZZbYJ3y3G6rX3iRLrgYibAlafK2nsc9VlTsYcEaK8w+9vtNDx0T2TJsznI5FIh+WoBnjJ5F0/26WaGRxXQ== + dependencies: + csstype "^2.4.0" + free-style "2.5.1" + uglify-js@^2.6: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"