commit 9e9942da19cdf7ad71a82bedcb34a76d20463b10
Author: Starbeamrainbowlabs <sbrl@starbeamrainbowlabs.com>
Date:   Thu Feb 16 19:05:43 2023 +0000

    Initial commit
    
    oops, should have committed before

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d06bd32
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,159 @@
+# Created by https://www.toptal.com/developers/gitignore/api/git,node
+# Edit at https://www.toptal.com/developers/gitignore?templates=git,node
+
+### Git ###
+# Created by git for backups. To disable backups in Git:
+# $ git config --global mergetool.keepBackup false
+*.orig
+
+# Created by git when using merge tools for conflicts
+*.BACKUP.*
+*.BASE.*
+*.LOCAL.*
+*.REMOTE.*
+*_BACKUP_*.txt
+*_BASE_*.txt
+*_LOCAL_*.txt
+*_REMOTE_*.txt
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+### Node Patch ###
+# Serverless Webpack directories
+.webpack/
+
+# Optional stylelint cache
+
+# SvelteKit build / generate output
+.svelte-kit
+
+# End of https://www.toptal.com/developers/gitignore/api/git,node
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..d0e16b4
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,399 @@
+{
+	"name": "tfsummaryvis",
+	"version": "1.0.0",
+	"lockfileVersion": 3,
+	"requires": true,
+	"packages": {
+		"": {
+			"name": "tfsummaryvis",
+			"version": "1.0.0",
+			"license": "MPL-2.0",
+			"dependencies": {
+				"esbuild": "^0.17.8",
+				"nomnoml": "^1.5.3"
+			}
+		},
+		"node_modules/@esbuild/android-arm": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.8.tgz",
+			"integrity": "sha512-0/rb91GYKhrtbeglJXOhAv9RuYimgI8h623TplY2X+vA4EXnk3Zj1fXZreJ0J3OJJu1bwmb0W7g+2cT/d8/l/w==",
+			"cpu": [
+				"arm"
+			],
+			"optional": true,
+			"os": [
+				"android"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/android-arm64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.8.tgz",
+			"integrity": "sha512-oa/N5j6v1svZQs7EIRPqR8f+Bf8g6HBDjD/xHC02radE/NjKHK7oQmtmLxPs1iVwYyvE+Kolo6lbpfEQ9xnhxQ==",
+			"cpu": [
+				"arm64"
+			],
+			"optional": true,
+			"os": [
+				"android"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/android-x64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.8.tgz",
+			"integrity": "sha512-bTliMLqD7pTOoPg4zZkXqCDuzIUguEWLpeqkNfC41ODBHwoUgZ2w5JBeYimv4oP6TDVocoYmEhZrCLQTrH89bg==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"android"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/darwin-arm64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.8.tgz",
+			"integrity": "sha512-ghAbV3ia2zybEefXRRm7+lx8J/rnupZT0gp9CaGy/3iolEXkJ6LYRq4IpQVI9zR97ID80KJVoUlo3LSeA/sMAg==",
+			"cpu": [
+				"arm64"
+			],
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/darwin-x64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.8.tgz",
+			"integrity": "sha512-n5WOpyvZ9TIdv2V1K3/iIkkJeKmUpKaCTdun9buhGRWfH//osmUjlv4Z5mmWdPWind/VGcVxTHtLfLCOohsOXw==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/freebsd-arm64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.8.tgz",
+			"integrity": "sha512-a/SATTaOhPIPFWvHZDoZYgxaZRVHn0/LX1fHLGfZ6C13JqFUZ3K6SMD6/HCtwOQ8HnsNaEeokdiDSFLuizqv5A==",
+			"cpu": [
+				"arm64"
+			],
+			"optional": true,
+			"os": [
+				"freebsd"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/freebsd-x64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.8.tgz",
+			"integrity": "sha512-xpFJb08dfXr5+rZc4E+ooZmayBW6R3q59daCpKZ/cDU96/kvDM+vkYzNeTJCGd8rtO6fHWMq5Rcv/1cY6p6/0Q==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"freebsd"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/linux-arm": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.8.tgz",
+			"integrity": "sha512-6Ij8gfuGszcEwZpi5jQIJCVIACLS8Tz2chnEBfYjlmMzVsfqBP1iGmHQPp7JSnZg5xxK9tjCc+pJ2WtAmPRFVA==",
+			"cpu": [
+				"arm"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/linux-arm64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.8.tgz",
+			"integrity": "sha512-v3iwDQuDljLTxpsqQDl3fl/yihjPAyOguxuloON9kFHYwopeJEf1BkDXODzYyXEI19gisEsQlG1bM65YqKSIww==",
+			"cpu": [
+				"arm64"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/linux-ia32": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.8.tgz",
+			"integrity": "sha512-8svILYKhE5XetuFk/B6raFYIyIqydQi+GngEXJgdPdI7OMKUbSd7uzR02wSY4kb53xBrClLkhH4Xs8P61Q2BaA==",
+			"cpu": [
+				"ia32"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/linux-loong64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.8.tgz",
+			"integrity": "sha512-B6FyMeRJeV0NpyEOYlm5qtQfxbdlgmiGdD+QsipzKfFky0K5HW5Td6dyK3L3ypu1eY4kOmo7wW0o94SBqlqBSA==",
+			"cpu": [
+				"loong64"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/linux-mips64el": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.8.tgz",
+			"integrity": "sha512-CCb67RKahNobjm/eeEqeD/oJfJlrWyw29fgiyB6vcgyq97YAf3gCOuP6qMShYSPXgnlZe/i4a8WFHBw6N8bYAA==",
+			"cpu": [
+				"mips64el"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/linux-ppc64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.8.tgz",
+			"integrity": "sha512-bytLJOi55y55+mGSdgwZ5qBm0K9WOCh0rx+vavVPx+gqLLhxtSFU0XbeYy/dsAAD6xECGEv4IQeFILaSS2auXw==",
+			"cpu": [
+				"ppc64"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/linux-riscv64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.8.tgz",
+			"integrity": "sha512-2YpRyQJmKVBEHSBLa8kBAtbhucaclb6ex4wchfY0Tj3Kg39kpjeJ9vhRU7x4mUpq8ISLXRXH1L0dBYjAeqzZAw==",
+			"cpu": [
+				"riscv64"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/linux-s390x": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.8.tgz",
+			"integrity": "sha512-QgbNY/V3IFXvNf11SS6exkpVcX0LJcob+0RWCgV9OiDAmVElnxciHIisoSix9uzYzScPmS6dJFbZULdSAEkQVw==",
+			"cpu": [
+				"s390x"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/linux-x64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.8.tgz",
+			"integrity": "sha512-mM/9S0SbAFDBc4OPoyP6SEOo5324LpUxdpeIUUSrSTOfhHU9hEfqRngmKgqILqwx/0DVJBzeNW7HmLEWp9vcOA==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/netbsd-x64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.8.tgz",
+			"integrity": "sha512-eKUYcWaWTaYr9zbj8GertdVtlt1DTS1gNBWov+iQfWuWyuu59YN6gSEJvFzC5ESJ4kMcKR0uqWThKUn5o8We6Q==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"netbsd"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/openbsd-x64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.8.tgz",
+			"integrity": "sha512-Vc9J4dXOboDyMXKD0eCeW0SIeEzr8K9oTHJU+Ci1mZc5njPfhKAqkRt3B/fUNU7dP+mRyralPu8QUkiaQn7iIg==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"openbsd"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/sunos-x64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.8.tgz",
+			"integrity": "sha512-0xvOTNuPXI7ft1LYUgiaXtpCEjp90RuBBYovdd2lqAFxje4sEucurg30M1WIm03+3jxByd3mfo+VUmPtRSVuOw==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"sunos"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/win32-arm64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.8.tgz",
+			"integrity": "sha512-G0JQwUI5WdEFEnYNKzklxtBheCPkuDdu1YrtRrjuQv30WsYbkkoixKxLLv8qhJmNI+ATEWquZe/N0d0rpr55Mg==",
+			"cpu": [
+				"arm64"
+			],
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/win32-ia32": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.8.tgz",
+			"integrity": "sha512-Fqy63515xl20OHGFykjJsMnoIWS+38fqfg88ClvPXyDbLtgXal2DTlhb1TfTX34qWi3u4I7Cq563QcHpqgLx8w==",
+			"cpu": [
+				"ia32"
+			],
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/@esbuild/win32-x64": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz",
+			"integrity": "sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg==",
+			"cpu": [
+				"x64"
+			],
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">=12"
+			}
+		},
+		"node_modules/esbuild": {
+			"version": "0.17.8",
+			"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.8.tgz",
+			"integrity": "sha512-g24ybC3fWhZddZK6R3uD2iF/RIPnRpwJAqLov6ouX3hMbY4+tKolP0VMF3zuIYCaXun+yHwS5IPQ91N2BT191g==",
+			"hasInstallScript": true,
+			"bin": {
+				"esbuild": "bin/esbuild"
+			},
+			"engines": {
+				"node": ">=12"
+			},
+			"optionalDependencies": {
+				"@esbuild/android-arm": "0.17.8",
+				"@esbuild/android-arm64": "0.17.8",
+				"@esbuild/android-x64": "0.17.8",
+				"@esbuild/darwin-arm64": "0.17.8",
+				"@esbuild/darwin-x64": "0.17.8",
+				"@esbuild/freebsd-arm64": "0.17.8",
+				"@esbuild/freebsd-x64": "0.17.8",
+				"@esbuild/linux-arm": "0.17.8",
+				"@esbuild/linux-arm64": "0.17.8",
+				"@esbuild/linux-ia32": "0.17.8",
+				"@esbuild/linux-loong64": "0.17.8",
+				"@esbuild/linux-mips64el": "0.17.8",
+				"@esbuild/linux-ppc64": "0.17.8",
+				"@esbuild/linux-riscv64": "0.17.8",
+				"@esbuild/linux-s390x": "0.17.8",
+				"@esbuild/linux-x64": "0.17.8",
+				"@esbuild/netbsd-x64": "0.17.8",
+				"@esbuild/openbsd-x64": "0.17.8",
+				"@esbuild/sunos-x64": "0.17.8",
+				"@esbuild/win32-arm64": "0.17.8",
+				"@esbuild/win32-ia32": "0.17.8",
+				"@esbuild/win32-x64": "0.17.8"
+			}
+		},
+		"node_modules/graphre": {
+			"version": "0.1.3",
+			"resolved": "https://registry.npmjs.org/graphre/-/graphre-0.1.3.tgz",
+			"integrity": "sha512-F4OO/+BQj3R2UB3PQOGLgdAUy+SQuwy2A5cVGELAQDJlloSyyHqRe60ESFouRf8W0K+sm6gtWKotOwZoX3BL8w=="
+		},
+		"node_modules/nomnoml": {
+			"version": "1.5.3",
+			"resolved": "https://registry.npmjs.org/nomnoml/-/nomnoml-1.5.3.tgz",
+			"integrity": "sha512-2LBKi3ygeYAC9l/wowgtBRxvQGuTryIczULVk2rFbWGUvs5aX68/sH+WgL95CschnSnxucTIv9L+3xScS71obQ==",
+			"dependencies": {
+				"graphre": "^0.1.3"
+			},
+			"bin": {
+				"nomnoml": "dist/nomnoml-cli.js"
+			}
+		}
+	}
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..38df157
--- /dev/null
+++ b/package.json
@@ -0,0 +1,32 @@
+{
+	"name": "tfsummaryvis",
+	"version": "1.0.0",
+	"description": "Visualise textual tensorflow summaries as a directed graph.",
+	"main": "src/index.mjs",
+	"scripts": {
+		"test": "echo \"No tests have been written yet.\"",
+		"build": "node ./src/esbuild.mjs"
+	},
+	"repository": {
+		"type": "git",
+		"url": "git+https://github.com/sbrl/tfsummaryvis.git"
+	},
+	"keywords": [
+		"tensorflow",
+		"data",
+		"visualisation",
+		"directed",
+		"graph"
+	],
+	"author": "Starbeamrainbowlabs",
+	"license": "MPL-2.0",
+	"bugs": {
+		"url": "https://github.com/sbrl/tfsummaryvis/issues"
+	},
+	"homepage": "https://github.com/sbrl/tfsummaryvis#readme",
+	"private": true,
+	"dependencies": {
+		"esbuild": "^0.17.8",
+		"nomnoml": "^1.5.3"
+	}
+}
diff --git a/src/app.css b/src/app.css
new file mode 100644
index 0000000..c3c4ebe
--- /dev/null
+++ b/src/app.css
@@ -0,0 +1,34 @@
+html, body {
+	margin: 0; padding: 0;
+	font-size: 100%;
+}
+body {
+	min-height: 100vh;
+	font-family: Ubuntu, sans-serif;
+	
+	display: grid;
+	grid-template-columns: 1fr 1fr;
+	grid-template-rows: auto 1fr auto;
+	grid-template-areas: "header header"
+		"left right"
+		"footer footer";
+}
+
+h1 {
+	grid-area: header;
+	margin: 0.5em;
+	text-align: center;
+}
+
+#input-summary {
+	grid-area: left;
+}
+
+#output-graph {
+	grid-area: right;
+}
+
+footer {
+	grid-area: footer;
+	margin: 0.5em;
+}
\ No newline at end of file
diff --git a/src/app.mjs b/src/app.mjs
new file mode 100644
index 0000000..9e85c41
--- /dev/null
+++ b/src/app.mjs
@@ -0,0 +1,32 @@
+"use strict";
+
+import parse_summary from './lib/parse_summary.mjs';
+import make_graph from './lib/make_graph.mjs';
+
+function do_visualisation(el_in, el_out) {
+	console.log(el_in, el_out);
+	const source = el_in.value.trim();
+	
+	if(source.length === 0) return;
+	
+	const summary = parse_summary(source);
+	console.log(summary);
+	
+	const svg = make_graph(summary);
+	console.log(svg);
+	
+	const container = document.createElement("div");
+	container.innerHTML = svg;
+	
+	el_out.replaceChildren(container);
+}
+
+window.addEventListener("load", () => {
+	const el_in = document.querySelector("#input-summary");
+	const el_out = document.querySelector("#output-graph");
+	
+	el_in.addEventListener("changed", do_visualisation.bind(this, el_in, el_out));
+	el_in.addEventListener("keyup", do_visualisation.bind(this, el_in, el_out));
+	
+	do_visualisation(el_in, el_out);
+});
\ No newline at end of file
diff --git a/src/esbuild.mjs b/src/esbuild.mjs
new file mode 100644
index 0000000..65623d7
--- /dev/null
+++ b/src/esbuild.mjs
@@ -0,0 +1,76 @@
+"use strict";
+
+import crypto from 'crypto';
+import path from 'path';
+import fs from 'fs';
+
+import esbuild from 'esbuild';
+
+const __dirname = import.meta.url.slice(7, import.meta.url.lastIndexOf("/"));
+const outdir = path.resolve(__dirname, "../dist");
+
+/**
+ * Hashes the contents of a file.
+ * @ref		https://stackoverflow.com/a/44643479/1460422
+ * @param	{string}	hashName	The name of the hash algorithm to use.
+ * @param	{string}	path		The path to the file to hash.
+ * @return	{string}	The resulting hash as a hexadecimal string.
+ */
+function hash_file(hashName, path) {
+	return new Promise((resolve, reject) => {
+		const hash = crypto.createHash(hashName);
+		const stream = fs.createReadStream(path);
+		stream.once("error", reject);
+		stream.on("data", chunk => hash.update(chunk));
+		stream.once("end", () => {
+			stream.off("error", reject);
+			resolve(hash.digest('base64'));
+		});
+	});
+}
+
+async function do_html() {
+	// We *would* use SHA3 here, but we need to use SHA2 for subresource integrity
+	let algorithm = "sha256";
+	let css_hash = await hash_file(algorithm, path.join(outdir, "app.css"));
+	let js_hash = await hash_file(algorithm, path.join(outdir, "app.js"));
+
+	let html = (await fs.promises.readFile(path.join(__dirname, "index.html"), "utf-8"))
+		.replace(/\{JS_HASH\}/g, js_hash)
+		.replace(/\{CSS_HASH\}/g, css_hash)
+		.replace(/\{JS_HASH_SHORT\}/g, js_hash.substring(0, 7).replace(/[+/=]/, ""))
+		.replace(/\{CSS_HASH_SHORT\}/g, css_hash.substring(0, 7).replace(/[+/=]/, ""))
+		.replace(/\{ALGO\}/g, algorithm);
+
+	await fs.promises.writeFile(path.join(outdir, "index.html"), html);
+}
+
+(async () => {
+	"use strict";
+	
+	const result = await esbuild.build({
+		entryPoints: [
+			"./app.mjs",
+			"./app.css",
+		].map(filepath => path.resolve(__dirname, filepath)),
+		outdir,
+		bundle: true,
+		minify: typeof process.env.NO_MINIFY === "undefined",
+		sourcemap: true,
+		treeShaking: true,
+		loader: {
+			".html": "text",
+			".svg": "file",
+			".woff2": "file",
+			".woff": "file",
+			".eot": "file",
+			".ttf": "file",
+		},
+		external: ["fs", "path"],
+	});
+	if (result.errors.length > 0 || result.warnings.length > 0)
+		console.log(result);
+
+	await do_html();
+	// console.log(await esbuild.analyzeMetafile(result.metafile));
+})();
\ No newline at end of file
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..3706feb
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta charset="utf-8" />
+		<title>Tensorflow Summary Visualisation</title>
+	</head>
+	<body>
+		<h1>Tensorflow Summary Visualisation</h1>
+		
+		<textarea name="input-summary" id="input-summary"></textarea>
+		<div id="output-graph"></div>
+		
+		<footer>
+			<p>Built with love by <a href="https://starbeamrainbowlabs.com/">Starbeamrainbowlabs</a></p>
+		</footer>
+		
+		<!---------------->
+		<link rel="stylesheet" href="/app.css?hash={CSS_HASH_SHORT}" integrity="{ALGO}-{CSS_HASH}" />
+		<script src="/app.js?hash={JS_HASH_SHORT}" integrity="{ALGO}-{JS_HASH}" charset="utf-8"></script>
+		<!-- <link rel="stylesheet" href="/app.css" />
+		<script src="/app.js"></script> -->
+	</body>
+</html>
\ No newline at end of file
diff --git a/src/lib/make_graph.mjs b/src/lib/make_graph.mjs
new file mode 100644
index 0000000..d06f122
--- /dev/null
+++ b/src/lib/make_graph.mjs
@@ -0,0 +1,20 @@
+"use strict";
+
+import nomnoml from 'nomnoml';
+
+export default function make_graph(summary) {
+	const result = [];
+	for(const layer of summary.layers) {
+		const output_shape = layer.output_shape.replace(/\[/, "\\[")
+			.replace(/\]/, "\\]");
+		result.push(`[${layer.name}|${layer.type}|Params: ${layer.params}|Output shape: ${output_shape}]`);
+	}
+	for(const edge of summary.edges) {
+		result.push(`[${edge.from}] --> [${edge.to}]`);
+	}
+	
+	const nomnoml_source = result.join("\n");
+	
+	const svg = nomnoml.renderSvg(nomnoml_source);
+	return svg;
+}
\ No newline at end of file
diff --git a/src/lib/parse_summary.mjs b/src/lib/parse_summary.mjs
new file mode 100644
index 0000000..1de7d9d
--- /dev/null
+++ b/src/lib/parse_summary.mjs
@@ -0,0 +1,60 @@
+"use strict";
+
+
+export default function parse_summary(text) {
+	const lines = text.trim().split(/\r?\n/);
+		
+	const layers_raw = lines.slice(
+		lines.findIndex(line => line.startsWith("====")) + 1,
+		lines.findLastIndex(line => line.startsWith("===="))
+	);
+	const layers = [];
+	
+	let acc = [];
+	for (const line of layers_raw) {
+		if(line.trim().length == 0) {
+			if(acc.length === 0) continue;
+			console.log(acc.map(layer_line => layer_line.substring(65).trim()).join(""));
+			// Handle parsed item
+			const result = {
+				name_raw: acc.map(layer_line => layer_line.substring(0, 32).trim()).join(""),
+				output_shape: acc.map(layer_line => layer_line.substring(32, 53).trim()).join(""),
+				params: parseInt(acc.map(layer_line => layer_line.substring(53, 65).trim()).join("")),
+				connected_to: JSON.parse(acc.map(layer_line => layer_line.substring(65).trim()).join("")
+					.replace(/'/g, '"'))
+						.map(connected_name => connected_name.replace(/(\[0\])+$/, ""))
+			};
+			
+			result.type = result.name_raw.match(/ \(([^)]+)\)/)[1];
+			result.name = result.name_raw.split(/\s+/)[0];
+			
+			
+			layers.push(result);
+			
+			acc.length = 0;
+		}
+		
+		acc.push(line);
+	}
+	
+	const edges = [];
+	
+	for(const layer of layers) {
+		for(const connection of layer.connected_to) {
+			edges.push({
+				from: connection,
+				to: layer.name
+			});
+		}
+	}
+	
+	return {
+		title: lines[0].match(/^Model: "([^"]+)"/)[1],
+		params: parseInt(lines.find(line => line.search(/^Total params:/) > -1).split(": ")[1].replace(/,/g, "")),
+		params_trainable: parseInt(lines.find(line => line.search(/^Trainable params:/) > -1).split(": ")[1].replace(/,/g, "")),
+		params_nontrainable: parseInt(lines.find(line => line.search(/^Non-trainable params:/) > -1).split(": ")[1].replace(/,/g, "")),
+		
+		layers,
+		edges
+	}
+}
\ No newline at end of file
