diff options
-rw-r--r-- | bs5/server/dune | 3 | ||||
-rw-r--r-- | bs5/server/pages/Index.re | 216 | ||||
-rw-r--r-- | bs5/server/pages/dune | 2 | ||||
-rw-r--r-- | bs5/server/rsc/Date.ml | 15 | ||||
-rw-r--r-- | bs5/server/rsc/Markdown.ml | 202 |
5 files changed, 426 insertions, 12 deletions
diff --git a/bs5/server/dune b/bs5/server/dune index 9565f90..7a838ba 100644 --- a/bs5/server/dune +++ b/bs5/server/dune @@ -13,6 +13,9 @@ rsc pages ; + ; sister packages + shared + ; dream server-reason-react.belt server-reason-react.js diff --git a/bs5/server/pages/Index.re b/bs5/server/pages/Index.re index d5af822..685069a 100644 --- a/bs5/server/pages/Index.re +++ b/bs5/server/pages/Index.re @@ -1,16 +1,208 @@ +open Rsc; +module Section = { + [@react.component] + let make = (~title, ~children, ~description=?) => { + <Stack gap=2 justify=`start> + <h2 + className={Cx.make([ + "text-3xl", + "font-bold", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string(title)} + </h2> + {switch (description) { + | Some(description) => + <Text color=Theme.Color.Gray10> description </Text> + | None => React.null + }} + <div className="mb-4" /> + children + </Stack>; + }; +}; + +module ExpandedContent = { + [@react.component] + let make = (~id, ~content: string, ~updatedAt: float, ~title: string) => { + let lastUpdatedAt = + if (Date.is_today(updatedAt)) { + Date.format_time(updatedAt); + } else { + Date.format_date(updatedAt); + }; + + let summary = + content |> Markdown.extract_text |> Markdown.summarize(~words=20); + + <Expander + id + title + expandedChildren={ + <div className="mt-2"> + {switch (String.trim(summary)) { + | "" => <i> {React.string("(No content)")} </i> + | s => <Text size=Small color=Theme.Color.Gray11> s </Text> + }} + <Counter.Double initial=22 /> + </div> + }> + <header + className={Cx.make(["max-w-[85%] flex flex-col gap-2"])} + style={ReactDOM.Style.make(~zIndex="1", ())}> + <Text size=Large weight=Bold> title </Text> + <Text size=Small> lastUpdatedAt </Text> + </header> + </Expander>; + }; +}; + module Page = { [@react.async.component] let make = () => { - // let promiseIn2 = - // Lwt.bind(Lwt_unix.sleep(2.0), _ => - // Lwt.return("Solusionao in 2 seconds!") - // ); - // let promiseIn4 = - // Lwt.bind(Lwt_unix.sleep(4.0), _ => - // Lwt.return("Solusionao in 4 seconds!") - // ); + let promiseIn2 = + Lwt.bind(Lwt_unix.sleep(2.0), _ => + Lwt.return("Solusionao in 2 seconds!") + ); + let promiseIn4 = + Lwt.bind(Lwt_unix.sleep(4.0), _ => + Lwt.return("Solusionao in 4 seconds!") + ); + Lwt.return( - <div> {React.string("Well hi")} </div>, + <Stack gap=8 justify=`start> + <Stack gap=2 justify=`start> + <h1 + className={Cx.make([ + "text-3xl", + "font-bold", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string( + "Server side rendering server components and client components", + )} + </h1> + <Text color=Theme.Color.Gray10> + "React server components. Lazy loading of client components. Client props encodings, such as promises, React elements, and primitive types." + </Text> + </Stack> + <Hr /> + <Section + title="Counter" + description="Passing int into a client component, the counter starts at 45 and counts by one"> + <Counter initial=45 /> + </Section> + <Hr /> + <Section + title="Debug client props" + description="Passing client props into a client component"> + <Debug_props + string="Title" + float=1.1 + bool_true=true + bool_false=false + header={Some(<div> {React.string("H E A D E R")} </div>)} + string_list=["Item 1", "Item 2"] + promise=promiseIn2> + <div> + {React.string( + "This footer is a React.element as a server component into client prop, yay!", + )} + </div> + </Debug_props> + </Section> + <Hr /> + <Section + title="Debug client props" + description="Passing client props into a client component"> + <Debug_props + string="Title" + int=99 + float=1.1 + bool_true=true + bool_false=false + header={Some(<div> {React.string("H E A D E R")} </div>)} + string_list=["Item 1", "Item 2"] + promise=promiseIn2> + <div> + {React.string( + "This footer is a React.element as a server component into client prop, yay!", + )} + </div> + </Debug_props> + </Section> + <Hr /> + <Section + title="Pass another promise prop" + description="Sending a promise from the server to the client"> + <Promise_renderer promise=promiseIn4 /> + </Section> + <Hr /> + <Section + title="Pass a client component prop" + description="Sending a client component from the server to the client (that contains another client component)"> + <ExpandedContent + id=1 + title="Titulaso" + updatedAt=1653561600.0 + content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + /> + </Section> + <Hr /> + <h1 + className={Cx.make([ + "text-5xl", + "font-bold", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string("Server functions")} + </h1> + <Hr /> + <Section + title="Server function from props on a Client Component" + description="In this case, react will use the server function from the window.__server_functions_manifest_map"> + <ServerActionFromPropsClient + actionOnClick=ServerFunctions.simpleResponse + /> + </Section> + <Hr /> + <Section + title="Server function with simple response" + description="Server function imported and called directly on a client component"> + <ServerActionWithSimpleResponse /> + </Section> + <Hr /> + <Section + title="Server function with error" + description="Server function with error"> + <ServerActionWithError /> + </Section> + <Hr /> + <Section + title="Server function with FormData" + description="Server function with FormData"> + <ServerActionWithFormData /> + </Section> + <Hr /> + <Section + title="Server function with FormData on action attribute on Server Component" + description="In this case, react will use the server function from the window.__server_functions_manifest_map"> + <ServerActionWithFormDataServer /> + </Section> + <Hr /> + <Section + title="Server function with FormData on formAction attribute on Server Component" + description="In this case, react will use the server function from the window.__server_functions_manifest_map"> + <ServerActionWithFormDataFormAction /> + </Section> + <Hr /> + <Section + title="Server function with FormData with extra arg" + description="It shows that it's possible to pass extra arguments to the server function on forms"> + <ServerActionWithFormDataWithArg /> + </Section> + <Hr /> + </Stack>, ); }; }; @@ -25,15 +217,15 @@ module App = { </head> <body> <div id="root"> - // <DemoLayout background=Theme.Color.Gray2> <Page /> </DemoLayout> - <div> <Page /> </div> </div> + <DemoLayout background=Theme.Color.Gray2> <Page /> </DemoLayout> + </div> </body> </html>; }; }; let handler = request => - Rsc.DreamRSC.create_from_request( + DreamRSC.create_from_request( ~bootstrap_modules=["/static/demo/SinglePageRSC.re.js"], <App />, request, diff --git a/bs5/server/pages/dune b/bs5/server/pages/dune index bc63199..47c4f2b 100644 --- a/bs5/server/pages/dune +++ b/bs5/server/pages/dune @@ -2,6 +2,8 @@ (name pages) (libraries rsc + shared + ; dream lwt.unix server-reason-react.belt diff --git a/bs5/server/rsc/Date.ml b/bs5/server/rsc/Date.ml new file mode 100644 index 0000000..5abf9c3 --- /dev/null +++ b/bs5/server/rsc/Date.ml @@ -0,0 +1,15 @@ +let is_today date = + let now = Unix.localtime (Unix.time ()) in + let d = Unix.localtime date in + now.tm_year = d.tm_year && now.tm_mon = d.tm_mon && now.tm_mday = d.tm_mday + +let format_time date = + let t = Unix.localtime date in + let hour = t.tm_hour mod 12 in + let hour = if hour = 0 then 12 else hour in + let ampm = if t.tm_hour >= 12 then "pm" else "am" in + Printf.sprintf "%d:%02d %s" hour t.tm_min ampm + +let format_date date = + let t = Unix.localtime date in + Printf.sprintf "%d/%d/%02d" (t.tm_mon + 1) t.tm_mday (t.tm_year mod 100) diff --git a/bs5/server/rsc/Markdown.ml b/bs5/server/rsc/Markdown.ml new file mode 100644 index 0000000..e8c8942 --- /dev/null +++ b/bs5/server/rsc/Markdown.ml @@ -0,0 +1,202 @@ +module List = struct + include List + let rec take lst n = + match (lst, n) with + | ([], _) -> [] + | (_, 0) -> [] + | (x :: xs, n) -> x :: take xs (n - 1) +end + +let convert_headings text = + text + |> Str.global_replace (Str.regexp "^#### \\(.*\\)$") "<h4>\\1</h4>" + |> Str.global_replace (Str.regexp "^### \\(.*\\)$") "<h3>\\1</h3>" + |> Str.global_replace (Str.regexp "^## \\(.*\\)$") "<h2>\\1</h2>" + |> Str.global_replace (Str.regexp "^# \\(.*\\)$") "<h1>\\1</h1>" + +let convert_emphasis text = + text + |> Str.global_replace + (Str.regexp "\\*\\*\\([^*]*\\)\\*\\*") + "<strong>\\1</strong>" + |> Str.global_replace + (Str.regexp "__\\([^_]*\\)__") + "<strong>\\1</strong>" + |> Str.global_replace (Str.regexp "\\*\\([^*]*\\)\\*") "<em>\\1</em>" + |> Str.global_replace (Str.regexp "_\\([^_]*\\)_") "<em>\\1</em>" + +let convert_code text = + text + |> Str.global_replace + (Str.regexp "```\\([^`]*\\)```") + "<pre><code>\\1</code></pre>" + |> Str.global_replace (Str.regexp "`\\([^`]*\\)`") "<code>\\1</code>" + +let convert_links text = + text + |> Str.global_replace + (Str.regexp "\\[\\([^]]*\\)\\](\\([^)]*\\))") + "<a href=\"\\2\">\\1</a>" + +let convert_lists text = + let lines = String.split_on_char '\n' text in + + let process_line line = + match line with + | line when Str.string_match (Str.regexp "^-\\s*\\(.*\\)$") line 0 -> + "<li>" ^ Str.matched_group 1 line ^ "</li>" + | line when Str.string_match (Str.regexp "^\\+\\s*\\(.*\\)$") line 0 -> + "<li>" ^ Str.matched_group 1 line ^ "</li>" + | line when Str.string_match (Str.regexp "^\\*\\s*\\(.*\\)$") line 0 -> + "<li>" ^ Str.matched_group 1 line ^ "</li>" + | line + when Str.string_match (Str.regexp "^\\d+\\.\\s*\\(.*\\)$") line 0 -> + "<li>" ^ Str.matched_group 1 line ^ "</li>" + | _ -> line + in + + let wrap_consecutive_items lines = + let rec aux acc current_list lines = + match (current_list, lines) with + | ([], []) -> List.rev acc + | (hd :: tl, []) -> + List.rev [ + "<ul>" ^ String.concat "\n" (List.rev (hd :: tl)) ^ "</ul>"; + ] @ acc + | ([], line :: rest) -> + if Str.string_match (Str.regexp "^<li>") line 0 then + aux acc [line] rest + else + aux (line :: acc) [] rest + | (items, line :: rest) -> + if Str.string_match (Str.regexp "^<li>") line 0 then + aux acc (line :: current_list) rest + else + aux + (line :: ("<ul>" ^ String.concat "\n" (List.rev items) ^ "</ul>") :: acc) + [] + rest + in + aux [] [] lines + in + + lines + |> List.map process_line + |> wrap_consecutive_items + |> String.concat "\n" + +let wrap_lists text = + text + |> Str.global_replace + (Str.regexp "<li>.*</li>\\(\n<li>.*</li>\\)*") + "<ul>\\0</ul>" + +let convert_blockquotes text = + let lines = String.split_on_char '\n' text in + + let rec process_lines acc in_quote lines = + match lines with + | [] when in_quote -> List.rev ("</blockquote>" :: acc) + | [] -> List.rev acc + | line :: rest -> + let trimmed = String.trim line in + if Str.string_match (Str.regexp "^>\\s*\\(.*\\)$") trimmed 0 then + let content = Str.matched_group 1 trimmed in + if in_quote then + process_lines (content :: acc) true rest + else + process_lines (content :: "<blockquote>" :: acc) true rest + else if trimmed = "" then + if in_quote then + process_lines ("</blockquote>" :: acc) false rest + else + process_lines (line :: acc) false rest + else if in_quote then + process_lines (line :: acc) true rest + else + process_lines (line :: acc) false rest + in + + lines |> process_lines [] false |> String.concat "\n" + +let convert_paragraphs text = + let lines = String.split_on_char '\n' text in + + let is_block_element line = + Str.string_match + (Str.regexp "^<\\(h[1-6]\\|ul\\|ol\\|blockquote\\|pre\\)>") + line + 0 + in + + let wrap_paragraphs lines = + let rec aux acc current_p lines = + match lines with + | [] when current_p <> "" -> + List.rev (("<p>" ^ current_p ^ "</p>") :: acc) + | [] -> List.rev acc + | line :: rest when is_block_element line -> + if current_p <> "" then + aux (line :: ("<p>" ^ current_p ^ "</p>") :: acc) "" rest + else + aux (line :: acc) "" rest + | line :: rest when String.trim line = "" -> + if current_p <> "" then + aux (("<p>" ^ current_p ^ "</p>") :: acc) "" rest + else + aux acc "" rest + | line :: rest -> + let sep = + if current_p = "" then + "" + else + " " + in + aux acc (current_p ^ sep ^ String.trim line) rest + in + aux [] "" lines + in + + lines |> wrap_paragraphs |> String.concat "\n" + +let to_html markdown = + markdown + |> convert_headings + |> convert_emphasis + |> convert_code + |> convert_links + |> convert_lists + |> wrap_lists + |> convert_blockquotes + |> convert_paragraphs + |> String.trim + +let extract_text markdown = + markdown + |> Str.global_replace (Str.regexp "\\[([^]]*)\\]\\([^)]*\\)") "\\1" + |> Str.global_replace (Str.regexp "\\*\\*\\([^*]*\\)\\*\\*") "\\1" + |> Str.global_replace (Str.regexp "\\*\\([^*]*\\)\\*") "\\1" + |> Str.global_replace (Str.regexp "__\\([^_]*\\)__") "\\1" + |> Str.global_replace (Str.regexp "_\\([^_]*\\)_") "\\1" + |> Str.global_replace (Str.regexp "~~\\([^~]*\\)~~") "\\1" + |> Str.global_replace (Str.regexp "`\\([^`]*\\)`") "\\1" + |> Str.global_replace (Str.regexp "```[^`]*```") "" + |> Str.global_replace (Str.regexp "^#+ .*$") "\n" + |> Str.global_replace (Str.regexp "^#* .*$") "\n" + |> Str.global_replace (Str.regexp "> \\|>") "" + |> Str.global_replace (Str.regexp "\\[\\|\\]\\|\\(\\|\\)") "" + |> Str.global_replace (Str.regexp "-\\|\\+\\|\\*\\s+") "" + |> Str.global_replace (Str.regexp "^\\d+\\.\\s+") "" + |> Str.global_replace (Str.regexp "\\\\") "" + |> String.trim + +let summarize text ~words:n = + let words = Str.split (Str.regexp "[ \n\r\t]+") text in + let truncated = List.take words n in + let dots = + if List.length words > n then + "..." + else + "" + in + String.concat " " truncated ^ dots
\ No newline at end of file |