feat: add prettier

This commit is contained in:
Araozu 2024-11-23 16:45:31 -05:00
parent b5e22cca62
commit 5814b145fe
91 changed files with 8759 additions and 3644 deletions

13
.prettierrc.mjs Normal file
View File

@ -0,0 +1,13 @@
/** @type {import("prettier").Config} */
export default {
plugins: ["prettier-plugin-astro"],
useTabs: true,
overrides: [
{
files: "*.astro",
options: {
parser: "astro",
},
},
],
};

View File

@ -1,12 +1,12 @@
import { defineConfig } from 'astro/config';
import { defineConfig } from "astro/config";
import tailwind from "@astrojs/tailwind";
import mdx from "@astrojs/mdx";
// https://astro.build/config
export default defineConfig({
integrations: [tailwind(), mdx()],
markdown: {
syntaxHighlight: "prism",
},
});
integrations: [tailwind(), mdx()],
markdown: {
syntaxHighlight: "prism",
},
});

View File

@ -20,4 +20,3 @@ networks:
proxy:
name: proxy
external: true

View File

@ -8,6 +8,7 @@
"build": "astro build",
"test": "vitest run",
"preview": "astro preview",
"prettier": "prettier \"./src/**/*.astro\" --write",
"astro": "astro"
},
"dependencies": {
@ -19,5 +20,9 @@
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vitest": "^1.5.0"
},
"devDependencies": {
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,110 +1,110 @@
/* Iosevka web */
@font-face {
font-family: "Iosevka Fixed Web";
font-display: swap;
font-weight: 400;
font-stretch: normal;
font-style: normal;
src: url("/Iosevka/Regular.woff2") format("woff2");
font-family: "Iosevka Fixed Web";
font-display: swap;
font-weight: 400;
font-stretch: normal;
font-style: normal;
src: url("/Iosevka/Regular.woff2") format("woff2");
}
@font-face {
font-family: 'Iosevka Fixed Web';
font-display: swap;
font-weight: 700;
font-stretch: normal;
font-style: normal;
src: url('/Iosevka/Bold.woff2') format('woff2');
font-family: "Iosevka Fixed Web";
font-display: swap;
font-weight: 700;
font-stretch: normal;
font-style: normal;
src: url("/Iosevka/Bold.woff2") format("woff2");
}
:root {
--c-thp: #f472b6;
--c-thp: #f472b6;
--font-display: 'Atkinson Hyperlegible', sans-serif;
--font-body: 'Atkinson Hyperlegible', sans-serif;
--font-code: "Iosevka Fixed Web", "Iosevka Nerd Font", Iosevka, monospace;
--font-display: "Atkinson Hyperlegible", sans-serif;
--font-body: "Atkinson Hyperlegible", sans-serif;
--font-code: "Iosevka Fixed Web", "Iosevka Nerd Font", Iosevka, monospace;
}
:root {
--c-bg: #121212;
--c-text: rgb(200, 200, 200);
--c-text-2: white;
--c-primary: #884b6a;
--c-purple: #7F669D;
--c-purple-light: #BA94D1;
--c-box-shadow: #FBFACD;
--c-pink: #AE508D;
--c-link: #38bdf8;
--c-bg: #121212;
--c-text: rgb(200, 200, 200);
--c-text-2: white;
--c-primary: #884b6a;
--c-purple: #7f669d;
--c-purple-light: #ba94d1;
--c-box-shadow: #fbfacd;
--c-pink: #ae508d;
--c-link: #38bdf8;
--c-nav-bg: rgb(18, 18, 18, 0.5);
--c-secondary: rgba(136, 75, 106, 0.5);
--c-nav-bg: rgb(18, 18, 18, 0.5);
--c-secondary: rgba(136, 75, 106, 0.5);
}
@media (prefers-color-scheme: light) {
:root {
--c-bg: rgb(255, 253, 255);
--c-text: #121212;
--c-text-2: black;
--c-purple: #374259;
--c-purple-light: #BA94D1;
--c-box-shadow: #374259;
--c-primary: rgb(255, 180, 180);
--c-pink: #374259;
--c-link: #0284c7;
--c-border-1: #909090;
:root {
--c-bg: rgb(255, 253, 255);
--c-text: #121212;
--c-text-2: black;
--c-purple: #374259;
--c-purple-light: #ba94d1;
--c-box-shadow: #374259;
--c-primary: rgb(255, 180, 180);
--c-pink: #374259;
--c-link: #0284c7;
--c-border-1: #909090;
--c-nav-bg: rgb(255, 247, 255, 0.5);
--c-secondary: rgba(255, 255, 240, 0.5);
}
--c-nav-bg: rgb(255, 247, 255, 0.5);
--c-secondary: rgba(255, 255, 240, 0.5);
}
}
html {
font-size: 16px;
font-size: 16px;
}
@media screen and (min-width: 1600px) {
html {
font-size: 17px;
}
html {
font-size: 17px;
}
}
body {
font-family: var(--font-body);
font-family: var(--font-body);
}
@media only screen and (max-width: 600px) {
#editor {
font-size: 0.9em;
}
#editor {
font-size: 0.9em;
}
}
pre,
code {
font-family: var(--font-code);
font-family: var(--font-code);
}
.button-effect-receiver {
transition: transform 150ms;
transition: transform 150ms;
}
.button-effect:hover .button-effect-receiver {
transform: translateX(-3px) translateY(-3px);
transform: translateX(-3px) translateY(-3px);
}
.markdown p {
margin: 0.75rem 0;
margin: 0.75rem 0;
}
/* Used by headers generated from markdown */
.heading-linked :hover::after {
color: var(--c-primary);
content: "#";
display: inline-block;
font-size: 1em;
line-height: 1;
margin-left: 0.5rem;
color: var(--c-primary);
content: "#";
display: inline-block;
font-size: 1em;
line-height: 1;
margin-left: 0.5rem;
}
.heading-linked :hover {
text-decoration: underline var(--c-primary);
}
text-decoration: underline var(--c-primary);
}

View File

@ -1,105 +1,103 @@
.markdown > h1 {
font-size: 2.25rem;
line-height: 2.5rem;
margin-bottom: 1rem;
margin-top: 2rem;
font-family: var(--font-display);
opacity: 0.9;
font-weight: 700;
color: var(--c-text-2);
scroll-margin-top: 50px;
font-size: 2.25rem;
line-height: 2.5rem;
margin-bottom: 1rem;
margin-top: 2rem;
font-family: var(--font-display);
opacity: 0.9;
font-weight: 700;
color: var(--c-text-2);
scroll-margin-top: 50px;
}
.markdown > h2 {
/* use these tailwind classes: text-2xl mt-10 mb-4 font-display opacity-90 font-bold text-c-text-2 */
font-size: 1.5rem;
line-height: 2rem;
margin-bottom: 1rem;
margin-top: 2.5rem;
font-family: var(--font-display);
opacity: 0.9;
font-weight: 600;
color: var(--c-text-2);
scroll-margin-top: 5rem;
/* use these tailwind classes: text-2xl mt-10 mb-4 font-display opacity-90 font-bold text-c-text-2 */
font-size: 1.5rem;
line-height: 2rem;
margin-bottom: 1rem;
margin-top: 2.5rem;
font-family: var(--font-display);
opacity: 0.9;
font-weight: 600;
color: var(--c-text-2);
scroll-margin-top: 5rem;
}
.markdown > h2::before {
content: "#";
display: block;
height: 0;
position: relative;
right: 1.5rem;
opacity: 0.2;
content: "#";
display: block;
height: 0;
position: relative;
right: 1.5rem;
opacity: 0.2;
}
.markdown > h2:target::before {
color: var(--c-thp);
opacity: 1;
color: var(--c-thp);
opacity: 1;
}
.markdown > h3 {
font-size: 1.35rem;
line-height: 1.75rem;
margin-bottom: 1rem;
margin-top: 1.75rem;
font-family: var(--font-display);
opacity: 0.9;
font-weight: 400;
color: var(--c-text-2);
scroll-margin-top: 5rem;
font-size: 1.35rem;
line-height: 1.75rem;
margin-bottom: 1rem;
margin-top: 1.75rem;
font-family: var(--font-display);
opacity: 0.9;
font-weight: 400;
color: var(--c-text-2);
scroll-margin-top: 5rem;
}
.markdown > h3::before {
content: "##";
display: block;
height: 0;
position: relative;
right: 2.25rem;
opacity: 0.2;
content: "##";
display: block;
height: 0;
position: relative;
right: 2.25rem;
opacity: 0.2;
}
.markdown > h3:target::before {
color: var(--c-thp);
opacity: 1;
color: var(--c-thp);
opacity: 1;
}
.markdown ul {
list-style-type: disc;
list-style-position: inside;
list-style-type: disc;
list-style-position: inside;
}
.markdown ul li {
padding: 0.5rem 0;
padding: 0.5rem 0;
}
.markdown p > code {
border: solid 1px var(--c-border-1);
border: solid 1px var(--c-border-1);
}
.markdown > pre {
margin: 0.5em 0;
padding: 0.75em 0.75em;
color: var(--code-theme-color);
background: var(--code-theme-bg-color);
margin: 0.5em 0;
padding: 0.75em 0.75em;
color: var(--code-theme-color);
background: var(--code-theme-bg-color);
}
.markdown > p > a {
color: var(--c-link);
text-decoration: underline;
color: var(--c-link);
text-decoration: underline;
}
.markdown blockquote {
color: red;
color: red;
}
.two-column a {
color: #2563eb;
text-decoration: underline;
display: inline-block;
color: #2563eb;
text-decoration: underline;
display: inline-block;
}
.two-column h3 {
margin: 0.75rem 0;
margin: 0.75rem 0;
}

View File

