aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoris2020-02-13 18:54:18 +0100
committerJoris2020-02-15 10:59:25 +0100
commit7a01b001ccc4a7bda3da92486903540e3f9754fd (patch)
treefbe269faa319fd7f21049612f8a62c0c169f369e /src
parent8328c11ac6706e3adf9b09877c02897132b8927a (diff)
Set up bucklescript
Diffstat (limited to 'src')
-rw-r--r--src/arrayUtils.ml8
-rw-r--r--src/dom.ts54
-rw-r--r--src/domUtils.ml37
-rw-r--r--src/main.ml63
-rw-r--r--src/main.ts71
-rw-r--r--src/number.ml57
-rw-r--r--src/number.ts66
7 files changed, 165 insertions, 191 deletions
diff --git a/src/arrayUtils.ml b/src/arrayUtils.ml
new file mode 100644
index 0000000..75319d8
--- /dev/null
+++ b/src/arrayUtils.ml
@@ -0,0 +1,8 @@
+let flatMap (f : 'a -> 'b option) (xs : 'a Js.Array.t) : 'b Js.Array.t =
+ xs |> Js.Array.map f
+ |> Js.Array.filter (fun maybe ->
+ match maybe with Some _ -> true | None -> false)
+ |> Js.Array.map (fun maybe ->
+ match maybe with
+ | Some x -> x
+ | None -> Js.Exn.raiseError "Unexpected None")
diff --git a/src/dom.ts b/src/dom.ts
deleted file mode 100644
index 6b1c803..0000000
--- a/src/dom.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-type Attribute = string | boolean | ((e: Event) => void)
-
-type Child = Element | string
-
-export function h(tag: string, attrs: {[key: string]: Attribute}, children: Array<Child> = []): Element {
- let element = document.createElement(tag)
-
- for (let name in attrs) {
- let value = attrs[name]
- if (typeof value === 'boolean') {
- if (value) {
- element.setAttribute(name, name)
- }
- } else if (typeof value === 'function') {
- (element as any)[name] = (e: Event) => {
- (value as ((e: Event) => void))(e)
- }
- } else {
- element.setAttribute(name, value)
- }
- }
-
- children.forEach(child => {
- if (typeof child === 'string') {
- element.appendChild(document.createTextNode(child))
- } else {
- element.appendChild(child)
- }
- })
-
- return element
-}
-
-export function toggleClassName(node: Element, className: string) {
- if (node.className === className) {
- node.className = ''
- } else {
- node.className = className
- }
-}
-
-export function nodeListToArray(nodeList: NodeListOf<HTMLElement>): HTMLElement[] {
- const xs: HTMLElement[] = [];
- nodeList.forEach(node => xs.push(node))
- return xs
-}
-
-export function replace(node: Node, replacement: Node) {
- const parentNode = node.parentNode
-
- if (parentNode) {
- parentNode.replaceChild(replacement, node)
- }
-}
diff --git a/src/domUtils.ml b/src/domUtils.ml
new file mode 100644
index 0000000..282ac12
--- /dev/null
+++ b/src/domUtils.ml
@@ -0,0 +1,37 @@
+open Webapi.Dom
+
+let toggleClassName (element : Dom.element) (className : string) : unit =
+ Element.setClassName element
+ (if Element.className element == className then "" else className)
+
+type child = TextChild of string | ElemChild of Dom.element
+
+let h (tag : string) (attributes : (string * string) Js.Array.t)
+ (children : child Js.Array.t) : Dom.element =
+ let element = Document.createElement tag document in
+ let () =
+ attributes
+ |> Js.Array.forEach (fun a -> Element.setAttribute (fst a) (snd a) element)
+ in
+ let () =
+ children
+ |> Js.Array.forEach (fun c ->
+ match c with
+ | TextChild t ->
+ Element.appendChild (Document.createTextNode t document) element
+ | ElemChild e -> Element.appendChild e element)
+ in
+ element
+
+external replace_child : Dom.node -> Dom.element -> Dom.element -> unit
+ = "replaceChild"
+ [@@bs.send]
+
+let replace (element : Dom.element) (replacement : Dom.element) : unit =
+ match Element.parentNode element with
+ | Some parent -> replace_child parent replacement element
+ | _ -> ()
+
+external value : Dom.eventTarget -> string option = "value" [@@bs.get]
+
+external setValue : Dom.element -> string -> unit = "value" [@@bs.set]
diff --git a/src/main.ml b/src/main.ml
new file mode 100644
index 0000000..a2174f2
--- /dev/null
+++ b/src/main.ml
@@ -0,0 +1,63 @@
+open Webapi.Dom
+
+(* Set up inputs for the ingredients *)
+
+type ingredient = { quantity : float; element : Dom.element }
+
+let ingredients : ingredient Js.Array.t =
+ document
+ |> Document.querySelectorAll ".g-Recipe__Content ul > li"
+ |> NodeList.toArray
+ |> ArrayUtils.flatMap (fun node ->
+ Belt.Option.map (Element.ofNode node) (fun e -> ("li", e)))
+ |> Js.Array.concat
+ ( match Document.querySelector ".g-Recipe__Content h1" document with
+ | Some element -> [| ("h1", element) |]
+ | _ -> [||] )
+ |> ArrayUtils.flatMap (fun (tag, element) ->
+ Belt.Option.map
+ (Number.parseInsideText (Element.innerHTML element))
+ (fun parsed ->
+ let created = Number.createElement tag parsed in
+ let () = DomUtils.replace element created.element in
+ { quantity = parsed.number; element = created.numberInput }))
+
+(* Update ingredients amounts *)
+
+let () =
+ ingredients
+ |> Js.Array.forEach (fun ingredient ->
+ Element.addEventListener "input"
+ (fun e ->
+ Belt.Option.forEach
+ (DomUtils.value (Event.target e))
+ (fun numberStr ->
+ Belt.Option.forEach (Number.parse numberStr) (fun parsed ->
+ let factor = parsed.number /. ingredient.quantity in
+ ingredients
+ |> Js.Array.forEach (fun otherIngredient ->
+ if ingredient.element != otherIngredient.element
+ then
+ DomUtils.setValue otherIngredient.element
+ (Number.prettyPrint
+ (factor *. otherIngredient.quantity))
+ else ()))))
+ ingredient.element)
+
+(* Set up done marks for steps *)
+
+let () =
+ document
+ |> Document.querySelectorAll ".g-Recipe__Content ol > li"
+ |> NodeList.toArray
+ |> Js.Array.forEach (fun node ->
+ match Element.ofNode node with
+ | Some element ->
+ Element.addEventListener "click"
+ (fun e ->
+ let () =
+ DomUtils.toggleClassName element "g-Recipe__Completed"
+ in
+ Event.stopPropagation e)
+ element
+ | _ -> ())
diff --git a/src/main.ts b/src/main.ts
deleted file mode 100644
index 184d26d..0000000
--- a/src/main.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import * as number from './number'
-import * as dom from './dom'
-
-/* Set up inputs for the ingredients */
-
-const itemEntries =
- dom.nodeListToArray(document.querySelectorAll('.g-Recipe__Content ul > li'))
- .map(itemNode => ({ name: 'li', node: itemNode }))
-
-const h1 = document.querySelector<HTMLElement>('.g-Recipe__Content h1')
-
-if (h1 !== null) {
- itemEntries.push({ name: 'h1', node: h1 })
-}
-
-const inputs = setupInputs(itemEntries)
-
-inputs.map(input => {
- input.node.oninput = e => {
- if (e.target !==null) {
- const parsed = number.parse((e.target as HTMLInputElement).value)
-
- if (parsed !== undefined) {
- const factor = parsed.number / input.number
- inputs.map(input2 => {
- if (input.node !== input2.node) {
- input2.node.value = number.prettyPrint(input2.number * factor)
- }
- })
- }
- }
- }
-})
-
-interface InputTag {
- name: string;
- node: HTMLElement;
-}
-
-interface InputResult {
- number: number,
- node: HTMLInputElement
-}
-
-function setupInputs(tags: InputTag[]): InputResult[] {
- const res: InputResult[] = []
-
- tags.forEach(tag => {
- const parsed = number.parseInsideText(tag.node.innerText)
-
- if (parsed !== undefined) {
- const numberNode = number.node(tag.name, parsed)
- dom.replace(tag.node, numberNode.node)
- res.push({
- number: parsed.number,
- node: numberNode.numberInput
- })
- }
- })
-
- return res
-}
-
-/* Set up done marks for steps */
-
-dom.nodeListToArray(document.querySelectorAll('.g-Recipe__Content ol > li')).forEach(todo => {
- todo.onclick = e => {
- dom.toggleClassName(todo, 'g-Recipe__Completed')
- e.stopPropagation()
- }
-})
diff --git a/src/number.ml b/src/number.ml
new file mode 100644
index 0000000..cdd9ef8
--- /dev/null
+++ b/src/number.ml
@@ -0,0 +1,57 @@
+type parseInsideTextResult = { before : string; number : float; after : string }
+
+let execRegex (regex : Js.Re.t) (str : string) : string option Js.Array.t =
+ match Js.Re.exec_ regex str with
+ | Some result -> Js.Array.map Js.toOption (Js.Re.captures result)
+ | None -> [||]
+
+let parseInsideText (str : string) : parseInsideTextResult option =
+ match execRegex [%re "/^([^\\d]*)(\\d+)((\\.|,)(\\d+))?(.*)/"] str with
+ | [| _; Some before; Some intPart; _; _; decPart; Some after |] ->
+ Some
+ {
+ before;
+ number =
+ Js.Float.fromString
+ ( intPart
+ ^ Belt.Option.mapWithDefault decPart "" (fun str -> "." ^ str) );
+ after;
+ }
+ | _ -> None
+
+type parseResult = { number : float; remaining : string }
+
+let parse (str : string) : parseResult option =
+ match parseInsideText str with
+ | Some parseResult ->
+ if parseResult.before == "" then
+ Some { number = parseResult.number; remaining = parseResult.after }
+ else None
+ | _ -> None
+
+type numberElement = { element : Dom.element; numberInput : Dom.element }
+
+let prettyPrint (number : float) : string =
+ let strNumber = Js.Float.toString number in
+ match Js.String.split "." strNumber with
+ | [| intPart; decPart |] ->
+ intPart ^ "," ^ Js.String.slice ~from:0 ~to_:2 decPart
+ | _ -> strNumber
+
+let createElement (tag : string) (content : parseInsideTextResult) :
+ numberElement =
+ let numberInput =
+ DomUtils.h "input"
+ [| ("class", "g-Number"); ("value", prettyPrint content.number) |]
+ [| DomUtils.TextChild "" |]
+ in
+ {
+ element =
+ DomUtils.h tag [||]
+ [|
+ DomUtils.TextChild content.before;
+ DomUtils.ElemChild numberInput;
+ DomUtils.TextChild content.after;
+ |];
+ numberInput;
+ }
diff --git a/src/number.ts b/src/number.ts
deleted file mode 100644
index 6663329..0000000
--- a/src/number.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { h } from './dom'
-
-interface ParseInsideTextResult {
- before: string;
- number: number;
- after: string;
-}
-
-export function parseInsideText(str: string): ParseInsideTextResult | undefined {
- let res = str.match(/^([^\d]*)(\d+)((\.|,)(\d+))?(.*)/)
-
- if (res !== null && res.length === 7) {
- return {
- before: res[1],
- number: parseFloat(res[2] + '.' + res[5]),
- after: res[6]
- }
- } else {
- return undefined;
- }
-}
-
-interface ParseResult {
- number: number;
- remaining: string;
-}
-
-export function parse(str: string): ParseResult | undefined {
- let res = str.match(/^(\d+)((\.|,)(\d+))?(.*$)/)
-
- if (res !== null && res.length === 6) {
- return {
- number: parseFloat(res[1] + '.' + res[4]),
- remaining: res[5]
- }
- } else {
- return undefined;
- }
-}
-
-export interface Node {
- node: Element;
- numberInput: HTMLInputElement;
-}
-
-export function node(tag: string, content: ParseInsideTextResult): Node {
- let numberInput = h('input', {
- 'class': 'g-Number',
- 'value': prettyPrint(content.number)
- }) as HTMLInputElement
-
- return {
- node: h(tag, {}, [content.before, numberInput, content.after]),
- numberInput: numberInput
- }
-}
-
-export function prettyPrint(n: number): string {
- const xs = n.toString().split('.')
-
- if (xs.length == 2) {
- return xs[0] + ',' + xs[1].substring(0, 2)
- } else {
- return xs[0]
- }
-}