diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..789b271 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM node:22.14.0-alpine + +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm + +# Copy package.json and pnpm-lock.yaml +COPY package.json pnpm-lock.yaml* ./ + +# Install dependencies +RUN pnpm install + +# Copy the rest of the application code +COPY . . + +# Create a volume mount point for sensitive data +VOLUME /app/data + +# Expose the port the app runs on +#EXPOSE 5000 + +# Create a startup script that initializes the database and starts the application +COPY docker-entrypoint.sh /app/ +RUN chmod +x /app/docker-entrypoint.sh + +# Command to run the startup script +CMD ["/app/docker-entrypoint.sh"] diff --git a/data/nextreg_bare.txt b/data/nextreg_records.txt similarity index 99% rename from data/nextreg_bare.txt rename to data/nextreg_records.txt index 187bff6..563a543 100644 --- a/data/nextreg_bare.txt +++ b/data/nextreg_records.txt @@ -1,10 +1,10 @@ 0x00 (00) => Machine ID (R) 0000 1000 = EMULATORS - +// 0000 1010 = ZX Spectrum Next 1111 1010 = ZX Spectrum Next Anti-brick - +// 1001 1010 = ZX Spectrum Next Core on UnAmiga Reloaded 1010 1010 = ZX Spectrum Next Core on UnAmiga 1011 1010 = ZX Spectrum Next Core on SiDi @@ -40,7 +40,7 @@ ** These signals are ignored if the multiface, divmmc, dma or external nmi master is active ** Copper cannot clear these bits ** An i/o trap could occur at the same time as mf / divmmc cause; always check this bit in nmi isr if important - + 0x03 (03) => Machine Type (R) bit 7 = nextreg 0x44 second byte indicator @@ -1234,6 +1234,4 @@ progress is made in the main program. bits 7:0 = MSB data connected to XADC DRP data bus D15:8 * DRP reads store result here, DRP writes take value from here --- - 0xFF (255) => Reserved for internal use diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..916e503 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -e + +# Start the application +echo "Starting the application..." +exec pnpm start diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/src/app/favicon.ico and /dev/null differ diff --git a/src/app/globals.css b/src/app/globals.css index a6f1d74..a3416ee 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -46,4 +46,13 @@ a { color: blue; margin-left: 4px; font-weight: bold; +} + +.bits-table th:first-child, +.bits-table td:first-child { + width: 120px; +} + +.bits-table td:last-child { + white-space: pre-wrap; } \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 29dee48..b01ab5a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import Link from 'next/link'; import "./globals.css"; import "bootstrap/dist/css/bootstrap.min.css"; @@ -14,8 +15,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Spectrum Next Registers", + description: "A platform for exploring the Spectrum Next registers", }; export default function RootLayout({ @@ -26,7 +27,27 @@ export default function RootLayout({ return ( - {children} + +
+ {children} +
); diff --git a/src/app/page.tsx b/src/app/page.tsx index 627d25d..86aeddb 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,98 +1,15 @@ -import Image from "next/image"; import styles from "./page.module.css"; +import Link from 'next/link'; export default function Home() { return (
- Next.js logo -
    -
  1. - Get started by editing src/app/page.tsx. -
  2. -
  3. Save and see your changes instantly.
  4. -
  5. - Explore the Spectrum Next Registers. -
  6. -
-
- - Vercel logomark - Deploy now - - - Read our docs - -
+ + Register Explorer → +
-
); } diff --git a/src/app/registers/RegisterBrowser.tsx b/src/app/registers/RegisterBrowser.tsx index 7ec30b8..b6f6b9f 100644 --- a/src/app/registers/RegisterBrowser.tsx +++ b/src/app/registers/RegisterBrowser.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import Link from 'next/link'; import { Register, RegisterAccess, Note } from './types'; import { Form, Card, Container, Row, Col, Tabs, Tab, Table, OverlayTrigger, Tooltip } from 'react-bootstrap'; @@ -8,7 +9,7 @@ interface RegisterBrowserProps { registers: Register[]; } -function renderAccess(access: RegisterAccess) { +export function renderAccess(access: RegisterAccess) { const renderTooltip = (notes: Note[]) => ( {notes.map((note, index) => ( @@ -19,32 +20,35 @@ function renderAccess(access: RegisterAccess) { return ( <> - - - - - - - - - {access.operations.map((op, index) => { - const notes = access.notes.filter(note => note.ref === op.footnoteRef); - return ( - - - - - ); - })} - -
BitsDescription
{op.bits} - {op.description} - {op.footnoteRef && notes.length > 0 && ( - - {op.footnoteRef} - - )} -
+ {access.description &&
{access.description}
} + {access.operations.length > 0 && + + + + + + + + + {access.operations.map((op, index) => { + const notes = access.notes.filter(note => note.ref === op.footnoteRef); + return ( + + + + + ); + })} + +
BitsDescription
{op.bits} + {op.description} + {op.footnoteRef && notes.length > 0 && ( + + {op.footnoteRef} + + )} +
+ } {access.notes.map((note, index) => (

{note.ref} {note.text}

))} @@ -84,17 +88,25 @@ export default function RegisterBrowser({ registers }: RegisterBrowserProps) { - {register.name} ({register.hex_address} / {register.dec_address}) + {register.hex_address} ( {register.dec_address} ) + {register.name} {register.issue_4_only && Issue 4 Only} + - {register.common && {renderAccess(register.common)}} + {register.common && {renderAccess(register.common)}} {register.read && {renderAccess(register.read)}} {register.write && {renderAccess(register.write)}} {register.notes.map((note, index) => (

{note.ref} {note.text}

))} + {register.text && register.text.length > 0 && ( +
+
Notes:
+
{register.text}
+
+ )}
diff --git a/src/app/registers/RegisterDetailClient.tsx b/src/app/registers/RegisterDetailClient.tsx new file mode 100644 index 0000000..2ebd692 --- /dev/null +++ b/src/app/registers/RegisterDetailClient.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { Container, Row, Col, Card, Tabs, Tab } from 'react-bootstrap'; +import { Register } from './types'; +import { renderAccess } from './RegisterBrowser'; + +export default function RegisterDetailClient({ + register, + defaultActiveKey, +}: { + register: Register; + defaultActiveKey?: string; +}) { + return ( + + + + + + {register.name} ({register.hex_address} / {register.dec_address}){' '} + {register.issue_4_only && Issue 4 Only} + + + {defaultActiveKey ? ( + + {register.common && ( + + {renderAccess(register.common)} + + )} + {register.read && ( + + {renderAccess(register.read)} + + )} + {register.write && ( + + {renderAccess(register.write)} + + )} + + ) : null} + {register.notes.map((note, index) => ( +

+ {note.ref} {note.text} +

+ ))} + {register.text && register.text.length > 0 && ( +
+
Notes:
+
{register.text}
+
+ )} +
+
+ +
+
+ ); +} diff --git a/src/app/registers/[hex]/page.tsx b/src/app/registers/[hex]/page.tsx new file mode 100644 index 0000000..e148db4 --- /dev/null +++ b/src/app/registers/[hex]/page.tsx @@ -0,0 +1,183 @@ +import { promises as fs } from 'fs'; +import path from 'path'; +import { notFound } from 'next/navigation'; +import Link from 'next/link'; +import { Register, RegisterAccess } from '../../registers/types'; +import RegisterDetailClient from '../../registers/RegisterDetailClient'; + +async function parseNextReg(fileContent: string): Promise { + const registers: Register[] = []; + const paragraphs = fileContent.split(/\n\s*\n/); + + for (const paragraph of paragraphs) { + if (!paragraph.trim()) { + continue; + } + processRegisterBlock(paragraph, registers); + } + + return registers; +} + +function processRegisterBlock(paragraph: string, registers: Register[]) { + const lines = paragraph.trim().split('\n'); + const firstLine = lines[0]; + + const registerMatch = firstLine.match(/([0-9a-fA-F,x]+)\s*\((.*?)\)\s*=>\s*(.*)/); + + if (!registerMatch) { + return; + } + + const hexAddresses = registerMatch[1].trim(); + const decAddresses = registerMatch[2].trim(); + const name = registerMatch[3] ? registerMatch[3].trim() : ''; + const description = lines.slice(1).join('\n').trim(); + + const hexList = hexAddresses.split(',').map(h => h.trim()); + const decList = decAddresses.includes('-') ? decAddresses.split('-') : decAddresses.split(',').map(d => d.trim()); + + const createRegister = (hex: string, dec: string | number, regName: string): Register => { + const reg: Register = { + hex_address: hex, + dec_address: dec, + name: regName, + description: description, + notes: [], + text: "", + issue_4_only: false + }; + + const descriptionLines = description.split('\n'); + let currentAccess: 'read' | 'write' | 'common' | null = null; + let accessData: RegisterAccess = { operations: [], notes: [] }; + + for (const line of descriptionLines) { + if(line.includes('Issue 4 Only')) reg.issue_4_only = true; + + const trimmedLine = line.trim(); + + if (trimmedLine.startsWith('//')) continue; + + if (trimmedLine.startsWith('(R)')) { + if (currentAccess) reg[currentAccess] = accessData; + accessData = { operations: [], notes: [] }; + currentAccess = 'read'; + continue; + } + if (trimmedLine.startsWith('(W)')) { + if (currentAccess) reg[currentAccess] = accessData; + accessData = { operations: [], notes: [] }; + currentAccess = 'write'; + continue; + } + if (trimmedLine.startsWith('(R/W')) { + if (currentAccess) reg[currentAccess] = accessData; + accessData = { operations: [], notes: [] }; + currentAccess = 'common'; + continue; + } + if (line.startsWith(trimmedLine)) { + if (currentAccess) reg[currentAccess] = accessData; + accessData = { operations: [], notes: [] }; + currentAccess = null; + } + + if (currentAccess) { + const bitMatch = trimmedLine.match(/^(bits?|bit)\s+([\d:-]+)\s*=\s*(.*)/); + const valueMatch = !line.match(/^\s+/) && trimmedLine.match(/^([01\s]+)\s*=\s*(.*)/); + + if (bitMatch) { + let bitDescription = bitMatch[3]; + const footnoteMatch = bitDescription.match(/(\*+)$/); + let footnoteRef: string | undefined = undefined; + if (footnoteMatch) { + footnoteRef = footnoteMatch[1]; + bitDescription = bitDescription.substring(0, bitDescription.length - footnoteRef.length).trim(); + } + accessData.operations.push({ + bits: bitMatch[2], + description: bitDescription, + footnoteRef: footnoteRef, + }); + } else if (valueMatch) { + accessData.operations.push({ + bits: valueMatch[1].trim().replace(/\s/g, ''), + description: valueMatch[2].trim(), + }); + } else if (trimmedLine.startsWith('*')) { + const noteMatch = trimmedLine.match(/^(\*+)\s*(.*)/); + if (noteMatch) { + accessData.notes.push({ + ref: noteMatch[1], + text: noteMatch[2], + }); + } + } else if (trimmedLine) { + if (line.match(/^\s+/) && accessData.operations.length > 0) { + accessData.operations[accessData.operations.length - 1].description += `\n${line}`; + } else { + + if (!accessData.description) { + accessData.description = ''; + } + accessData.description += `\n${trimmedLine}`; + } + } + } else { + if (trimmedLine.startsWith('*')) { + const noteMatch = trimmedLine.match(/^(\*+)\s*(.*)/); + if (noteMatch) { + (reg as Register).notes.push({ + ref: noteMatch[1], + text: noteMatch[2], + }); + } + } + else { + reg.text += `${line}\n`; + } + } + } + if (currentAccess) { + (reg as Register)[currentAccess] = accessData; + } + + return reg; + }; + + if (hexList.length > 1) { + for (let i = 0; i < hexList.length; i++) { + const hexAddr = hexList[i]; + const decAddr = (decList as string[])[i] || decAddresses; + const dec = isNaN(parseInt(decAddr, 10)) ? (decAddr) : parseInt(decAddr, 10); + registers.push(createRegister(hexAddr, dec, `${name} (${hexAddr})`)); + } + } else { + const dec = isNaN(parseInt(decAddresses, 10)) ? (decAddresses) : parseInt(decAddresses, 10); + registers.push(createRegister(hexAddresses, dec, name)); + } +} + +export default async function RegisterDetailPage({ params }: { params: { hex: string } }) { + const filePath = path.join(process.cwd(), 'data', 'nextreg_records.txt'); + const fileContent = await fs.readFile(filePath, 'utf8'); + const registers = await parseNextReg(fileContent); + + const targetHex = decodeURIComponent((await params).hex).toLowerCase(); + + const register = registers.find(r => r.hex_address.toLowerCase() === targetHex); + + if (!register) return notFound(); + + const defaultActiveKey = register.common ? 'common' : (register.read ? 'read' : (register.write ? 'write' : undefined)); + + return ( +
+
+ ← Back to Registers +
+ +
+ ); +} diff --git a/src/app/registers/page.tsx b/src/app/registers/page.tsx index 07f87ce..e1651f5 100644 --- a/src/app/registers/page.tsx +++ b/src/app/registers/page.tsx @@ -42,6 +42,8 @@ function processRegisterBlock(paragraph: string, registers: Register[]) { name: regName, description: description, notes: [], + text: "", + issue_4_only: false }; const descriptionLines = description.split('\n'); @@ -49,28 +51,40 @@ function processRegisterBlock(paragraph: string, registers: Register[]) { let accessData: RegisterAccess = { operations: [], notes: [] }; for (const line of descriptionLines) { + if(line.includes('Issue 4 Only')) reg.issue_4_only = true; + const trimmedLine = line.trim(); - if (trimmedLine === '(R)') { + + if (trimmedLine.startsWith('//')) continue; + + if (trimmedLine.startsWith('(R)')) { if (currentAccess) reg[currentAccess] = accessData; accessData = { operations: [], notes: [] }; currentAccess = 'read'; continue; } - if (trimmedLine === '(W)') { + if (trimmedLine.startsWith('(W)')) { if (currentAccess) reg[currentAccess] = accessData; accessData = { operations: [], notes: [] }; currentAccess = 'write'; continue; } - if (trimmedLine === '(R/W)') { + if (trimmedLine.startsWith('(R/W')) { if (currentAccess) reg[currentAccess] = accessData; accessData = { operations: [], notes: [] }; currentAccess = 'common'; continue; } + if (line.startsWith(trimmedLine)) { + if (currentAccess) reg[currentAccess] = accessData; + accessData = { operations: [], notes: [] }; + currentAccess = null; + } if (currentAccess) { const bitMatch = trimmedLine.match(/^(bits?|bit)\s+([\d:-]+)\s*=\s*(.*)/); + const valueMatch = !line.match(/^\s+/) && trimmedLine.match(/^([01\s]+)\s*=\s*(.*)/); + if (bitMatch) { let bitDescription = bitMatch[3]; const footnoteMatch = bitDescription.match(/(\*+)$/); @@ -84,6 +98,11 @@ function processRegisterBlock(paragraph: string, registers: Register[]) { description: bitDescription, footnoteRef: footnoteRef, }); + } else if (valueMatch) { + accessData.operations.push({ + bits: valueMatch[1].trim().replace(/\s/g, ''), + description: valueMatch[2].trim(), + }); } else if (trimmedLine.startsWith('*')) { const noteMatch = trimmedLine.match(/^(\*+)\s*(.*)/); if (noteMatch) { @@ -92,10 +111,16 @@ function processRegisterBlock(paragraph: string, registers: Register[]) { text: noteMatch[2], }); } - } else if(trimmedLine) { - if(accessData.operations.length > 0) { - accessData.operations[accessData.operations.length-1].description += `\n${trimmedLine}`; + } else if (trimmedLine) { + if (line.match(/^\s+/) && accessData.operations.length > 0) { + accessData.operations[accessData.operations.length - 1].description += `\n${line}`; + } else { + + if (!accessData.description) { + accessData.description = ''; } + accessData.description += `\n${trimmedLine}`; + } } } else { if (trimmedLine.startsWith('*')) { @@ -107,6 +132,9 @@ function processRegisterBlock(paragraph: string, registers: Register[]) { }); } } + else { + reg.text += `${line}\n`; + } } } if (currentAccess) { @@ -130,18 +158,17 @@ function processRegisterBlock(paragraph: string, registers: Register[]) { } -import { Container } from 'react-bootstrap'; export default async function RegistersPage() { - const filePath = path.join(process.cwd(), 'data', 'nextreg_bare.txt'); + const filePath = path.join(process.cwd(), 'data', 'nextreg_records.txt'); const fileContent = await fs.readFile(filePath, 'utf8'); const registers = await parseNextReg(fileContent); return ( - +

Spectrum Next Registers

- +
); } diff --git a/src/app/registers/types.ts b/src/app/registers/types.ts index 89eb6fb..5ff646a 100644 --- a/src/app/registers/types.ts +++ b/src/app/registers/types.ts @@ -11,10 +11,10 @@ } export interface RegisterAccess { + description?: string; operations: BitwiseOperation[]; notes: Note[]; } - export interface Register { hex_address: string; dec_address: number | string; @@ -23,5 +23,7 @@ read?: RegisterAccess; write?: RegisterAccess; common?: RegisterAccess; + text: string; notes: Note[]; + issue_4_only: boolean; } diff --git a/src/app/utils/parser.ts b/src/app/utils/parser.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/middleware.js b/src/middleware.js new file mode 100644 index 0000000..f77cc69 --- /dev/null +++ b/src/middleware.js @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server' + +export function middleware(request) { + const { method, nextUrl } = request + + // Filter out internal Next.js assets if desired + if (!nextUrl.pathname.startsWith('/_next')) { + console.log(`${method} ${nextUrl.pathname}`) + } + + return NextResponse.next() +} \ No newline at end of file