@ -1,3 +1,118 @@
/* PrismJS 1.29.0
https://prismjs.com/download.html#themes=prism */
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
code[class*="language-"],
pre[class*="language-"] {
color: #000;
background: 0 0;
text-shadow: 0 1px #fff;
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
code[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
pre[class*="language-"]::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
code[class*="language-"] ::selection,
code[class*="language-"]::selection,
pre[class*="language-"] ::selection,
pre[class*="language-"]::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
pre[class*="language-"] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
:not(pre) > code[class*="language-"] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.cdata,
.token.comment,
.token.doctype,
.token.prolog {
color: #708090;
}
.token.punctuation {
color: #999;
}
.token.namespace {
opacity: 0.7;
}
.token.boolean,
.token.constant,
.token.deleted,
.token.number,
.token.property,
.token.symbol,
.token.tag {
color: #905;
}
.token.attr-name,
.token.builtin,
.token.char,
.token.inserted,
.token.selector,
.token.string {
color: #690;
}
.language-css .token.string,
.style .token.string,
.token.entity,
.token.operator,
.token.url {
color: #9a6e3a;
background: hsla(0, 0%, 100%, 0.5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.class-name,
.token.function {
color: #dd4a68;
}
.token.important,
.token.regex,
.token.variable {
color: #e90;
}
.token.bold,
.token.important {
font-weight: 700;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@ -1,270 +1,270 @@
/* colors from: https://raw.githubusercontent.com/WhiteVermouth/XcodeTheme/master/assets/color-palette.png */
:root {
--code-theme-color: #dedede;
--code-theme-bg-color: #1f1f24;
--code-theme-bg-color_selection: #515b70;
--code-theme-color_selection: inherit;
--code-theme-color: #dedede;
--code-theme-bg-color: #1f1f24;
--code-theme-bg-color_selection: #515b70;
--code-theme-color_selection: inherit;
--code-theme-comment: #6c7986;
--code-theme-comment: #6c7986;
/* number */
--code-theme-c1: #d0bf69;
/* keyword */
--code-theme-c2: #fc5fa3;
/* ? */
--code-theme-c3: #39adb5;
/* string */
--code-theme-c4: rgb(124, 201, 117);
/* #fc6a5d; */
/* declaration */
--code-theme-c5: #5dd8ff;
/* proyect function */
--code-theme-c6: #67b7a4;
/*#e53935;*/
/* function */
--code-theme-c7: rgb(179, 146, 240);
/* number */
--code-theme-c1: #d0bf69;
/* keyword */
--code-theme-c2: #fc5fa3;
/* ? */
--code-theme-c3: #39adb5;
/* string */
--code-theme-c4: rgb(124, 201, 117);
/* #fc6a5d; */
/* declaration */
--code-theme-c5: #5dd8ff;
/* proyect function */
--code-theme-c6: #67b7a4;
/*#e53935;*/
/* function */
--code-theme-c7: rgb(179, 146, 240);
--code-theme-punctuation: #dedede;
--code-theme-punctuation: #dedede;
}
@media (prefers-color-scheme: light) {
:root {
--code-theme-color: #3a3a3a;
--code-theme-bg-color: #ffffff;
--code-theme-bg-color_selection: #a4cdff;
--code-theme-color_selection: inherit;
--code-theme-border-color: #c2c2c2;
:root {
--code-theme-color: #3a3a3a;
--code-theme-bg-color: #ffffff;
--code-theme-bg-color_selection: #a4cdff;
--code-theme-color_selection: inherit;
--code-theme-border-color: #c2c2c2;
--code-theme-comment: #5d6c79;
--code-theme-comment: #5d6c79;
/* number */
--code-theme-c1: #1c00cf;
/* keyword */
--code-theme-c2: #9b2393;
--code-theme-c3: #39adb5;
/* string */
--code-theme-c4: #c41a16;
/* declaration */
--code-theme-c5: #0f68a0;
--code-theme-c6: #326d74;
/*#e53935;*/
/* function */
--code-theme-c7: rgb(95, 74, 134);
/* number */
--code-theme-c1: #1c00cf;
/* keyword */
--code-theme-c2: #9b2393;
--code-theme-c3: #39adb5;
/* string */
--code-theme-c4: #c41a16;
/* declaration */
--code-theme-c5: #0f68a0;
--code-theme-c6: #326d74;
/*#e53935;*/
/* function */
--code-theme-c7: rgb(95, 74, 134);
--code-theme-punctuation: #202020;
}
--code-theme-punctuation: #202020;
}
}
code[class*="language-"],
pre[class*="language-"] {
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
color: var(--code-theme-color);
background: var(--code-theme-bg-color);
line-height: 1.5em;
border-radius: 0.3em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
color: var(--code-theme-color);
background: var(--code-theme-bg-color);
line-height: 1.5em;
border-radius: 0.3em;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
code[class*="language-"]::-moz-selection,
pre[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection,
pre[class*="language-"] ::-moz-selection {
background: var(--code-theme-bg-color_selection);
color: var(--code-theme-color_selection);
background: var(--code-theme-bg-color_selection);
color: var(--code-theme-color_selection);
}
code[class*="language-"]::selection,
pre[class*="language-"]::selection,
code[class*="language-"] ::selection,
pre[class*="language-"] ::selection {
background: var(--code-theme-bg-color_selection);
color: var(--code-theme-color_selection);
background: var(--code-theme-bg-color_selection);
color: var(--code-theme-color_selection);
}
:not(pre)>code[class*="language-"] {
white-space: normal;
border-radius: 0.2em;
padding: 0.1em;
:not(pre) > code[class*="language-"] {
white-space: normal;
border-radius: 0.2em;
padding: 0.1em;
}
:not(pre)>code {
background-color: var(--code-theme-bg-color);
padding: 0 0.25rem;
border-radius: 5px;
:not(pre) > code {
background-color: var(--code-theme-bg-color);
padding: 0 0.25rem;
border-radius: 5px;
}
pre[class*="language-"] {
overflow: auto;
position: relative;
margin: 0.5em 0;
padding: 0.75em 0.75em;
border: 1px solid var(--code-theme-border-color);
overflow: auto;
position: relative;
margin: 0.5em 0;
padding: 0.75em 0.75em;
border: 1px solid var(--code-theme-border-color);
}
.language-css>code,
.language-sass>code,
.language-scss>code {
color: var(--code-theme-c1);
.language-css > code,
.language-sass > code,
.language-scss > code {
color: var(--code-theme-c1);
}
[class*="language-"] .namespace {
opacity: 0.7;
opacity: 0.7;
}
.token.atrule {
color: var(--code-theme-c2);
color: var(--code-theme-c2);
}
.token.attr-name {
color: var(--code-theme-c3);
color: var(--code-theme-c3);
}
.token.attr-value {
color: var(--code-theme-c4);
color: var(--code-theme-c4);
}
.token.attribute {
color: var(--code-theme-c4);
color: var(--code-theme-c4);
}
.token.boolean {
color: var(--code-theme-c2);
color: var(--code-theme-c2);
}
.token.builtin {
color: var(--code-theme-c3);
color: var(--code-theme-c3);
}
.token.cdata {
color: var(--code-theme-c3);
color: var(--code-theme-c3);
}
.token.char {
color: var(--code-theme-c3);
color: var(--code-theme-c3);
}
.token.class {
color: var(--code-theme-c3);
color: var(--code-theme-c3);
}
.token.class-name {
color: var(--code-theme-c5);
color: var(--code-theme-c5);
}
.token.comment {
color: var(--code-theme-comment);
color: var(--code-theme-comment);
}
.token.constant {
color: var(--code-theme-c2);
color: var(--code-theme-c2);
}
.token.deleted {
color: var(--code-theme-c6);
color: var(--code-theme-c6);
}
.token.doctype {
color: var(--code-theme-comment);
color: var(--code-theme-comment);
}
.token.entity {
color: var(--code-theme-c6);
color: var(--code-theme-c6);
}
.token.function {
color: var(--code-theme-c7);
color: var(--code-theme-c7);
}
.token.hexcode {
color: var(--code-theme-c1);
color: var(--code-theme-c1);
}
.token.id {
color: var(--code-theme-c2);
font-weight: bold;
color: var(--code-theme-c2);
font-weight: bold;
}
.token.important {
color: var(--code-theme-c2);
font-weight: bold;
color: var(--code-theme-c2);
font-weight: bold;
}
.token.inserted {
color: var(--code-theme-c3);
color: var(--code-theme-c3);
}
.token.keyword {
color: var(--code-theme-c2);
color: var(--code-theme-c2);
}
.token.number {
color: var(--code-theme-c1);
color: var(--code-theme-c1);
}
.token.operator {
color: var(--code-theme-punctuation);
color: var(--code-theme-punctuation);
}
.token.prolog {
color: var(--code-theme-comment);
color: var(--code-theme-comment);
}
.token.property {
color: var(--code-theme-c3);
color: var(--code-theme-c3);
}
.token.pseudo-class {
color: var(--code-theme-c4);
color: var(--code-theme-c4);
}
.token.pseudo-element {
color: var(--code-theme-c4);
color: var(--code-theme-c4);
}
.token.punctuation {
color: var(--code-theme-punctuation);
color: var(--code-theme-punctuation);
}
.token.regex {
color: var(--code-theme-c5);
color: var(--code-theme-c5);
}
.token.selector {
color: var(--code-theme-c6);
color: var(--code-theme-c6);
}
.token.string {
color: var(--code-theme-c4);
color: var(--code-theme-c4);
}
.token.symbol {
color: var(--code-theme-c2);
color: var(--code-theme-c2);
}
.token.tag {
color: var(--code-theme-c6);
color: var(--code-theme-c6);
}
.token.unit {
color: var(--code-theme-c1);
color: var(--code-theme-c1);
}
.token.url {
color: var(--code-theme-c6);
color: var(--code-theme-c6);
}
.token.variable {
color: var(--code-theme-c6);
}
color: var(--code-theme-c6);
}

File diff suppressed because one or more lines are too long

View File

@ -5,54 +5,54 @@ import { splitAndLast } from "../utils";
const hierarchy: Hierarchy = Astro.props.hierarchy;
function postComparison(a: Post, b: Post): number {
const s1 = splitAndLast(a.url);
const s2 = splitAndLast(b.url);
const s1 = splitAndLast(a.url);
const s2 = splitAndLast(b.url);
return s1 > s2 ? 0 : 1;
return s1 > s2 ? 0 : 1;
}
function appendSlash(s: string): string {
if (s.endsWith("/")) {
return s;
} else {
return s + "/";
}
if (s.endsWith("/")) {
return s;
} else {
return s + "/";
}
}
---
{
Object.entries(hierarchy.children).map(
([folderName, [folderPost, children]]) => (
<>
{folderPost !== null ? (
<a
class="inline-block rounded-2xl w-full hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors px-3 py-2"
href={appendSlash(folderPost.url)}
>
{splitAndLast(folderPost.url)}
</a>
) : (
<div class="mt-6 px-2 py-1 uppercase font-display text-c-text-2 font-medium">
{folderName}
</div>
)}
Object.entries(hierarchy.children).map(
([folderName, [folderPost, children]]) => (
<>
{folderPost !== null ? (
<a
class="inline-block rounded-2xl w-full hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors px-3 py-2"
href={appendSlash(folderPost.url)}
>
{splitAndLast(folderPost.url)}
</a>
) : (
<div class="mt-6 px-2 py-1 uppercase font-display text-c-text-2 font-medium">
{folderName}
</div>
)}
<div class="pl-2 my-1">
<ul class="border-l border-c-border-1">
<Astro.self hierarchy={children} />
</ul>
</div>
</>
),
)
<div class="pl-2 my-1">
<ul class="border-l border-c-border-1">
<Astro.self hierarchy={children} />
</ul>
</div>
</>
),
)
}
{
hierarchy.posts.sort(postComparison).map((p) => (
<a
class="inline-block rounded-2xl w-full hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors px-3 py-2"
href={appendSlash(p.url)}
>
{splitAndLast(p.url)}
</a>
))
hierarchy.posts.sort(postComparison).map((p) => (
<a
class="inline-block rounded-2xl w-full hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors px-3 py-2"
href={appendSlash(p.url)}
>
{splitAndLast(p.url)}
</a>
))
}

View File

@ -5,13 +5,16 @@ import CodeError from "./docs/CodeError.astro";
const { thpcode, no_warnings, level } = Astro.props;
const [native_html, error_message] = await native_highlighter(thpcode, level as HighlightLevel);
const [native_html, error_message] = await native_highlighter(
thpcode,
level as HighlightLevel,
);
---
<pre
class="language-thp"><code class="language-thp" set:html={native_html} /><span class="absolute top-1 right-1 text-right inline-block text-sm select-none opacity-75">thp</span></pre>
class="language-thp"><code class="language-thp" set:html={native_html} /><span class="absolute top-1 right-1 text-right inline-block text-sm select-none opacity-75">thp</span></pre>
{
no_warnings !== true && error_message !== null && (
<CodeError error_message={error_message} />
)
no_warnings !== true && error_message !== null && (
<CodeError error_message={error_message} />
)
}

View File

@ -1,11 +1,11 @@
---
import Code from "./Code.astro"
import Code from "./Code.astro";
const { thpcode, no_warnings, level } = Astro.props;
---
<div class="flex h-full md:py-8 items-center">
<div class="p-4 w-full">
<Code thpcode={thpcode} no_warnings={no_warnings} level={level} />
</div>
<div class="p-4 w-full">
<Code thpcode={thpcode} no_warnings={no_warnings} level={level} />
</div>
</div>

View File

@ -5,35 +5,35 @@ import CodeEditor from "./CodeEditor.astro";
const { title, thpcode } = Astro.props;
if (!thpcode) {
throw new Error("thpcode is required");
throw new Error("thpcode is required");
}
if (!title) {
throw new Error("title is required");
throw new Error("title is required");
}
---
<div>
<div
class="md:bg-c-thp md:text-c-bg text-c-thp border-t-4 border-b-4 border-c-thp"
>
<h1
class="container mx-auto font-medium md:py-8 py-4 px-4 md:text-3xl text-2xl font-display"
>
✅ <span class="font-black">{title}</span>
</h1>
</div>
<div
class="md:bg-c-thp md:text-c-bg text-c-thp border-t-4 border-b-4 border-c-thp"
>
<h1
class="container mx-auto font-medium md:py-8 py-4 px-4 md:text-3xl text-2xl font-display"
>
✅ <span class="font-black">{title}</span>
</h1>
</div>
<div class="container mx-auto lg:grid lg:grid-cols-2 gap-4">
<div class="md:px-8 md:py-12 px-4 pt-6 flex h-full items-center">
<div>
<slot />
</div>
</div>
<CodeEditor
thpcode={thpcode}
no_warnings={true}
level={HighlightLevel.Lexic}
/>
</div>
<div class="container mx-auto lg:grid lg:grid-cols-2 gap-4">
<div class="md:px-8 md:py-12 px-4 pt-6 flex h-full items-center">
<div>
<slot />
</div>
</div>
<CodeEditor
thpcode={thpcode}
no_warnings={true}
level={HighlightLevel.Lexic}
/>
</div>
</div>

View File

@ -1,63 +1,58 @@
---
const { showSidebarButton = true } = Astro.props;
const { showSidebarButton = true, version = "latest" } = Astro.props;
---
<nav
class="fixed w-full top-0 h-12 border-b border-[rgba(150,150,150,0.25)] bg-c-nav-bg backdrop-blur-md z-20"
class="fixed w-full top-0 h-12 border-b border-[rgba(150,150,150,0.25)] bg-c-nav-bg backdrop-blur-md z-20"
>
<div class="container mx-auto h-full w-full flex items-center">
{
showSidebarButton && (
<button id="sidebar-toggle" class="w-10 h-full lg:hidden">
<i class="ph-bold ph-list text-xl inline-block" />
</button>
)
}
<div class="container mx-auto h-full w-full flex items-center">
{
showSidebarButton && (
<button id="sidebar-toggle" class="w-10 h-full lg:hidden">
<i class="ph-bold ph-list text-xl inline-block" />
</button>
)
}
<a
href="/"
class="px-4 flex gap-2 items-center"
>
<img
class="inline-block h-10"
src="/img/thp_logo_exp.svg"
alt="thp"
/>
</a>
<a
href="/en/latest/learn/"
class="hidden sm:inline-block px-4 font-display font-bold-text-xl hover:underline"
>
Learn
</a>
<a
href="/api/std/"
class="hidden sm:inline-block px-4 font-display font-bold-text-xl hover:underline"
>
Standard Library
</a>
<a
href="/spec/"
class="hidden sm:inline-block px-4 font-display font-bold-text-xl hover:underline"
>
Language spec
</a>
</div>
<a href="/" class="px-4 flex gap-2 items-center">
<img class="inline-block h-10" src="/img/thp_logo_exp.svg" alt="thp" />
</a>
<a
href={`/en/${version}/learn/`}
class="hidden sm:inline-block px-4 font-display font-bold-text-xl hover:underline"
>
Learn
</a>
<a
href="/api/std/"
class="hidden sm:inline-block px-4 font-display font-bold-text-xl hover:underline"
>
Standard Library
</a>
<a
href="/spec/"
class="hidden sm:inline-block px-4 font-display font-bold-text-xl hover:underline"
>
Language spec
</a>
</div>
</nav>
<script>
document.addEventListener("DOMContentLoaded", () => {
const sidebar = document.getElementById("sidebar");
const sidebarToggle = document.getElementById("sidebar-toggle");
document.addEventListener("DOMContentLoaded", () => {
const sidebar = document.getElementById("sidebar");
const sidebarToggle = document.getElementById("sidebar-toggle");
if (!sidebar || !sidebarToggle) {
console.log("Sidebar or Sidebar toggle not found. Not enabling sidebar on mobile");
return;
}
if (!sidebar || !sidebarToggle) {
console.log(
"Sidebar or Sidebar toggle not found. Not enabling sidebar on mobile",
);
return;
}
sidebarToggle.addEventListener("click", () => {
sidebar.classList.toggle("-translate-x-64");
console.log(sidebar.classList.contains("-translate-x-64"));
});
});
sidebarToggle.addEventListener("click", () => {
sidebar.classList.toggle("-translate-x-64");
console.log(sidebar.classList.contains("-translate-x-64"));
});
});
</script>

View File

@ -7,38 +7,33 @@ const post_url = entry.url + (entry.url.endsWith("/") ? "" : "/");
// this may deal with folders.
// if so, it will turn any `-` into whitespace,
// and remove any leading number
const entry_title = entry.title
.replaceAll("-", " ")
.replaceAll(/\d+_/g, "");
const entry_title = entry.title.replaceAll("-", " ").replaceAll(/\d+_/g, "");
---
{
!entry.children && (
<li>
<a
class="inline-block rounded-lg w-full hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors px-3 py-1"
href={post_url}
>
{entry.title}
</a>
</li>
)
!entry.children && (
<li>
<a
class="inline-block rounded-lg w-full hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors px-3 py-1"
href={post_url}
>
{entry.title}
</a>
</li>
)
}
{
entry.children && (
<>
<div class="mt-6 px-3 py-1 uppercase font-display text-c-text-2 font-medium">
{entry_title}
</div>
entry.children && (
<>
<div class="mt-6 px-3 py-1 uppercase font-display text-c-text-2 font-medium">
{entry_title}
</div>
<ul class="my-1">
{entry.children!.map((nextEntry) => (
<Astro.self
entry={nextEntry}
/>
))}
</ul>
</>
)
<ul class="my-1">
{entry.children!.map((nextEntry) => (
<Astro.self entry={nextEntry} />
))}
</ul>
</>
)
}

View File

@ -7,37 +7,36 @@ const { headings } = Astro.props;
const toc = buildHierarchy(headings);
function buildHierarchy(headings: any) {
const toc: any[] = [];
const parentHeadings = new Map();
const toc: any[] = [];
const parentHeadings = new Map();
if (!headings) return toc;
if (!headings) return toc;
headings.forEach((h: any) => {
const heading = { ...h, subheadings: [] };
parentHeadings.set(heading.depth, heading);
// Change 2 to 1 if your markdown includes your <h1>
if (heading.depth === 2) {
toc.push(heading);
} else if (heading.depth === 1) {
/** empty */
}
else {
parentHeadings.get(heading.depth - 1).subheadings.push(heading);
}
});
headings.forEach((h: any) => {
const heading = { ...h, subheadings: [] };
parentHeadings.set(heading.depth, heading);
// Change 2 to 1 if your markdown includes your <h1>
if (heading.depth === 2) {
toc.push(heading);
} else if (heading.depth === 1) {
/** empty */
} else {
parentHeadings.get(heading.depth - 1).subheadings.push(heading);
}
});
return toc;
return toc;
}
---
{
toc && toc.length > 0 && (
<nav class="article-toc opacity-80">
<ul>
{toc.map((heading) => (
<TOCHeading heading={heading} />
))}
</ul>
</nav>
)
toc && toc.length > 0 && (
<nav class="article-toc opacity-80">
<ul>
{toc.map((heading) => (
<TOCHeading heading={heading} />
))}
</ul>
</nav>
)
}

View File

@ -5,20 +5,23 @@ const { heading, parentMono } = Astro.props;
// If a heading starts with `API: `
// then its children should be rendered with a mono font
const isMono = heading.text.startsWith("API: ");
const monoClass = parentMono? " font-mono" : "";
const monoClass = parentMono ? " font-mono" : "";
---
<li>
<a class={"inline-block py-1 hover:underline" + monoClass} href={"#" + heading.slug}>
{heading.text}
</a>
{
heading.subheadings.length > 0 && (
<ul class="px-2">
{heading.subheadings.map((subheading: any) => (
<Astro.self heading={subheading} parentMono={isMono} />
))}
</ul>
)
}
<a
class={"inline-block py-1 hover:underline" + monoClass}
href={"#" + heading.slug}
>
{heading.text}
</a>
{
heading.subheadings.length > 0 && (
<ul class="px-2">
{heading.subheadings.map((subheading: any) => (
<Astro.self heading={subheading} parentMono={isMono} />
))}
</ul>
)
}
</li>

View File

@ -1,8 +1,8 @@
---
const {cols} = Astro.props;
const { cols } = Astro.props;
const grid_cols = cols ?? "grid-cols-[10rem_auto]";
---
<div class={`two-column grid ${grid_cols}`}>
<slot />
<slot />
</div>

View File

@ -3,6 +3,6 @@ const { error_message } = Astro.props;
---
<div class="px-4 py-2 rounded bg-red-200 dark:bg-red-950">
<span class="inline-block font-bold">Compilation error:</span>
<span class="whitespace-pre-wrap">{error_message}</span>
<span class="inline-block font-bold">Compilation error:</span>
<span class="whitespace-pre-wrap">{error_message}</span>
</div>

View File

@ -6,4 +6,8 @@ const { thpcode, href } = Astro.props;
const [native_html] = await native_highlighter(thpcode);
---
<a href={href} class="inline-block w-full py-2 font-mono whitespace-pre" set:html={native_html} />
<a
href={href}
class="inline-block w-full py-2 font-mono whitespace-pre"
set:html={native_html}
/>

View File

@ -1,4 +1,6 @@
<div class="my-6 px-4 py-2 rounded bg-[#f8e287] text-[#221b00] dark:bg-[#dbc66e] dark:text-[#3a3000]">
<div class="font-bold pt-2">Warning</div>
<slot />
<div
class="my-6 px-4 py-2 rounded bg-[#f8e287] text-[#221b00] dark:bg-[#dbc66e] dark:text-[#3a3000]"
>
<div class="font-bold pt-2">Warning</div>
<slot />
</div>

View File

@ -1,4 +1,4 @@
<div class="my-6 px-4 py-2 rounded bg-red-200 dark:bg-red-950">
<div class="font-bold pt-2">Warning</div>
<slot />
</div>
<div class="font-bold pt-2">Warning</div>
<slot />
</div>

View File

@ -1,133 +1,97 @@
import { expect, test } from 'vitest'
import { leftTrimDedent } from "./utils"
import { expect, test } from "vitest";
import { leftTrimDedent } from "./utils";
test("should trim empty string", () => {
const input = ``;
const input = ``;
expect(leftTrimDedent(input)).toEqual([""]);
})
expect(leftTrimDedent(input)).toEqual([""]);
});
test("should work on a single line", () => {
const input = `hello`
const input = `hello`;
expect(leftTrimDedent(input)).toEqual([
"hello"
]);
})
expect(leftTrimDedent(input)).toEqual(["hello"]);
});
test("should trim a single line", () => {
const input = ` hello`
const input = ` hello`;
expect(leftTrimDedent(input)).toEqual([
"hello"
]);
})
expect(leftTrimDedent(input)).toEqual(["hello"]);
});
test("should trim multiple lines", () => {
const input = ` hello\n world`
const input = ` hello\n world`;
expect(leftTrimDedent(input)).toEqual([
"hello",
"world"
]);
})
expect(leftTrimDedent(input)).toEqual(["hello", "world"]);
});
test("should trim multiple lines without indentation", () => {
const input = `hello\nworld`
const input = `hello\nworld`;
expect(leftTrimDedent(input)).toEqual([
"hello",
"world"
]);
})
expect(leftTrimDedent(input)).toEqual(["hello", "world"]);
});
test("should consume only whitespace", () => {
const input = ` hello\nworld`;
const input = ` hello\nworld`;
try {
const res = leftTrimDedent(input);
expect(res).not.toEqual([
"hello",
"rld",
]);
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect(e).toHaveProperty("message", "Invalid indentation while trimming: Expected 2 spaces, got 0");
}
})
try {
const res = leftTrimDedent(input);
expect(res).not.toEqual(["hello", "rld"]);
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect(e).toHaveProperty(
"message",
"Invalid indentation while trimming: Expected 2 spaces, got 0",
);
}
});
test("should preserve deeper indentation", () => {
const input = ` hello\n world`
const input = ` hello\n world`;
expect(leftTrimDedent(input)).toEqual([
"hello",
" world",
]);
})
expect(leftTrimDedent(input)).toEqual(["hello", " world"]);
});
test("should ignore empty lines", () => {
const input = ` hello\n\n world`
const input = ` hello\n\n world`;
expect(leftTrimDedent(input)).toEqual([
"hello",
"",
"world",
]);
})
expect(leftTrimDedent(input)).toEqual(["hello", "", "world"]);
});
test("should ignore lines with only whitespace", () => {
const input = ` hello\n \n \n world`
const input = ` hello\n \n \n world`;
expect(leftTrimDedent(input)).toEqual([
"hello",
"",
" ",
"world",
]);
})
expect(leftTrimDedent(input)).toEqual(["hello", "", " ", "world"]);
});
test("should trim multiple without indentation", () => {
const input = `hello\nworld\n!`
const input = `hello\nworld\n!`;
expect(leftTrimDedent(input)).toEqual([
"hello",
"world",
"!",
]);
})
expect(leftTrimDedent(input)).toEqual(["hello", "world", "!"]);
});
test("should ignore empty first line", () => {
const input = `
const input = `
hello
world`;
expect(leftTrimDedent(input)).toEqual([
"hello",
"world",
]);
})
expect(leftTrimDedent(input)).toEqual(["hello", "world"]);
});
test("should ignore empty first line 2", () => {
const input = `
const input = `
hello
world`;
expect(leftTrimDedent(input)).toEqual([
"hello",
"",
"world",
]);
})
expect(leftTrimDedent(input)).toEqual(["hello", "", "world"]);
});
test("should ignore empty last line", () => {
const input = `
const input = `
hello
world
`;
expect(leftTrimDedent(input)).toEqual([
"hello",
"world",
]);
expect(leftTrimDedent(input)).toEqual(["hello", "world"]);
});

View File

@ -1,64 +1,65 @@
/**
/**
* Performs the following:
* - Removes the first & last line, if they are empty
* - Picks the indentation level from the first non-white line
* - Dedents the following lines
*/
*/
export function leftTrimDedent(input: string): Array<string> {
let lines = input.split("\n");
let output: Array<string> = [];
let lines = input.split("\n");
let output: Array<string> = [];
// Ignore first line
if (lines[0] === "" && lines.length > 1) {
lines = lines.slice(1);
// Ignore first line
if (lines[0] === "" && lines.length > 1) {
lines = lines.slice(1);
}
// Get indentation level of the first line
let indentationLevel = 0;
for (const char of lines[0]!) {
if (char === " " || char === "\n") {
indentationLevel += 1;
} else {
break;
}
}
// Get indentation level of the first line
let indentationLevel = 0;
for (const char of lines[0]!) {
if (char === " " || char === "\n") {
indentationLevel += 1;
} else {
break;
}
for (const line of lines) {
// Ignore empty lines
if (line === "") {
output.push("");
continue;
}
output.push(trimWhitespace(line, indentationLevel));
}
for (const line of lines) {
// Ignore empty lines
if (line === "") {
output.push("");
continue;
}
output.push(trimWhitespace(line, indentationLevel));
}
if (output.length > 1 && output[output.length - 1] === "") {
output = output.slice(0, -1);
}
if (output.length > 1 && output[output.length - 1] === "") {
output = output.slice(0, -1);
}
return output;
return output;
}
function trimWhitespace(input: string, count: number): string {
let indentCount = 0;
let indentCount = 0;
for (const char of input) {
if (char === " ") {
indentCount += 1;
} else {
break;
}
}
if (indentCount >= count || indentCount == input.length) {
return input.slice(count);
for (const char of input) {
if (char === " ") {
indentCount += 1;
} else {
throw new Error(`Invalid indentation while trimming: Expected ${count} spaces, got ${indentCount}`);
break;
}
}
if (indentCount >= count || indentCount == input.length) {
return input.slice(count);
} else {
throw new Error(
`Invalid indentation while trimming: Expected ${count} spaces, got ${indentCount}`,
);
}
}
export function splitAndLast(s: string): string {
const segments = s.split("/");
return segments[segments.length - 1]!;
const segments = s.split("/");
return segments[segments.length - 1]!;
}

View File

@ -8,12 +8,12 @@ import { splitAndLast } from "../components/utils";
const { headings } = Astro.props;
export type Post = {
frontmatter: any;
getHeadings: any;
url: string;
file: any;
Content: any;
default: any;
frontmatter: any;
getHeadings: any;
url: string;
file: any;
Content: any;
default: any;
};
type Posts = Array<Readonly<Post>>;
@ -21,128 +21,128 @@ const basePath = "/api/std";
const posts: Posts = await Astro.glob("../pages/api/std/**/*.{md,mdx}");
export type Hierarchy = {
posts: Array<Post>;
children: Record<string, [Post | null, Hierarchy]>;
posts: Array<Post>;
children: Record<string, [Post | null, Hierarchy]>;
};
function createHierarchy(posts: Posts): Hierarchy {
const hierarchy: Hierarchy = {
posts: [],
children: {},
};
const hierarchy: Hierarchy = {
posts: [],
children: {},
};
for (const post of posts) {
const postUrl: string = post.url;
const urlAfterBase = postUrl.split(basePath)[1] ?? "";
for (const post of posts) {
const postUrl: string = post.url;
const urlAfterBase = postUrl.split(basePath)[1] ?? "";
// handle / (index)
if (urlAfterBase === "") {
hierarchy.posts.push(post);
continue;
}
// handle / (index)
if (urlAfterBase === "") {
hierarchy.posts.push(post);
continue;
}
const urlSegments = urlAfterBase.split("/").slice(1);
const urlSegments = urlAfterBase.split("/").slice(1);
// top level urls
if (urlSegments.length === 1) {
hierarchy.posts.push(post);
continue;
}
// top level urls
if (urlSegments.length === 1) {
hierarchy.posts.push(post);
continue;
}
// folders
const folders = urlSegments.slice(0, -1);
// folders
const folders = urlSegments.slice(0, -1);
// traverse the hierarchy until the neccesary hierarchy is found
let currentHierarchy = hierarchy;
for (const folderName of folders) {
// check if folder exists, create otherwise
if (!hierarchy.children[folderName]) {
// create if doesnt exist
hierarchy.children[folderName] = [
null,
{
posts: [],
children: {},
},
];
}
// traverse the hierarchy until the neccesary hierarchy is found
let currentHierarchy = hierarchy;
for (const folderName of folders) {
// check if folder exists, create otherwise
if (!hierarchy.children[folderName]) {
// create if doesnt exist
hierarchy.children[folderName] = [
null,
{
posts: [],
children: {},
},
];
}
const [_, childrenHierarchy] = hierarchy.children[folderName]!;
currentHierarchy = childrenHierarchy;
}
const [_, childrenHierarchy] = hierarchy.children[folderName]!;
currentHierarchy = childrenHierarchy;
}
// add the page
currentHierarchy.posts.push(post);
}
// add the page
currentHierarchy.posts.push(post);
}
return hierarchy;
return hierarchy;
}
function normalizeHierarchy(h: Hierarchy): Hierarchy {
let posts = h.posts;
for (const folderName in h.children) {
// search if there is a post with the same name
// as the folder
const postIdx = h.posts.findIndex(
(post) => splitAndLast(post.url) === folderName,
);
let posts = h.posts;
for (const folderName in h.children) {
// search if there is a post with the same name
// as the folder
const postIdx = h.posts.findIndex(
(post) => splitAndLast(post.url) === folderName,
);
if (postIdx !== -1) {
const post = h.posts[postIdx]!;
h.children[folderName]![0] = post;
posts.splice(postIdx, 1);
}
}
if (postIdx !== -1) {
const post = h.posts[postIdx]!;
h.children[folderName]![0] = post;
posts.splice(postIdx, 1);
}
}
// do the same to all children
// TODO
// do the same to all children
// TODO
return {
children: h.children,
posts,
};
return {
children: h.children,
posts,
};
}
const hierarchy = normalizeHierarchy(createHierarchy(posts));
---
<BaseLayout>
<Navbar />
<Navbar />
<div class="lg:grid lg:grid-cols-[14rem_auto_12rem] lg:container mx-auto">
<div
id="sidebar"
class="pt-12 h-screen lg:sticky top-0 fixed z-10 bg-c-bg w-60 lg:w-auto border-r-2 lg:border-0
<div class="lg:grid lg:grid-cols-[14rem_auto_12rem] lg:container mx-auto">
<div
id="sidebar"
class="pt-12 h-screen lg:sticky top-0 fixed z-10 bg-c-bg w-60 lg:w-auto border-r-2 lg:border-0
border-c-border-1 -translate-x-64 lg:translate-x-0 transition-transform"
>
<nav class="py-4 pr-2 overflow-x-scroll h-[calc(100vh-3rem)]">
<Sidebar hierarchy={hierarchy} />
</nav>
</div>
>
<nav class="py-4 pr-2 overflow-x-scroll h-[calc(100vh-3rem)]">
<Sidebar hierarchy={hierarchy} />
</nav>
</div>
<main
class="py-[3.5rem] lg:pl-12 lg:pr-4 markdown min-w-0 small-container mx-auto"
>
<slot />
<div class="h-32"></div>
</main>
<main
class="py-[3.5rem] lg:pl-12 lg:pr-4 markdown min-w-0 small-container mx-auto"
>
<slot />
<div class="h-32"></div>
</main>
<div
class="lg:pt-12 hidden lg:block pt-4 max-h-screen overflow-x-scroll sticky top-0"
>
<nav class="rounded-md lg:mt-10">
<h2 class="font-display font-medium pb-2 text-c-text-2">
On this page
</h2>
<div
class="lg:pt-12 hidden lg:block pt-4 max-h-screen overflow-x-scroll sticky top-0"
>
<nav class="rounded-md lg:mt-10">
<h2 class="font-display font-medium pb-2 text-c-text-2">
On this page
</h2>
<TOC headings={headings} />
</nav>
</div>
</div>
<TOC headings={headings} />
</nav>
</div>
</div>
<script>
import { sidebarHighlight } from "./utils";
// Highlight the current url of the sidebar
document.addEventListener("DOMContentLoaded", sidebarHighlight);
</script>
<script>
import { sidebarHighlight } from "./utils";
// Highlight the current url of the sidebar
document.addEventListener("DOMContentLoaded", sidebarHighlight);
</script>
</BaseLayout>

View File

@ -4,38 +4,40 @@ const { title } = Astro.props;
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title ?? "THP: Typed Hypertext Processor"}</title>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title ?? "THP: Typed Hypertext Processor"}</title>
<link rel="stylesheet" href="/css/global.css" />
<link rel="stylesheet" href="/css/pages.css" />
<link rel="stylesheet" href="/css/xcode-colors.css" />
<link rel="stylesheet" href="/css/global.css" />
<link rel="stylesheet" href="/css/pages.css" />
<link rel="stylesheet" href="/css/xcode-colors.css" />
<link
rel="stylesheet"
type="text/css"
href="https://unpkg.com/@phosphor-icons/web@2.1.1/src/bold/style.css"
/>
<link
rel="stylesheet"
type="text/css"
href="https://unpkg.com/@phosphor-icons/web@2.1.1/src/bold/style.css"
/>
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&display=swap"
rel="stylesheet"
/>
</head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&display=swap"
rel="stylesheet"
/>
<body class="bg-c-bg text-c-text">
<slot />
<script src="//unpkg.com/alpinejs" defer></script>
</head>
<script src="/js/alpine-3.14.0.min.js" defer></script>
<script>
import { highlightOnDom } from "./thpHighlighter";
document.addEventListener("DOMContentLoaded", highlightOnDom);
</script>
</body>
<body class="bg-c-bg text-c-text">
<slot />
<script src="/js/alpine-3.14.0.min.js" defer></script>
<script>
import { highlightOnDom } from "./thpHighlighter";
document.addEventListener("DOMContentLoaded", highlightOnDom);
</script>
</body>
</html>

View File

@ -5,44 +5,46 @@ import TOC from "../components/TOC.astro";
import Sidebar from "../components/Sidebar.astro";
export type PageEntry = {
path: string;
title?: string;
children?: Array<PageEntry>;
path: string;
title?: string;
children?: Array<PageEntry>;
};
export interface AstroFile {
frontmatter: Frontmatter
__usesAstroImage: boolean
url: string
file: string
relative_file: string
frontmatter: Frontmatter;
__usesAstroImage: boolean;
url: string;
file: string;
relative_file: string;
}
export interface Frontmatter {
layout: string
title: string
order: number
layout: string;
title: string;
order: number;
}
type Props = {
/** Base url. It is used to later build a tree file system */
base_url: string,
frontmatter: Frontmatter;
headings: any;
posts: Array<AstroFile>;
/** Base url. It is used to later build a tree file system */
base_url: string;
frontmatter: Frontmatter;
headings: any;
posts: Array<AstroFile>;
version: string;
};
const {
base_url,
frontmatter,
headings,
posts
posts,
version = "latest",
}: Props = Astro.props;
const base_len = base_url.length;
const posts_2 = posts
.map(post => ({
.map((post) => ({
...post,
title: post.frontmatter.title,
// this should be a path relative to the base url.
@ -50,11 +52,11 @@ const posts_2 = posts
// being `/spec/ast/tokens` would be `/ast/tokens`
path: post.url.substring(base_len),
}))
.sort((p1, p2) => p1.frontmatter.order > p2.frontmatter.order? 1 : -1);
.sort((p1, p2) => (p1.frontmatter.order > p2.frontmatter.order ? 1 : -1));
// build a hierarchy of the files
const second_level: Record<string, Array<AstroFile>> = {
"_": [],
_: [],
};
for (const post of posts_2) {
const fragments = post.path.split("/");
@ -66,8 +68,7 @@ for (const post of posts_2) {
second_level[folder_name] = [];
}
second_level[folder_name].push(post);
}
else {
} else {
// add to root folder
second_level["_"]!.push(post);
}
@ -88,8 +89,7 @@ for (const levels_key of levels_keys) {
if (levels_key === "_") {
// top level, already inserted
continue;
}
else {
} else {
const posts = second_level[levels_key]!;
const sorted_posts = posts.toSorted(sort_posts);
const parentEntry = {
@ -106,69 +106,72 @@ function sort_posts(p1, p2) {
if (!!p1.frontmatter.order && !!p2.frontmatter.order) {
return p1.frontmatter.order > p2.frontmatter.order ? 1 : -1;
} else {
return p1.title > p2.title? 1 : -1;
return p1.title > p2.title ? 1 : -1;
}
}
const versions = ["latest", "v0.0.1"];
---
<BaseLayout title={frontmatter.title}>
<Navbar />
<Navbar />
<div
class="lg:grid lg:grid-cols-[14rem_auto_12rem] lg:container mx-auto font-display"
>
<div
id="sidebar"
class="pt-12 h-screen lg:sticky top-0 fixed z-10 bg-c-bg w-60 lg:w-auto border-r-2 lg:border-0
<div
class="lg:grid lg:grid-cols-[14rem_auto_12rem] lg:container mx-auto font-display"
>
<div
id="sidebar"
class="pt-12 h-screen lg:sticky top-0 fixed z-10 bg-c-bg w-60 lg:w-auto border-r-2 lg:border-0
border-c-border-1 -translate-x-64 lg:translate-x-0 transition-transform"
>
<nav class="py-4 pr-2 overflow-x-scroll h-[calc(100vh-3rem)]">
>
<nav class="py-4 pr-2 overflow-x-scroll h-[calc(100vh-3rem)]">
<form class="pb-8 px-1">
<label for="version-select">THP version:</label>
<select
id="version-select"
class="bg-c-bg text-c-on-bg border border-pink-700 rounded px-3 p-1 w-full font-mono"
x-on:change="console.log(':D')"
>
{
versions.map((x) => (
<option selected={x === version} value={x}>
{x}
</option>
))
}
</select>
</form>
<form class="pb-8 px-1">
<label for="version-select">THP version:</label>
<select
id="version-select"
class="bg-c-bg text-c-on-bg border border-pink-700 rounded px-3 p-1 w-full font-mono"
>
<option selected>latest</option>
<option>v0.0.1</option>
</select>
</form>
{entries.map((entry) => <Sidebar entry={entry} />)}
</nav>
</div>
{
entries.map((entry) => (
<Sidebar entry={entry} />
))
}
</nav>
</div>
<main
class="pt-[3.5rem] pb-[10rem] lg:pl-12 lg:pr-4 markdown min-w-0 small-container mx-auto"
>
<slot />
</main>
<main
class="pt-[3.5rem] pb-[10rem] lg:pl-12 lg:pr-4 markdown min-w-0 small-container mx-auto"
>
<slot />
</main>
<div
class="lg:pt-12 hidden lg:block pt-4 max-h-screen overflow-x-scroll sticky top-0"
>
<nav class="rounded-md lg:mt-10">
<h2 class="font-display font-medium pb-2 text-c-text-2">
On this page
</h2>
<div
class="lg:pt-12 hidden lg:block pt-4 max-h-screen overflow-x-scroll sticky top-0"
>
<nav class="rounded-md lg:mt-10">
<h2 class="font-display font-medium pb-2 text-c-text-2">
On this page
</h2>
<TOC headings={headings} />
</nav>
</div>
</div>
<TOC headings={headings} />
</nav>
</div>
</div>
<script>
import { highlightOnDom } from "./thpHighlighter";
document.addEventListener("DOMContentLoaded", highlightOnDom);
</script>
<script>
import { sidebarHighlight } from "./utils";
// Highlight the current url of the sidebar
document.addEventListener("DOMContentLoaded", sidebarHighlight);
</script>
<script>
import { highlightOnDom } from "./thpHighlighter";
document.addEventListener("DOMContentLoaded", highlightOnDom);
</script>
<script>
import { sidebarHighlight } from "./utils";
// Highlight the current url of the sidebar
document.addEventListener("DOMContentLoaded", sidebarHighlight);
</script>
</BaseLayout>

View File

@ -1,19 +1,23 @@
export function highlightOnDom() {
const pre_elements = document.querySelectorAll("pre");
for (const pre_el of pre_elements) {
const language = pre_el.getAttribute("data-language");
if (language === null) { continue; }
// Create a visual indicador
const indicator = document.createElement("span");
let indicator_bg_class = "";
if (language === "php") { indicator_bg_class = "bg-[#4f5b93]"; }
else if (language === "html") { indicator_bg_class = "bg-[#dc4a20]"; }
indicator.className = `absolute top-1 right-0 inline-block text-sm select-none opacity-85 ${indicator_bg_class} px-2 rounded-full`;
indicator.innerText = language;
pre_el.appendChild(indicator);
const pre_elements = document.querySelectorAll("pre");
for (const pre_el of pre_elements) {
const language = pre_el.getAttribute("data-language");
if (language === null) {
continue;
}
// Create a visual indicador
const indicator = document.createElement("span");
let indicator_bg_class = "";
if (language === "php") {
indicator_bg_class = "bg-[#4f5b93]";
} else if (language === "html") {
indicator_bg_class = "bg-[#dc4a20]";
}
indicator.className = `absolute top-1 right-0 inline-block text-sm select-none opacity-85 ${indicator_bg_class} px-2 rounded-full`;
indicator.innerText = language;
pre_el.appendChild(indicator);
}
}

View File

@ -1,17 +1,15 @@
export function sidebarHighlight() {
let current_uri = window.location.pathname;
let current_uri = window.location.pathname;
const sidebar = document.getElementById("sidebar")!
.children[0]! as HTMLElement;
const links = sidebar.querySelectorAll("a");
for (const link of [...links]) {
if (link.getAttribute("href") === current_uri) {
sidebar.scrollTop =
link.offsetTop - sidebar.offsetTop - 250;
const sidebar = document.getElementById("sidebar")!
.children[0]! as HTMLElement;
const links = sidebar.querySelectorAll("a");
for (const link of [...links]) {
if (link.getAttribute("href") === current_uri) {
sidebar.scrollTop = link.offsetTop - sidebar.offsetTop - 250;
link.classList.add("bg-pink-200", "dark:bg-pink-950");
break;
}
link.classList.add("bg-pink-200", "dark:bg-pink-950");
break;
}
}
}

View File

@ -1,9 +1,16 @@
import { spawn } from "node:child_process";
import { leftTrimDedent } from "../components/utils";
import { HighlightLevel } from "./types";
import type { ErrorLabel, MistiErr, Token, TokenizeResult, TokenType } from "./types";
import type {
ErrorLabel,
MistiErr,
Token,
TokenizeResult,
TokenType,
} from "./types";
const error_classes = "underline underline-offset-4 decoration-wavy decoration-red-500";
const error_classes =
"underline underline-offset-4 decoration-wavy decoration-red-500";
/**
* Highlights code using the compiler
@ -12,15 +19,18 @@ const error_classes = "underline underline-offset-4 decoration-wavy decoration-r
* - The tokens as a list of <span /> elements
* - An error message, if any
*/
export async function native_highlighter(code: string, level = HighlightLevel.Lexic): Promise<[string, string | null]> {
let formatted_code = leftTrimDedent(code).join("\n");
export async function native_highlighter(
code: string,
level = HighlightLevel.Lexic,
): Promise<[string, string | null]> {
let formatted_code = leftTrimDedent(code).join("\n");
try {
let result = await native_lex(formatted_code, level);
return highlight_syntax(formatted_code, result);
} catch (error) {
return compiler_error(formatted_code, error as MistiErr);
}
try {
let result = await native_lex(formatted_code, level);
return highlight_syntax(formatted_code, result);
} catch (error) {
return compiler_error(formatted_code, error as MistiErr);
}
}
/**
@ -30,35 +40,38 @@ export async function native_highlighter(code: string, level = HighlightLevel.Le
* - The tokens as a list of <span /> elements
* - An error message, if any
*/
function highlight_syntax(code: string, result: TokenizeResult): [string, string | null] {
if (result.Ok) {
const tokens_html = render_tokens(code, result.Ok);
function highlight_syntax(
code: string,
result: TokenizeResult,
): [string, string | null] {
if (result.Ok) {
const tokens_html = render_tokens(code, result.Ok);
return [tokens_html, null];
} else if (result.MixedErr) {
const [tokens, errors] = result.MixedErr;
// TODO: Implement error rendering, based on the new error schema
return [tokens_html, null];
} else if (result.MixedErr) {
const [tokens, errors] = result.MixedErr;
// TODO: Implement error rendering, based on the new error schema
const tokens_html = render_tokens(code, tokens, errors.labels);
return [tokens_html, `error code ${errors.error_code}`];
} else if (result.Err) {
// TODO: Implement error rendering, based on the new error schema
const tokens_html = render_tokens(code, tokens, errors.labels);
return [tokens_html, `error code ${errors.error_code}`];
} else if (result.Err) {
// TODO: Implement error rendering, based on the new error schema
return [code, `lexical error ${result.Err.error_code}`]
} else {
console.error(result);
throw new Error("Web page error: The compiler returned a case that wasn't handled.");
}
return [code, `lexical error ${result.Err.error_code}`];
} else {
console.error(result);
throw new Error(
"Web page error: The compiler returned a case that wasn't handled.",
);
}
}
/** A fatal error with the THP compiler */
function compiler_error(code: string, error: MistiErr): [string, string] {
console.log(error);
return [code, "Fatal compiler error"];
console.log(error);
return [code, "Fatal compiler error"];
}
/**
* Transforms a list of tokens into colored HTML, and underlines present errors
*
@ -66,178 +79,239 @@ function compiler_error(code: string, error: MistiErr): [string, string] {
* @param tokens The list of tokens
* @param error_start Absolute position from where the error starts.
* @param error_end Absolute position to where the error ends.
* @returns
* @returns
*/
function render_tokens(input: string, tokens: Array<Token>, error_labels: Array<ErrorLabel> = []): string {
const input_chars = input.split("");
let output = "";
function render_tokens(
input: string,
tokens: Array<Token>,
error_labels: Array<ErrorLabel> = [],
): string {
const input_chars = input.split("");
let output = "";
// Collects all the token ranges in all error labels
const error_ranges: Array<[number, number]> = error_labels.map(l => [l.start, l.end]);
// Collects all the token ranges in all error labels
const error_ranges: Array<[number, number]> = error_labels.map((l) => [
l.start,
l.end,
]);
let current_pos = 0;
for (let i = 0; i < tokens.length; i += 1) {
const t = tokens[i]!;
const token_start = t.position;
const token_end = t.position + t.value.length;
let current_pos = 0;
for (let i = 0; i < tokens.length; i += 1) {
const t = tokens[i]!;
const token_start = t.position;
const token_end = t.position + t.value.length;
// check if the current token is in any error label
let is_errored = false;
for (const range of error_ranges) {
const [error_start, error_end] = range;
// check if the current token is in any error label
let is_errored = false;
for (const range of error_ranges) {
const [error_start, error_end] = range;
if (token_start >= error_start && token_end <= error_end) {
is_errored = true;
break;
}
}
// Some tokens require processing (like multiline comments)
// There are some tokens that are empty, ignore them
if (t.value == "") {
continue;
}
// Append all characters before the token
output += input_chars.slice(current_pos, token_start).join("");
// Append the token
const [token_value, new_token_end] = process_token_value_and_end(t.value, t.token_type, token_end);
const token_type = translate_token_type(t.token_type, token_value);
output += `<span class="token ${token_type} ${is_errored ? error_classes : ""}">${token_value}</span>`;
current_pos = new_token_end;
if (token_start >= error_start && token_end <= error_end) {
is_errored = true;
break;
}
}
// at this point `output` is a string with tokens
// now i want to append the label messages:
// - split the output by newlines
// - for every label, append a new line after each error
// Some tokens require processing (like multiline comments)
const lines = output.split("\n");
let offset = 0;
for (const label of error_labels) {
// get the line number of the label
const [line_number, col_number] = absolute_to_line_column(input, label.start);
let spaces_len = col_number - 1;
if (spaces_len < 0) { spaces_len = 0 }
const spaces = new Array(spaces_len).fill("&nbsp;").join("");
lines.splice(line_number + offset, 0, create_inline_error_message(spaces, label.message));
offset += 1;
// There are some tokens that are empty, ignore them
if (t.value == "") {
continue;
}
return lines.join("\n");
// Append all characters before the token
output += input_chars.slice(current_pos, token_start).join("");
// Append the token
const [token_value, new_token_end] = process_token_value_and_end(
t.value,
t.token_type,
token_end,
);
const token_type = translate_token_type(t.token_type, token_value);
output += `<span class="token ${token_type} ${is_errored ? error_classes : ""}">${token_value}</span>`;
current_pos = new_token_end;
}
// at this point `output` is a string with tokens
// now i want to append the label messages:
// - split the output by newlines
// - for every label, append a new line after each error
const lines = output.split("\n");
let offset = 0;
for (const label of error_labels) {
// get the line number of the label
const [line_number, col_number] = absolute_to_line_column(
input,
label.start,
);
let spaces_len = col_number - 1;
if (spaces_len < 0) {
spaces_len = 0;
}
const spaces = new Array(spaces_len).fill("&nbsp;").join("");
lines.splice(
line_number + offset,
0,
create_inline_error_message(spaces, label.message),
);
offset += 1;
}
return lines.join("\n");
}
function create_inline_error_message(spaces: string, message: string): string {
return `<span class="relative inline-block w-full before:h-full before:block before:absolute before:left-0 before:w-[calc(100%+1.5rem)] before:-translate-x-3 before:bg-red-200 before:dark:bg-red-950" style="white-space: initial"><span class="relative dark:text-red-200 text-red-900 font-bold">${spaces}╰╴${message}</span></span>`;
return `<span class="relative inline-block w-full before:h-full before:block before:absolute before:left-0 before:w-[calc(100%+1.5rem)] before:-translate-x-3 before:bg-red-200 before:dark:bg-red-950" style="white-space: initial"><span class="relative dark:text-red-200 text-red-900 font-bold">${spaces}╰╴${message}</span></span>`;
}
/**
* Transform an absolute position in source code to a line:column combination.
*
*
* Both line and column are 1-based
*
*
* @param input the source code
* @param absolute the absolute position
*/
function absolute_to_line_column(input: string, absolute: number): [number, number] {
let line_count = 1;
let last_newline_pos = 0;
function absolute_to_line_column(
input: string,
absolute: number,
): [number, number] {
let line_count = 1;
let last_newline_pos = 0;
// Count lines
for (let i = 0; i < input.length; i += 1) {
if (i === absolute) {
break;
}
if (input[i] === "\n") {
line_count += 1;
last_newline_pos = i;
}
// Count lines
for (let i = 0; i < input.length; i += 1) {
if (i === absolute) {
break;
}
return [line_count, absolute - last_newline_pos];
if (input[i] === "\n") {
line_count += 1;
last_newline_pos = i;
}
}
return [line_count, absolute - last_newline_pos];
}
/**
* Certain tokens store values that differ from the source code representation.
* For example, the multiline comment token stores the content of the comment
* without `/*` and `* /`, this function handles those cases.
*
*
* @param value The value of the token
* @param token_type The type of the token, used to know if it needs preprocessing
* @param first_end The position where the token ends according to the token value
* @returns
* @returns
*/
function process_token_value_and_end(value: string, token_type: TokenType, first_end: number): [string, number] {
let token_value = value;
let new_end = first_end;
if (token_type === "MultilineComment") {
token_value = `/*${token_value}*/`;
new_end += 4;
} else if (token_type === "String") {
token_value = `"${token_value}"`;
new_end += 2;
}
function process_token_value_and_end(
value: string,
token_type: TokenType,
first_end: number,
): [string, number] {
let token_value = value;
let new_end = first_end;
if (token_type === "MultilineComment") {
token_value = `/*${token_value}*/`;
new_end += 4;
} else if (token_type === "String") {
token_value = `"${token_value}"`;
new_end += 2;
}
// Escape html and return
return [
token_value.replaceAll(/</g, "&lt;").replaceAll(/>/g, "&gt;"),
new_end
];
// Escape html and return
return [
token_value.replaceAll(/</g, "&lt;").replaceAll(/>/g, "&gt;"),
new_end,
];
}
function translate_token_type(tt: TokenType, value: string): string {
const keywords = ["throws", "extends", "constructor", "static", "const",
"enum", "union", "use", "break", "catch", "continue", "as", "do",
"finally", "fun", "fn", "nil", "return", "throw",
"try", "type", "with", "of", "abstract", "class", "interface",
"private", "protected", "pub", "override", "open", "init", "val", "var", "mut", "clone"];
const keywords = [
"throws",
"extends",
"constructor",
"static",
"const",
"enum",
"union",
"use",
"break",
"catch",
"continue",
"as",
"do",
"finally",
"fun",
"fn",
"nil",
"return",
"throw",
"try",
"type",
"with",
"of",
"abstract",
"class",
"interface",
"private",
"protected",
"pub",
"override",
"open",
"init",
"val",
"var",
"mut",
"clone",
];
switch (tt) {
case "Datatype":
return "class-name";
case "Identifier": {
if (keywords.includes(value)) {
return "keyword";
}
switch (tt) {
case "Datatype":
return "class-name";
case "Identifier": {
if (keywords.includes(value)) {
return "keyword";
}
return "identifier";
}
case "Int":
return "number";
case "Float":
return "number";
case "String":
return "string";
case "Comment":
case "MultilineComment":
return "comment";
// keywords:
case "VAL":
case "VAR":
case "FUN":
case "IF":
case "ELSE":
case "FOR":
case "IN":
case "WHILE":
case "MATCH":
case "CASE":
return "keyword";
default:
return tt;
return "identifier";
}
case "Int":
return "number";
case "Float":
return "number";
case "String":
return "string";
case "Comment":
case "MultilineComment":
return "comment";
// keywords:
case "VAL":
case "VAR":
case "FUN":
case "IF":
case "ELSE":
case "FOR":
case "IN":
case "WHILE":
case "MATCH":
case "CASE":
return "keyword";
default:
return tt;
}
}
const native_lex = (code: string, level: HighlightLevel) => new Promise<TokenizeResult>((resolve, reject) => {
const native_lex = (code: string, level: HighlightLevel) =>
new Promise<TokenizeResult>((resolve, reject) => {
// Get binary path from .env
const binary = import.meta.env.THP_BINARY;
if (!binary) {
throw new Error("THP_BINARY not set in .env");
throw new Error("THP_BINARY not set in .env");
}
const subprocess = spawn(binary, ["tokenize", "-l", level.toString()]);
@ -248,18 +322,18 @@ const native_lex = (code: string, level: HighlightLevel) => new Promise<Tokenize
subprocess.stdin.end();
subprocess.stdout.on("data", (data) => {
response += data.toString();
response += data.toString();
});
subprocess.stderr.on("data", (data) => {
error += data.toString();
error += data.toString();
});
subprocess.on("close", (code) => {
if (code === 0) {
resolve(JSON.parse(response));
} else {
reject(new Error(error));
}
if (code === 0) {
resolve(JSON.parse(response));
} else {
reject(new Error(error));
}
});
})
});

View File

@ -1,73 +1,72 @@
export type ReferenceItem = {
symbol_start: number
symbol_end: number
reference: string
}
symbol_start: number;
symbol_end: number;
reference: string;
};
export interface Token {
token_type: TokenType
value: string
position: number
token_type: TokenType;
value: string;
position: number;
}
export type TokenType =
| "Identifier"
| "Datatype"
| "Int"
| "Float"
| "String"
| "Operator"
| "LeftParen"
| "RightParen"
| "LeftBracket"
| "RightBracket"
| "LeftBrace"
| "RightBrace"
| "NewLine"
| "Comment"
| "MultilineComment"
| "Comma"
| "INDENT"
| "DEDENT"
| "VAL"
| "VAR"
| "EOF"
| "FUN"
| "IF"
| "ELSE"
| "ELSE"
| "FOR"
| "IN"
| "WHILE"
| "MATCH"
| "CASE"
;
| "Identifier"
| "Datatype"
| "Int"
| "Float"
| "String"
| "Operator"
| "LeftParen"
| "RightParen"
| "LeftBracket"
| "RightBracket"
| "LeftBrace"
| "RightBrace"
| "NewLine"
| "Comment"
| "MultilineComment"
| "Comma"
| "INDENT"
| "DEDENT"
| "VAL"
| "VAR"
| "EOF"
| "FUN"
| "IF"
| "ELSE"
| "ELSE"
| "FOR"
| "IN"
| "WHILE"
| "MATCH"
| "CASE";
export interface MistiErr {
error_code: number
error_offset: number
labels: Array<ErrorLabel>
note: string | null,
help: string | null,
error_code: number;
error_offset: number;
labels: Array<ErrorLabel>;
note: string | null;
help: string | null;
}
export interface ErrorLabel {
message: string
start: number
end: number
message: string;
start: number;
end: number;
}
export interface TokenizeResult {
/** All checks passed */
Ok?: Array<Token>,
/** A non lexic error was found */
MixedErr?: [Array<Token>, MistiErr],
/** A lexic error was found */
Err?: MistiErr,
/** All checks passed */
Ok?: Array<Token>;
/** A non lexic error was found */
MixedErr?: [Array<Token>, MistiErr];
/** A lexic error was found */
Err?: MistiErr;
}
export enum HighlightLevel {
Lexic = 0,
Syntactic = 1,
Semantic = 2,
Lexic = 0,
Syntactic = 1,
Semantic = 2,
}

View File

@ -1,15 +1,15 @@
export function is_digit(c: string): boolean {
return c >= '0' && c <= '9';
return c >= "0" && c <= "9";
}
export function is_lowercase(c: string): boolean {
return c >= 'a' && c <= 'z';
return c >= "a" && c <= "z";
}
export function is_uppercase(c: string): boolean {
return c >= 'A' && c <= 'Z';
return c >= "A" && c <= "Z";
}
export function is_identifier_char(c: string): boolean {
return is_lowercase(c) || is_uppercase(c) || is_digit(c) || c === '_';
return is_lowercase(c) || is_uppercase(c) || is_digit(c) || c === "_";
}

View File

@ -1,10 +1,11 @@
---
layout: ../../../layouts/ApiLayout.astro
---
import TwoColumn from "../../../components/TwoColumn.astro"
import Code from "../../../components/Code.astro"
import CodeMin from "../../../components/docs/CodeMin.astro"
import Warning from "../../../components/docs/Warning.astro"
import TwoColumn from "../../../components/TwoColumn.astro";
import Code from "../../../components/Code.astro";
import CodeMin from "../../../components/docs/CodeMin.astro";
import Warning from "../../../components/docs/Warning.astro";
# Array
@ -19,28 +20,32 @@ THP arrays are 0-indexed.
## Signature
<Code thpcode={`
<Code
thpcode={`
type Array[T] = Map[Int, T]
`} />
`}
/>
Where `T` is the datatype that the Array stores. For example:
<Code thpcode={`
<Code
thpcode={`
Array[Int] // An array of integers
Array[Float] // An array of floats
Array[Array[String]] // A 2-dimensional array of strings
`} />
`}
/>
## PHP array internals
<Warning>
TL;DR: **Never** assign to an array using an invalid index. If you do
the program will not crash, instead it will not behaved as expected
[(this is a common problem in PHP)](https://eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design/).
THP tries its best to solve such behavior.
TL;DR: **Never** assign to an array using an invalid index. If you do the
program will not crash, instead it will not behaved as expected [(this is a
common problem in
PHP)](https://eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design/). THP tries
its best to solve such behavior.
</Warning>
Since THP compiles down to PHP, it's important to understand how PHP
represents arrays internally.
PHP doesn't have arrays. Instead, PHP has ordered maps and syntax sugar
@ -48,9 +53,11 @@ to make them look like arrays.
When declaring an array like:
<Code thpcode={`
<Code
thpcode={`
var arr = ["a", "b", "c"]
`} />
`}
/>
in reality what goes into memory is a map with numbers as keys:
@ -83,51 +90,51 @@ numbers[-10] = "?" // Assign to a negative index
// Now the array will be (using thp notation):
// .{
// 0: "a",
// 1: "b",
// 2: "c",
// -10: "?",
// 0: "a",
// 1: "b",
// 2: "c",
// -10: "?",
// }
numbers[7] = "!" // Out of bounds assignment
numbers[7] = "!" // Out of bounds assignment
// Now the array will be:
// .{
// 0: "a",
// 1: "b",
// 2: "c",
// -10: "?",
// 7: "!",
// 0: "a",
// 1: "b",
// 2: "c",
// -10: "?",
// 7: "!",
// }
// In this loop, values will be printed following when
// they were inserted, not by order of the index.
for #(_, value) in numbers
for #(\_, value) in numbers
{
print("{value} ") // Will print: a b c ? !
print("{value} ") // Will print: a b c ? !
}
numbers[-4] = "???"
// .{
// 0: "a",
// 1: "b",
// 2: "c",
// -10: "?",
// 7: "!",
// -4: "???",
// 0: "a",
// 1: "b",
// 2: "c",
// -10: "?",
// 7: "!",
// -4: "???",
// }
// When pushing, the value will be assigned to the highest key + 1
numbers.push("/") // This will be at position 8
numbers.push("/") // This will be at position 8
// .{
// 0: "a",
// 1: "b",
// 2: "c",
// -10: "?",
// 7: "!",
// -4: "???",
// 8: "/",
// 0: "a",
// 1: "b",
// 2: "c",
// -10: "?",
// 7: "!",
// -4: "???",
// 8: "/",
// }
`} />
@ -135,12 +142,9 @@ This is one of many fundamental flaws with PHP. The only way to solve it
would be to check every insertion at runtime, and this would have a
performance penalty.
From now on, the documentation will continue to work with the Array
abstraction, as if all indexes were valid.
## Usage
### Empty array
@ -148,31 +152,36 @@ abstraction, as if all indexes were valid.
To create an empty array use square brackets.
If you create an empty array, you need to specify the datatype.
<Code thpcode={`
<Code
thpcode={`
Array[Int] empty = []
`} />
`}
/>
### Creation
To create an array use square brackets notation:
<Code thpcode={`
<Code
thpcode={`
val numbers = [0, 1, 2, 3, 4, 5]
`} />
`}
/>
When the array is not empty, you don't need to specify a datatype.
When the Array is declared over many lines, the last
item should have a trailing comma:
<Code thpcode={`
<Code
thpcode={`
val colors = [
"red",
"blue",
"green", // trailing comma
]
`} />
`}
/>
If it doesn't, the code formatter will automatically
insert one for you.
@ -198,13 +207,14 @@ mutable[0] = 322
To append an element to an array, use the method `push()`:
<Code thpcode={`
<Code
thpcode={`
mutable.push(4)
`} />
`}
/>
Do not insert into an invalid index. See [PHP array internals](#php-array-internals) to learn why.
### Iteration
Use a `for` loop to iterate over the elements of an array:
@ -214,7 +224,7 @@ val colors = ["red", "green", "blue"]
for c in colors
{
print("{c} ")
print("{c} ")
}
`} />
@ -230,12 +240,11 @@ val colors = ["red", "green", "blue"]
for c in colors
{
c = "orange" // Compile error: Can't assign to an immutable variable
print("{c} ")
c = "orange" // Compile error: Can't assign to an immutable variable
print("{c} ")
}
`} />
You can also declare an index along with the value:
<Code thpcode={`
@ -243,7 +252,7 @@ val colors = ["red", "green", "blue"]
for index, color in colors
{
println("item {index}: {c}")
println("item {index}: {c}")
}
`} />
@ -253,93 +262,89 @@ item 1: green
item 2: blue
```
### Access
To access a value of the array use square brackets notation:
<Code thpcode={`
<Code
thpcode={`
print(colors[0])
`} />
`}
/>
Since the index might not exist, this will return a
Since the index might not exist, this will return a
[nullable type](/learn/error-handling/null/) that you have to handle.
### Destructuring
THP arrays don't have destructuring, since the values can all be `null`.
If you know that the number of elements is fixed and valid, use Tuples instead.
### Operators
While PHP allows using certain operators with arrays, THP disallows that.
Methods that perform comparisons should be used instead.
### Assignment
// TODO: Detail that assignment of arrays is copy on write
## Methods
In the parameters, <code class="token keyword">self</code> is the array to operate on.
<table class="table-fixed w-full border-collapse border-2 dark:border-zinc-900 border-stone-400">
<thead>
<td class="p-2">Method</td>
<td class="py-2">Description</td>
</thead>
<tbody>
<tr class="dark:odd:bg-zinc-900 odd:bg-stone-200">
<td class="px-2">
<CodeMin href="./concat" thpcode={`
<thead>
<td class="p-2">Method</td>
<td class="py-2">Description</td>
</thead>
<tbody>
<tr class="dark:odd:bg-zinc-900 odd:bg-stone-200">
<td class="px-2">
<CodeMin
href="./concat"
thpcode={`
fun concat[T](
self,
Array[T]... arrays,
) -> Array[T]
`} />
</td>
<td>
Concatenate with other arrays, and return the result
as a new array.
</td>
</tr>
<tr class="dark:odd:bg-zinc-900 odd:bg-stone-200">
<td class="px-2">
<CodeMin thpcode={`
`}
/>
</td>
<td>
Concatenate with other arrays, and return the result as a new array.
</td>
</tr>
<tr class="dark:odd:bg-zinc-900 odd:bg-stone-200">
<td class="px-2">
<CodeMin
thpcode={`
fun filter[T](
self,
(T) -> (Bool) callback,
) -> Array[T]
`} />
</td>
<td>
Filters elements using a callback function, and returns
them in a new array.
</td>
</tr>
<tr class="dark:odd:bg-zinc-900 odd:bg-stone-200">
<td class="px-2">
<CodeMin thpcode="fun push[T](self, T... elements) -> Int" />
</td>
<td>
Appends one or more elements to the end of the array.
Returns the new length of the array.
</td>
</tr>
<tr class="dark:odd:bg-zinc-900 odd:bg-stone-200">
<td class="px-2">
<CodeMin thpcode="fun pop[T](self) -> T" />
</td>
<td>
Removes the last value of the array, and returns it.
</td>
</tr>
</tbody>
`}
/>
</td>
<td>
Filters elements using a callback function, and returns them in a new
array.
</td>
</tr>
<tr class="dark:odd:bg-zinc-900 odd:bg-stone-200">
<td class="px-2">
<CodeMin thpcode="fun push[T](self, T... elements) -> Int" />
</td>
<td>
Appends one or more elements to the end of the array. Returns the new
length of the array.
</td>
</tr>
<tr class="dark:odd:bg-zinc-900 odd:bg-stone-200">
<td class="px-2">
<CodeMin thpcode="fun pop[T](self) -> T" />
</td>
<td>Removes the last value of the array, and returns it.</td>
</tr>
</tbody>
</table>

View File

@ -1,7 +1,8 @@
---
layout: ../../../../layouts/ApiLayout.astro
---
import Code from "../../../../components/Code.astro"
import Code from "../../../../components/Code.astro";
# `Array.concat`
@ -9,12 +10,14 @@ Concatenate with other arrays, and return the result as a new array.
## Signature
<Code thpcode={`
<Code
thpcode={`
fun concat[T](
self,
Array[T]... arrays,
) -> Array[T]
`} />
`}
/>
## Parameters
@ -25,7 +28,6 @@ Concatenate with other arrays, and return the result as a new array.
`Array[T]`: A new array that contains the elements from all arrays.
## Description
Concatenates the elements of the callee and the elements of each array in the
@ -49,6 +51,7 @@ Example concatenating 2 arrays:
val result = first.concat(second)
assert_eq(result, [1, 2, 3, 4, 5, 6])
`} />
Example concatenating 3 arrays:
@ -60,6 +63,7 @@ Example concatenating 3 arrays:
val result = first.concat(second, third)
assert_eq(result, ["a", "b", "c", "d", "e", "f"])
`} />
Example concatenating without any other array. In this case
@ -70,6 +74,7 @@ Example concatenating without any other array. In this case
val result = first.concat()
assert_eq(result, [1, 2, 3])
`} />
Example concatenating an empty array with a filled array:
@ -80,8 +85,8 @@ Example concatenating an empty array with a filled array:
val result = first.concat(second)
assert_eq(result, [10, 20, 30])
`} />
`} />
## PHP interop
@ -107,6 +112,3 @@ array(3) {
[2]=> string(1) "c"
}
```

View File

@ -1,7 +1,8 @@
---
layout: ../../../../layouts/ApiLayout.astro
---
import Code from "../../../../components/Code.astro"
import Code from "../../../../components/Code.astro";
# `Array.fold`
@ -11,13 +12,15 @@ the left.
## Signature
<Code thpcode={`
<Code
thpcode={`
fun fold[A, B](
self[A],
B init,
(B acc, A next) -> (B) transform_function,
) -> B
`} />
`}
/>
## Parameters
@ -31,7 +34,6 @@ the left.
- Otherwise, returns the result of applying `transform_function` to the accumulator `acc`,
and the next element `next`.
## Description
Fold allows you to transform an array of `A` into a single `B`, following
@ -39,20 +41,21 @@ an arbitraty function.
For example, let's say that you have an array of numbers:
<Code thpcode={`
<Code
thpcode={`
val digits = [1, 9, 9, 5]
`} />
`}
/>
And you want to join all digits into a String like `"1985"`. You can
achieve this with a fold.
<Code thpcode={`
val digits = [1, 9, 8, 5]
val init = ""
fun f(String acc, Int next) = acc ++ next
digits.fold("", f) // "1985"
digits.fold("", f) // "1985"
`} />
- `f` is a function that takes a previous value (known as the accumulator)
@ -93,10 +96,12 @@ The `transform_function` can be any function, and can operate over any type.
For example, you could sum all numbers instead of concatenating
like this:
<Code thpcode={`
<Code
thpcode={`
val digits = [1, 9, 8, 5]
digits.fold(0, fun(acc, next) = acc + next) // 23
`} />
`}
/>
```php
f( f( f( f(0, 1), 9), 8), 5)
@ -118,8 +123,3 @@ val result = builder.build()
`} />
Or anything.

View File

@ -1,7 +1,8 @@
---
layout: ../../../../layouts/ApiLayout.astro
---
import Code from "../../../../components/Code.astro"
import Code from "../../../../components/Code.astro";
# `Array.map`
@ -19,16 +20,15 @@ and returns their result in a new array.
self[],
(A) -> (B) map_function
) -> Array[B]
`} />
<Code thpcode={`
fun multi_def
| ("prosor prosor", 322) -> Float
{
fmt.Println("prosor prosor %d", 322)
fmt.Println("prosor prosor %d", 322)
}
| (String a, Int b) -> Float
{
@ -40,4 +40,3 @@ fun multi_def
}
`} />

View File

@ -1,8 +1,9 @@
---
layout: ../../../layouts/ApiLayout.astro
---
import TwoColumn from "../../../components/TwoColumn.astro"
import Code from "../../../components/Code.astro"
import TwoColumn from "../../../components/TwoColumn.astro";
import Code from "../../../components/Code.astro";
# module `std`
@ -28,12 +29,14 @@ if (str_contains("abc", "a")) {
In THP there is no `str_contains` function. Instead, you'd call the
`contains` method on the string:
<Code thpcode={`
<Code
thpcode={`
if "abc".contains("a")
{
// ...
}
`} />
`}
/>
## On naming
@ -56,10 +59,11 @@ If you need to use a PHP class with a lowercase name you can use the following s
class animal {}
```
<Code thpcode={`
<Code
thpcode={`
val my_animal = 'animal()
`} />
`}
/>
## API: Datatypes
@ -96,5 +100,3 @@ A [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) double precision floating p
Prints text into stdout.
</TwoColumn>

