Skip to content

Commit

Permalink
Add my solution
Browse files Browse the repository at this point in the history
The original repository lives at: https://github.com/TomasHubelbauer/leveret
  • Loading branch information
TomasHubelbauer committed Sep 15, 2024
1 parent 72b4600 commit eacf2bb
Show file tree
Hide file tree
Showing 27 changed files with 3,955 additions and 0 deletions.
11 changes: 11 additions & 0 deletions tomashubelbauer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# NPM
node_modules

# Bun
bun.lockb

# Caches
fetchCached.ts.*.html

# Outputs
index
21 changes: 21 additions & 0 deletions tomashubelbauer/fetchCached.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { file, write } from 'bun';

export default async function fetchCached(url: string) {
const protocol = new URL(url).protocol;
if (protocol !== 'http:' && protocol !== 'https:') {
throw new Error('Only HTTP(S) URLs are supported');
}

const name = url.slice((protocol + '//').length).replace(/[^a-z0-9]+/gi, '-');
const path = `${import.meta.file}.${name}.html`;

const cache = file(path);
if (await cache.exists()) {
return await cache.text();
}

const response = await fetch(url);
const text = await response.text();
write(path, text);
return text;
}
55 changes: 55 additions & 0 deletions tomashubelbauer/fitText.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { expect, test } from 'bun:test';
import fitText from './fitText';
import { createCanvas } from '@napi-rs/canvas';

test('empty', () => {
const canvas = createCanvas(640, 480);
const context = canvas.getContext('2d');
expect(fitText('', 100, context)).toEqual({ text: '', width: 0 });
});

test('fit', () => {
const canvas = createCanvas(640, 480);
const context = canvas.getContext('2d');
expect(fitText('short', 100, context)).toEqual({ text: 'short', width: 22 });
});

test('one break', () => {
const canvas = createCanvas(640, 480);
const context = canvas.getContext('2d');

let text = 'short but long enough to break';

const slice = fitText(text, 100, context);
expect(slice).toEqual({
text: 'short but long enough ',
width: 99,
});

const rest = text.slice(slice.text.length);
expect(fitText(rest, 100, context)).toEqual({ text: 'to break', width: 36 });
});

test('two breaks', () => {
const canvas = createCanvas(640, 480);
const context = canvas.getContext('2d');

let text = 'short but long enough to break and then break again';

const slice1 = fitText(text, 100, context);
expect(slice1).toEqual({
text: 'short but long enough ',
width: 99,
});

text = text.slice(slice1.text.length);

const slice2 = fitText(text, 100, context);
expect(slice2).toEqual({
text: 'to break and then brea',
width: 100,
});

const rest = text.slice(slice2.text.length);
expect(fitText(rest, 100, context)).toEqual({ text: 'k again', width: 32 });
});
31 changes: 31 additions & 0 deletions tomashubelbauer/fitText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SKRSContext2D } from '@napi-rs/canvas';

export default function fitText(text: string, limit: number, context: SKRSContext2D) {
if (limit < 0) {
throw new Error('limit must be greater than or equal to 0');
}

let low = 0;
let high = text.length;
let result = text;
let finalWidth = 0;

while (low <= high) {
const mid = ~~((low + high) / 2);
const candidate = text.slice(0, mid);
const width = ~~context.measureText(candidate).width;

if (width <= limit) {
result = candidate;
finalWidth = width;
low = mid + 1;
} else {
high = mid - 1;
}
}

return {
text: result,
width: finalWidth,
};
}
Binary file added tomashubelbauer/index.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions tomashubelbauer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { write } from 'bun';
import { createCanvas } from '@napi-rs/canvas';
import parse from './parse';
import layout from './layout';
import render from './render';
import fetchCached from './fetchCached';

// TODO: Accept a custom URL from the CLI
// Download the 1st WWW website page and parse it
// Use `view-source:http://info.cern.ch/hypertext/WWW/TheProject.html` to debug
const url = 'http://info.cern.ch/hypertext/WWW/TheProject.html';

// Set up the native, Skia-based `canvas` and its 2D rendering `context`
const canvas = createCanvas(640, 480);
const context = canvas.getContext('2d');

context.save();
context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);
context.restore();

const html = await fetchCached(url);
let document = parse(html);
if (document.errors.length > 0) {
let html = `<DIV color=red>Leveret failed to parse ${url}!</DIV>`;
for (const error of document.errors) {
html += `<DIV>${error}</DIV>`;
}

document = parse(html);
}

const { node, getSuperNode, errors, infos } = document;
if (errors.length > 0) {
throw new Error(`Failed to parse ${url}:\n${errors.join('\n')}`);
}

for (const info of infos) {
console.log(info);
}

render(layout(node, getSuperNode, context), context);
await write('index.png', await canvas.encode('png'));
7 changes: 7 additions & 0 deletions tomashubelbauer/isBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ElementNode } from './parse';

const BLOCKS = ['HTML', 'BODY', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P', 'DL', 'DD', 'DT', 'UL', 'LI', 'MENU', 'DIV', 'HEADER'];

export default function isBlock(node: ElementNode) {
return BLOCKS.includes(node.tag);
}
7 changes: 7 additions & 0 deletions tomashubelbauer/isNonVisual.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LayoutNode } from './layout';

const NON_VISUALS = ['HEADER'];

export default function isNonVisual(node: LayoutNode) {
return (node.type === 'element' && NON_VISUALS.includes(node.tag)) || node.layout === 'removed';
}
Loading

0 comments on commit eacf2bb

Please sign in to comment.