Skip to content

Commit 4905827

Browse files
committed
Implement state related dynamic styling
1 parent e17f3d1 commit 4905827

9 files changed

+214
-48
lines changed

.eslintrc.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ module.exports = {
1717
'warn',
1818
{ allowConstantExport: true },
1919
],
20+
'react/prop-types': 'off',
2021
},
2122
}

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta charset="UTF-8" />
66
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
77
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8-
<title>CSS Turing Machine</title>
8+
<title>CSS Turing Machine Compiler</title>
99
</head>
1010

1111
<body>

src/App.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import React from 'react';
21
import './App.css';
32
import AppLayout from './AppLayout.jsx';
43
import TuringMachineForm from './TuringMachineForm.jsx';

src/AppLayout.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import React from 'react';
21
import './AppLayout.css';
32

43
export default function AppLayout({ main, footer }) {

src/CompiledMachinePageBody.css

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ p::before {
7070
grid-row: 3;
7171
}
7272

73-
[id*="1BUFFER_SUFFIXHEAD_POS_PREFIX"] {
73+
[id*="BUFFER_PREFIX1HEAD_POS_PREFIX"] {
7474
grid-row: 6;
7575
}
7676

@@ -133,13 +133,13 @@ p+i+i {
133133
}
134134

135135
/* Allow the user to toggle the top row tape cell inputs before starting. */
136-
#STARTED_ID:not(:checked)~[id*="0BUFFER_SUFFIXTAPE_VALUE_PREFIX"] {
136+
#STARTED_ID:not(:checked)~[id*="BUFFER_PREFIX0TAPE_VALUE_PREFIX"] {
137137
display: block;
138138
}
139139

140140
/* Hide display cells/bottom tape head before starting. */
141141
#STARTED_ID:not(:checked)~p,
142-
#STARTED_ID:not(:checked)~[id*="1BUFFER_SUFFIXHEAD_POS_PREFIX"] {
142+
#STARTED_ID:not(:checked)~[id*="BUFFER_PREFIX1HEAD_POS_PREFIX"] {
143143
display: none;
144144
}
145145

@@ -150,14 +150,14 @@ p+i+i {
150150

151151
/* Fade the top elements after starting and the bottom is the destination. */
152152
#STARTED_ID:checked~#BUFFER_SWITCH_ID:not(:checked)~:not(p)+p,
153-
#STARTED_ID:checked~#BUFFER_SWITCH_ID:not(:checked)~[id*="0BUFFER_SUFFIXHEAD_POS_PREFIX"],
153+
#STARTED_ID:checked~#BUFFER_SWITCH_ID:not(:checked)~[id*="BUFFER_PREFIX0HEAD_POS_PREFIX"],
154154
#STARTED_ID:checked~#BUFFER_SWITCH_ID:not(:checked)~p+i {
155155
opacity: 0.5;
156156
}
157157

158158
/* Fade the bottom elements when the top is the destination. */
159159
#BUFFER_SWITCH_ID:checked~p+p,
160-
#BUFFER_SWITCH_ID:checked~[id*="1BUFFER_SUFFIXHEAD_POS_PREFIX"],
160+
#BUFFER_SWITCH_ID:checked~[id*="BUFFER_PREFIX1HEAD_POS_PREFIX"],
161161
#BUFFER_SWITCH_ID:checked~p+i+i {
162162
opacity: 0.5;
163163
}

src/CompiledMachinePageBody.jsx

Lines changed: 121 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import React from 'react';
21
import ReactDOMServer from 'react-dom/server';
32
import AppLayout from './AppLayout.jsx';
43
import TuringMachineStateTable from './TuringMachineStateTable.jsx';
54
import { GITHUB_LINK } from './contants.js';
5+
import { select, id, checked, unchecked, attr, attrContains } from './selector.js';
66
import indexCSS from './index.css?inline';
77
import appLayoutCSS from './AppLayout.css?inline';
88
import turingMachineStateTableCSS from './TuringMachineStateTable.css?inline';
@@ -21,7 +21,8 @@ function CompiledMachinePageBody({ config, numTapeCells }) {
2121
/>
2222
}
2323