View File

@ -1,8 +1,9 @@
---
layout: ../../../layouts/ApiLayout.astro
---
import TwoColumn from "../../../components/TwoColumn.astro"
import Code from "../../../components/Code.astro"
import TwoColumn from "../../../components/TwoColumn.astro";
import Code from "../../../components/Code.astro";
# `print`
@ -10,9 +11,11 @@ Prints to stdout.
## Signature
<Code thpcode={`
<Code
thpcode={`
fun print(String value) {}
`} />
`}
/>
## Description
@ -20,10 +23,8 @@ Prints a single `String` into stdout. Doesn't return anything.
## Examples
<Code thpcode={`
<Code
thpcode={`
print("Hello world!")
`} />
`}
/>

View File

@ -3,13 +3,13 @@ layout: "../_wrapper.astro"
title: Comments
order: 2
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Comments
THP supports single and multi line comments:
## Single line
Begin with double slash `//` and continue until the end of the line.
@ -21,33 +21,37 @@ print("hello!")
print("the result is {5 + 5}") // This will print 10
`} />
## Multi line
These begin with `/*` and end with `*/`. Everything in between is ignored.
Multi line comments can be nested in THP.
<Code thpcode={`
<Code
thpcode={`
/*
This is a
multiline comment
*/
`} />
`}
/>
<Code thpcode={`
<Code
thpcode={`
/*
Multiline comments
can be /* nested */
*/
`} />
`}
/>
## Documentation comments
Documentation comments use triple slashes `///`.
These use [CommonMark Markdown](https://commonmark.org/) syntax.
<Code thpcode={`
<Code
thpcode={`
/// Transforms the format from A to B...
///
/// ## Errors
@ -57,6 +61,5 @@ These use [CommonMark Markdown](https://commonmark.org/) syntax.
fun transform() {
// ...
}
`} />
`}
/>

View File

@ -3,7 +3,8 @@ layout: "../_wrapper.astro"
title: Datatypes
order: 4
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Datatypes
@ -28,50 +29,54 @@ Int char_code = 0b01000110
// IMPORTANT!
// Since Octal starts with \`0o\`, using just a leading 0
// will result in a decimal!
Int not_octal = 032 // This is 32, not 26
Int not_octal = 032 // This is 32, not 26
`} />
// TODO: Make it a compile error to have leading zeroes,
and force users to use `0o` for octal
## Float
Same as php float
<Code thpcode={`
<Code
thpcode={`
Float pi = 3.141592
Float light = 2.99e+8
`} />
`}
/>
## String
THP strings use **only** double quotes. Single quotes are
used elsewhere.
<Code thpcode={`
<Code
thpcode={`
String name = "Rose"
`} />
`}
/>
Strings have interpolation with `{}`.
<Code thpcode={`
<Code
thpcode={`
print("Hello, {name}") // Hello, Rose
`} />
`}
/>
Unlike PHP, THP strings are concatenated with `++`, not with `.`.
This new operator implicitly converts any operator into a string.
<Code thpcode={`
<Code
thpcode={`
val name = "John" ++ " " ++ "Doe"
val greeting = "My name is " ++ name ++ " and I'm " ++ 32 ++ " years old"
`} />
`}
/>
The plus operator `+` is reserved for numbers.
## Bool
THP booleans are `true` and `false`. They are case sensitive,
@ -84,4 +89,3 @@ Bool is_false = false
// This is a compile error
val invalid = TRUE
`} />

View File

@ -3,8 +3,9 @@ layout: "../_wrapper.astro"
title: Hello world
order: 1
---
import InteractiveCode from "@/components/InteractiveCode.astro";
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Hello, world!
@ -18,13 +19,14 @@ detailed later on.
To write a hello world program write the following code in a file:
<Code thpcode={`
<Code
thpcode={`
print("Hello, world!")
`} />
`}
/>
Then run `thp hello.thp` from your terminal.
## Instruction separation
THP uses whitespace to determine when a statement is over. In short,
@ -36,11 +38,12 @@ echo("B");
echo("C");
```
<Code thpcode={`
<Code
thpcode={`
print("A")
print("B")
print("C")
`} />
`}
/>
As a consequence of this, there can only be 1 statement per line.

View File

@ -3,11 +3,11 @@ layout: "../_wrapper.astro"
title: Operators
order: 5
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Operators
Most of the PHP operators are present in THP.
## Numbers
@ -17,13 +17,13 @@ var number = 322
number + 1
number - 1
number * 1
number \* 1
number / 1
number % 2
number += 1
number -= 1
number *= 1
number \*= 1
number /= 1
number %= 2
`} />
@ -36,7 +36,7 @@ use `+=` or `-=` instead.
number += 1
// instead of
number++ // This is a compile error
number++ // This is a compile error
`} />
### Comparison
@ -44,62 +44,71 @@ number++ // This is a compile error
These operators will not do implicit type conversion. They can
only be used with same datatypes.
<Code thpcode={`
<Code
thpcode={`
v1 < v2
v1 <= v2
v1 > v2
v1 >= v2
`} />
`}
/>
There is only `==` and `!=`. They are equivalent to `===` and `!==`.
<Code thpcode={`
<Code
thpcode={`
v1 == v2
v1 != v2
`} />
`}
/>
### Bitwise
TBD
<Code thpcode={`
<Code
thpcode={`
number and 1
number or 2
number xor 1
number xand 1
`} />
`}
/>
## Strings
Strings **do not use `.`** for concatenation. They use `++`.
<Code thpcode={`
<Code
thpcode={`
"Hello " ++ "world."
`} />
`}
/>
This new operator **implicitly converts** types to string
<Code thpcode={`
<Code
thpcode={`
"Hello " ++ 322 // 322 will be converted to "322"
`} />
`}
/>
## Boolean
These operators work **only with booleans**, they do not perform
type coercion.
<Code thpcode={`
<Code
thpcode={`
c1 && c2
c1 || c2
`} />
`}
/>
## Ternary
There is no ternary operator. See [Conditionals](/learn/flow-control/conditionals) for alternatives.
## Null
These are detailed in their section: [Nullable types](/learn/error-handling/null)
@ -112,9 +121,6 @@ person?.name ?? "Jane"
person?.greet?.()
if person? {
person.name
person.name
}
`} />

