Skip to content

Commit

Permalink
Created LaTeX keyboard and added quantified translations to Set J.
Browse files Browse the repository at this point in the history
  • Loading branch information
malikpiara committed Jan 20, 2025
1 parent b073d4a commit 9d2155c
Show file tree
Hide file tree
Showing 10 changed files with 648 additions and 4 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Malik Piara
Copyright (c) 2025 Malik Piara

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
294 changes: 294 additions & 0 deletions app/keyboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
'use client';

import { useState, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import KatexSpan from '@/components/katexSpan';
import { ClipboardCopy } from 'lucide-react';
import { toast } from 'sonner';

/** Shorthand replacements (each gets a trailing space). */
const SHORTHAND_REPLACEMENTS = [
{ find: '~', replaceWith: '\\sim ' },
{ find: ',n', replaceWith: '\\square ' },
{ find: ',p', replaceWith: '\\lozenge ' },
{ find: ',.', replaceWith: '\\cdot ' },
{ find: ',v', replaceWith: '\\vee ' },
{ find: ',i', replaceWith: '\\supset ' },
{ find: ',eq', replaceWith: '\\equiv ' },
{ find: ',ex', replaceWith: '\\exists ' },
];

/** KaTeX button definitions. */
const LATEX_BUTTONS = [
{ command: '\\sim', label: 'NOT', preview: '$ \\sim $' },
{ command: '\\square', label: 'NECESSARY', preview: '$ \\square $' },
{ command: '\\lozenge', label: 'POSSIBLE', preview: '$ \\lozenge $' },
{ command: '\\cdot', label: 'AND', preview: '$ \\cdot $' },
{ command: '\\vee', label: 'OR', preview: '$ \\vee $' },
{ command: '\\supset', label: 'IMPLIES', preview: '$ \\supset $' },
{ command: '\\equiv', label: 'EQUIV', preview: '$ \\equiv $' },
{ command: '\\exists', label: 'EXISTS', preview: '$ \\exists $' },
];

/**
* Replaces all shorthands in `text` from left to right,
* adjusting `cursorPos` if a match occurs before the old cursor.
*/
function replaceShorthandsWithCursor(
text: string,
cursorPos: number
): { newText: string; newCursor: number } {
let newText = '';
let currentIndex = 0;
let newCursor = cursorPos;

while (currentIndex < text.length) {
// Among all shorthand patterns, find the earliest match after currentIndex.
let earliestMatchIndex = -1;
let matched = null as (typeof SHORTHAND_REPLACEMENTS)[number] | null;

for (const s of SHORTHAND_REPLACEMENTS) {
const foundIndex = text.indexOf(s.find, currentIndex);
if (foundIndex !== -1) {
if (earliestMatchIndex === -1 || foundIndex < earliestMatchIndex) {
earliestMatchIndex = foundIndex;
matched = s;
}
}
}

if (earliestMatchIndex === -1 || !matched) {
// No more matches; append the rest
newText += text.slice(currentIndex);
break;
} else {
// Copy everything up to earliestMatchIndex
newText += text.slice(currentIndex, earliestMatchIndex);

// Insert the replacement text
const { find, replaceWith } = matched;
newText += replaceWith;

// If the match is before the old cursor, shift cursor by the difference
if (earliestMatchIndex < cursorPos) {
const diff = replaceWith.length - find.length;
newCursor += diff;
}

// Move currentIndex past the matched text
currentIndex = earliestMatchIndex + matched.find.length;
}
}

return { newText, newCursor };
}

export default function LabelGeneratorPage() {
const [input, setInput] = useState('');
const textareaRef = useRef<HTMLTextAreaElement>(null);

/**
* Auto-closing parentheses. If user presses '(',
* wrap the selected text or insert "()".
*/
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === '(') {
e.preventDefault();

const target = e.currentTarget;
const start = target.selectionStart;
const end = target.selectionEnd;

const oldValue = target.value;
const selectedText = oldValue.slice(start, end);

// Insert "(selectedText)"
const newValue =
oldValue.slice(0, start) +
'(' +
selectedText +
')' +
oldValue.slice(end);

// Figure out new cursor position
let newCursorPos = start + 1;
if (start !== end) {
// If text was selected, place cursor after ')'
newCursorPos = end + 2;
}

setAndReplaceShorthands(newValue, newCursorPos);
}
}

/**
* Called onChange for typed text. We replace any
* typed shorthands while preserving the cursor.
*/
function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
if (!textareaRef.current) {
setInput(e.target.value);
return;
}
const newValue = e.target.value;
const cursorPos = textareaRef.current.selectionStart;
setAndReplaceShorthands(newValue, cursorPos);
}

/**
* Insert the LaTeX command at the current cursor or over the selection,
* then run the same logic that processes shorthands + preserves cursor.
*/
function insertLatex(latex: string) {
if (!textareaRef.current) return;

const oldVal = input;
const start = textareaRef.current.selectionStart;
const end = textareaRef.current.selectionEnd;

// Insert "latex + " " at the cursor/selection
const newVal = oldVal.slice(0, start) + latex + ' ' + oldVal.slice(end);
const newCursor = start + latex.length + 1; // after inserted command + 1 space

setAndReplaceShorthands(newVal, newCursor);
}