24-
const BUFFER_SUFFIX = 'b';
24+
const BUFFER_PREFIX0 = 'a';
25+
const BUFFER_PREFIX1 = 'b';
2526
const STATE_PREFIX = 's';
2627
const HEAD_POS_PREFIX = 'h';
2728
const TAPE_VALUE_PREFIX = 't';
@@ -32,12 +33,10 @@ function CompiledTuringMachine({ config, numTapeCells }) {
3233
// TURING MACHINE FORMAT:
3334
// "Started" radio input
3435
// "Buffer 0 is destination" checkbox input
36+
// State radio inputs (interleaved between buffers)
3537
// Switch buffer label
36-
// Buffer 0 state radio inputs
37-
// Buffer 1 state radio inputs
3838
// TAPE CELLS
39-
// Buffer 0 state labels
40-
// Buffer 1 state labels
39+
// State labels (interleaved between buffers)
4140
// Start label
4241

4342
// TAPE CELL FORMAT:
@@ -52,20 +51,25 @@ function CompiledTuringMachine({ config, numTapeCells }) {
5251
// Buffer 0 tape cell X value display (cannot make checkboxes visible or else user can toggle them via mouse/keyboard)
5352
// Buffer 1 tape cell X value display
5453

55-
const dynElems = [];
54+
const elems = [];
5655
let counter = 0;
5756

57+
elems.push(<input id={STARTED_ID} type="radio" key={counter++} />);
58+
elems.push(<input id={BUFFER_SWITCH_ID} type="checkbox" key={counter++} />);
59+
5860
const numStates = config.length + 1; // User configuration does not include halting state.
5961
// State radio inputs:
60-
for (let buffer = 0; buffer < 2; buffer++) {
61-
for (let stateIdx = 0; stateIdx < numStates; stateIdx++) {
62+
for (let stateIdx = 0; stateIdx < numStates; stateIdx++) {
63+
for (let buffer = 0; buffer < 2; buffer++) {
6264
const inputId = getInputId(buffer, STATE_PREFIX, stateIdx);
6365
const inputGroup = getInputGroup(buffer, STATE_PREFIX);
6466
// Initialize with the Turing machine in the first state.
65-
dynElems.push(<input id={inputId} type="radio" name={inputGroup} defaultChecked={stateIdx === 0} key={counter++} />);
67+
elems.push(<input id={inputId} type="radio" name={inputGroup} defaultChecked={stateIdx === 0} key={counter++} />);
6668
}
6769
}
6870

71+
elems.push(<label htmlFor={BUFFER_SWITCH_ID} key={counter++} />);
72+
6973
// Tape cells:
7074
const halfwayVisibleTape = Math.min(Math.ceil(numTapeCells / 2) - 1, 12);
7175
// There is an extra dummy tape cell to fit the last tape head label.
@@ -77,71 +81,82 @@ function CompiledTuringMachine({ config, numTapeCells }) {
7781
const tapeHeadInputId = getInputId(buffer, HEAD_POS_PREFIX, tapeCellIdx);
7882
const tapeHeadInputGroup = getInputGroup(buffer, HEAD_POS_PREFIX);
7983
// Initialize the head position halfway along the visible tape.
80-
dynElems.push(<input id={tapeHeadInputId} type="radio" name={tapeHeadInputGroup} defaultChecked={tapeCellIdx === halfwayVisibleTape} key={counter++} />);
84+
elems.push(<input id={tapeHeadInputId} type="radio" name={tapeHeadInputGroup} defaultChecked={tapeCellIdx === halfwayVisibleTape} key={counter++} />);
8185
} else {
8286
// Maintain the relative positioning of elements for the last tape head label.
83-
dynElems.push(<i key={counter++} />);
87+
elems.push(<i key={counter++} />);
8488
}
8589
}
8690
// Tape cell value inputs:
8791
for (let buffer = 0; buffer < 2; buffer++) {
8892
if (!isDummyIdx) {
8993
const tapeValInputId = getInputId(buffer, TAPE_VALUE_PREFIX, tapeCellIdx);
90-
dynElems.push(<input id={tapeValInputId} type="checkbox" key={counter++} />);
94+
elems.push(<input id={tapeValInputId} type="checkbox" key={counter++} />);
9195
} else {
9296
// Maintain the relative positioning of elements for the last tape head label.
93-
dynElems.push(<i key={counter++} />);
97+
elems.push(<i key={counter++} />);
9498
}
9599
}
96100
// Tape head labels:
97101
for (let buffer = 0; buffer < 2; buffer++) {
98102
// Tape head labels are offset by 1, the first label is just a dummy element.
99103
if (tapeCellIdx > 0) {
100104
const forTapeHeadInputId = getInputId(buffer, HEAD_POS_PREFIX, tapeCellIdx - 1);
101-
dynElems.push(<label htmlFor={forTapeHeadInputId} key={counter++} />);
105+
elems.push(<label htmlFor={forTapeHeadInputId} key={counter++} />);
102106
} else {
103-
dynElems.push(<i key={counter++} />);
107+
elems.push(<i key={counter++} />);
104108
}
105109
}
106110
if (!isDummyIdx) {
107111
// Tape cell value labels:
108112
for (let buffer = 0; buffer < 2; buffer++) {
109113
const forTapeValInputId = getInputId(buffer, TAPE_VALUE_PREFIX, tapeCellIdx);
110-
dynElems.push(<label htmlFor={forTapeValInputId} key={counter++} />);
114+
elems.push(<label htmlFor={forTapeValInputId} key={counter++} />);
111115
}
112116
// Tape cell value display:
113117
for (let buffer = 0; buffer < 2; buffer++) {
114-
dynElems.push(<p key={counter++} />);
118+
elems.push(<p key={counter++} />);
115119
}
116120
}
117121
}
118122

119123
// State labels:
120-
for (let buffer = 0; buffer < 2; buffer++) {
121-
for (let stateIdx = 0; stateIdx < numStates; stateIdx++) {
124+
for (let stateIdx = 0; stateIdx < numStates; stateIdx++) {
125+
for (let buffer = 0; buffer < 2; buffer++) {
122126
const forInputId = getInputId(buffer, STATE_PREFIX, stateIdx);
123-
dynElems.push(<label htmlFor={forInputId} key={counter++} />)
127+
elems.push(<label htmlFor={forInputId} key={counter++} />)
124128
}
125129
}
130+
131+
elems.push(<label htmlFor={STARTED_ID} key={counter++} />)
132+
126133
return <div className="machine">
127134
<div className="scroll-x">
128135
<div className="machine-grid">
129-
<input id={STARTED_ID} type="radio" />
130-
<input id={BUFFER_SWITCH_ID} type="checkbox" />
131-
<label htmlFor={BUFFER_SWITCH_ID} />
132-
{dynElems}
133-
<label htmlFor={STARTED_ID} />
136+
{elems}
134137
</div>
135138
</div>
136139
</div>;
137140
}
138141

139142
function getInputId(buffer, prefix, idx) {
140-
return `${buffer}${BUFFER_SUFFIX}${prefix}${idx}`;
143+
return `${getBufferPrefix(buffer)}${prefix}${idx}`;
141144
}
142145

143146
function getInputGroup(buffer, prefix) {
144-
return `${buffer}${BUFFER_SUFFIX}${prefix}`;
147+
return `${getBufferPrefix(buffer)}${prefix}`;
148+
}
149+
150+
function getBufferPrefix(buffer) {
151+
return buffer === 0 ? BUFFER_PREFIX0 : BUFFER_PREFIX1;
152+
}
153+
154+
export default function toHTML(config, numTapeCells) {
155+
return '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="utf-8"/>\n<style>\n' +
156+
getCompiledMachinePageStyles(config) +
157+
'\n</style>\n<title>CSS Turing Machine</title>\n</head>\n<body>\n' +
158+
getCompiledMachinePageBody(config, numTapeCells) +
159+
'\n</body>\n</html>';
145160
}
146161

147162
function getCompiledMachinePageBody(config, numTapeCells) {
@@ -155,22 +170,90 @@ function getCompiledMachinePageStyles(config) {
155170
compiledMachinePageBodyCSS
156171
.replaceAll('STARTED_ID', STARTED_ID)
157172
.replaceAll('TAPE_VALUE_PREFIX', TAPE_VALUE_PREFIX)
158-
.replaceAll('BUFFER_SUFFIX', BUFFER_SUFFIX)
173+
.replaceAll('BUFFER_PREFIX0', BUFFER_PREFIX0)
174+
.replaceAll('BUFFER_PREFIX1', BUFFER_PREFIX1)
159175
.replaceAll('HEAD_POS_PREFIX', HEAD_POS_PREFIX)
160176
.replaceAll('BUFFER_SWITCH_ID', BUFFER_SWITCH_ID);
161-
return staticStyles;
177+
162178
// Refer to the project's README for a detailed description of how this is supposed to work.
163-
// addMachineDisplayStyling(sb, config);
179+
const dynStyles = [];
180+
addBufferSwitchLabelStyling(dynStyles, config);
181+
addStateDisplayStyling(dynStyles, config);
182+
addStateLabelStyling(dynStyles, config);
164183
// addToggleLabelStyling(sb, config);
165-
// addStateLabelStyling(sb, config);
166184
// addTapeCellStyling(sb, config);
167185
// addHeadPosStyling(sb, config);
186+
return staticStyles + '\n' + dynStyles.join('\n');
168187
}
169188

170-
export default function toHTML(config, numTapeCells) {
171-
return '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="utf-8"/>\n<style>\n' +
172-
getCompiledMachinePageStyles(config) +
173-
'\n</style>\n<title>CSS Turing Machine</title>\n</head>\n<body>\n' +
174-
getCompiledMachinePageBody(config, numTapeCells) +
175-
'\n</body>\n</html>';
189+
function addBufferSwitchLabelStyling(sb, config) {
190+
// Show the buffer switch label if we're not in the halting state.
191+
const haltingState = config.length;
192+
const b0HaltingStateInputId = getInputId(0, STATE_PREFIX, haltingState);
193+
194+
const showBufferSwitchLabel = select(id(b0HaltingStateInputId).unchecked(), '+', unchecked(), '~', attr('for', BUFFER_SWITCH_ID))
195+
.displayBlock();
196+
sb.push(showBufferSwitchLabel);
197+
}
198+
199+
function addStateDisplayStyling(sb, config) {
200+
const stateNames = getOrderedStateNames(config);
201+
stateNames.forEach((stateName, idx) => {
202+
const b0StateInputId = getInputId(0, STATE_PREFIX, idx);
203+
const b1StateInputId = getInputId(1, STATE_PREFIX, idx);
204+
205+
const displayStateNameTop = select(id(b0StateInputId).checked(), '~', 'p', '+', 'i::after')
206+
.content(stateName);
207+
const displayStateNameBottom = select(id(STARTED_ID).checked(), '~', id(b1StateInputId).checked(), '~', 'p', '+', 'i', '+', 'i::after')
208+
.content(stateName);
209+
210+
sb.push(displayStateNameTop);
211+
sb.push(displayStateNameBottom);
212+
});
213+
}
214+
215+
function addStateLabelStyling(sb, config) {
216+
const stateNames = getOrderedStateNames(config);
217+
for (let source = 0; source < 2; source++) {
218+
const dest = 1 - source;
219+
const matchesSourceSelector = source === 0 ? id(BUFFER_SWITCH_ID).unchecked() : id(BUFFER_SWITCH_ID).checked();
220+
221+
// Outer loop represents the destination state for which we are computing the visibility of its label.
222+
stateNames.forEach((stateName, idx) => {
223+
const destStateInputId = getInputId(dest, STATE_PREFIX, idx);
224+
const destStateLabelSelector = attr('for', destStateInputId);
225+
const destStateUncheckedSelector = id(destStateInputId).unchecked();
226+
227+
const currentHeadSelector = attrContains('id', getBufferPrefix(source) + HEAD_POS_PREFIX).checked();
228+
229+
// Inner loop represents each possible value of the current state.
230+
// No rules necessary for case that current state is halting state (cannot transition to any other state).
231+
config.forEach((currentState, currentStateIdx) => {
232+
const currentStateInputId = getInputId(source, STATE_PREFIX, currentStateIdx);
233+
const currentStateCheckedSelector = id(currentStateInputId).checked();
234+
// currentStateCheckedSelector/destStateUncheckedSelector need to be ordered when creating the selector based on DOM layout.
235+
const [first, second] = [currentStateIdx, source] < [idx, dest] ?
236+
[currentStateCheckedSelector, destStateUncheckedSelector] :
237+
[destStateUncheckedSelector, currentStateCheckedSelector];
238+
239+
if (currentState[1].next === stateName) {
240+
const displayNextStateFor1 = select(matchesSourceSelector, '~', first, '~', second, '~', currentHeadSelector, '+', '*', '+', checked(), '~', destStateLabelSelector)
241+
.displayBlock();
242+
sb.push(displayNextStateFor1);
243+
}
244+
245+
if (currentState[0].next === stateName) {
246+
const displayNextStateFor0 = select(matchesSourceSelector, '~', first, '~', second, '~', currentHeadSelector, '+', '*', '+', unchecked(), '~', destStateLabelSelector)
247+
.displayBlock();
248+
sb.push(displayNextStateFor0);
249+
}
250+
});
251+
});
252+
}
253+
}
254+
255+
function getOrderedStateNames(config) {
256+
const stateNames = config.map(s => s.name);
257+
stateNames.push('HALT');
258+
return stateNames;
176259
}

src/ShareableHtmlLink.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import { useState } from 'react';
22

33
export default function ShareableHtmlLink({ html }) {
44
if (html === null) {

src/TuringMachineForm.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import { useState } from 'react';
22
import './TuringMachineForm.css';
33
import TuringMachineStateTable from './TuringMachineStateTable.jsx'
44
import ShareableHtmlLink from './ShareableHtmlLink.jsx';

0 commit comments

Comments
 (0)