View File

@ -3,7 +3,8 @@ layout: "../_wrapper.astro"
title: Variables
order: 3
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Variables
@ -24,47 +25,59 @@ As a regex: `[a-z_][a-zA-Z0-9_]*`
Defined with `val`, followed by a variable name and a value.
<Code level={2} thpcode={`
<Code
level={2}
thpcode={`
val surname = "Doe"
val year_of_birth = 1984
`} />
`}
/>
It's a compile error to attempt to modify it
<Code level={2} thpcode={`
<Code
level={2}
thpcode={`
val surname = "Doe"
surname = "Dane" // Error
`} />
`}
/>
### Datatype annotation
Written after the `val` keyword but before the variable name.
<Code level={2} thpcode={`
<Code
level={2}
thpcode={`
val String surname = "Doe"
val Int year_of_birth = 1984
`} />
`}
/>
When annotating an immutable variable the `val` keyword is optional
<Code level={2} thpcode={`
<Code
level={2}
thpcode={`
// Equivalent to the previous code
String surname = "Doe"
Int year_of_birth = 1984
`} />
`}
/>
This means that if a variable only has a datatype, it is immutable.
It is a compile error to declare a variable of a datatype,
but use another.
<Code level={2} thpcode={`
<Code
level={2}
thpcode={`
// Declare the variable as a String, but use a Float as its value
String capital = 123.456
`} />
`}
/>
## Mutable variables
@ -81,18 +94,21 @@ age = 33
Written after the `var` keywords but before the variable name.
<Code level={2} thpcode={`
<Code
level={2}
thpcode={`
var String name = "John"
var Int age = 32
`} />
`}
/>
When annotating a mutable variable the keyword `var` is still **required**.
<Code level={2} thpcode={`
<Code
level={2}
thpcode={`
// Equivalent to the previous code
var String name = "John"
var Int age = 32
`} />
`}
/>

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Arrays
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Arrays
@ -17,29 +18,25 @@ Arrays only store values of a single datatype.
val fruits = ["apple", "banana", "cherry"]
val apple = fruits[0]
print(apple) // apple
print(apple) // apple
// To mutate an array, you need to declare it as var
var numbers = [0, 1, 2, 3]
numbers[3] = 5
print(numbers[3]) // 5
print(numbers[3]) // 5
`} />
## Type signature
<Code thpcode={`
<Code
thpcode={`
Array[String]
Array[Int]
`} />
`}
/>
The Array signature __requires__ the word `Array`.
The Array signature **requires** the word `Array`.
There is no `Int[]` or `[Int]` signature, since that would cause
problems with the language's grammar.

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Enums
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Enums
@ -33,7 +34,8 @@ val suit = Suit::Hearts
Backed enums can have a scalar for each case. The scalar values can only be
`String` or `Int`.
<Code thpcode={`
<Code
thpcode={`
enum Suit(String)
{
Hearts = "H",
@ -41,8 +43,7 @@ enum Suit(String)
Clubs = "C",
Spades = "S",
}
`} />
`}
/>
All cases must explicitly define their value, there is no automatic generation.

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Maps
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Maps
@ -25,57 +26,64 @@ map Person {
// Here we declare an instance of a Person.
val john_doe = Person {
name: "John",
surname: "Doe",
age: 33,
name: "John",
surname: "Doe",
age: 33,
}
// If the compiler can infer the type of a Map,
// we can omit its type
var Person mary_jane = .{
name: "Mary",
surname: "Jane",
age: 27,
name: "Mary",
surname: "Jane",
age: 27,
}
`} />
To access the fields of a map we use square braces `[]`.
<Code thpcode={`
<Code
thpcode={`
mary_jane["age"] += 1
print(mary_jane["name"]) // Mary
`} />
`}
/>
Or dot access `.` if the field's name is a valid identifier.
<Code thpcode={`
<Code
thpcode={`
mary_jane.age += 1
print(mary_jane.name)
`} />
`}
/>
## Anonymous maps
An anonymous map allows us to store and retrieve any key of any datatype.
They are declared as `Map`.
<Code thpcode={`
<Code
thpcode={`
val car = Map {
brand: "Toyota",
model: "Corolla",
year: 2012,
}
`} />
`}
/>
Anonymous maps can also can have their type omitted.
<Code thpcode={`
<Code
thpcode={`
var car = .{
brand: "Toyota",
model: "Corolla",
year: 2012,
}
`} />
`}
/>
If the compiler encounters a map without a type (that is, `.{}`)
and doesn't expect a specific type, it will assume it is an
@ -83,12 +91,14 @@ anonymous map.
We can freely assign fields to an anonymous map:
<Code thpcode={`
<Code
thpcode={`
// Modify an existing field
car["year"] = 2015
// Create a new field
car["status"] = "used"
`} />
`}
/>
However, if we try to access a field of an anonymous map we'll get
a nullable type, and we must annotate it.
@ -104,9 +114,11 @@ var car_status = car["status"]
Instead, we can use the `get` function of the map, which expects a
datatype and returns that type as nullable
<Code thpcode={`
<Code
thpcode={`
val car_status = car.get[String]("status")
`} />
`}
/>
Both ways to get a value will check that the key exists in the map,
and that it has the correct datatype. If either the key doesn't exist

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Tuples
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Tuples
@ -16,13 +17,10 @@ val person = #("John", "Doe", 32)
val #(name, surname, age) = person
`} />
## Signature
<Code thpcode={`
<Code
thpcode={`
#(String, String, Int)
`} />
`}
/>

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Tagged unions
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Tagged unions
@ -16,14 +17,15 @@ union Shape
Rectangle(Int, Int),
}
val dot = Shape::Dot
val square1 = Shape::Square(10)
val dot = Shape::Dot
val square1 = Shape::Square(10)
val rectangle1 = Shape::Rectangle(5, 15)
`} />
## Pattern matching
<Code thpcode={`
<Code
thpcode={`
match shape_1
case ::Square(side)
{
@ -33,7 +35,8 @@ case ::Rectangle(length, height)
{
print("Area of the rectangle: {length * height}")
}
`} />
`}
/>
## Internal representation
@ -42,7 +45,6 @@ When compiled down to PHP, tagged unions are a combination of an enum and an arr
THP creates an enum of the same name and with the same cases, and the values
are contained as part of an array.
```php
// The first snippet is compiled to:
enum Shape
@ -56,4 +58,3 @@ $dot = [Shape::Dot];
$square1 = [Shape::Square, 10];
$rectangle1 = [Shape::Rectangle, 5, 15]
```

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Blocks
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Blocks
@ -17,8 +18,8 @@ val result = {
val temp = 161
temp * 2 // This will be assigned to \`result\`
}
print(result) // 322
print(result) // 322
`} />

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Conditionals
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Conditionals
@ -12,7 +13,6 @@ import Code from "@/components/Code.astro"
- Paretheses for the condition are not required.
- There's no ternary operator
<Code thpcode={`
if condition {
// code
@ -24,22 +24,20 @@ else {
// even more code
}
val result = if condition { value1 } else { value2 }
`} />
## Check for datatypes
TBD
<Code thpcode={`
<Code
thpcode={`
if variable is Datatype {
// code using variable
}
`} />
`}
/>
## If variable is of enum
@ -49,13 +47,10 @@ TBD
val user_id = POST::get("user_id")
if Some(user_id) = user_id {
print("user_id exists: {user_id}")
print("user_id exists: {user_id}")
}
if Some(user_id) = user_id && user_id > 0 {
print("user_id is greater than 0: {user_id}")
print("user_id is greater than 0: {user_id}")
}
`} />

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Loops
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Loops
@ -18,7 +19,7 @@ Braces are required.
val numbers = [0, 1, 2, 3]
for number in numbers {
print(number)
print(number)
}
`} />
@ -30,18 +31,17 @@ val dict = .{
}
for value in dict {
print("{value}")
print("{value}")
}
`} />
### Loop over keys and values
<Code thpcode={`
val numbers = [0, 1, 2, 3]
for index, number in numbers {
print("{index} : {number}")
print("{index} : {number}")
}
`} />
@ -53,11 +53,10 @@ val dict = .{
}
for key, value in dict {
print("{key} => {value}")
print("{key} => {value}")
}
`} />
## While loop
<Code thpcode={`
@ -65,25 +64,24 @@ val colors = ["red", "green", "blue"]
var index = 0
while index < colors.size() {
print("{colors[index]}")
index += 1
print("{colors[index]}")
index += 1
}
`} />
## Labelled loops
TBD
You can give labels to loops, allowing you to `break` and `continue` in nested loops.
<Code thpcode={`
<Code
thpcode={`
:top for i in values_1 {
for j in values_2 {
// ...
break :top
}
}
`} />
`}
/>

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Match
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Match
@ -13,45 +14,42 @@ Braces are **required**.
<Code thpcode={`
val user_id = POST::get("user_id")
match user_id
case Some(id) { print("user_id exists: {id}") }
case None { print("user_id doesn't exist") }
match user_id
case Some(id) {
print("user_id exists: {id}")
print("user_id exists: {id}")
}
case None {
print("user_id doesn't exist")
print("user_id doesn't exist")
}
match user_id
case Some(id) if id > 0 {
print("user_id exists: {id}")
print("user_id exists: {id}")
}
else {
print("user_id has other values ")
print("user_id has other values ")
}
match customer_id
case 1, 2, 3 {
print("special discount for our first 3 customers!")
print("special discount for our first 3 customers!")
}
else {
print("hello dear")
print("hello dear")
}
match customer_id
match customer*id
| 1 | 2 | 3 {
print("ehhhh")
print("ehhhh")
}
| 4 | 5 {
print("ohhh")
print("ohhh")
}
| _ {
print("???")
| * {
print("???")
}
`} />

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Pipes
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Pipes
@ -11,24 +12,30 @@ expression as input to another.
For example, instead of writing:
<Code thpcode={`
<Code
thpcode={`
val result = third_function(second_function(first_function(my_value)))
`} />
`}
/>
You can use pipes:
<Code thpcode={`
<Code
thpcode={`
val result = my_value
|> first_function
|> second_function
|> third_function
`} />
`}
/>
Or use it to group expressions:
<Code thpcode={`
<Code
thpcode={`
print <| (2 + 3 * 4) / 7
`} />
`}
/>
TBD: How to handle piping to functions with more than 1 param
@ -40,13 +47,11 @@ fun add_one(x: Int) -> Int {
}
fun times_two(x: Int) -> Int {
x * 2
x \* 2
}
// (Int) -> (Int)
val plus_one_times_2 = add_one >> times_two
print(plus_one_times_2(5)) // 12
print(plus_one_times_2(5)) // 12
`} />

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Declaration
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Declaration
@ -10,7 +11,6 @@ Functions in THP have a different syntax than PHP.
Function names **must** begin with a lowercase letter.
## Minimal function
The following code shows a function without parameters
@ -27,14 +27,12 @@ say_hello()
Functions are called the same way as in PHP.
## With return type
If your function return any value, annotating the return
type is __mandatory__, and it's done by placing an
type is **mandatory**, and it's done by placing an
arrow `->` followed by the return datatype:
<Code thpcode={`
fun get_random_number() -> Int
{
@ -44,38 +42,38 @@ fun get_random_number() -> Int
val number = get_random_number()
`} />
It's an error to return from a function that doesn't declare
a return type:
<Code thpcode={`
<Code
thpcode={`
fun get_random_number()
{
// Error: the function does not define a return type
return Random::get(0, 35_222)
}
`} />
`}
/>
You can omit the `return` keyword if it's placed on the last
expression on the function:
<Code thpcode={`
<Code
thpcode={`
fun get_random_number() -> Int
{
// The last expression of a function is
// automatically returned
Random::get(0, 35_222)
}
`} />
`}
/>
## Function parameters
Parameters are declared like C-style languages:
`Type name`, separated by commas.
<Code thpcode={`
fun add(Int a, Int b) -> Int
{
@ -89,8 +87,6 @@ THP has different semantics on parameters concerning
pass by value, pass by reference and copy on write.
This is detailed in another chapter.
## Generic types
Functions can declare generic types by using the syntax
@ -99,7 +95,6 @@ Functions can declare generic types by using the syntax
The following example declares a generic `T` and uses it
in the parameters and return type:
<Code thpcode={`
fun get_first_item[T](Array[T] array) -> T
{
@ -112,27 +107,24 @@ val first = get_first_item[Int](numbers)
val first = get_first_item(numbers)
`} />
## Default arguments
TBD
## Variadic arguments
TBD
## Signature
<Code thpcode={`
<Code
thpcode={`
() -> ()
() -> (Int)
(Int, Int) -> (Int)
[T](Array[T]) -> (T)
`} />
`}
/>
## Named arguments
@ -165,8 +157,3 @@ fun greet(
greet("John", from: "LA")
`} />

View File

@ -1,12 +1,12 @@
---
layout: "../_wrapper.astro"
title: Higher Order Functions
title: Higher Order Functions
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Higher Order functions
## Function as parameters
<Code thpcode={`
@ -28,10 +28,6 @@ fun generate_generator() -> () -> Int
}
}
val generator = generate_generator() // A function
val value = generate_generator()() // An Int
val generator = generate_generator() // A function
val value = generate_generator()() // An Int
`} />

View File

@ -2,40 +2,45 @@
layout: "../_wrapper.astro"
title: Lambdas
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Lambdas / Anonymous functions
## Anonymous function
An anonymous function is declared with `fn`.
Other than not having a name, it can declare parameter
and return types.
<Code thpcode={`
<Code
thpcode={`
fn(Int x, Int y) -> Int {
x + y
}
`} />
`}
/>
Anonymous function can omit declaring those types as well:
<Code thpcode={`
<Code
thpcode={`
numbers.map(fun(x) {
x * 2
})
`} />
`}
/>
## Anonymous function short form
If you need an anonymous function that returns a single
expression you can write it like this:
<Code thpcode={`
<Code
thpcode={`
fun(x, y) = x + y
`} />
`}
/>
It uses an equal `=` instead of an arrow. It can contain
only a single expression.
@ -43,56 +48,54 @@ only a single expression.
This is common when declaring functions that immediately
return a map.
<Code thpcode={`
<Code
thpcode={`
fn(value) = .{
value
}
`} />
`}
/>
## Closure types
By default closures **always** capture variables as **references**.
<Code thpcode={`
var x = 20
val f = fun() {
print(x)
print(x)
}
f() // 20
f() // 20
x = 30
f() // 30
f() // 30
`} />
You can force a closure to capture variables by value.
<Code thpcode={`
<Code
thpcode={`
fun(parameters) clone(variables) {
// code
}
`} />
`}
/>
<Code thpcode={`
var x = 20
val f = fun() clone(x) {
print(x)
print(x)
}
f() // 20
f() // 20
x = 30
f() // 20
f() // 20
`} />
## Lambdas
Lambdas are a short form of anonymous functions. They are declared with `#{}`.
@ -109,9 +112,6 @@ numbers.map() #{
// the above lambda is equivalent to:
numbers.map(fun(param1) {
$1 * 2
$1 \* 2
})
`} />

View File

@ -2,18 +2,20 @@
layout: "../_wrapper.astro"
title: Function parameters
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Function parameters
## Immutable reference
<Code thpcode={`
<Code
thpcode={`
fun add_25(Array[Int] numbers) {
numbers.push(25) // Error: \`numbers\` is immutable
}
`} />
`}
/>
When using a regular type as a parameter, only it's immutable
properties can be used
@ -23,19 +25,21 @@ fun count(Array[Int] numbers) -> Int {
val items_count = numbers.size() // Ok, \`size\` is pure
items_count
}
`} />
To use immutable properties you must use a mutable reference.
## Mutable reference
<Code thpcode={`
<Code
thpcode={`
fun push_25(mut Array[Int] numbers) {
numbers.push(25) // Ok, will also mutate the original array
}
`} />
`}
/>
Placing a `mut` before the type makes the parameter a mutable
reference. Mutable methods can be used, and the original
@ -47,33 +51,30 @@ must use `mut`.
<Code thpcode={`
var numbers = Array(0, 1, 2, 3)
push_25(mut numbers) // Pass \`numbers\` as reference.
push_25(mut numbers) // Pass \`numbers\` as reference.
print(numbers(4)) // \`Some(25)\`
print(numbers(4)) // \`Some(25)\`
`} />
## Clone
<Code thpcode={`
<Code
thpcode={`
fun add_25(clone Array[Int] numbers) {
numbers.push(25) // Ok, the original array is unaffected
}
`} />
`}
/>
Using the `clone` keyword before the type creates a mutable copy
of the parameter (CoW). The original data will **not** be mutated.
The caller must also use `clone` when calling the function.
<Code thpcode={`
val numbers = Array(1, 2, 3, 4)
add_25(clone numbers) // Pass \`numbers\` as clone.
add_25(clone numbers) // Pass \`numbers\` as clone.
print(numbers(4)) // None
print(numbers(4)) // None
`} />

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Nullable types
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Nullable types
@ -13,9 +14,11 @@ by the question mark `?` character.
For instance, a POST request may have a `username` parameter,
or it may not. This can be represented with an `?String`.
<Code thpcode={`
<Code
thpcode={`
?String new_username = POST::get("username")
`} />
`}
/>
When we have a `?Type` we cannot use it directly. We must first
check if the value is null, and then use it.
@ -31,7 +34,7 @@ if new_username?
// you can also manually check for null
if new_username == null
{
// This is the same as above
// This is the same as above
}
`} />
@ -46,8 +49,8 @@ To create a nullable type we must explicitly annotate the type.
<Code thpcode={`
val favorite_color = null // Error, we must define the type
?String favorite_color = null // Ok
`} />
?String favorite_color = null // Ok
`} />
Other examples:
@ -71,7 +74,6 @@ val name = person?.name
- If `person` is null, `person?.name` will return `null`
- If `person` is not null, `person?.name` will return `name`
## Null unboxing
The `!!` operator transforms a `?Type` into `Type`.
@ -90,16 +92,17 @@ String s = lastname!!
You can use it to chain access:
<Code thpcode={`
<Code
thpcode={`
val children_lastname = person!!.child!!.lastname
`} />
`}
/>
However, if at runtime you use `!!` on a null value,
the null value will be returned and your program will
blow up later. So make sure to use this operator
only when you are sure a value cannot be null.
## Elvis operator
The Elvis operator `??` is used to give a default value in case a `null` is found.
@ -114,13 +117,12 @@ val test_score = get_score() ?? 0
For the above code:
- If `get_score()` is not null, `??` will return `get_score()`
- If `get_score()` *is* null, `??` will return `0`
- If `get_score()` _is_ null, `??` will return `0`
You can use the Elvis operator to return early
<Code thpcode={`
<Code
thpcode={`
val username = get_username() ?? return
`} />
`}
/>

