diff --git a/packages/glow/package.json b/packages/glow/package.json index 0557b953..9aa715b9 100644 --- a/packages/glow/package.json +++ b/packages/glow/package.json @@ -1,6 +1,6 @@ { "name": "nue-glow", - "version": "0.2.0", + "version": "0.2.1", "description": "Tiny and powerful Markdown syntax highlighter", "homepage": "https://nuejs.org/blog/introducing-glow", "license": "MIT", diff --git a/packages/glow/src/glow.js b/packages/glow/src/glow.js index b8c1aa74..050552f2 100644 --- a/packages/glow/src/glow.js +++ b/packages/glow/src/glow.js @@ -15,14 +15,19 @@ const COMMON_WORDS = 'null|true|false|undefined|import|from|async|await|package| const SPECIAL_WORDS = { cpp: 'cout|cin|using|namespace', python: 'None|nonlocal|lambda', - go: 'chan|fallthrough', - css: 'important' + go: 'chan|fallthrough' } // special rules (growing list) const RULES = { css: [ { tag: 'strong', re: /#[0-9a-f]{3,7}/gi }, + { tag: 'label', re: /!important/gi }, + { tag: 'em', re: /--[\w\d\-]+/gi }, + ], + + json: [ + { tag: 'b', re: /(".+"):/gi }, ] } @@ -39,14 +44,13 @@ const HTML_TAGS = [ // HTML tag name { tag: 'strong', re: /<([\w\-]+ )/g, shift: true, lang: MIXED_HTML }, - { tag: 'strong', re: /<\/?([\w\-]+)>/g, shift: true, lang: MIXED_HTML }, // ALL CAPS (constants) { tag: 'b', re: /\b[A-Z]{2,}\b/g }, // @special - { tag: 'label', re: /\B@[\w]+/gi }, + { tag: 'label', re: /\B@[\w\-]+/gi }, // char { tag: 'i', re: /[^\w •]/g }, @@ -98,8 +102,8 @@ function elem(name, str) { } /* - Markdown code block inside Markdown is so different, - that it requires a special treatment + Markdown/MDX requires a special treatment, because it's so + different from others (not a programming language) */ function isMD(lang) { return ['md', 'mdx', 'nuemark'].includes(lang) @@ -230,15 +234,6 @@ export function parseSyntax(str, lang) { } str.split(/\r\n|\r|\n/).forEach((line, i) => { - - // hack to join lines when there was newline in the middle of a line - const quote = /^("|')/.exec(line) - if (quote && line[1] != quote[0]) { - const prev = lines[lines.length -1] - if (prev?.line) prev.line += '\\n' + line - return - } - if (!comment) { if (comm_start.test(line)) { comment = [line] @@ -262,6 +257,7 @@ export function parseSyntax(str, lang) { } }) + return lines } diff --git a/packages/glow/test/generate.js b/packages/glow/test/generate.js index 1d4a3ae7..a8931411 100644 --- a/packages/glow/test/generate.js +++ b/packages/glow/test/generate.js @@ -49,24 +49,30 @@ const CSS = ` /* Let's check out CSS code */ .syntax { border: 1px solid #fff1 !important; - background-color: #20293A; - border-radius: 4px; + background-color: var(--base-600); + border-radius: var(--radius); margin-bottom: 3em; - header { - border-bottom: 1px solid #fff1; - padding: .7em 1.5em; + @media(width < 900) { + transform: scale(1.1); + filter: blur(4px); + } + + @starting-style { + transition: transform 4s; } } ` const JAVASCRIPT = ` +"use strict" + // import some UI stuff import { layout } from 'components/layout' // environment const ENV = { scripts: ['lol.js'], - styles: ['lmao.css'], + styles: ['lma\\no.css'], desc: undefined } @@ -247,6 +253,20 @@ func main() { } ` +const JSON = ` +{ + "author": "John Doe ", + "keywords": ["json", "es5"], + "version": 1.5, + "keywords": ["json", "json5"], + "version": 1.7, + + "scripts": { + "test": "mocha --ui exports --reporter spec", + "build": "./lib/cli.js -c package.json5", + } +} +` const JSON5 = ` { @@ -669,7 +689,8 @@ await renderPage([ { title: 'Haskell', code: HASKELL, }, { title: 'HTML', lang: 'html', code: HTML, }, { title: 'Java', code: JAVA, lang: 'java' }, - { title: 'JavaScript', code: JAVASCRIPT, lang: 'js numbered', }, + { title: 'JavaScript', code: JAVASCRIPT, lang: 'js', }, + { title: 'JSON', code: JSON, lang: 'json', }, { title: 'JSON5', code: JSON5, lang: 'json5', }, { title: 'JSX', code: JSX, lang: 'jsx' }, { title: 'Julia', code: JULIA, lang: 'julia' }, @@ -692,7 +713,8 @@ await renderPage([ { title: 'TypeScript', code: TS, lang: 'ts', }, { title: 'ZIG', code: ZIG, lang: 'zig', }, - ] // .filter(el => ['html'].includes(el.lang)) + ].filter(el => ['js'].includes(el.lang)) + // ] ) diff --git a/packages/glow/test/glow-test.html b/packages/glow/test/glow-test.html index 75f658d9..f790af2d 100644 --- a/packages/glow/test/glow-test.html +++ b/packages/glow/test/glow-test.html @@ -1,242 +1,17 @@ -
-

Astro

-
--
-import MyComponent from "./MyComponent.astro";
-const items = ["Dog", "Cat", "Platypus"];
---
-
-<ul>
-  {items.map((item) => (
-    <li>{item}</li>
-  ))}
-</ul>
-
-<!-- renders as <div>Hello!</div> -->
-<Element>
-  <p>Hello</p>
-  <p>Hello!</p>
-</Element>
-
- - -
-

C#

-
public void MyTaskAsync(string[] files) {
-
-  MyTaskWorker worker = new MyTaskWorker(MyTaskWorker);
-  AsyncCallback fooback = new AsyncCallback(MyTask);
-
-  lock (_sync) {
-    if (_myTaskIsRunning)
-      throw new OperationException(
-        "The control is busy."
-      );
-
-    // one-line comment here
-    AsyncOperation async = Async.CreateOperation(null);
-    bool cancelled;
-
-    worker.BeginInvoke(files, context, out cancelled);
-
-    _myTaskIsRunning = true;
-    _myTaskContext = context;
-  }
-}
-
- - -
-

C++

-
#include <iostream>
-using namespace std;
-
-int main() {
-
-  int first_number, second_number, sum;
-
-  cout << "Enter two integers: ";
-  cin >> first_number >> second_number;
-
-  // sum of two numbers in stored
-  sum = first_number + second_number;
-
-  // prints sum
-  cout << first_number << " + "
-    <<  second_number << " = " << sum;
-
-  return 0;
-}
-
- - -
-

Clojure Script

-
(ns clojure.examples.hello
-   (:genclass))
-
-;; This program displays Hello World
-(defn Example []
-  (println [+ 1 2 3]))
-(Example)
-
-:dev-http {8080 "public"}
-  :builds
-  {:app
-    {:target :browser
-      :output-dir "public/app/js"
-
- - -
-

Crystal

-
# A very basic HTTP server
-require "http/server"
-
-server = HTTP::Server.new do |context|
-  context.response.content_type = "text/plain"
-  context.response.print "Hello world, got #{context}"
-end
-
-puts "Listening http://127.0.0.1:8080"
-server.listen(8080)
-
- - -
-

CSS

-
 "../css/dark.css";
-
-/* Let's check out CSS code */
-.syntax {
-  border: 1px solid #fff1 !important;
-  background-color: #20293A;
-  border-radius: 4px;
-  margin-bottom: 3em;
-
-  header {
-    border-bottom: 1px solid #fff1;
-    padding: .7em 1.5em;
-  }
-}
-
- - -
-

GO

-
package main
-
-import "fmt"
-
-// fibonacci is a function that returns a function
-func fibonacci() func() int {
-  f2, f1 := 0, 1
-  return func() int {
-    f := f2
-    f2, f1 = f1, f+f1
-    return f
-  }
-}
-
-func main() {
-  f := fibonacci()
-  for i := 0; i < 10; i++ {
-    fmt.Println(f())
-  }
-}
-
- - -
-

Handlebars

-
{#
-  Mixed Django style comment
-#}
-
-<h1>
-  {{#if quotaFull}}
-    Please come back tomorrow.
-  {{/if}}
-</h1>
-
-<!-- handlebars example -->
-<ul>
-  {{#each serialList}}
-    <li>{{this}}</li>
-  {{/each}}
-</ul>
-
- - -
-

Haskell

-
putTodo :: (Int, String) -> IO ()
-putTodo (n, todo) = putStrLn (show n ++ ": " ++ todo)
-
-prompt :: [String] -> IO ()
-prompt todos = do
-  putStrLn ""
-  putStrLn "Current TODO list:"
-  mapM_ putTodo (zip [0..] todos)
-
-delete :: Int -> [a] -> Maybe [a]
-
- - -
-

HTML

-
<figure ="img" class="baz { foo } ${ bar }">
-  <img loading="lazy" :alt="alt" :src="_ || src">
-
-  <!-- HTML comment here -->
-  <p>I finally made it to the public</p>
-
-  <figcaption :if="caption">{{ caption }}</figcaption>
-
-  <script>
-    constructor(data) {
-      this.caption = data.caption || ''
-    }
-  </script>
-</figure>
-
- - -
-

Java

-
// Importing generic Classes/Files
-import java.io.*;
-
-class GFG {
-
-  // Function to find the biggest of three numbers
-  static int biggestOfThree(int x, int y, int z) {
-    return z > (x > y ? x : y) ? z : ((x > y) ? x : y);
-  }
-
-  // Main driver function
-  public static void main(String[] args) {
-    int a, b, c;
-    a = 5; b = 10; c = 3;
-
-    // Calling the above function in main
-    largest = biggestOfThree(a, b, c);
-  }
-}
-
- -

JavaScript

-
// import some UI stuff
+        
"use strict"
+
+// import some UI stuff
 import { layout } from 'components/layout'
 
 // environment
 const ENV = {
   scripts: ['lol.js'],
-  styles: ['lmao.css'],
+  styles: ['lma\no.css'],
   desc: undefined
 }
 
@@ -246,445 +21,4 @@
 }
- -
-

JSON5

-
{
-  // this is a JSON5 snippet
-  author: 'John Doe <john.doe@gmail.com>',
-  keywords: ['json', 'es5'],
-  version: 1.5,
-  keywords: ['json', 'json5'],
-  version: 1.7,
-
-  scripts: {
-    test: 'mocha --ui exports --reporter spec',
-    build: './lib/cli.js -c package.json5',
-  }
-}
-
- - -
-

JSX

-
import { FormEvent } from 'react';
-
-/*
-  Multi-line comment goes here
-*/
-export default function Page() {
-  async function onSubmit(event: FormEvent<Element>) {
-    const response = await fetch('/api/submit', {
-      method: 'POST',
-      body: formData,
-    });
-  }
-
-  return (
-    <form onSubmit={onSubmit}>
-      <input type="text" name="name" />
-      <button type="submit">Submit</button>
-    </form>
-  );
-}
-
- - -
-

Julia

-
function finalize_ref(r::AbstractRemoteRef)
-  # Check if the finalizer is already run
-
-  if islocked(client_refs) || 100
-      # delay finalizer for later
-      finalizer(finalize_ref, r)
-      return nothing # really nothing
-  end
-
-  t =  begin; sleep(5); println('done\n'); end
-
-  # lock should always be followed by try
-  Threads. for i = 1:10
-    a[i] = Threads.threadid()
-  end
-end
-
- - -
-

Kotlin

-
(DelicateCoroutinesApi::class)
-
-fun main() = runBlocking {
-  val job = GlobalScope.launch {
-    // root coroutine with launch
-    println("Throwing exception from launch")
-    throw IndexOutOfBoundsException()
-  }
-  try {
-    deferred.await()
-    println("Unreached")
-  } catch (e: ArithmeticException) {
-    println("Caught ArithmeticException")
-  }
-}
-
- - -
-

Lua

-
- This here is a comment
-function perm (a)
-  local n = table.getn(a)
-  return coroutine.wrap(function () permgen(a, n) end)
-end
-
-
-- Another function
-function printResult (a)
-  for i,v in ipairs(a) do
-    io.write(v, " ")
-  end
-  io.write("hello")
-end
-
- - -
-

Markdown

-
---
-title: "Lightning CSS might change our thinking"
-tags: [ css, design systems ]
-pubDate: 2024-02-12
----
-
-
-I'm baby truffaut umami wolf small batch iceland
-adaptogen. Iceland **chambray** raclette stumptown
-
-![Hey](/world.png)
-
-> Air plant adaptogen artisan gastropub deep v dreamcatcher
-> Pinterest intelligentsia gluten-free truffaut.
-
- - -
-

MDX

-
import {Chart} from './snowfall.js'
-export const year = 2023
-
-
-
-In {year}, the snowfall was above average.
-It was followed by a warm spring which caused
-flood conditions in many of the nearby rivers.
-
-![Hey](/world.png)
-
-> Air plant adaptogen artisan gastropub deep v dreamcatcher
-> Pinterest intelligentsia gluten-free truffaut.
-
-<Chart year={year} color="#fcb32c" />
-
-<Elemment { ...attr }>
-  <p class="epic">Yo</p>
-</Element>
-
- - -
-

Nim

-
import std/strformat
-
-type
-  Person = object
-    name: string
-    age: Natural # Ensures the age is positive
-
-let people = [
-  Person(name: "John", age: 45),
-  Person(name: "Kate", age: 30)
-]
-
-for person in people:
-  # Type-safe string interpolation,
-  # evaluated at compile time.
-  echo(fmt"{person.name} is {person.age} years old")
-
- - -
-

Nuemark

-
---
-title: Noel's cringe content
-description: Not much to say
-draft: true
----
-
-
-I'm baby truffaut umami wolf small batch iceland
-adaptogen. Iceland **chambray** raclette stumptown
-
-// line comment here
-[ head="Foo | Bar | Baz"]
-  - Content first               | + | + | +
-  - Content collections         | + | + | +
-  - Hot-reloading               | + | + | +
-  - AI content generation       | + | + | +
-
-> This is my blockquote right here
-
-[.]
-  * Nothing here to see
-  * This one is a banger
-
-  ![Hello](/banger.png)
-
-[ loading="eager"]
-  small: "/img/explainer-tall.png"
-  src: "/img/explainer.png"
-  hidden: true
-  width: 800
-
- - -
-

Perl

-
#!/usr/bin/perl
-use warnings;
-use Path::Tiny;
-
-# foo/bar
-my $dir = path('foo','bar');
-
-# Iterate over the content of foo/bar
-my $iter = $dir->iterator;
-
-while (my $file = $iter->()) {
-
-  # Print out the file name and path
-  print "$file";
-}
-
- - -
-

PHP

-
<!DOCTYPE html>
-
-<!-- HTML comment -->
-<form method="get" action="target_proccessor.php">
-  <input type="search" name="search">
-  <input type="submit" name="submit" value="Search">
-
-  <?php
-    // inline PHP comment
-    $camp = array("zero" => "free", "one" => "code" );
-    print_r($camp);
-  ?>
-</form>
-
- - -
-

Python

-
# Function definition
-def find_square(num):
-    result = num * num
-    return result
-
-'''
-This is a multiline comment
-'''
-square = find_square(3) // 2
-
-# Weirdoes
-if (False) continue
-elif (True) nonlocal + zoo
-else None
-
-print('Square:', square)
-
- - -
-

Ruby

-
# line comment here
-def get_numbers_stack(list)
-  stack  = [[0, []]]
-  output = []
-
-  =begin
-    Ruby multiline comments are pretty weirdoes
-    Or maybe not??
-  =end
-  until stack.empty?
-    index, taken = stack.pop
-    next output << taken if index == list.size
-    stack.unshift [index + 1, taken]
-    stack.unshift [index + 1, taken + [list[index]]]
-  end
-  output
-end
-
- - -
-

Rust

-
use std::fmt::{ Debug, Display };
-
-// all drinks are emptied
-fn compare_prints<T: Debug + Display>(t: &T) {
-  println!("Debug: `{:?}`", t);
-}
-
-fn compare_types<T: Debug, U: Debug>(t: &T, u: &U) {
-  println!("t: `{:?}`", t);
-}
-
- - -
-

Shell

-
#!/bin/bash
-
-myfile = 'cars.txt'
-
-touch $myfile
-if [ -f $myfile ]; then
-   rm cars.txt
-   echo "$myfile deleted"
-fi
-
-# open demo on the browser
-open "http://localhost:8080"
-
- - -
-

SQL

-
SELECT
-  date_trunc('week', orderdate),
-  count(1)
-
-FROM orders
-
-WHERE orderdate between '2024-01-01' AND '2024-02-01'
-
-RANK() OVER (ORDER BY __ order_amount DESC)
-
-INNER JOIN payment_status p ON o.status_id = p.id;
-
- - -
-

Styled component

-
import styled from 'styled-components';
-
-const Wrapper = styled.section`
-  background: papayawhip;
-  color: ${aquasky},
-  padding: 4em;
-`;
-
-// Wrapper becomes a React component
-render(
-  <Wrapper defaultOpened="yes">
-    <Title>
-      Hello World!
-    </Title>
-  </Wrapper>
-);
-
- - -
-

Svelte

-
<script>
-  // line comment
-  import Info from './Info.svelte';
-
-  const pkg = {
-    name: 'svelte',
-    version: 3,
-    speed: 'blazing',
-    website: 'https://svelte.dev'
-  };
-</script>
-
-<!-- layout goes here -->
-<p>These styles...</p>
-<Nested />
-<Info {...pkg} />
-
-<style>
-  /* CSS comment */
-  p {
-    color: purple;
-    font-family: 'Comic Sans MS', cursive;
-    font-size: 2em;
-  }
-</style>
-
- - -
-

TOML

-
# This is a TOML document
-
-title = "TOML Example"
-
-[]
-name = "Tom Preston-Werner"
-dob = 1979-05-27T07:32:00-08:00
-
-[]
-enabled = true
-ports = [ 8000, 8001, 8002 ]
-data = [ ["delta", "phi"], [3.14] ]
-temp_targets = { cpu = 79.5, case = 72.0 }
-
- - -
-

TypeScript

-
// user interface
-interface User { name: string;  id: number; }
-
-// account interface
-class UserAccount {
-  name: string;
-  id: number;
-
-  constructor(name: string, id: number) {
-    this.name = name;
-    this.id = id;
-  }
-}
-
-const user: User = new UserAccount("Murphy", 1);
-
- - -
-

ZIG

-
const std = ("std");
-const parseInt = std.fmt.parseInt;
-
-test "parse integers" {
-    const input = "123 67 89,99";
-    const ally = std.testing.allocator;
-
-    // Ensure the list is freed at scope exit
-    defer list.deinit();
-
-    var it = std.mem.tokenizeAny(u8, input, " ,");
-    while (it.next()) |num| {
-        const n = try parseInt(u32, num, 10);
-        try list.append(n); // EOL comment
-    }
-}
-
- \ No newline at end of file diff --git a/packages/nuejs.org/@global/global.css b/packages/nuejs.org/@global/global.css index 68b1cf3d..0b369fe9 100644 --- a/packages/nuejs.org/@global/global.css +++ b/packages/nuejs.org/@global/global.css @@ -117,13 +117,3 @@ h1, h2, h3, h4, h5 { color: var(--gray-900); } -article { - view-transition-name: article; - will-change: transform; -} - -/* view transition (scales down the old page) */ -::view-transition-old(article) { - transform: scale(.8) translateY(4em); - transition: .4s; -} diff --git a/packages/nuejs.org/@global/popover.css b/packages/nuejs.org/@global/popover.css index a48ff4ac..69053868 100644 --- a/packages/nuejs.org/@global/popover.css +++ b/packages/nuejs.org/@global/popover.css @@ -55,6 +55,6 @@ dialog { /* toggle visibility */ &:popover-open { display: block; - z-index: 1;; + z-index: 1; } } \ No newline at end of file diff --git a/packages/nuejs.org/blog/nue-1-beta/index.md b/packages/nuejs.org/blog/nue-1-beta/index.md index 46f3f36d..1bae0fc9 100644 --- a/packages/nuejs.org/blog/nue-1-beta/index.md +++ b/packages/nuejs.org/blog/nue-1-beta/index.md @@ -2,6 +2,7 @@ hero_title: "*Nue 1.0 (Beta)* — A web framework for UX developers" title: Nue 1.0 (Beta) desc: A web framework for UX developers +date: 2024-08-15 --- Exactly one year ago I [decided](/blog/backstory/) to create the slickest website generator for UX developers and design-focused organizations. Today this vision is becoming a reality: diff --git a/packages/nuejs.org/docs/command-line-interface.md b/packages/nuejs.org/docs/command-line-interface.md index 7e0705d3..f656e095 100644 --- a/packages/nuejs.org/docs/command-line-interface.md +++ b/packages/nuejs.org/docs/command-line-interface.md @@ -20,14 +20,13 @@ Usage Commands serve Start development server (default command) build Build the site under - stats Show site statistics - create Create a new website with a starter template + init Re-initialize Nue system directory + create Create a new website with a starter template. See installation docs. Options -r or --root Source directory. Default "." (current working dir) - -p or --production Build production version / Show production stats + -p or --production Build production version -e or --environment Read extra options to override defaults in site.yaml - -s or --stats Show site statistics after current command -I or --init Force clear and initialize output directory -n or --dry-run Show what would be built. Does not create outputs -b or --esbuild Use esbuild as bundler. Please install it manually @@ -53,8 +52,6 @@ Examples # more examples open https://nuejs.org/docs/command-line-interface.html -Less is more - ┏━┓┏┓┏┳━━┓ ┃┏┓┫┃┃┃┃━┫ ┃┃┃┃┗┛┃┃━┫ nuejs.org @@ -74,9 +71,6 @@ nue --production # build to production with custom settings nue build -p --environment custom.yaml -# show production stats -nue -p stats - # show what will be built (without building) nue build .js .ts .nue --dry-run diff --git a/packages/nuejs.org/docs/content.md b/packages/nuejs.org/docs/content.md index c60fa53c..90785b45 100644 --- a/packages/nuejs.org/docs/content.md +++ b/packages/nuejs.org/docs/content.md @@ -39,7 +39,7 @@ Followed with - An unordered - list of items -\```js +\```js numbered // here is a javascript code block function hello() { return "world" diff --git a/packages/nuejs.org/docs/index.md b/packages/nuejs.org/docs/index.md index a559b67c..7c039eef 100644 --- a/packages/nuejs.org/docs/index.md +++ b/packages/nuejs.org/docs/index.md @@ -3,22 +3,7 @@ inline_css: true --- # Web framework for UX developers -Nue is a web framework for design-minded people. You can turn your idea into a beautifully designed website using mostly CSS. You end up with a beautifully designed website, not just from the outside, but from the inside as well. Most importantly: You can build things faster, since there is no complex JavaScript ecosystem on your way. - -[image.gray] - small: /img/ux-development.png - large: /img/ux-development-big.png - size: 747 x 474 - - -Nue's content-first [development flow](ux-development.html) focuses solely on the ~user experience~ because, it's the only thing that matters when building new products. Or as the master UX developer **Steve Jobs** once said: - -> You've got to start with the customer experience and work back toward the technology, not the other way around. *Steve Jobs* - -- - - - -## Target audience -Nue is a great fit for the following group of people: +Nue is an extremely simple web development environment. It is a great fit for: 1. **UX developers**: who natively jump between **Figma** and **CSS** without a confusing [designer-developer handoff](//medium.com/design-warp/5-most-common-designer-developer-handoff-mishaps-ba96012be8a7) process in the way. diff --git a/packages/nuejs/package.json b/packages/nuejs/package.json index e2628f7c..30495725 100644 --- a/packages/nuejs/package.json +++ b/packages/nuejs/package.json @@ -1,6 +1,6 @@ { "name": "nuejs-core", - "version": "0.5.0", + "version": "0.5.1", "description": "HTML microlibrary for UX developers", "homepage": "https://nuejs.org", "license": "MIT", diff --git a/packages/nuekit/README.md b/packages/nuekit/README.md index 9331af69..1a6323c8 100644 --- a/packages/nuekit/README.md +++ b/packages/nuekit/README.md @@ -20,7 +20,7 @@ Nue is designed for the following people: 3. **Experienced JS developers**: frustrated with the absurd amount of layers in the [React stack](https://roadmap.sh/react) and look for simpler ways to develop professional websites. -4. **Designers**: aiming to learn web development, but find the React/JavaScript ecosystem impossible to grasp +4. **Designers**: aiming to transfer their design skills to CSS code, but find the React/JavaScript/CSS-in-JS ecosystem impossible to grasp 5. **Parents & Teachers**: who wants to educate people [how the web works](https://www.websitearchitecture.co.uk/resources/examples/web-standards-model/) diff --git a/packages/nuekit/package.json b/packages/nuekit/package.json index 0e50ff35..be49babd 100644 --- a/packages/nuekit/package.json +++ b/packages/nuekit/package.json @@ -1,6 +1,6 @@ { "name": "nuekit", - "version": "1.0.0-beta.1", + "version": "1.0.0-beta.2", "description": "Web Framework For UX Developers. Build the slickest websites in the world and wonder why you ever did them any other way", "homepage": "https://nuejs.org", "license": "MIT", diff --git a/packages/nuekit/src/browser/error.css b/packages/nuekit/src/browser/error.css index a694ad9e..640c2907 100644 --- a/packages/nuekit/src/browser/error.css +++ b/packages/nuekit/src/browser/error.css @@ -1,9 +1,9 @@ dialog.nuerr { - font-family: BlinkMacSystemFont, sans-serif; box-shadow: rgba(0, 0, 0, 0.3) 0px 19px 38px, rgba(0, 0, 0, 0.22) 0px 15px 12px; + font-family: BlinkMacSystemFont, sans-serif; + border-radius: var(--dialog-radius, 6px); margin: 10vh auto 0; - border-radius: 6px; padding: 2em 2.5em; font-size: 15px; min-width: 22em; diff --git a/packages/nuekit/src/browser/hotreload.js b/packages/nuekit/src/browser/hotreload.js index e411e14a..82025e71 100644 --- a/packages/nuekit/src/browser/hotreload.js +++ b/packages/nuekit/src/browser/hotreload.js @@ -15,11 +15,11 @@ sse.onmessage = async function(e) { if (data.site_updated) return location.reload() // error + $('.nuerr')?.remove() + if (error) { Object.assign(error, { path, ext: data.ext?.slice(1) }) import('./error.js').then(el => el.showError(error)) - } else { - $('.nuerr')?.remove() } // content @@ -46,16 +46,40 @@ sse.onmessage = async function(e) { const style = createStyle(href, css) if (orig) orig.replaceWith(style) - else document.head.appendChild(style) + else if (canAdd(data)) document.head.appendChild(style) } - // remove css + // remove css (note: is_css not available in remove events) if (data.remove && data.ext == '.css') { const orig = $(`[href="/${data.path}"]`) if (orig) orig.remove() } } +function canAdd({ dir, name, basedir }) { + + // exclude + if (contains(getMeta('exclude'), name)) return false + + // global + if (getMeta('globals')?.includes(dir)) return true + + // library + if (getMeta('libs')?.includes(dir) && contains(getMeta('include'), name)) return true + + // current app + const appdir = location.pathname.split('/')[1] + return appdir == basedir +} + +function getMeta(key) { + return $(`[name="nue:${key}"]`)?.getAttribute('content')?.split(' ') +} + +function contains(matches, name) { + return matches?.find(match => name.includes(match)) +} + function createStyle(href, css) { const el = document.createElement('style') diff --git a/packages/nuekit/src/browser/mount.js b/packages/nuekit/src/browser/mount.js index 2bdce06d..08b76ee2 100644 --- a/packages/nuekit/src/browser/mount.js +++ b/packages/nuekit/src/browser/mount.js @@ -16,20 +16,23 @@ async function importAll(hmr_path) { const arr = [] for (let path of comps.split(' ')) { - if (path == hmr_path) path += `?${++remounts}` - const { lib } = await import(path) - if (lib) arr.push(...lib) + if (path) { + if (path == hmr_path) path += `?${++remounts}` + const { lib } = await import(path) + if (lib) arr.push(...lib) + } } return arr } -export async function mountAll() { +export async function mountAll(hmr_path) { const els = document.querySelectorAll('[is]') - const lib = els[0] ? await importAll() : [] + const lib = els[0] ? await importAll(hmr_path) : [] if (!lib[0]) return + const { createApp } = await import('./nue.js') for (const node of [...els]) { diff --git a/packages/nuekit/src/browser/view-transitions.js b/packages/nuekit/src/browser/view-transitions.js index 419775ad..4143fe84 100644 --- a/packages/nuekit/src/browser/view-transitions.js +++ b/packages/nuekit/src/browser/view-transitions.js @@ -1,4 +1,6 @@ +// Router for multi-page applications + // exported export function $(query, root=document) { return root.querySelector(query) @@ -8,21 +10,24 @@ export function $$(query, root=document) { return [ ...root.querySelectorAll(query)] } +const scrollPos = {} -// Router for multi-page applications - -export async function loadPage(path, no_push) { +export async function loadPage(path, replace_state) { dispatchEvent(new Event('before:route')) - if (!no_push) history.pushState({ path }, 0, path) + // save scroll position + scrollPos[location.pathname] = window.scrollY + + if (!replace_state) history.pushState({ path }, 0, path) // DOM of the new page const dom = mkdom(await getHTML(path)) // change title - document.title = $('title', dom)?.textContent + const title = $('title', dom)?.textContent + if (title) document.title = title - // update + // update component list const query = '[name="nue:components"]' $(query).content = $(query, dom).content @@ -33,27 +38,11 @@ export async function loadPage(path, no_push) { await import(script.getAttribute('src')) } - // Inline CSS / development - const new_styles = swapStyles($$('style'), $$('style', dom)) - new_styles.forEach(style => $('head').appendChild(style)) - - // external CSS - const paths = swapStyles($$('link'), $$('link', dom)) - - /* production (single style element) */ - const orig_style = findPlainStyle() - const new_style = findPlainStyle(dom) + const css_paths = updateStyles(dom) - if (orig_style) orig_style.replaceWith(new_style) - else if (new_style) $('head').appendChild(new_style) - - - // body class - $('body').classList.value = $('body2', dom).classList.value || '' - - - loadCSS(paths, () => { - updateBody(dom) + loadCSS(css_paths, () => { + simpleDiff($('main'), $('main', dom)) + simpleDiff($('body'), $('body2', dom)) setActive(path) // scroll @@ -67,74 +56,6 @@ export async function loadPage(path, no_push) { } -function findPlainStyle(dom) { - return $$('style', dom).find(el => !el.attributes.length) -} - - -// TODO: make a recursive diff to support for all custom layouts -function updateBody(dom) { - - ;['header', 'main', 'footer', 'nav'].forEach(function(query) { - const a = $('body >' + query) - const b = $('body2 >' + query, dom) - const clone = b && b.cloneNode(true) - - // update - if (a && b) { - - if (query == 'main') { - updateMain(dom) - - } else { - updateBlock(a, clone) - } - - - // remove original - } else if (a) { - a.remove() - - // add new one - } else if (b) { - if (query == 'header') $('body').prepend(clone) - if (query == 'footer') $('body').append(clone) - if (query == 'nav') $('body > header').after(clone) - } - }) - -} - -// primitive DOM diffing -function updateBlock(a, clone) { - const orig = a.outerHTML.replace(' aria-selected=""', '') - const diff = orig != clone.outerHTML - if (diff) a.replaceWith(clone) -} - -// TODO: remove this hack -function updateMain(dom) { - ;['article', 'aside:first-child', 'article + aside'].forEach(function(query, i) { - const a = $('main >' + query) - const b = $('main >' + query, dom) - const clone = b && b.cloneNode(true) - - // update - if (a && b) { - updateBlock(a, clone) - - } else if (a) { - a.remove() - - } else if (b) { - if (!i) $('main').append(clone) - if (i == 1) $('main').prepend(clone) - if (i == 2) $('article').after(clone) - } - - }) -} - // setup linking export function onclick(root, fn) { @@ -151,7 +72,7 @@ export function onclick(root, fn) { (name?.includes('.') && !name?.endsWith('.html')) || target == '_blank') return // all good - if (path != location.pathname) fn(path) + if (path != location.pathname) fn(path, el) e.preventDefault() }) @@ -160,7 +81,6 @@ export function onclick(root, fn) { // developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-selected export function setActive(path, attrname='aria-selected') { - // remove old selections $$(`[${attrname}]`).forEach(el => el.removeAttribute(attrname)) @@ -186,8 +106,11 @@ if (is_browser) { history.pushState({ path: location.pathname }, 0) // autoroute / document clicks - onclick(document, async path => { - document.startViewTransition(async function() { + onclick(document, async (path, el) => { + const img = $('img', el) + if (img) img.style.viewTransitionName = 'active-image' + + document.startViewTransition(async () => { await loadPage(path) }) }) @@ -198,7 +121,14 @@ if (is_browser) { // back button addEventListener('popstate', e => { const { path } = e.state || {} - if (path) loadPage(path, true) + if (path) { + const pos = scrollPos[path] + + document.startViewTransition(async () => { + await loadPage(path, true) + setTimeout(() => window.scrollTo(0, pos || 0), 10) + }) + } }) } @@ -206,23 +136,64 @@ if (is_browser) { /* -------- utilities ---------- */ -function hasStyle(sheet, sheets) { +// primitive DOM diffing +function simpleDiff(a, b) { + if (a.children.length == b.children.length) { + ;[...a.children].forEach((el, i) => updateBlock(el, b.children[i])) + } else { + a.classList.value = b.classList.value + a.innerHTML = b.innerHTML + } +} + +function updateBlock(a, b) { + const orig = a.outerHTML.replace(' aria-selected=""', '') + if (orig != b.outerHTML) a.replaceWith(b.cloneNode(true)) +} + + +function updateStyles(dom) { + + // Inline CSS / development + const orig = $$('link, style') + const new_styles = swapStyles(orig, $$('link, style', dom)) + new_styles.forEach(style => $('head').appendChild(style)) + + // inline style element + updateProductionStyles(dom) + + // external CSS + return new_styles.filter(el => el.tagName == 'link') +} + + +function hasStyle(sheet, sheets) { return sheets.find(el => el.getAttribute('href') == sheet.getAttribute('href')) } + +// disable / enable function swapStyles(orig, styles) { + orig.forEach((el, i) => el.disabled = !hasStyle(el, styles)) + return styles.filter(el => !hasStyle(el, orig)) +} - // disable / enable - orig.forEach((el, i) => { - el.disabled = !hasStyle(el, styles) - }) +function findPlainStyle(dom) { + return $$('style', dom).find(el => !el.attributes.length) +} +// production: single inline style element without attributes +function updateProductionStyles(dom) { + const plain = findPlainStyle() + const new_plain = findPlainStyle(dom) - // add new - return styles.filter(el => !hasStyle(el, orig)) + if (plain) plain.replaceWith(new_plain) + else if (new_plain) $('head').appendChild(new_plain) } + + const cache = {} async function getHTML(path) { @@ -233,7 +204,9 @@ async function getHTML(path) { html = await resp.text() if (resp.status == 404 && html?.trim()[0] != '<') { - $('article').innerHTML = '

Page not found

' + const title = document.title = 'Page not found' + $('article').innerHTML = `

${title}

` + } else { cache[path] = html } diff --git a/packages/nuekit/src/cli-help.js b/packages/nuekit/src/cli-help.js index 1c3177c5..3852077f 100644 --- a/packages/nuekit/src/cli-help.js +++ b/packages/nuekit/src/cli-help.js @@ -9,15 +9,13 @@ Usage Commands serve Start development server (default command) build Build the site under - stats Show site statistics create Use a project starter template + init Re-generate /@nue system files Options -r or --root Source directory. Default "." (current working dir) -p or --production Build production version / Show production stats -e or --environment Read extra options to override defaults in site.yaml - -s or --stats Show site statistics after current command - -I or --init Force clear and initialize output directory -n or --dry-run Show what would be built. Does not create outputs -b or --esbuild Use esbuild as bundler. Please install it manually -P or --port Port to serve the site on @@ -42,15 +40,13 @@ Examples # more examples ${openUrl} https://nuejs.org/docs/command-line-interface.html -Less is more - ┏━┓┏┓┏┳━━┓ - ┃┏┓┫┃┃┃┃━┫ kit ${await getVersion()} + ┃┏┓┫┃┃┃┃━┫ ${await getVersion()} ┃┃┃┃┗┛┃┃━┫ nuejs.org ┗┛┗┻━━┻━━┛ ` -const commands = ['serve', 'build', 'stats', 'create'] +const commands = ['serve', 'build', 'init', 'create'] function formatLine(line) { const { gray, magenta, cyan, green } = colors diff --git a/packages/nuekit/src/cli.js b/packages/nuekit/src/cli.js index d370cf7e..60b9de40 100755 --- a/packages/nuekit/src/cli.js +++ b/packages/nuekit/src/cli.js @@ -19,7 +19,7 @@ export function expandArgs(args) { // TODO: tests export function getArgs(argv) { - const commands = ['serve', 'build', 'stats', 'create'] + const commands = ['serve', 'build', 'init', 'create'] const args = { paths: [], root: null } const checkExecutable = /[\\\/]nue(\.(cmd|ps1|bunx|exe))?$/ let opt @@ -45,10 +45,9 @@ export function getArgs(argv) { else if (['-n', '--dry-run'].includes(arg)) args.dryrun = true else if (['-h', '--help'].includes(arg)) args.help = true else if (['-v', '--verbose'].includes(arg)) args.verbose = true - else if (['-s', '--stats'].includes(arg)) args.stats = true else if (['-b', '--esbuild'].includes(arg)) args.esbuild = true else if (['-d', '--deploy'].includes(arg)) args.deploy = args.is_prod = true - else if (['-I', '--init'].includes(arg)) args.init = true + else if (['-I', '--incremental'].includes(arg)) args.incremental = true // string values else if (['-e', '--environment'].includes(arg)) opt = 'env' @@ -87,6 +86,8 @@ async function printVersion() { async function runCommand(args) { const { createKit } = await import('./nuekit.js') const { cmd='serve', dryrun, deploy, root=null, port } = args + const init = cmd == 'init' + if (!root) args.root = '.' // ensure root is unset for create, if not set manually console.info('') @@ -98,23 +99,27 @@ async function runCommand(args) { } const nue = await createKit(args) + if (!nue) return + args.nuekit_version = await printVersion() - // stats - if (cmd == 'stats') await nue.stats(args) + // deployer (private repo) + const { deploy: deployer } = deploy ? await import('nue-deployer') : {} // build - if (dryrun || deploy || args.paths[0] || cmd == 'build') { - const paths = await nue.build(args.paths, dryrun) + if (init) { + await nue.init(true) + if (deploy) await deployer({ root: nue.dist, init: true }) - // deploy (private repo ATM) - if (!dryrun && deploy) { - const { deploy: deployer } = await import('nue-deployer') - await deployer(paths, { root: nue.dist, init: args.init }) - } + + } else if (dryrun || deploy || args.paths[0] || cmd == 'build') { + const paths = await nue.build(args.paths) + if (!dryrun && deploy && paths[0]) await deployer({ paths, root: nue.dist, init }) // serve - } else await nue.serve() + } else { + await nue.serve() + } } diff --git a/packages/nuekit/src/init.js b/packages/nuekit/src/init.js index d417b759..7ee038a1 100644 --- a/packages/nuekit/src/init.js +++ b/packages/nuekit/src/init.js @@ -1,20 +1,20 @@ import { compileFile as nueCompile } from 'nuejs-core' +import { promises as fs, existsSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' -import { promises as fs, existsSync } from 'node:fs' import { resolve } from 'import-meta-resolve' import { buildJS } from './builder.js' import { colors, srcdir } from './util.js' -export async function init({ dist, is_dev, esbuild, force }) { +export async function initNueDir({ dist, is_dev, esbuild, force }) { // directories const cwd = process.cwd() const outdir = join(cwd, dist, '@nue') // has all latest? - const latest = join(outdir, '.05') + const latest = join(outdir, '.beta-2') if (force || !existsSync(latest)) { await fs.rm(outdir, { recursive: true, force: true }) diff --git a/packages/nuekit/src/layout/gallery.js b/packages/nuekit/src/layout/gallery.js index 278ea33b..7b9e9260 100644 --- a/packages/nuekit/src/layout/gallery.js +++ b/packages/nuekit/src/layout/gallery.js @@ -60,6 +60,6 @@ export function renderGallery(data) { return '' } - const pages = items.filter(el => !el.unlisted).map(renderGalleryItem) + const pages = items.map(renderGalleryItem) return elem('ul', join(pages)) } diff --git a/packages/nuekit/src/layout/head.js b/packages/nuekit/src/layout/head.js index a7a9cdf3..682e11f4 100644 --- a/packages/nuekit/src/layout/head.js +++ b/packages/nuekit/src/layout/head.js @@ -60,6 +60,14 @@ export function renderHead(data) { // components (must always be rendered) pushMeta('nue:components', components.map(uri => `${base}${uri}`).join(' ') || ' ') + // helper info for hot-reloading + if (!is_prod) { + for (const key of 'include exclude globals libs'.split(' ')) { + const arr = data[key] + pushMeta(`nue:${key}`, arr?.join(' ')) + } + } + // misc if (favicon) head.push(``) diff --git a/packages/nuekit/src/layout/page-layout.js b/packages/nuekit/src/layout/page-layout.js index 90710adc..004c1099 100644 --- a/packages/nuekit/src/layout/page-layout.js +++ b/packages/nuekit/src/layout/page-layout.js @@ -5,6 +5,7 @@ import { renderGallery, renderPrettyDate } from './gallery.js' import { renderNav, renderTOC } from './navi.js' import { renderPage as nuemark } from 'nuemark' import { parse as parseNue } from 'nuejs-core' +import { tags } from 'nuemark/src/tags.js' import { renderInline } from 'nuemark' import { renderHead } from './head.js' @@ -38,7 +39,7 @@ const MAIN = ` const MENU = ` - + ` @@ -93,31 +94,36 @@ export function renderSinglePage(body='', data) { // system components -const html_tags = [ +const system_tags = [ { name: 'navi', create: renderNav }, { name: 'gallery', create: renderGallery }, - { name: 'markdown', create: ({ content }) => renderInline(content) }, + { name: 'markdown', create: ({ content }) => content ? renderInline(content) : '' }, { name: 'pretty-date', create: ({ date }) => renderPrettyDate(date) }, { name: 'toc', create: renderTOC }, + { name: 'image', create: tags.image }, ] const nuemark_tags = { gallery: renderGallery, toc: renderTOC } -export function renderPage(data, lib) { +export function renderPage(data, comps) { + + const lib = [...system_tags, ...comps] function renderBlock(name, html) { - if (data[name] === false || data[name.slice(1)] === false) return null + // main: false --> render default MAIN + if (name == 'main' && data.main === false) name = '' - let comp = lib.find(el => name[0] == '@' ? el.name == name.slice(1) : !el.name && el.tagName == name) + else if (data[name] === false || data[name.slice(1)] === false) return null - if (!comp && html) comp = parseNue(html)[0] + let comp = comps.find(el => name[0] == '@' ? el.name == name.slice(1) : !el.name && el.tagName == name) + if (!comp && html) comp = parseNue(html)[0] try { - return comp ? comp.render(data, [...html_tags, ...lib]) : '' + return comp ? comp.render(data, lib) : '' } catch (e) { delete data.inline_css console.error(`Error on <${name}> component`, e) diff --git a/packages/nuekit/src/nuefs.js b/packages/nuekit/src/nuefs.js index d6cc3b30..0db617e0 100644 --- a/packages/nuekit/src/nuefs.js +++ b/packages/nuekit/src/nuefs.js @@ -1,65 +1,104 @@ import { watch, promises as fs } from 'node:fs' -import { join, parse } from 'node:path' +import { join, parse, relative } from 'node:path' /* - Super minimalistic file system watcher. + Extremely minimal and efficient cross-platform file watching library +*/ - Auto-follows new directories and symbolic (file) links +// for avoiding double events +let last = {} - Simple alternative to Chokidar and Nodemon when you "just want to watch" +export async function fswatch(root, callback, onremove) { - TODO: symlink directory support -*/ + const watchers = {} -// avoid double events and looping (seen on Bun only) -let last = {} + async function watchLink(link) { + const real = await fs.realpath(link.path) + const is_dir = (await fs.lstat(real)).isDirectory() + + const watcher = watch(real, { recursive: is_dir }, (e, path) => { + if (is_dir) { + path = join(link.name, path) + const file = parse(path) + callback({ path, ...file }) + + } else { + callback(link) + } + }) + + watchers[link.path] = watcher + } + + // watch symlinks + const paths = await fswalk({ root, symdirs: false }) + for (const path of paths) { + const file = parse(path) -export function fswatch(dir, onfile, onremove) { - return watch(dir, { recursive: true }, async function(e, path) { + if (isLegit(file)) { + const stat = await fs.lstat(join(root, path)) + if (stat.isSymbolicLink()) { + watchLink({ ...file, path }) + } + } + } + + return watch(root, { recursive: true }, async function(e, path) { try { const file = parse(path) + file.path = path // skip paths (files and dirs) that start with _ or . if (!isLegit(file)) return - // skip double events + // skip double events (not needed anymore?) if (last.path == path && Date.now() - last.ts < 50) return - // regular flie -> callback - const stat = await fs.lstat(join(dir, path)) + const stat = await fs.lstat(join(root, path)) - if (stat.isDirectory()) { - const paths = await fswalk(dir, path) + // deploy everything on a directory + if (stat.isDirectory() || stat.isSymbolicLink() && await isSymdir(path)) { + const paths = await fswalk(root, path) - // deploy everything on the directory for (const path of paths) { const file = parse(path) - if (isLegit(file)) await onfile({ ...file, path }) + if (isLegit(file)) await callback(file) } - } else { - if (file.ext) await onfile({ ...file, path, size: stat.size }) + } else if (file.ext) { + if (stat.isSymbolicLink()) await watchLink(file) + await callback(file) } last = { path, ts: Date.now() } } catch (e) { - if (e.errno == -2) await onremove(path) - else console.error(e) + if (e.errno != -2) return console.error(e) + await onremove(path) + + // unwatch symlink + const watcher = watchers[path] + if (watcher) watcher.close() } }) } -export async function fswalk(root, _dir='', _ret=[]) { +export async function fswalk(opts, _dir='', _ret=[]) { + if (typeof opts == 'string') opts = { root: opts } + const { root, symdirs=true } = opts + + const files = await fs.readdir(join(root, _dir), { withFileTypes: true }) for (const f of files) { if (isLegit(f)) { const path = join(_dir, f.name) - if (isDir(f)) await fswalk(root, path, _ret) + const is_symdir = symdirs && f.isSymbolicLink() && await isSymdir(join(root, path)) + + if (f.isDirectory() || is_symdir) await fswalk(opts, path, _ret) else _ret.push(path) } } @@ -76,9 +115,10 @@ function isLegit(file) { return !ignore(file.name) && !ignore(file.base) && !ignore(file.dir) } -// TODO: real symdir detection -function isDir(f) { - return f.isDirectory() || f.isSymbolicLink() && !f.name.includes('.') + +async function isSymdir(linkpath) { + const real = await fs.realpath(linkpath) + return (await fs.lstat(real)).isDirectory() } diff --git a/packages/nuekit/src/nuekit.js b/packages/nuekit/src/nuekit.js index 274a1f75..f07c5c6d 100644 --- a/packages/nuekit/src/nuekit.js +++ b/packages/nuekit/src/nuekit.js @@ -1,23 +1,23 @@ import { log, colors, getAppDir, parsePathParts, extendData } from './util.js' -import { join, parse as parsePath } from 'node:path' import { renderPage, renderSinglePage } from './layout/page-layout.js' import { parse as parseNue, compile as compileNue } from 'nuejs-core' +import { promises as fs, existsSync } from 'node:fs' +import { join, parse as parsePath } from 'node:path' import { lightningCSS, buildJS } from './builder.js' import { createServer, send } from './nueserver.js' import { printStats, categorize } from './stats.js' -import { promises as fs } from 'node:fs' +import { initNueDir } from './init.js' import { createSite } from './site.js' import { fswatch } from './nuefs.js' import { parsePage } from 'nuemark' -import { init } from './init.js' // the HTML5 doctype const DOCTYPE = '\n\n' export async function createKit(args) { - const { root, is_prod, esbuild } = args + const { root, is_prod, esbuild, dryrun } = args // site: various file based functions const site = await createSite(args) @@ -25,9 +25,18 @@ export async function createKit(args) { const { dist, port, read, copy, write, is_empty } = site const is_dev = !is_prod - // make sure @nue dir has all the latest - if (!args.dryrun) await init({ dist, is_dev, esbuild, force: args.init }) + async function init(force) { + await initNueDir({ dist, is_dev, esbuild, force }) + } + + if (!existsSync(join(root, 'site.yaml'))) { + console.error('No site.yaml found. Please go to project root\n') + return false + } + + // make sure @nue dir has all the latest + if (!dryrun) await init() async function setupStyles(dir, data) { const paths = await site.getStyles(dir, data) @@ -249,7 +258,7 @@ export async function createKit(args) { } // build all / given matches - async function build(matches=[], dryrun) { + async function build(matches=[]) { const begin = Date.now() log('Building site to:', colors.cyan(dist)) @@ -259,7 +268,11 @@ export async function createKit(args) { // ignore layouts paths = paths.filter(p => !p.endsWith('layout.html')) - if (matches[0]) { + + if (args.incremental) { + paths = await site.filterUpdated(paths) + + } else if (matches[0]) { paths = paths.filter(p => matches.find(m => m == '.' ? p == 'index.md' : p.includes(m))) } @@ -305,9 +318,8 @@ export async function createKit(args) { return { path, code: name == 404 ? 404 : 200 } }) - // dev mode -> watch for changes - let watcher - if (is_dev) watcher = fswatch(root, async file => { + // watch for changes + const watcher = is_prod ? null : await fswatch(root, async file => { try { const ret = await processFile(file) if (ret) send({ ...file, ...parsePathParts(file.path), ...ret }) @@ -326,11 +338,8 @@ export async function createKit(args) { if (file.ext) send({ remove: true, path, ...file }) }) - const cleanup = () => { - if (watcher) watcher.close() - } const terminate = () => { - cleanup() + if (watcher) watcher.close() server.close() } @@ -352,7 +361,7 @@ export async function createKit(args) { gen, getPageData, renderMPA, renderSPA, // public API - build, serve, stats, dist, port, + build, serve, stats, init, dist, port, } } diff --git a/packages/nuekit/src/site.js b/packages/nuekit/src/site.js index 8b9be7a3..6a14878a 100644 --- a/packages/nuekit/src/site.js +++ b/packages/nuekit/src/site.js @@ -10,8 +10,8 @@ import { joinRootPath } from './util.js' import { join, extname, parse as parsePath } from 'node:path' -import { parse as parseNue } from 'nuejs-core' import { promises as fs, existsSync } from 'node:fs' +import { parse as parseNue } from 'nuejs-core' import { fswalk } from './nuefs.js' import { nuemark } from 'nuemark' import yaml from 'js-yaml' @@ -71,14 +71,12 @@ export async function createSite(args) { libs: site_data.libs || [], } - const port = args.port ? args.port : - site_data.port ? site_data.port : - is_prod ? 8081 : 8080 - const dist = joinRootPath(root, site_data.dist || join('.dist', is_prod ? 'prod' : 'dev')) + const port = args.port || site_data.port || (is_prod ? 8081 : 8080) + // flag if .dist is empty - if (!existsSync(dist)) self.is_empty = true + self.is_empty = !existsSync(dist) async function write(content, dir, filename) { const todir = join(dist, dir) @@ -86,7 +84,7 @@ export async function createSite(args) { try { const to = join(todir, filename) await fs.writeFile(to, content) - !is_bulk && !self.is_empty && log(join(dir, filename)) + if (!is_bulk && !self.is_empty) log(join(dir, filename)) return to } catch (e) { @@ -102,7 +100,7 @@ export async function createSite(args) { try { await fs.copyFile(join(root, dir, base), to) - !is_bulk && !self.is_empty && log(join(dir, base)) + if (!is_bulk && !self.is_empty) log(join(dir, base)) } catch (e) { if (!fileNotFound(e)) throw e @@ -154,7 +152,6 @@ export async function createSite(args) { const { include=[], exclude=[] } = data const subdirs = !dir ? [] : self.globals.map(el => join(dir, el)) - let paths = [ ...await walkDirs(self.globals), ...await walkDirs(subdirs), @@ -245,7 +242,7 @@ export async function createSite(args) { for (const path of mds) { const raw = await read(path) const { meta } = nuemark(raw) - arr.push({ ...meta, ...parsePathParts(path) }) + if (!meta.unlisted) arr.push({ ...meta, ...parsePathParts(path) }) } arr.sort((a, b) => { @@ -330,5 +327,32 @@ export async function createSite(args) { } } + + async function getLastRun() { + const path = join(dist, '.lastrun') + + try { + const stat = await fs.stat(path) + return stat.mtimeMs + + } catch { + await fs.writeFile(path, '') + return 0 + } + } + + self.filterUpdated = async function(paths) { + const since = await getLastRun() + const arr = [] + + for (const path of paths) { + const stat = await fs.stat(path) + if (stat.mtimeMs > since) arr.push(path) + } + + return arr + } + + return { ...self, dist, port, read, write, copy } } diff --git a/packages/nuekit/src/stats.js b/packages/nuekit/src/stats.js index 759efb80..a93de41f 100644 --- a/packages/nuekit/src/stats.js +++ b/packages/nuekit/src/stats.js @@ -9,6 +9,7 @@ async function readSize(dist, path) { return raw.length } +// not currently in use. make something actually useful later export async function printStats(site, args) { if (args.dryrun) return diff --git a/packages/nuekit/src/util.js b/packages/nuekit/src/util.js index d6066524..34b31eb6 100644 --- a/packages/nuekit/src/util.js +++ b/packages/nuekit/src/util.js @@ -81,15 +81,25 @@ export function toPosix(path) { } export function extendData(to, from={}) { - const { include = [], exclude = [] } = to - if (from.include) include.push(...from.include) - if (from.exclude) exclude.push(...from.exclude) + const include = addUnique(to.include, from.include) + const exclude = addUnique(to.exclude, from.exclude) Object.assign(to, from) to.include = include to.exclude = exclude } +function addUnique(to=[], from=[]) { + if (to.length) to = [...to] + + for (const el of from) { + if (!to.includes(el)) to.push(el) + } + + return to +} + + export function sortCSS({ paths, globals, dir }) { function score(path) { if (path[0] == '/') path = path.slice(1) diff --git a/packages/nuekit/test/kit-init.test.js b/packages/nuekit/test/kit-init.test.js index dc2ae646..52d59378 100644 --- a/packages/nuekit/test/kit-init.test.js +++ b/packages/nuekit/test/kit-init.test.js @@ -1,6 +1,6 @@ import { promises as fs } from 'node:fs' import { join } from 'node:path' -import { init } from '../src/init.js' +import { initNueDir } from '../src/init.js' // temporary directory const dist = '_test' @@ -13,14 +13,15 @@ beforeAll(async () => { afterAll(async () => await fs.rm(dist, { recursive: true, force: true })) + test('bun init', async () => { - await init({ dist, is_dev: true }) + await initNueDir({ dist, is_dev: true }) const names = await fs.readdir(join(dist, '@nue')) expect(names.length).toBe(11) }) test('esbuild init', async () => { - await init({ dist, is_dev: true, esbuild: true }) + await initNueDir({ dist, is_dev: true, esbuild: true }) const names = await fs.readdir(join(dist, '@nue')) expect(names.length).toBe(11) }) diff --git a/packages/nuekit/test/nuekit.test.js b/packages/nuekit/test/nuekit.test.js index 6e66fd65..63b602d2 100644 --- a/packages/nuekit/test/nuekit.test.js +++ b/packages/nuekit/test/nuekit.test.js @@ -1,9 +1,9 @@ +import { promises as fs, existsSync } from 'node:fs' import { buildJS } from '../src/builder.js' import { createSite } from '../src/site.js' import { createKit } from '../src/nuekit.js' -import { promises as fs } from 'node:fs' import { join, parse } from 'node:path' import { toMatchPath } from './match-path.js' @@ -18,6 +18,7 @@ beforeEach(async () => { await fs.rm(root, { recursive: true, force: true }) await fs.mkdir(root, { recursive: true }) }) + afterEach(async () => await fs.rm(root, { recursive: true, force: true })) // helper function for creating files to the root directory @@ -41,8 +42,9 @@ async function getSite() { return await createSite({ root }) } -async function getKit() { - return await createKit({ root, dryrun: true }) +async function getKit(dryrun=true) { + if (!existsSync(join(root, 'site.yaml'))) await write('site.yaml', '') + return await createKit({ root, dryrun }) } function createFront(title, pubDate) { @@ -363,7 +365,7 @@ test('the project was started for the first time', async () => { await write('home.css') await write('index.md') - const kit = await getKit() + const kit = await getKit(false) const terminate = await kit.serve() try { const html = await readDist(kit.dist, 'index.html') diff --git a/packages/nuemark/package.json b/packages/nuemark/package.json index 2533f78c..e33f72a6 100644 --- a/packages/nuemark/package.json +++ b/packages/nuemark/package.json @@ -1,6 +1,6 @@ { "name": "nuemark", - "version": "0.4.1", + "version": "0.4.2", "description": "Markdown dialect for rich, interactive web content", "homepage": "https://nuejs.org", "license": "MIT", diff --git a/packages/nuemark/src/component.js b/packages/nuemark/src/component.js index f3821b18..ae3413a5 100644 --- a/packages/nuemark/src/component.js +++ b/packages/nuemark/src/component.js @@ -1,6 +1,6 @@ -const ATTR = 'id is class style'.split(' ') +const ATTR = 'id is class style hidden'.split(' ') /* --> { name, attr, data } diff --git a/packages/nuemark/src/parse.js b/packages/nuemark/src/parse.js index 5e889e1a..226fea36 100644 --- a/packages/nuemark/src/parse.js +++ b/packages/nuemark/src/parse.js @@ -154,7 +154,11 @@ export function parseBlocks(lines) { // fenced code start/end if (line.startsWith('```')) { if (!fenced) { - fenced = { is_code: true, content: [], ...parseSpecs(line.slice(3).trim()) } + let specs = line.slice(3).trim() + const numbered = specs.includes('numbered') + specs = specs.replace('numbered', '') + fenced = { ...parseSpecs(specs), numbered, is_code: true, content: [] } + md = null } else { blocks.push(fenced) diff --git a/packages/nuemark/src/render.js b/packages/nuemark/src/render.js index e664e75c..1c3bc691 100644 --- a/packages/nuemark/src/render.js +++ b/packages/nuemark/src/render.js @@ -3,6 +3,24 @@ import { tags, elem, join } from './tags.js' import { parsePage, parseHeading } from './parse.js' import { marked } from 'marked' + +/* + ":" prefix support for property names, for example: + + [image-gallery :items="gallery"] +*/ +function extractData(to, from) { + for (const key in from) { + if (key[0] == ':') { + const name = key.slice(1) + to[name] = to[from[key]] + } else { + to[key] = from[key] + } + } + return to +} + export function renderPage(page, opts) { const { lib=[] } = opts const data = { ...opts.data, ...page.meta } @@ -17,7 +35,7 @@ export function renderPage(page, opts) { const html = join(section.blocks.map(el => { const { name, md, attr } = el const comp = name && lib.find(el => [name, toCamelCase(name)].includes(el.name)) - const alldata = { ...data, ...el.data, attr } + const alldata = extractData({ ...data, attr }, el.data) const tag = custom_tags[name] || tags[name] // tag @@ -104,6 +122,7 @@ marked.setOptions({ mangle: false }) + export function renderHeading(html, depth, raw) { const plain = parseHeading(raw) const { id } = plain diff --git a/packages/nuemark/src/tags.js b/packages/nuemark/src/tags.js index 7ddd7607..ae17b606 100644 --- a/packages/nuemark/src/tags.js +++ b/packages/nuemark/src/tags.js @@ -2,22 +2,12 @@ /* Build-in tag library - Why - - Common syntax for content teams - - Re-usable components accross projects - - Shared semantics for design systems - - Features - - Can be combined to form more complex layouts - - Fully headless & semantic = externally styleable - - Fast, reliable, unit tested - - Globally & local configuration - - Usable outside Nue - - TODO - - Available on templates with "-tag" suffix: - - Nuekit Error reporting + - Common syntax for content teams + - Re-usable components accross projects + - Shared semantics for design systems + - Fast, reliable, unit tested */ + import { readFileSync } from 'node:fs' import { nuemarkdown } from '../index.js' import { parseInline } from 'marked' diff --git a/packages/nuemark/test/nuemark.test.js b/packages/nuemark/test/nuemark.test.js index 054999f6..01be0eeb 100644 --- a/packages/nuemark/test/nuemark.test.js +++ b/packages/nuemark/test/nuemark.test.js @@ -15,7 +15,23 @@ test('fenced code', () => { test('fenced code: space before class name', () => { const { html } = renderLines(['``` md #go.pink', '# Hey', '```']) expect(html).toInclude('
') + expect(html).toInclude('
')
   expect(html).toInclude('')
+  expect(html).not.toInclude('')
+})
+
+test('fenced code & numbered', () => {
+  const { html } = renderLines(['``` numbered .pink', '

', '```']) + expect(html).toInclude('') + expect(html).toInclude('

') + expect(html).toInclude('') + +}) + +test('fenced code & numbered last', () => { + const { html } = renderLines(['``` md .pink numbered', '# Hey', '```']) + expect(html).toInclude('
') + expect(html).toInclude('') }) test('[code.foo]', () => {