Skip to content

Commit

Permalink
Merge pull request #460 from alexpricedev/feat/link-components-attrs
Browse files Browse the repository at this point in the history
feat(components): added support for all anchor attrs
  • Loading branch information
tipiirai authored Jan 27, 2025
2 parents 66c4f8b + e1c9e9f commit e7cf831
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 57 deletions.
41 changes: 31 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,30 @@ The draft looks good and covers the key aspects, but let's enhance it to better
First and foremost: Thank you for helping make web development simpler and more standards-focused! ❤️❤️

## Writing and Sharing

The most valuable contribution right now is spreading awareness about standards-first development. Write blog posts, create demos, or share examples that showcase the power of modern web standards. Show how native browser capabilities can replace complex framework abstractions.

Focus especially on:

- How modern CSS enables sophisticated design systems
- The untapped power of semantic HTML
- Progressive enhancement through vanilla JavaScript
- Real-world examples of framework complexity vs web standards simplicity

## Community Support

Help fellow developers discover the elegant simplicity of standards-first development. While I (Tero Piirainen, product lead) focus intensely on the core engineering, the community's guidance is essential. Answer questions, suggest solutions, and share your experiences.

## Bug Fixes

When fixing bugs, always include a test case that demonstrates both the issue and the solution. This helps maintain the codebase's minimalist nature while ensuring reliability.

## Feature Proposals
Nue has a clear vision: take modern web standards to their absolute peak. Before implementing any new feature, no matter how small, let's discuss how it aligns with this goal. The framework's power comes from ruthless simplicity - every addition must justify its existence.

Nue has a clear vision: take modern web standards to their absolute peak. Before implementing any new feature, no matter how small, let's discuss how it aligns with this goal. The framework's power comes from ruthless simplicity - every addition must justify its existence.

## Development Philosophy

Nue's development style might surprise those coming from traditional JavaScript projects. For engineers steeped in TypeScript, dependency injection, and "enterprise patterns", the codebase might even look stupid or feel like a toy project at first glance. This reaction reveals a fundamental divide in how we think about web development.

While most codebases optimize for type safety, abstraction layers, and "proper engineering practices", Nue pursues radical minimalism. We strive to make each line of code meaningful through its functionality and clarity. The goal is to figure out what's truly needed (and only that) and find out the cleanest way to implement it.
Expand All @@ -34,9 +39,8 @@ Or take view transitions: Nue's entire implementation fits in about 250 lines of

By working directly with web standards rather than building layers of abstractions, we strive to create systems that are both more powerful and easier to maintain.



### Code Style

Maintain the existing minimalist aesthetic:

1. No semicolons - they add visual noise without value
Expand All @@ -47,6 +51,7 @@ Maintain the existing minimalist aesthetic:
Nue avoids Prettier/ESLint as they would add 40MB of complexity. The `.prettierrc.yaml` provides sufficient consistency.

### Testing

```sh
# Bun (recommended)
bun install
Expand All @@ -60,23 +65,39 @@ npm test
```

### Local Development

#### Using Bun (recommended)

```sh
# Bun (recommended)
# Clone the Nue repository
cd your-projects-dir
git clone [email protected]:nuejs/nue.git
cd nue
# Install dependencies
bun install
# Link the nuekit package
cd packages/nuekit
bun link
cd my/nue/project
# Link the nuekit package in your project
cd your-projects-dir/your-nue-project-dir
bun link nuekit
# Serve the project using the local nuekit package
nue
nue build --production
```

# Node
#### Using Node

```sh
cd your-projects-dir
git clone [email protected]:nuejs/nue.git
cd nue
npm install
cd packages/nuekit
npm link
cd my/nue/project
cd your-projects-dir/your-nue-project-dir
npm link nuekit
npm install --save-dev esbuild
node $(which nue)
node $(which nue) build --production
```

Let's maintain this clear vision of simplicity and standards as we build something extraordinary together.
Let's maintain this clear vision of simplicity and standards as we build something extraordinary together.
64 changes: 38 additions & 26 deletions packages/nuekit/src/layout/components.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { elem, parseSize, renderInline, renderIcon } from 'nuemark'