View File

@ -2,8 +2,9 @@
layout: "../_wrapper.astro"
title: Try/Exceptions
---
import InteractiveCode from "@/components/InteractiveCode.astro";
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Try/exceptions
@ -27,6 +28,7 @@ fun invert(Int number) -> DivisionByZero!Int
}
return 1 / number
}
`} />
@ -36,7 +38,6 @@ or an `Int`.
There is no `throw` keyword, errors are just returned.
### Multiple error returns
TODO: properly define syntax, how this interacts with type unions.
@ -44,12 +45,12 @@ TODO: properly define syntax, how this interacts with type unions.
Multiple errors are chained with `!`. The last one is always
the success value.
<Code thpcode={`
<Code
thpcode={`
fun sample() -> Error1!Error2!Error3!Int
{ /* ... */}
`} />
`}
/>
## Error handling
@ -110,17 +111,17 @@ Use a naked `try` when you want to rethrow an error, if there is any.
unset " = = ="
set " dangerous()" "Int 50"
}
`}
></InteractiveCode>
`}
> </InteractiveCode>
In the previous example:
- If `dangerous()` returns an `Exception`, this exception
will be returned by `run()`;
will be returned by `run()`;
- If `dangerous()` succeedes, its value is assigned
to `result`, and the function continues executing.
to `result`, and the function continues executing.
### Try/return
@ -136,16 +137,16 @@ fun run() -> Int
val result = try dangerous() return 0
// ...
}
`} />
In the previous example:
- If `dangerous()` fails, its error will be ignored, and `0` will
be returned from `run()`.
be returned from `run()`.
- If `dangerous()` succeedes, its value will be assigned to `result`,
and the function continues executing.
and the function continues executing.
### Try/else
@ -210,7 +211,9 @@ Try/else will assign a new value if an expression fails.
}
step{line 0}
`}
>
</InteractiveCode>
- If `possible_value` is an error, the value `666` is used.
@ -218,12 +221,10 @@ Try/else will assign a new value if an expression fails.
Either way, the function will continue executing.
### Try/catch
Try/catch allows the error to be manually used & handled.
<Code thpcode={`
fun run()
{
@ -239,12 +240,14 @@ fun run()
// Return a new value to be assigned to \`result\`
0
}
}
`} />
A try/catch may have many `catch` clauses:
<Code thpcode={`
<Code
thpcode={`
try dangerous()
catch Exception1 e
{...}
@ -252,6 +255,5 @@ catch Exception2 e
{...}
catch Exception3 e
{...}
`} />
`}
/>

View File

@ -2,8 +2,7 @@
layout: "../_wrapper.astro"
title: Abstract
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Abstract

View File

@ -2,11 +2,11 @@
layout: "../_wrapper.astro"
title: Anonymous classes
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Anonymous classes
<Code thpcode={`
class Logger {
pub fun log(String msg) {
@ -19,16 +19,18 @@ setLogger(Logger())
// Using an anonymous class
setLogger(class {
pub fun log(String msg) {
print(msg)
}
pub fun log(String msg) {
print(msg)
}
})
`} />
<Code thpcode={`
<Code
thpcode={`
setLogger(class(Int param1) -> SomeClass(param1), SomeInterface {
pub fun method() {
// code
}
})
`} />
`}
/>

View File

@ -2,11 +2,11 @@
layout: "../_wrapper.astro"
title: Constructor/Destructor
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Constructor/Destructor
## Constructor
The constructor syntax in THP is inspired by Kotlin.
@ -28,7 +28,7 @@ Note that the parameters in the constructor (`fullname`,
inside the class methods, only in the
[`init` block](#init-block) and properties declaration.
To declare properties in the constructor see
To declare properties in the constructor see
[Constructor promotion](#constructor-promotion).
### Constructor visibility
@ -37,7 +37,6 @@ If you want to declare a constructor as protected
or private you need to add the `constructor`
keyword, after the visibility modifier:
<Code thpcode={`
// Cow has a protected constructor
class Cow
@ -48,7 +47,6 @@ class Bat
private constructor(Int height)
`} />
### Init block
The `init` block allow us to run code during the
@ -62,6 +60,7 @@ class Dog(String name) {
init {
print("Dog created: {name}")
}
}
`} />
@ -80,15 +79,13 @@ class Dog {
}
```
### Constructor promotion
Constructor parameters can serve as class properties.
This is done by adding a modifier and `var`/`val`.
<Code thpcode={`
<Code
thpcode={`
class Parrot(
// A public property
pub val String name,
@ -97,15 +94,14 @@ class Parrot(
// A private property
var String last_name,
)
`} />
`}
/>
By using this syntax you are declaring properties and assigning them
at the same time.
The contructor parameters can also have default values.
### Derived properties
You can declare properties whose values depend on values
@ -122,43 +118,40 @@ class Animal(
}
val a2 = Animal("Doa")
print(a2.name_length) //: 3
print(a2.name_length) //: 3
`} />
### Constructor that may fail
A constructor may only fail if there is code
that can fail on the `init` block.
TBD
Proposal 1:
<Code thpcode={`
<Code
thpcode={`
class PrayingMantis(String name)
throws Error {
init -> Error! {
// Initialization code that may fail
}
}
`} />
`}
/>
## Destructor
The destructor in THP is the same as PHP.
<Code thpcode={`
<Code
thpcode={`
class Owl() {
pub fun __destruct() {
// Cleanup
print("Destroying Owl...")
}
}
`} />
`}
/>

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Basics
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Classes
@ -14,25 +15,27 @@ Classes in THP are significantly different than PHP.
A class is defined as follows:
<Code thpcode={`
<Code
thpcode={`
class Animal() {}
`} />
`}
/>
The name of the class **MUST** begin with an uppercase letter.
Classes have a parameter list even if they have no parameters
for consistency sake.
## Instanciation
To create an instance of a class call it as if it was a function,
without `new`.
<Code thpcode={`
<Code
thpcode={`
val animal = Animal()
`} />
`}
/>
## Properties
@ -50,6 +53,7 @@ class Person() {
// This is also okay
String name = "Jane Doe"
}
`} />
@ -62,11 +66,12 @@ class Person() {
// To make a property public use \`pub\`
pub var Int age = 30
}
val p = Person()
print(p.name) // Compile error: \`name\` is private
print(p.age) // 30
print(p.name) // Compile error: \`name\` is private
print(p.age) // 30
`} />
Unlike PHP, to access properties and methods use dot notation `.`
@ -79,19 +84,19 @@ Readonly properties are explained in the Readonly page.
The interaction between properties and the constructor is explained
in the Constructor page.
## Methods
Methods are declared with `fun`, as regular functions.
<Code thpcode={`
<Code
thpcode={`
class Person() {
fun greet() {
print("Hello")
}
}
`} />
`}
/>
**Methods are private by default**, and are made public with `pub`.
@ -106,11 +111,12 @@ class Person() {
pub fun greet() {
print("Hello from greet")
}
}
val p = Person()
p.greet() //: Hello from greet
p.private_greet() // Compile time error. Private method.
p.greet() //: Hello from greet
p.private_greet() // Compile time error. Private method.
`} />
[Unlike PHP](https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.properties-methods),
@ -125,10 +131,10 @@ class Person() {
fun name() -> String {
"Rose"
}
}
`} />
## This
THP uses the dollar sign `$` as this inside classes.
@ -146,21 +152,23 @@ class Person() {
val person_name = $get_name()
print("Hello, I'm {person_name}")
}
}
`} />
## Mutable methods
By default methods cannot mutate the state of the object.
<Code thpcode={`
<Code
thpcode={`
class Animal(var String name) {
pub fun set_name(String new_name) {
$name = new_name // Error: Cannot mutate $name
}
}
`} />
`}
/>
To do so the method must be annotated. The caller must also
declare a mutable variable.
@ -175,4 +183,3 @@ class Animal(var String name) {
var michi = Animal("Michifu")
michi.set_name("Garfield")
`} />

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Inheritance
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Inheritance
@ -19,7 +20,7 @@ class Animal(var String name)
}
// Child class
class Cat(String name, Int lives)
class Cat(String name, Int lives)
extends Animal(name) {}
Cat("Michi", 9).say_name()
@ -28,12 +29,14 @@ Cat("Michi", 9).say_name()
The call to the parent constructor is done right there, after
the parent class name.
<Code thpcode={`
<Code
thpcode={`
class Dog(String name)
extends Animal(name) {}
// |----|
// This is the call to the parent constructor
`} />
`}
/>
You must always call super, even if the parent doesn't
define any parameters:
@ -44,10 +47,3 @@ class Parent() {}
class Child()
extends Parent() {}
`} />

View File

@ -2,12 +2,11 @@
layout: "../_wrapper.astro"
title: Interfaces
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Interfaces
<Code thpcode={`
interface Serializable
{
@ -15,17 +14,14 @@ interface Serializable
fun serialize() -> String
}
class Cat -> Serializable
{
pub fun Serializable() -> String
{
// code
}
pub fun Serializable() -> String
{
// code
}
}
`} />
No interface inheritance.

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Magic methods
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Magic methods
@ -12,15 +13,14 @@ Don't get special treatment.
class Cat
{
pub fun __sleep() -> Array[String]
{
// logic
}
pub fun \_\_sleep() -> Array[String]
{
// logic
}
}
`} />
<Code thpcode={`
val option = Some("GAAA")
val Some(value) = option

View File

@ -2,14 +2,15 @@
layout: "../_wrapper.astro"
title: Readonly
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Readonly
<Code thpcode={`
<Code
thpcode={`
class Caño
{
}
`} />
`}
/>

View File

@ -2,11 +2,11 @@
layout: "../_wrapper.astro"
title: Static
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Static in classes
## Class constants
<Code thpcode={`
@ -17,18 +17,16 @@ class Cat
static Cat
{
const CONSTANT = "constant value"
const CONSTANT = "constant value"
}
print(Cat::CONSTANT)
`} />
## Static methods
aka. plain, old functions
<Code thpcode={`
static Cat
{
@ -41,23 +39,17 @@ static Cat
Cat::static_method()
`} />
## Static properties
aka. global variables
<Code thpcode={`
static Cat
{
pub var access_count = 0
}
print(Cat::access_count) // 0
print(Cat::access_count) // 0
Cat::access_count += 1
print(Cat::access_count) // 1
print(Cat::access_count) // 1
`} />

View File

@ -2,6 +2,7 @@
layout: "../_wrapper.astro"
title: Visibility
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Visibility

View File

@ -2,8 +2,9 @@
layout: "../_wrapper.astro"
title: Components
---
import Code from "@/components/Code.astro"
import Info from "@/components/docs/Info.astro"
import Code from "@/components/Code.astro";
import Info from "@/components/docs/Info.astro";
# Components
@ -14,14 +15,17 @@ Like React, a component is any function that returns `HTML`.
will be uppercase or not.
For now they will be uppercase.
</Info>
<Code thpcode={`
<Code
thpcode={`
fun MyComponent() -> HTML
{
<p>Hello templates!</p>
}
`} />
`}
/>
Inside the HTML tags you can (mostly) write the HTML you already
know.
@ -37,6 +41,7 @@ fun MyComponent() -> HTML
val name = "John"
<p>Hello {name}!</p>
}
`} />
@ -57,6 +62,7 @@ fun MyComponent() -> HTML
{if is_vip {"Welcome"} else {"Hi"}}
{name}!
</p>
}
`} />
@ -74,8 +80,10 @@ fun MyComponent() -> HTML
val user_input = "<b>BOLD</b>"
<p>answer: {user_input}</p>
}
`} />
```html
<p>answer: &lt;b&gt;BOLD&lt;/b&gt;</p>
```
@ -88,12 +96,15 @@ fun MyComponent() -> HTML
val user_input = "<b>BOLD</b>"
<p>answer: <span raw-html={user_input}></span></p>
}
`} />
```html
<p>answer: <span><b>BOLD</b></span></p>
```
```html
<p>
answer: <span><b>BOLD</b></span>
</p>
```
## Dynamic attributes
@ -101,12 +112,14 @@ TODO: boolean attributes
Normal attributes (plain strings) work as you'd expect:
<Code thpcode={`
<Code
thpcode={`
fun MyComponent() -> HTML
{
<button class="red">hello</button>
}
`} />
`}
/>
```html
<button class="red">hello</button>
```
@ -121,29 +134,30 @@ fun MyComponent() -> HTML
// Note the braces
<button class={button_class}>hello</button>
}
`} />
```html
<button class="blue">hello</button>
```
## Fragments
An HTML expression consist of a single tag that may have children inside.
If you need to return multiple tags at once you can use fragments.
The following code doesn't work as you would expect:
<Code thpcode={`
<Code
thpcode={`
fun MyComponent() -> HTML
{
<p>hello</p> // This is an error, an ignored expression
<p>world</p>
}
`} />
`}
/>
Each `<p>` is a single expression, they are not grouped.
And since we cannot have unused expressions, the code will not compile.
@ -151,7 +165,8 @@ And since we cannot have unused expressions, the code will not compile.
To have these two `<p>` tags as a single expression use a fragment:
`<></>`
<Code thpcode={`
<Code
thpcode={`
fun MyComponent() -> HTML
{
// This is the root "element"
@ -160,7 +175,8 @@ fun MyComponent() -> HTML
<p>world</p>
</>
}
`} />
`}
/>
```html
<p>hello</p>
<p>world</p>
@ -179,16 +195,14 @@ fun User() -> HTML
fun MyComponent() -> HTML
{
<>
<p>status</p>
<User /> // Here we are using the other component
</>
}
`} />
<>
<p>status</p>
<User /> // Here we are using the other component
</>
} `} />
```html
<p>status</p>
<span>world</span>
```

View File

@ -2,8 +2,9 @@
layout: "../_wrapper.astro"
title: Control flow
---
import Code from "@/components/Code.astro"
import Info from "@/components/docs/Info.astro"
import Code from "@/components/Code.astro";
import Info from "@/components/docs/Info.astro";
# Control flow
@ -12,7 +13,8 @@ import Info from "@/components/docs/Info.astro"
Use `@if`, `@else if` and `@else` to branch during the template
creation.
<Code thpcode={`
<Code
thpcode={`
fun User(User user) -> HTML
{
<div>
@ -26,8 +28,8 @@ fun User(User user) -> HTML
}
</div>
}
`} />
`}
/>
## Loops
@ -44,15 +46,16 @@ fun Users() -> HTML
<User user={user} />
}
</div>
}
`} />
## Match
Use `@match` for pattern matching:
<Code thpcode={`
<Code
thpcode={`
fun UserDetail(User user) -> HTML
{
<div>
@ -67,8 +70,5 @@ fun UserDetail(User user) -> HTML
}
</div>
}
`} />
`}
/>

