diff --git a/app/Console/Commands/ProLangAssets.php b/app/Console/Commands/ProLangAssets.php
index 99b7fde..5cd41c7 100644
--- a/app/Console/Commands/ProLangAssets.php
+++ b/app/Console/Commands/ProLangAssets.php
@@ -762,9 +762,7 @@ function member
'mainRepository' => 'https://github.com/eclipse-archived/ceylon',
'rawCodeLink' => 'https://raw.githubusercontent.com/leachim6/hello-world/refs/heads/main/c/Ceylon.ceylon',
),
- 'Genie' => array(
- 'rawCodeLink' => 'https://raw.githubusercontent.com/leachim6/hello-world/refs/heads/main/p/ParaSail.psi',
- ),
+ 'Genie' => array(),
'Forth' => array(
'mainRepository' => 'https://github.com/Forth-Standard/forth-standard',
'rawCodeLink' => 'https://raw.githubusercontent.com/leachim6/hello-world/refs/heads/main/f/Forth.fth',
@@ -846,7 +844,9 @@ function member
),
'Boo' => array(),
'Cobra' => array(),
- 'CoffeeScript' => array(),
+ 'CoffeeScript' => array(
+ 'rawCodeLink' => 'https://raw.githubusercontent.com/acmeism/RosettaCodeData/refs/heads/main/Task/Fibonacci-sequence/CoffeeScript/fibonacci-sequence-2.coffee',
+ ),
'Commodore BASIC' => array(),
'Compiler Description Language' => array(),
'Component Pascal' => array(),
@@ -1050,7 +1050,27 @@ function member
),
'GPSS' => array(),
'Hamilton C shell' => array(),
- 'Io' => array(),
+ 'Io' => array(
+ 'mainRepository' => 'https://github.com/IoLanguage/io?tab=readme-ov-file#example-code',
+ 'rawCode' => <<<'EOD'
+ #!/usr/bin/env io
+
+ Account := Object clone do(
+ balance := 0.0
+ deposit := method(v, balance = balance + v)
+ show := method(write("Account balance: $", balance, "\n"))
+ )
+
+ "Inital: " print
+ Account show
+
+ "Depositing $10\n" print
+ Account deposit(10.0)
+
+ "Final: " print
+ Account show
+ EOD
+ ),
'JOSS II' => array(),
'Jacquard machine' => array(),
'KornShell' => array(),
@@ -1092,7 +1112,9 @@ function member
'occam' => array(
'rawCodeLink' => 'https://raw.githubusercontent.com/leachim6/hello-world/refs/heads/main/o/occam.occam',
),
- 'Dart' => array(),
+ 'Dart' => array(
+ 'rawCodeLink' => 'https://raw.githubusercontent.com/acmeism/RosettaCodeData/refs/heads/main/Task/Fibonacci-sequence/Dart/fibonacci-sequence-4.dart',
+ ),
'Common Lisp' => array(),
'Windows PowerShell' => array(),
);
diff --git a/app/Http/Controllers/MainController.php b/app/Http/Controllers/MainController.php
index 6134c6d..b1ca391 100644
--- a/app/Http/Controllers/MainController.php
+++ b/app/Http/Controllers/MainController.php
@@ -135,6 +135,8 @@ public function searchStarredRepository(Request $r) {
*/
public function langHistory() {
$groups = YearGroup::with('languages.authors')
+ ->with('languages.parents')
+ ->with('languages.children')
->orderBy('position', 'ASC')
->get();
@@ -143,6 +145,17 @@ public function langHistory() {
));
}
+ public function langFamily() {
+ $groups = ProLang::with('authors')
+ ->with('parents')
+ ->with('children')
+ ->get();
+
+ return Inertia::render('LangFamily', array(
+ 'langs' => $groups,
+ ));
+ }
+
public function road() {
Log::info('action=show_road');
diff --git a/justfile b/justfile
index f404524..e51e6c4 100644
--- a/justfile
+++ b/justfile
@@ -80,3 +80,9 @@ sync:
clean_smala:
php artisan config:clear
php artisan config:cache
+
+start:
+ just up
+ just up_db
+ just up_adminer
+ just open
diff --git a/package-lock.json b/package-lock.json
index 884b64b..faf7b7f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"@awesome.me/webawesome": "^3.0.0",
"@hackernoon/pixel-icon-library": "^1.0.6",
"@inertiajs/vue3": "^2.2.19",
+ "@nanostores/vue": "^1.0.1",
"@tailwindcss/vite": "^4.1.11",
"@vueuse/core": "^12.8.2",
"@yeger/vue-masonry-wall": "^5.1.4",
@@ -15,10 +16,13 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"concurrently": "^9.0.1",
+ "d3-force": "^3.0.0",
"language-colors": "^2.1.55",
"laravel-vite-plugin": "^2.0.0",
+ "nanostores": "^1.1.0",
"typescript": "^5.2.2",
"typewriter-effect": "^2.22.0",
+ "v-network-graph": "^0.9.22",
"vite": "^7.0.4",
"vue": "^3.5.13"
},
@@ -630,6 +634,12 @@
"node": ">=14"
}
},
+ "node_modules/@dash14/svg-pan-zoom": {
+ "version": "3.6.9",
+ "resolved": "https://registry.npmjs.org/@dash14/svg-pan-zoom/-/svg-pan-zoom-3.6.9.tgz",
+ "integrity": "sha512-6u+KTQec+9+3bRdk2mReix8AGsp2mB40cw0iYfQQzo22QBkeCNpXl2amnfwQzK7xB9oH/62Wvf2z7l6l2w+csA==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
@@ -807,6 +817,35 @@
"@lit-labs/ssr-dom-shim": "^1.4.0"
}
},
+ "node_modules/@nanostores/vue": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@nanostores/vue/-/vue-1.0.1.tgz",
+ "integrity": "sha512-0VwubMTMvEdWQhVN4BAvDZ+vHQH3O1G9BaOfgrjfF4erqBsWScoK/zyaBeRfFjptNOb25947EFPHBZwEf9JcMg==",
+ "funding": [
+ {
+ "type": "buymeacoffee",
+ "url": "https://buymeacoffee.com/euaaaio"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": "^20.0.0 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "@nanostores/logger": "^0.4.0 || ^1.0.0",
+ "@vue/devtools-api": ">=7.6.2",
+ "nanostores": "^0.11.3 || ^1.0.0",
+ "vue": ">=3.3.1"
+ },
+ "peerDependenciesMeta": {
+ "@nanostores/logger": {
+ "optional": true
+ },
+ "@vue/devtools-api": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
@@ -2406,6 +2445,47 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/dargs": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz",
@@ -3452,9 +3532,9 @@
"license": "MIT"
},
"node_modules/lodash-es": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
- "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+ "version": "4.17.22",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
+ "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
"license": "MIT"
},
"node_modules/lodash.camelcase": {
@@ -3762,6 +3842,12 @@
"node": ">= 18"
}
},
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "license": "MIT"
+ },
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
@@ -3802,6 +3888,21 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/nanostores": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.1.0.tgz",
+ "integrity": "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": "^20.0.0 || >=22.0.0"
+ }
+ },
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -5038,6 +5139,26 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/v-network-graph": {
+ "version": "0.9.22",
+ "resolved": "https://registry.npmjs.org/v-network-graph/-/v-network-graph-0.9.22.tgz",
+ "integrity": "sha512-kgG3uiGF6DSA9DrjNEWmD6yy4/AP0jf/uhkq2yVkfub3mJavxcXU0ow2y5z5jFyFlA9L+uzzIZVTSY2iJLMv1w==",
+ "license": "MIT",
+ "dependencies": {
+ "@dash14/svg-pan-zoom": "^3.6.9",
+ "lodash-es": "^4.17.22",
+ "mitt": "^3.0.1"
+ },
+ "peerDependencies": {
+ "d3-force": "^3.0.0",
+ "vue": "^3.5.13"
+ },
+ "peerDependenciesMeta": {
+ "d3-force": {
+ "optional": true
+ }
+ }
+ },
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
diff --git a/package.json b/package.json
index ab48f36..0ef0851 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"@awesome.me/webawesome": "^3.0.0",
"@hackernoon/pixel-icon-library": "^1.0.6",
"@inertiajs/vue3": "^2.2.19",
+ "@nanostores/vue": "^1.0.1",
"@tailwindcss/vite": "^4.1.11",
"@vueuse/core": "^12.8.2",
"@yeger/vue-masonry-wall": "^5.1.4",
@@ -32,10 +33,13 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"concurrently": "^9.0.1",
+ "d3-force": "^3.0.0",
"language-colors": "^2.1.55",
"laravel-vite-plugin": "^2.0.0",
+ "nanostores": "^1.1.0",
"typescript": "^5.2.2",
"typewriter-effect": "^2.22.0",
+ "v-network-graph": "^0.9.22",
"vite": "^7.0.4",
"vue": "^3.5.13"
},
diff --git a/resources/js/app.ts b/resources/js/app.ts
index 6e75c70..772cd4c 100644
--- a/resources/js/app.ts
+++ b/resources/js/app.ts
@@ -1,8 +1,10 @@
import "./app.ui"
import { createInertiaApp } from "@inertiajs/vue3"
import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers"
+import VNetworkGraph from "v-network-graph"
import type { DefineComponent } from "vue"
import { createApp, h } from "vue"
+import "v-network-graph/lib/style.css"
const appName = import.meta.env.VITE_APP_NAME || "Laravel"
@@ -12,6 +14,7 @@ createInertiaApp({
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
+ .use(VNetworkGraph)
.mount(el)
},
progress: {
diff --git a/resources/js/components/Layout.vue b/resources/js/components/Layout.vue
index 5793f7f..06a72a3 100644
--- a/resources/js/components/Layout.vue
+++ b/resources/js/components/Layout.vue
@@ -2,21 +2,32 @@
-
+
diff --git a/resources/js/components/prolang/LangCard.vue b/resources/js/components/prolang/LangCard.vue
index 2370a09..41b66c9 100644
--- a/resources/js/components/prolang/LangCard.vue
+++ b/resources/js/components/prolang/LangCard.vue
@@ -1,7 +1,7 @@
@@ -34,6 +34,60 @@
+
+
Family history
+
+
+
+ Inspired by these languages
+
+
+ {{ p.name }}
+
+
+
+ Just thank you to :
+ {{ props.lang.authors.map((a) => a.name).join(", ") }}
+
+
+ I'm going to search the internet the old-fashioned way.
+
+ No laziness, no AI.
+
+
+
+
+ Inspired these languages
+
+
+ {{ p.name }}
+
+
+
+ Not yet, but it could happen. Don't ask
+ AI, it can't predict the
+ future.
+
+
+
+
+
Close(false)
const code = ref(null)
+const emits = defineEmits(["select-lang"])
const props = defineProps<{
lang: ProLangLanguage
+ selectedLang: string | null
}>()
const hasEnoughData = computed(() => {
@@ -124,7 +179,7 @@ async function showCode() {
}
}
- drawerOpen.value = true
+ emits("select-lang", props.lang.name)
}
@@ -140,6 +195,12 @@ wa-drawer {
}
}
+.lang__family {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+}
+
.drawer__section {
margin-bottom: 20px;
}
diff --git a/resources/js/components/prolang/LangGraphData.ts b/resources/js/components/prolang/LangGraphData.ts
new file mode 100644
index 0000000..c9bc2fe
--- /dev/null
+++ b/resources/js/components/prolang/LangGraphData.ts
@@ -0,0 +1,46 @@
+import type { Edges, Layouts, Nodes } from "v-network-graph"
+import type { ProLangLanguage } from "@/types/main"
+
+function langNameMatch(lang: ProLangLanguage, search: string) {
+ return lang.name.toLowerCase().includes(search.toLowerCase())
+}
+
+export function isMemberOf(search: string, lang: ProLangLanguage): boolean {
+ return (
+ lang.parents.some((l) => langNameMatch(l, search)) ||
+ lang.children.some((l) => langNameMatch(l, search)) ||
+ langNameMatch(lang, search)
+ )
+}
+
+export function getGraphData(prolangs: ProLangLanguage[]) {
+ const orphan = prolangs.filter((l) => l.parents.length === 0)
+ const smala = prolangs.filter((l) => l.parents.length > 0)
+
+ const edges: Edges = {}
+ const nodes: Nodes = {
+ humain: { name: "Human" },
+ }
+
+ for (const l of prolangs) {
+ nodes[l.name] = { name: l.name }
+ }
+
+ for (const l of orphan) {
+ edges[l.name] = {
+ source: "humain",
+ target: l.name,
+ }
+ }
+
+ for (const l of smala) {
+ l.parents.forEach((p) => {
+ edges[`${l.name}_${p.name}`] = {
+ source: p.name,
+ target: l.name,
+ }
+ })
+ }
+
+ return { nodes, edges }
+}
diff --git a/resources/js/components/prolang/LangGraphStore.ts b/resources/js/components/prolang/LangGraphStore.ts
new file mode 100644
index 0000000..b2f226c
--- /dev/null
+++ b/resources/js/components/prolang/LangGraphStore.ts
@@ -0,0 +1,45 @@
+import { atom, computed, type ReadableAtom } from "nanostores"
+import type { ProLangLanguage } from "@/types/main"
+import { getGraphData, isMemberOf } from "./LangGraphData"
+
+export class LangGraphStore {
+ private readonly prolangs = atom([])
+
+ private readonly hasName = atom(null)
+
+ // computed
+
+ private readonly $prolangs = computed([this.prolangs, this.hasName], (prolangs, hasName) => {
+ return prolangs.filter((l) => (hasName ? isMemberOf(hasName, l) : true))
+ })
+
+ public readonly $graphData = computed(this.$prolangs, (langs) => {
+ return getGraphData(langs)
+ })
+
+ public get $hasName(): ReadableAtom {
+ return this.hasName
+ }
+
+ // methods
+
+ public initGraph(langs: ProLangLanguage[]) {
+ this.prolangs.set(langs)
+ }
+
+ public searchByName(value: string) {
+ this.hasName.set(value)
+ }
+
+ // instance
+
+ private static instance?: LangGraphStore
+
+ public static getInstance(): LangGraphStore {
+ if (!LangGraphStore.instance) {
+ LangGraphStore.instance = new LangGraphStore()
+ }
+
+ return LangGraphStore.instance
+ }
+}
diff --git a/resources/js/components/prolang/LangNetworkFilter.vue b/resources/js/components/prolang/LangNetworkFilter.vue
new file mode 100644
index 0000000..25cd98f
--- /dev/null
+++ b/resources/js/components/prolang/LangNetworkFilter.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
diff --git a/resources/js/components/prolang/useLangGraph.ts b/resources/js/components/prolang/useLangGraph.ts
new file mode 100644
index 0000000..b25e2c0
--- /dev/null
+++ b/resources/js/components/prolang/useLangGraph.ts
@@ -0,0 +1,14 @@
+import { useStore } from "@nanostores/vue"
+import { LangGraphStore } from "./LangGraphStore"
+
+export function useLangGraph() {
+ const graphStore = LangGraphStore.getInstance()
+ const graphData = useStore(graphStore.$graphData)
+ const hasName = useStore(graphStore.$hasName)
+
+ return {
+ graphStore,
+ graphData,
+ hasName,
+ }
+}
diff --git a/resources/js/pages/LangFamily.vue b/resources/js/pages/LangFamily.vue
new file mode 100644
index 0000000..c1ba4ee
--- /dev/null
+++ b/resources/js/pages/LangFamily.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/pages/LangHistory.vue b/resources/js/pages/LangHistory.vue
index e531fa8..981fb6a 100644
--- a/resources/js/pages/LangHistory.vue
+++ b/resources/js/pages/LangHistory.vue
@@ -53,7 +53,12 @@
-
+
@@ -70,6 +75,8 @@ import type { YearGroup } from "@/types/main"
const searchYear = ref("")
const searchLang = ref("")
+const selectedLang = ref(null)
+
const otpions = ["=", ">=", "<=", ">", "<"]
const selectedOption = ref("=")
@@ -77,6 +84,14 @@ const props = defineProps<{
groups: YearGroup[]
}>()
+function updateSelectedLang(value: string) {
+ if (searchLang.value.length > 0) {
+ searchLang.value = value
+ }
+
+ selectedLang.value = value
+}
+
const showGroups = computed(() => {
return props.groups
.map((g) => {
@@ -86,7 +101,7 @@ const showGroups = computed(() => {
const search = searchLang.value.toLowerCase()
const year = Number(searchYear.value)
- const ops: Record = {
+ const ops: Record boolean> = {
"=": (n: number) => n === year,
">": (n: number) => n > year,
">=": (n: number) => n >= year,
diff --git a/resources/js/types/main.d.ts b/resources/js/types/main.d.ts
index b50e1f4..71e7ef0 100644
--- a/resources/js/types/main.d.ts
+++ b/resources/js/types/main.d.ts
@@ -125,6 +125,8 @@ export type ProLangLanguage = {
codeTitle: string
rawCode: string | null
rawCodeLink: string | null
+ children: ProLangLanguage[]
+ parents: ProLangLanguage[]
}
export type YearGroup = {
diff --git a/routes/web.php b/routes/web.php
index 2b39120..11d8328 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -32,6 +32,10 @@
->name('lang-history')
->middleware(HandleGithubRateLimit::class);
+Route::get('/lang-family', array(MainController::class, 'langFamily'))
+ ->name('lang-family')
+ ->middleware(HandleGithubRateLimit::class);
+
Route::post('/search-oldest', array(MainController::class, 'searchOldestRepository'))
->name('search-oldest')
->middleware(HandleGithubRateLimit::class);
diff --git a/vite.config.ts b/vite.config.ts
index af9be27..ea5edc9 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -4,6 +4,9 @@ import laravel from "laravel-vite-plugin"
import { defineConfig } from "vite"
export default defineConfig({
+ optimizeDeps: {
+ exclude: ["v-network-graph"],
+ },
plugins: [
laravel({
input: ["resources/js/app.ts"],