/**
* Utility: replace shorthands in the given text, then set input + cursor.
*/
function setAndReplaceShorthands(newValue: string, cursorPos: number) {
const { newText, newCursor } = replaceShorthandsWithCursor(
newValue,
cursorPos
);
setInput(newText);

// Re-focus and set selection
requestAnimationFrame(() => {
if (textareaRef.current) {
textareaRef.current.focus();
textareaRef.current.selectionStart = newCursor;
textareaRef.current.selectionEnd = newCursor;
}
});
}

/**
* Copy everything to the clipboard (original).
*/
function copyToClipboard() {
// Convert every single '\' to a double '\\'
const doubledBackslashes = input.replace(/\\/g, '\\\\');
navigator.clipboard.writeText(doubledBackslashes).then(() => {
toast('Copied to clipboard!');
});
}

/**
* 2nd copy button: Transform each line into
* { id: <i>, label: '$ <line> $' },
* and copy the resulting snippet.
*/
function copyAsJsonFormat() {
// 1) Convert single backslashes to double backslashes
const doubled = input.replace(/\\/g, '\\\\');
// 2) Split by newlines
const lines = doubled.split(/\r?\n/);

// 3) Transform each line into the desired snippet
const snippet = lines
.map((line, i) => {
const trimmed = line.trim();
return `{ id: ${i}, label: '$ ${trimmed} $' },`;
})
.join('\n');

navigator.clipboard.writeText(snippet).then(() => {
toast('Copied JSON format!');
});
}

// For preview: split into lines so each line is its own KaTeX block
const previewLines = input.split(/\r?\n/);

return (
<div className='container mx-auto p-4'>
<h1 className='text-3xl font-bold mb-6 text-center font-stretch'>
LaTeX Keyboard
</h1>

{/* Two-column grid */}
<div className='grid md:grid-cols-2 gap-6'>
{/* Left card: user input */}
<Card>
<CardHeader>
<CardTitle>Enter Your Expression</CardTitle>
</CardHeader>
<CardContent className='space-y-4'>
<Textarea
ref={textareaRef}
value={input}
onKeyDown={handleKeyDown} // auto-closing parens
onChange={handleChange} // typed shorthands
placeholder="Type text or click buttons. Press '(' to auto-close."
className='w-full h-40'
/>

<div className='flex flex-wrap gap-2'>
{LATEX_BUTTONS.map(({ command, label, preview }) => (
<Button
key={command}
variant='secondary'
onClick={() => insertLatex(command)}
>
<div className='flex items-center space-x-3'>
<KatexSpan text={preview} />
<span className='tracking-wide'>{label}</span>
</div>
</Button>
))}
</div>

<div className='flex flex-col gap-2'>
{/* 1) Original copy button */}
<Button
variant='outline'
onClick={copyToClipboard}
className='w-full'
>
<ClipboardCopy className='mr-2 h-4 w-4' />
Copy Everything
</Button>

{/* 2) New copy button with JSON format */}
<Button
variant='outline'
onClick={copyAsJsonFormat}
className='w-full'
>
<ClipboardCopy className='mr-2 h-4 w-4' />
Copy JSON
</Button>
</div>
</CardContent>
</Card>

{/* Right card: KaTeX preview */}
<Card>
<CardHeader>
<CardTitle>Preview</CardTitle>
</CardHeader>
<CardContent>
<div className='bg-gray-100 dark:bg-gray-800 p-4 rounded-md overflow-x-auto space-y-2'>
{previewLines.map((line, idx) => (
<KatexSpan key={idx} text={`$${line}$`} />
))}
</div>
</CardContent>
</Card>
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CSPostHogProvider } from '@/components/providers';
import thumbnail from '../public/thumbnail.jpg';
import ExerciseNavbar from '@/components/mobile/exerciseNavbar';
import RegisterSW from '@/components/providers/service-worker';
import { Toaster } from '@/components/ui/sonner';

const robotoFlex = Roboto_Flex({
subsets: ['latin'],
Expand Down Expand Up @@ -73,6 +74,7 @@ export default function RootLayout({

<Navbar />
<main className={`flex`}>{children}</main>
<Toaster />
<RegisterSW />
<Footer />
</body>
Expand Down
17 changes: 14 additions & 3 deletions components/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,25 @@ export function Footer() {
Classic Logicola
</Link>
</li>
<li>
<li className='mb-4'>
<Link
href='https://www.routledge.com/Introduction-to-Logic/Gensler/p/book/9781138910591'
className='hover:underline'
>
Get the Book
</Link>
</li>
<li>
<Link
href='/keyboard'
className='hover:underline flex gap-2'
>
Keyboard{' '}
<div className='bg-primaryColor text-white text-xs font-semibold me-5 px-2 py-1 rounded-full'>
New
</div>
</Link>
</li>
</ul>
</div>
<div>
Expand Down Expand Up @@ -98,15 +109,15 @@ export function Footer() {
<hr className='my-6 border-gray-200 sm:mx-auto lg:my-8' />
<div className='sm:flex sm:items-center sm:justify-between'>
<span className='text-sm text-gray-500 sm:text-center '>
© 2024{' '}
© 2025{' '}
<Link href='https://logicola.com' className='hover:underline'>
Logicola
</Link>
. Some Rights Reserved.
</span>
<div className='flex mt-4 space-x-6 sm:justify-center sm:mt-0'>
<Link
href='https://twitter.com/LogicolaWeb'
href='https://twitter.com/LogicolaThree'
className='text-gray-500 hover:text-gray-900'
>
<svg
Expand Down
Loading

1 comment on commit 9d2155c

@vercel
Copy link

@vercel vercel bot commented on 9d2155c Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.