View File

@ -2,8 +2,9 @@
layout: "../_wrapper.astro"
title: Introduction
---
import Code from "@/components/Code.astro"
import Info from "@/components/docs/Info.astro"
import Code from "@/components/Code.astro";
import Info from "@/components/docs/Info.astro";
# THP templating
@ -44,13 +45,13 @@ function render_button(string $name) {
This approach has many problems:
- You have to create a new file for every new component,
polluting the file system and the project. This may be significant
for some web hosting providers that establish a fixed inode limit.
polluting the file system and the project. This may be significant
for some web hosting providers that establish a fixed inode limit.
- Data is passed dinamically, via strings. If either the render
function or the template change, the component will behave
incorrectly without any warning.
function or the template change, the component will behave
incorrectly without any warning.
- It's hard to include components inside components. Every new
one requires a change in 2 files.
one requires a change in 2 files.
Maybe for these (and other) reasons components are not used with
templating libraries. Instead people use sections, layouts, etc.
@ -62,18 +63,19 @@ and compose them.
The following would be the equivalent in THP:
<Code thpcode={`
<Code
thpcode={`
fun Button(String name) -> HTML {
<button class="some tailwind classes">
Hello {name}!
</button>
}
`} />
`}
/>
It is very similar to React. The HTML is inside the THP code, not the other
way around, so you can have arbitrary logic in the component.
<Code thpcode={`
fun User(String name) -> HTML {
// Get info from the database
@ -92,15 +94,15 @@ fun User(String name) -> HTML {
<TransactionItem t={t} />
}
</div>
}
fun TransactionItem(Transaction t) -> HTML {
<li>
{t.date} - {t.name} ({t.price})
</li>
}
`} />
<li>
{t.date} - {t.name} ({t.price})
</li>
} `} />
## Is this a JavaScript Front-End Framework?
@ -114,19 +116,14 @@ If you need reactivity on the front-end and want to use this templating
take a look at [htmx](https://htmx.org/), [Alpine.js](https://alpinejs.dev/)
and [hyperscript](https://hyperscript.org/).
## Styling
We don't provide any syntax or facility for styling.
However, this component model is good to use with TailwindCSS.
## Conversion
<Info>
TBD: This is a draft, subject to change.
</Info>
<Info>TBD: This is a draft, subject to change.</Info>
HTML expressions will be compiled to plain strings, and
those will be then composed.
@ -145,6 +142,7 @@ fun Sample(String name) -> HTML {
Not logged in
}
</button>
}
`} />
@ -163,6 +161,3 @@ function Sample(name) {
return "<button>{$__expr_1}</button>"
}
```

View File

@ -2,8 +2,9 @@
layout: "../_wrapper.astro"
title: Props
---
import Code from "@/components/Code.astro"
import Info from "@/components/docs/Info.astro"
import Code from "@/components/Code.astro";
import Info from "@/components/docs/Info.astro";
# Props
@ -15,23 +16,27 @@ and they are defined as normal parameters.
For example, to receive a `String` declare it as a
parameter:
<Code thpcode={`
<Code
thpcode={`
fun Greeter(String name) -> HTML
{
<p>Hello {name}!</p>
}
`} />
`}
/>
And to send its value type the parameter name as an attribute:
<Code thpcode={`
<Code
thpcode={`
fun Home() -> HTML
{
<div>
<Greeter name="Rose" />
</div>
}
`} />
`}
/>
```html
<div><p>Hello Rose</p></div>
@ -39,18 +44,21 @@ fun Home() -> HTML
You can have as many props as you'd like, of any datatype:
<Code thpcode={`
<Code
thpcode={`
fun Greeter(String name, Int age, Array[String] friends) -> HTML
{
// ...
}
`} />
`}
/>
## Static props
If the prop has a type `String` you can use a normal attribute.
<Code thpcode={`
<Code
thpcode={`
fun Home() -> HTML
{
<div>
@ -58,8 +66,8 @@ fun Home() -> HTML
<Greeter name="Rose" />
</div>
}
`} />
`}
/>
## Dynamic props
@ -75,21 +83,22 @@ fun Sample(Cat cat) -> HTML
fun Home() -> HTML
{
val my_cat = Cat("Michifu")
val my_cat = Cat("Michifu")
<div>
<Sample cat={my_cat} />
</div>
}
`} />
## Components as props
If for some reason you want to use a component as a prop
use the `HTML` datatype:
<Code thpcode={`
<Code
thpcode={`
// The parameter can have any name, not only \`child\`
fun Sample(HTML child) -> HTML
{
@ -98,24 +107,27 @@ fun Sample(HTML child) -> HTML
{child}
</>
}
`} />
`}
/>
This, however, means that your prop component must be declared
as an attribute:
<Code thpcode={`
<Code
thpcode={`
fun Home() -> HTML
{
<div>
<Sample child={<span>I am the child</span>} />
</div>
}
`} />
`}
/>
```html
<div>
<p>Sup</p>
<span>I am the child</span>
<p>Sup</p>
<span>I am the child</span>
</div>
```
@ -136,21 +148,16 @@ fun MyButton() -> HTML
fun Home() -> HTML
{
<div>
<MyButton>
buy <b>now!</b>
</MyButton>
</div>
}
`} />
<div>
<MyButton>
buy <b>now!</b>
</MyButton>
</div>
} `} />
```html
<div>
<button>
buy <b>now!</b>
</button>
<button>buy <b>now!</b></button>
</div>
```

View File

@ -4,17 +4,21 @@ import NewDocsLayout, { type AstroFile } from "@/layouts/NewDocsLayout.astro";
const { frontmatter, headings } = Astro.props;
// Get all the posts from this dir
const posts = await Astro.glob("./**/*.{md,mdx}") as unknown as Array<AstroFile>;
const posts = (await Astro.glob(
"./**/*.{md,mdx}",
)) as unknown as Array<AstroFile>;
// The base of every URL under this glob
const base_url = "/en/latest/learn"
const base_url = "/en/latest/learn";
const version = "latest";
---
<NewDocsLayout
base_url={base_url}
frontmatter={frontmatter}
headings={headings}
posts={posts}
base_url={base_url}
frontmatter={frontmatter}
headings={headings}
posts={posts}
version={version}
>
<slot />
</NewPagesLayout>
<slot />
</NewDocsLayout>

View File

@ -3,9 +3,9 @@ layout: "./_wrapper.astro"
title: Welcome
order: 1
---
import InteractiveCode from "@/components/InteractiveCode.astro";
import Code from "@/components/Code.astro"
import InteractiveCode from "@/components/InteractiveCode.astro";
import Code from "@/components/Code.astro";
# Welcome
@ -15,16 +15,14 @@ THP is a new programming language that compiles to PHP.
![Accurate visual description of THP](/img/desc_thp.jpg)
This page details the main design desitions of the language,
if you want to install THP go to the [installation guide](install)
## Why?
PHP is an old language. It has been growing since 1995, adopting a
lot of features from many places like C, Perl, Java, etc. This was
done in a very inconsistent way, as detailed by Eevee in their article
done in a very inconsistent way, as detailed by Eevee in their article
[PHP: a fractal of bad design.](https://eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design/)
Along the years PHP has been improving. PHP added classes, exceptions,
@ -71,10 +69,6 @@ essential today: A good, unified LSP, type definitions,
accesible documentation, an opinionated code formatter
and plugins for major editors like VSCode and Neovim.
## Goals
- Bring static typing to PHP: Generics, type checks at compile and runtime, etc.
@ -93,7 +87,6 @@ and plugins for major editors like VSCode and Neovim.
![Friendship ended with Rust, now Zig is my best friend.](/img/mudasir.jpg)
## Not goals
These are **not** things that THP wants to solve or implement
@ -104,7 +97,6 @@ These are **not** things that THP wants to solve or implement
THP **_intentionally_** uses a different syntax from PHP to signal
that it is a different language, and has different semantics.
## Some differences with PHP
```php
@ -113,11 +105,13 @@ $has_key = str_contains($haystack, 'needle');
print("has key? " . $has_key);
```
<Code thpcode={`
<Code
thpcode={`
// THP
val has_key = haystack.contains("needle")
print("has key? " + has_key)
`} />
`}
/>
- Explicit variable declaration
- No `$` for variable names (and thus no `$$variable`, use a map instead)
@ -126,9 +120,8 @@ print("has key? " + has_key)
- Strings use only double quotes
- String concatenation with `+`
<br/>
<br/>
<br />
<br />
```php
// PHP
@ -139,20 +132,22 @@ $obj = [
]
```
<Code thpcode={`
<Code
thpcode={`
// THP
val obj = .{
names: #("Toni", "Stark"), // Tuple
age: 33,
numbers: [32, 64, 128]
}
`} />
`}
/>
- Tuples, Arrays, Sets, Maps are clearly different
- JSON-like object syntax
<br/>
<br/>
<br />
<br />
```php
// PHP
@ -160,18 +155,19 @@ $cat = new Cat("Michifu", 7);
$cat->meow();
```
<Code thpcode={`
<Code
thpcode={`
// THP
val cat = Cat("Michifu", 7)
cat.meow()
`} />
`}
/>
- Instantiate classes without `new`
- Use dot `.` instead of arrow `->` syntax
<br/>
<br/>
<br />
<br />
```php
// PHP
@ -179,34 +175,34 @@ use \Some\Deeply\Nested\Class
use \Some\Deeply\Nested\Interface
```
<Code thpcode={`
<Code
thpcode={`
// THP
use Some::Deeply::Nested::{Class, Interface}
`} />
`}
/>
- Different module syntax
- Explicit module declaration
- PSR-4 required
- No `include`, `include_once`, `require` or `require_once`
<br/>
<br/>
<br />
<br />
Other things:
- Pattern matching
- ADTs
## Runtime changes
Where possible THP will compile to available PHP functions/classes/methods/etc.
For example:
<Code thpcode={`
<Code
thpcode={`
// This expression
val greeting =
match get_person()
@ -222,7 +218,8 @@ val greeting =
{
"Nobody is here"
}
`} />
`}
/>
```php
// Would compile to:
@ -243,5 +240,3 @@ else {
However, more advanced datatypes & helper functions will require a sort of
runtime (new classes/functions/etc) or abuse the language's syntax/semantics.

View File

@ -0,0 +1,24 @@
---
import NewDocsLayout, { type AstroFile } from "@/layouts/NewDocsLayout.astro";
const { frontmatter, headings } = Astro.props;
// Get all the posts from this dir
const posts = (await Astro.glob(
"./**/*.{md,mdx}",
)) as unknown as Array<AstroFile>;
// The base of every URL under this glob
const base_url = "/en/v0.0.1/learn";
const version = "v0.0.1";
---
<NewDocsLayout
base_url={base_url}
frontmatter={frontmatter}
headings={headings}
posts={posts}
version={version}
>
<slot />
</NewDocsLayout>

View File

@ -0,0 +1,12 @@
---
layout: "./_wrapper.astro"
title: Welcome
order: 1
---
import InteractiveCode from "@/components/InteractiveCode.astro";
import Code from "@/components/Code.astro";
# THP :D
Version 0.0.1

View File

@ -22,111 +22,107 @@ case Cat(name, lives) {
}`;
const [thp_html] = await native_highlighter(
leftTrimDedent(thpcode).join("\n"),
HighlightLevel.Lexic,
leftTrimDedent(thpcode).join("\n"),
HighlightLevel.Lexic,
);
---
<BaseLayout>
<Navbar showSidebarButton={false} />
<Navbar showSidebarButton={false} />
<div
class="container mx-auto lg:py-16 pb-2 pt-20 lg:grid 2xl:grid-cols-[auto_32rem] lg:grid-cols-[auto_36rem] gap-4 lg:px-10 px-2 min-h-[85vh]"
>
<div class="lg:pl-10 table">
<div class="table-cell align-middle">
<h1
class="font-display font-bold text-c-text-2 lg:text-5xl text-3xl lg:text-left text-center leading-tight"
>
A modern, type safe,
<br class="hidden lg:inline-block" />
secure language
<br class="hidden lg:inline-block" />
compiled to PHP
</h1>
<p class="text-c-text text-lg pt-6 lg:pr-12">
Inspired by Rust, Zig and Kotlin, THP has a modern syntax,
semantics, type system and stdlib.
</p>
<div class="text-center pb-4">
<a
class="inline-block font-display text-lg rounded-full border-2 border-pink-400 py-2 px-8
<div
class="container mx-auto lg:py-16 pb-2 pt-20 lg:grid 2xl:grid-cols-[auto_32rem] lg:grid-cols-[auto_36rem] gap-4 lg:px-10 px-2 min-h-[85vh]"
>
<div class="lg:pl-10 table">
<div class="table-cell align-middle">
<h1
class="font-display font-bold text-c-text-2 lg:text-5xl text-3xl lg:text-left text-center leading-tight"
>
A modern, type safe,
<br class="hidden lg:inline-block" />
secure language
<br class="hidden lg:inline-block" />
compiled to PHP
</h1>
<p class="text-c-text text-lg pt-6 lg:pr-12">
Inspired by Rust, Zig and Kotlin, THP has a modern syntax, semantics,
type system and stdlib.
</p>
<div class="text-center pb-4">
<a
class="inline-block font-display text-lg rounded-full border-2 border-pink-400 py-2 px-8
transition-colors hover:text-black hover:bg-pink-400 mt-2"
href="/en/latest/learn/"
>
Learn
</a>
href="/en/latest/learn/"
>
Learn
</a>
<a
class="inline-block font-display text-lg border-2 border-sky-400 py-2 px-8 mx-6 rounded-full
<a
class="inline-block font-display text-lg border-2 border-sky-400 py-2 px-8 mx-6 rounded-full
transition-colors hover:text-black hover:bg-sky-400 mt-2 opacity-50 cursor-not-allowed"
>
Install
</a>
</div>
</div>
</div>
>
Install
</a>
</div>
</div>
</div>
<div>
<div class="flex h-full justify-center items-center">
<div
class="bg-[var(--code-theme-bg-color)] lg:p-6 p-2 rounded-lg w-full max-w-[30rem] relative"
>
<span
class="absolute lg:inline-block hidden h-[35rem] w-[35rem] -z-10 top-1/2 left-1/2 rounded-full
<div>
<div class="flex h-full justify-center items-center">
<div
class="bg-[var(--code-theme-bg-color)] lg:p-6 p-2 rounded-lg w-full max-w-[30rem] relative"
>
<span
class="absolute lg:inline-block hidden h-[35rem] w-[35rem] -z-10 top-1/2 left-1/2 rounded-full
transform -translate-x-1/2 -translate-y-1/2"
style="background-image: conic-gradient(from 180deg at 50% 50%,#5BCEFA 0deg,#a853ba 180deg,#F5A9B8 1turn);
style="background-image: conic-gradient(from 180deg at 50% 50%,#5BCEFA 0deg,#a853ba 180deg,#F5A9B8 1turn);
filter: blur(75px); opacity: 0.75;"
>
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="54"
height="14"
viewBox="0 0 54 14"
>
<g
fill="none"
fill-rule="evenodd"
transform="translate(1 1)"
>
<circle
cx="6"
cy="6"
r="6"
fill="#FF5F56"
stroke="#E0443E"
stroke-width=".5"></circle>
<circle
cx="26"
cy="6"
r="6"
fill="#FFBD2E"
stroke="#DEA123"
stroke-width=".5"></circle>
<circle
cx="46"
cy="6"
r="6"
fill="#27C93F"
stroke="#1AAB29"
stroke-width=".5"></circle>
</g>
</svg>
<div class="h-1"></div>
<div id="editor" class="font-mono language-thp">
<pre set:html={thp_html} />
</div>
</div>
</div>
</div>
</div>
>
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="54"
height="14"
viewBox="0 0 54 14"
>
<g fill="none" fill-rule="evenodd" transform="translate(1 1)">
<circle
cx="6"
cy="6"
r="6"
fill="#FF5F56"
stroke="#E0443E"
stroke-width=".5"></circle>
<circle
cx="26"
cy="6"
r="6"
fill="#FFBD2E"
stroke="#DEA123"
stroke-width=".5"></circle>
<circle
cx="46"
cy="6"
r="6"
fill="#27C93F"
stroke="#1AAB29"
stroke-width=".5"></circle>
</g>
</svg>
<div class="h-1"></div>
<div id="editor" class="font-mono language-thp">
<pre set:html={thp_html} />
</div>
</div>
</div>
</div>
</div>
<div class="h-8 md:hidden"></div>
<div class="h-8 md:hidden"></div>
<HeroSection
title="Generics & Types"
thpcode={`
<HeroSection
title="Generics & Types"
thpcode={`
Array[Int] numbers = [0, 1, 2, 3, 4, 5]
numbers.push("7") // Compile error: expected an Int, found a String
@ -143,17 +139,17 @@ const [thp_html] = await native_highlighter(
// Ok
val result = mock() as String
`}
>
Type safety is enforced at compile time. Everything
has a specific type. There is no `mixed`.
<br>
<br>
You can use generics where neccesary.
</HeroSection>
>
Type safety is enforced at compile time. Everything has a specific type.
There is no `mixed`.
<br />
<br />
You can use generics where neccesary.
</HeroSection>
<HeroSection
title="Tagged unions"
thpcode={`
<HeroSection
title="Tagged unions"
thpcode={`
union DirEntry {
File(String),
Dir(String),
@ -162,17 +158,17 @@ const [thp_html] = await native_highlighter(
val root = DirEntry::Dir("/")
val test_file = DirEntry::File("test.txt")
`}
>
Make invalid state irrepresentable.
<br />
Model data in a type-safe way.
<br />
Ensure all cases are handled.
</HeroSection>
>
Make invalid state irrepresentable.
<br />
Model data in a type-safe way.
<br />
Ensure all cases are handled.
</HeroSection>
<HeroSection
title="Pattern matching"
thpcode={`
<HeroSection
title="Pattern matching"
thpcode={`
match test_file
case DirEntry::File(filename)
if !filename.starts_with(".") {
@ -182,15 +178,15 @@ const [thp_html] = await native_highlighter(
print("A valid file was not found")
}
`}
>
Match on values, tuples, enums, unions, types etc.
<br />
Guards available!
</HeroSection>
>
Match on values, tuples, enums, unions, types etc.
<br />
Guards available!
</HeroSection>
<HeroSection
title="Null safety"
thpcode={`
<HeroSection
title="Null safety"
thpcode={`
String? username = Post::get("username")
if username? {
@ -198,16 +194,16 @@ const [thp_html] = await native_highlighter(
print("Hello, {username}")
}
`}
>
Nulls are explicit and require handling. Types can be nullable,
and they must be checked before usage.
<br>
The stdlib makes extensive use of them.
</HeroSection>
>
Nulls are explicit and require handling. Types can be nullable, and they
must be checked before usage.
<br />
The stdlib makes extensive use of them.
</HeroSection>
<HeroSection
title="Errors as values"
thpcode={`
<HeroSection
title="Errors as values"
thpcode={`
val user_id = POST::get("id")
val user = try User::find(user_id)
catch DBException e {
@ -215,12 +211,10 @@ const [thp_html] = await native_highlighter(
return page(.{}, 404)
}
`}
>
Exceptions are values and don't disrupt
control flow.
<br>
<br>
Errors cannot be ignored, and we have
syntax sugar to ease them.
</HeroSection>
>
Exceptions are values and don't disrupt control flow.
<br />
<br />
Errors cannot be ignored, and we have syntax sugar to ease them.
</HeroSection>
</BaseLayout>

View File

@ -4,18 +4,19 @@ import NewDocsLayout, { type AstroFile } from "@/layouts/NewDocsLayout.astro";
const { frontmatter, headings } = Astro.props;
// Get all the posts from this dir
const posts = await Astro.glob("./**/*.{md,mdx}") as unknown as Array<AstroFile>;
const posts = (await Astro.glob(
"./**/*.{md,mdx}",
)) as unknown as Array<AstroFile>;
// The base of every URL under this glob
const base_url = "/spec"
const base_url = "/spec";
---
<NewDocsLayout
base_url={base_url}
frontmatter={frontmatter}
headings={headings}
posts={posts}
base_url={base_url}
frontmatter={frontmatter}
headings={headings}
posts={posts}
>
<slot />
</NewPagesLayout>
<slot />
</NewDocsLayout>

View File

@ -69,7 +69,6 @@ BlockMember = Statement
| Expression
```
## Assignment
The target of an assignment can only be an identifier for now.
@ -87,5 +86,3 @@ AssignmentOperator = "="
| "/="
| "%="
```

View File

@ -7,15 +7,13 @@ title: Expression
The expression parser effectively implements a precedence table.
| Operator | Precedence |
|------------|------------|
| == != | 5 |
| > >= < <= | 4 |
| - + ++ | 3 |
| . ?. !. | 2 |
| / * % | 1 |
| Operator | Precedence |
| --------- | ---------- |
| == != | 5 |
| > >= < <= | 4 |
| - + ++ | 3 |
| . ?. !. | 2 |
| / \* % | 1 |
```ebnf
Expression = Equality
@ -29,7 +27,6 @@ Unary = ("!" | "-"), Expression
| CallExpression
```
## CallExpression
It's so hard to properly name these constructions.
@ -40,6 +37,3 @@ CallExpression = primary, "(", (arguments list)?, ")"
| primary, "[", (expression, (comma, expression)*, comma?)? "]"
| primary
```

View File

@ -2,8 +2,8 @@
layout: "./_wrapper.astro"
title: Welcome
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# The THP Language Specification
@ -42,14 +42,11 @@ The compiler consists of 5 common phases:
- **IR**: Transforms the THP AST into a PHP AST
- **Codegen**: Generates PHP source code from the PHP AST
## Source Code representation
Source code is encoded in UTF-8, and a single UTF-8 codepoint is
a single character.
## Basic characters
Although the source code must be encoded in UTF-8, most of the actual
@ -68,7 +65,6 @@ lowercase_letter = "a".."z"
uppercase_letter = "A".."Z"
```
## Whitespace & Automatic semicolon insertion
This section is being reworked on the Zig rewrite of the compiler.
@ -85,10 +81,12 @@ parenthesis, square brackets, etc.
Other statements require a explicit terminator. For example,
the assignment statement:
<Code thpcode={`
<Code
thpcode={`
val computation = 123 + 456 // how to detect if the statement ends here
* 789 // or extends up to here?
`} />
`}
/>
In other languages a semicolon would be used to signal the end of the
statement:
@ -106,21 +104,25 @@ to the rule:
No matter the indentation, whitespace or others, every statement ends
with a newline.
<Code thpcode={`
<Code
thpcode={`
val compute = 1 + 2 * 3 / 4
// statement ends here ↑
`} />
`}
/>
As mentioned before, this does not affect statements that have clear delimiters.
For example, the following code will work as expected:
<Code thpcode={`
<Code
thpcode={`
val compute = my_function(
param1,
param2,
) / 64
// ↑ statement ends here
`} />
`}
/>
In a way, the parenthesis will "disable" the rule.
@ -133,11 +135,13 @@ continues.
For example:
<Code thpcode={`
<Code
thpcode={`
val computation = 123 + 456
* 789
// ↑ statement ends here, and there is a single statement
`} />
`}
/>
This is so no matter the indentation:
@ -145,25 +149,26 @@ This is so no matter the indentation:
// weird indentation:
val computation = 123 + 456
* 789
// ↑ statement still ends here
`} />
- 789
// ↑ statement still ends here
`} />
What is important is that an operator begins the new line.
If the operator is left on the previous line, this will not work:
<Code thpcode={`
<Code
thpcode={`
// statement ends here ↓, and now there is a syntax error (dangling operator)
val computation = 123 + 456 *
789
// ↑ this is a different statement
`} />
`}
/>
For this the parser must do look-ahead of 1 token. This is the only place the parser
does so.
## Old Whitespace rules
THP is partially whitespace sensitive. It uses the following tokens: Indent, Dedent & NewLine
@ -174,32 +179,33 @@ compares the previous indentation to the new one. If the amount of whitespace is
greater than before, it emits a Indent token. If it's lower, emits a Dedent token, and
if it's the same it does nothing.
<Code thpcode={`
<Code
thpcode={`
1 + 2
+ 3
+ 4
`} />
`}
/>
The previous code would emit the following tokens: `1` `+` `2` `NewLine` `Indent` `+` `3` `NewLine`
`+` `4` `Dedent`
Additionaly, it is a lexical error to have wrong indentation. The lexer stores all
previous indentation levels in a stack, and reports an error if a decrease in indentation
doesn't match a previous level.
<Code thpcode={`
<Code
thpcode={`
if true { // 0 indentation
print() // 4 indentation
print() // 2 indentation. Error. There is no 2-indentation level
}
`} />
`}
/>
All productions of the grammar ignore whitespace/indentation, except those involved in
semicolon inference.
## Statement termination / Semicolon inference
**Only inside a block of code** whitespace is used to determine where a statement ends
@ -207,41 +213,43 @@ and a new one begins. Everywhere else whitespace is ignored.
Statements in THP end when a new line is encountered:
<Code thpcode={`
<Code
thpcode={`
// The statement ends | here, on the newline
val value = (123 + 456) * 0.75
`} />
`}
/>
<Code thpcode={`
// Each line contains a different statement. They all end on their new lines
var a = 1 + 2 // a = 3
+ 3 // this is not part of \`a\`, this is a different statement
`} />
var a = 1 + 2 // a = 3
- 3 // this is not part of \`a\`, this is a different statement
`} />
This is true even if the line ends with an operator:
<Code thpcode={`
// These are still different statements
var a = 1 + 2 + // This is now a compile error, there is a hanging `+`
3 // This is still a different statement
var a = 1 + 2 + // This is now a compile error, there is a hanging `+`
3 // This is still a different statement
`} />
### Parenthesis
Exception 1: When a parenthesis is open, all following whitespace is ignored
until the closing parenthesis.
<Code thpcode={`
<Code
thpcode={`
// open parenthesis found, all whitespace is ignored until the closing
name.contains(
"weird"
)
`} />
`}
/>
However, for a parenthesis to begin to act, it needs to be open on the same line.
@ -254,7 +262,7 @@ print
// Now it's one single statement
print(
"hello"
"hello"
)
`} />
@ -264,17 +272,21 @@ Exception 2:
- When a binary operator is followed by indentation:
<Code thpcode={`
<Code
thpcode={`
val sum = 1 + 2 + // The line ends with a binary operator
3 // There is indentation
`} />
`}
/>
- Or when indentation is followed by a binary operator:
<Code thpcode={`
<Code
thpcode={`
val sum = 1 + 2
+ 3 // Indentation and a binary operator
`} />
`}
/>
In theses cases, all whitespace will be ignored
until the indentation returns to the initial level.
@ -291,5 +303,3 @@ val person = PersonBuilder()
// Here indentation returns, and a new statement begins
print(person)
`} />

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Comment
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Comment
@ -10,8 +11,10 @@ import Code from "@/components/Code.astro"
Comment = "//", any_except_new_line
```
<Code thpcode={`
<Code
thpcode={`
// This is a comment
//
// Another // comment
`} />
`}
/>

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Identifiers & Datatypes
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Identifiers & Datatypes
@ -19,14 +20,15 @@ Identifier = (underscore | lowercase_letter), identifier_letter*
identifier_letter = underscore | lowercase_letter | uppercase_letter | decimal_digit
```
<Code thpcode={`
<Code
thpcode={`
identifier
_identifier
_123
_many_letters
camelCase
`} />
`}
/>
## Datatype
@ -34,22 +36,23 @@ camelCase
Datatype = uppercase_letter, indentifier_letter*
```
<Code thpcode={`
<Code
thpcode={`
Datatype
PDO
WEIRD_DATATYPE
`} />
`}
/>
## Keywords
The following are (currently) THP keywords:
<Code thpcode={`
<Code
thpcode={`
val var fun
`} />
`}
/>
Keywords are scanned first as identifiers, then transformed
to their respective tokens.

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: Numbers
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Numbers
@ -20,7 +21,8 @@ binary_number = "0", ("b" | "B"), binary_digit+
decimal_number = "1".."9", decimal_digit*
```
<Code thpcode={`
<Code
thpcode={`
12345
01234 // This is an error, a decimal number cant have
// leading zeroes
@ -28,12 +30,11 @@ decimal_number = "1".."9", decimal_digit*
0b0110
0xff25
0XFfaA
`} />
`}
/>
`TODO`: Allow underscores `_` between any number: `1_000_000`.
## Float
```ebnf
@ -52,12 +53,7 @@ scientific_notation = "e", ("+" | "-"), decimal_digit+
123e-3
`} />
All floating point numbers must start with at least 1 digit.
`.5` is not a valid floating point number.
`.5` is not a valid floating point number.
`TODO`: Allow scientific notation to omit the `+`/`-`: `10e4`.

View File

@ -2,11 +2,11 @@
layout: "../_wrapper.astro"
title: Operator
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# Operator
```ebnf
Operator = operator_char+
@ -15,14 +15,16 @@ operator_char = "+" | "-" | "=" | "*" | "!" | "/" | "|"
| "<" | ">" | "^" | "." | ":"
```
<Code thpcode={`
<Code
thpcode={`
+ - / * % < > <= >= -> =>
`} />
`}
/>
These are all the characters that can make an operator.
The lexer doesn't know about any operator in particular.
In other languages something like `+-1` would be interpreted
In other languages something like `+-1` would be interpreted
as `+` `-` `1`. In THP, this is always `+-` `1`, and that
would throw an error because the operator `+-` doesn't exist.

View File

@ -2,7 +2,8 @@
layout: "../_wrapper.astro"
title: String
---
import Code from "@/components/Code.astro"
import Code from "@/components/Code.astro";
# String
@ -20,11 +21,13 @@ escape_seq = "\n"
string_char = any_unicode_except_newline_and_double_quote
```
<Code thpcode={`
<Code
thpcode={`
"hello"
""
"it's me"
"\\"Mario\\""
`} />
`}
/>
`TODO`: String interpolation

View File

@ -34,5 +34,3 @@ pub enum TokenType {
```
Every keyword has its own token.

View File

@ -1,64 +1,64 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,ts,tsx}'],
theme: {
extend: {
colors: {
"c-thp": "var(--c-thp)",
"c-bg": "var(--c-bg)",
"c-text": "var(--c-text)",
"c-text-2": "var(--c-text-2)",
"c-purple": "var(--c-purple)",
"c-border-1": "var(--c-border-1)",
"c-purple-light": "var(--c-purple-light)",
"c-box-shadow": "var(--c-box-shadow)",
"c-ping": "var(--c-pink)",
"c-background-2": "var(--c-background-2)",
"c-primary": "var(--c-primary)",
"c-secondary": "var(--c-secondary)",
"c-nav-bg": "var(--c-nav-bg)",
}
},
fontFamily: {
"mono": "var(--font-code)",
"display": "var(--font-display)",
"body": "var(--font-body)",
},
},
corePlugins: {
container: false
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,ts,tsx}"],
theme: {
extend: {
colors: {
"c-thp": "var(--c-thp)",
"c-bg": "var(--c-bg)",
"c-text": "var(--c-text)",
"c-text-2": "var(--c-text-2)",
"c-purple": "var(--c-purple)",
"c-border-1": "var(--c-border-1)",
"c-purple-light": "var(--c-purple-light)",
"c-box-shadow": "var(--c-box-shadow)",
"c-ping": "var(--c-pink)",
"c-background-2": "var(--c-background-2)",
"c-primary": "var(--c-primary)",
"c-secondary": "var(--c-secondary)",
"c-nav-bg": "var(--c-nav-bg)",
},
},
plugins: [
function ({ addComponents }) {
addComponents({
'.container': {
width: '98%',
'@screen sm': {
maxWidth: '640px',
},
'@screen md': {
maxWidth: '768px',
},
'@screen lg': {
maxWidth: '1024px',
},
'@screen xl': {
maxWidth: '1400px',
},
},
'.small-container': {
width: '98%',
'@screen sm': {
maxWidth: '640px',
},
'@screen md': {
maxWidth: '768px',
},
'@screen lg': {
maxWidth: 'inherit',
},
}
})
}
],
}
fontFamily: {
mono: "var(--font-code)",
display: "var(--font-display)",
body: "var(--font-body)",
},
},
corePlugins: {
container: false,
},
plugins: [
function ({ addComponents }) {
addComponents({
".container": {
width: "98%",
"@screen sm": {
maxWidth: "640px",
},
"@screen md": {
maxWidth: "768px",
},
"@screen lg": {
maxWidth: "1024px",
},
"@screen xl": {
maxWidth: "1400px",
},
},
".small-container": {
width: "98%",
"@screen sm": {
maxWidth: "640px",
},
"@screen md": {
maxWidth: "768px",
},
"@screen lg": {
maxWidth: "inherit",
},
},
});
},
],
};

View File

@ -3,7 +3,7 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@/*": ["src/*"]
}
}
}