import { readFileSync } from 'node:fs'
Expand All @@ -17,16 +16,17 @@ export function renderPageList(data) {
return elem('ul', this.attr, pages.join('\n'))
}


// the "main" method called by the <navi/> tag
export function renderNavi(data) {
const { items, icon_dir } = data

return Array.isArray(items) ? renderNav({ items, icon_dir }) :
typeof items == 'object' ? renderMultiNav(items, data) : ''
return Array.isArray(items)
? renderNav({ items, icon_dir })
: typeof items == 'object'
? renderMultiNav(items, data)
: ''
}


function renderTOC(data) {
const { document, attr } = data
return document.renderTOC(attr)
Expand All @@ -38,7 +38,6 @@ function renderPrettyDate(date) {
return elem('time', { datetime: date.toISOString() }, prettyDate(date))
}


// in Nue JS component format
export function getLayoutComponents() {
return [
Expand All @@ -47,15 +46,15 @@ export function getLayoutComponents() {
{ name: 'toc', create: renderTOC },
{ name: 'markdown', create: ({ content }) => renderInline(content) },
{ name: 'pretty-date', create: ({ date }) => renderPrettyDate(date) },
{ name: 'icon', create: (data) => renderIcon(data.src, data.symbol, data.icon_dir) },
{ name: 'icon', create: data => renderIcon(data.src, data.symbol, data.icon_dir) },
]
}

/****** utilities ********/

export function renderPage(page) {
const { title, url } = page
const desc = page.desc || page.description
const desc = page.desc || page.description
const thumb = toAbsolute(page.thumb, page.dir)

// date
Expand All @@ -66,18 +65,22 @@ export function renderPage(page) {
const h2 = title ? elem('h2', renderInline(title)) : ''
const p = desc ? elem('p', renderInline(desc)) : ''

const body = !thumb ? time + elem('a', { href: url }, h2 + p) :

// figure
elem('a', { href: url }, elem('figure',
elem('img', { src: thumb, loading: 'lazy' }) + elem('figcaption', time + h2 + p))
)
const body = !thumb
? time + elem('a', { href: url }, h2 + p)
: // figure
elem(
'a',
{ href: url },
elem(
'figure',
elem('img', { src: thumb, loading: 'lazy' }) + elem('figcaption', time + h2 + p)
)
)

return elem('li', { class: isNew(date) && 'is-new' }, body)
}


function isNew(date, offset=4) {
function isNew(date, offset = 4) {
const diff = new Date() - date
return diff < offset * 24 * 3600 * 1000
}
Expand All @@ -86,12 +89,12 @@ function prettyDate(date) {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
day: 'numeric',
})
}

export function toAbsolute(path, dir) {
return path && path[0] != '/' ? `/${dir}/${path}`: path
return path && path[0] != '/' ? `/${dir}/${path}` : path
}

export function parseLink(item) {
Expand All @@ -100,11 +103,10 @@ export function parseLink(item) {
if (typeof item == 'string') {
return item.startsWith('---') ? { separator: item } : { label: item, url: '' }
}
const [ label, url ] = Object.entries(item)[0]
const [label, url] = Object.entries(item)[0]
return { label, ...parseClass(url) }
}


export function renderLink(item, icon_dir) {
const img = item.image ? renderImage(item) : ''
const icon = renderIcon(item.icon, item.symbol, icon_dir)
Expand All @@ -113,7 +115,20 @@ export function renderLink(item, icon_dir) {

const link = parseLink(item)

const attr = { href: link.url, class: link.class, role: item.role }
// Attributes supported by <a> in addition to "class" and "role"
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attributes
const attr = {
class: link.class,
download: item.download,
href: link.url,
hreflang: item.hreflang,
ping: item.ping,
referrerpolicy: item.referrerpolicy,
rel: item.rel,
role: item.role,
target: item.target,
type: item.type,
}
return elem('a', attr, icon + img + renderInline(link.label) + icon_right + kbd)
}

Expand All @@ -132,14 +147,12 @@ export function parseClass(url) {
return data
}


export function renderNav({ items, icon_dir, heading='' }) {
export function renderNav({ items, icon_dir, heading = '' }) {
const html = items.map(item => renderLink(item, icon_dir))
return elem('nav', heading + html.join('\n'))
}


export function renderMultiNav(cats, data={}) {
export function renderMultiNav(cats, data = {}) {
const { icon_dir } = data
const html = []

Expand All @@ -152,4 +165,3 @@ export function renderMultiNav(cats, data={}) {

return elem('div', { class: data.class }, html.join('\n'))
}

46 changes: 25 additions & 21 deletions packages/nuekit/test/component.test.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,55 @@

// tests for helper/core components
import {
renderPage,
parseClass,
parseLink,
renderNav,
renderLink,
renderMultiNav } from '../src/layout/components.js'

renderMultiNav,
} from '../src/layout/components.js'

test('render page', () => {
const html = renderPage({
desc: 'Wassup *bro*',
title: 'Yo',
url: '/bruh/'
url: '/bruh/',
})

expect(html).toStartWith('<li class="is-new"><time datetime="')
expect(html).toInclude('<a href="/bruh/"><h2>Yo</h2>')
expect(html).toEndWith('<p>Wassup <em>bro</em></p></a></li>')
})


test('render page with a thumb', () => {
const html = renderPage({ title: 'Yo', thumb: 'thumb.png', url: '/' })
expect(html).toStartWith('<li class="is-new"><a href="/"><figure><img')
expect(html).toEndWith('</figure></a></li>')
})


test('parse class', () => {
expect(parseClass('/foo "bar"')).toEqual({ url: "/foo", class: "bar" })
expect(parseClass('/foo "bar"')).toEqual({ url: '/foo', class: 'bar' })
})

test('parse link', () => {
expect(parseLink({ FAQ: '/faq' })).toEqual({ label: "FAQ", url: "/faq" })
expect(parseLink({ Hey: '/ "baz"' })).toEqual({ label: "Hey", url: '/', class: 'baz' })
expect(parseLink({ FAQ: '/faq' })).toEqual({ label: 'FAQ', url: '/faq' })
expect(parseLink({ Hey: '/ "baz"' })).toEqual({ label: 'Hey', url: '/', class: 'baz' })
})


test('parse link / plain string', () => {
expect(parseLink('FAQ')).toEqual({ label: "FAQ", url: "" })
expect(parseLink('FAQ')).toEqual({ label: 'FAQ', url: '' })
expect(parseLink('---')).toEqual({ separator: '---' })
})

test('render link', () => {
expect(renderLink({ 'Hey': '/' })).toBe('<a href="/">Hey</a>')
expect(renderLink({ url: '/', label: 'Hey'})).toBe('<a href="/">Hey</a>')
expect(renderLink({ Hey: '/' })).toBe('<a href="/">Hey</a>')
expect(renderLink({ url: '/', label: 'Hey' })).toBe('<a href="/">Hey</a>')
})

test('render link with attributes', () => {
expect(renderLink({ Hey: '/', target: '_blank' })).toBe('<a href="/" target="_blank">Hey</a>')
expect(renderLink({ url: '/', label: 'Hey', target: '_blank' })).toBe(
'<a href="/" target="_blank">Hey</a>'
)
})

test('render image link', () => {
Expand All @@ -58,24 +61,25 @@ test('render image link', () => {
url: '/',
})

expect(html).toStartWith('<a href="/" class="logo">')
expect(html).toStartWith('<a class="logo" href="/">')
expect(html).toInclude('<img src="logo.svg" width="60" height="18" alt="Nue logo">')
})

test('render nav', () => {
const items = [{ 'Hey': '/'}]
const items = [{ Hey: '/' }]
const html = renderNav({ items })
expect(html).toBe('<nav><a href="/">Hey</a></nav>')
})

test('render categorized nav', () => {
const html = renderMultiNav({
Hey: [{ Foo: '/'}],
Foo: [{ Bar: '/'}],

}, { class: 'epic' })
const html = renderMultiNav(
{
Hey: [{ Foo: '/' }],
Foo: [{ Bar: '/' }],
},
{ class: 'epic' }
)

expect(html).toStartWith('<div class="epic"><nav><h4>Hey</h4><a href="/">Foo</a></nav>')
expect(html).toEndWith('<nav><h4>Foo</h4><a href="/">Bar</a></nav></div>')
})

0 comments on commit e7cf831

Please sign in to comment.