Step By Step Code Example Prototype 8
Overview
This is a prototype for a step-by-step process to demonstrate code in a tutorial I'm working on called Typing Rust. The goal is to put descriptions for each line instead of showing a chunk of code and a multi-paragraph description separately. It starts out with the full code sample then goes through each step independently.
I need to dig into accessibility, but I'm pleased with the progress so far.
Example
Step By Step
01
02
03
04
05
06
07
08
09
10
11
12
13
use std::env;
fn main() {
let alfa = env::var("HOME");
match alfa {
Ok(item) => {
println!("got {}", item);
}
Err(error) => {
println!("error {}", error);
}
}
}
out
This is the full source code example.
Click through the buttons below to explinations of for each part of the code.
TODO
- Figure out accessibility
- Add cookie to keep place
- Add arrow key navigation
- Make a web component
HTML Source
<h3 id="step-by-step">Step By Step</h3>
CSS Source
:root {
--active-button-color: #381;
--base-padding: 1rem;
--border-color-1: #ccc;
--border-color-2: #556;
--border-radius: 0.8rem;
--box-shadow-color: #0008;
--code-background-color: #000;
--font-size: 1rem;
--example-color: #234;
--font-color-1: #888;
--font-color-2: #ccc;
--line-height: 1.3rem;
--lines-left-pad: 2ch;
--line-numbers-background-color: #000;
--notes-background-color: #000;
--notes-font-color: #bbb;
--output-color: black;
--output-font-color: #ccc;
}
pre {
font-size: var(--font-size);
}
#stepByStepButtonWrapper {
padding-top: var(--base-padding);
padding-left: 9ch;
}
#stepByStepCodeLines {
background-color: var(--code-background-color);
border-top-right-radius: var(--border-radius);
color: var(--font-color-1);
padding-bottom: calc(var(--line-height) * 2);
padding-left: var(--lines-left-pad);
padding-top: var(--base-padding);
}
#stepByStepLineNumbers {
background-color: var(--line-numbers-background-color);
border-top-left-radius: var(--border-radius);
color: var(--font-color-1);
padding-top: var(--base-padding);
padding-left: var(--base-padding);
padding-bottom: var(--base-padding);
}
#stepByStepNotes {
font-family: sans-serif;
font-size: var(--font-size);
color: var(--font-color-2);
font-weight: 500;
}
#stepByStepNotes p:nth-child(n + 2) {
margin-top: 0.8rem;
}
#stepByStepNotesSpacer {
background-color: var(--notes-background-color);
color: var(--notes-font-color);
font-family: monospace;
font-size: var(--font-size);
margin-top: var(--base-padding);
position: absolute;
padding-left: 1ch;
padding-right: 1ch;
padding-top: 1ch;
padding-bottom: 1ch;
border: 1px solid var(--border-color-1);
border-radius: 0.4rem;
box-shadow: 2px 2px 4px black;
}
#stepByStepOutputLines {
background-color: var(--output-color);
border-bottom-right-radius: var(--border-radius);
border-top: 1px solid var(--border-color-2);
color: var(--output-font-color);
padding-bottom: var(--base-padding);
padding-left: var(--lines-left-pad);
padding-top: var(--base-padding);
}
#stepByStepOutputNumbers {
color: var(--font-color-1);
background-color: var(--line-numbers-background-color);
border-bottom-left-radius: var(--border-radius);
border-top: 1px solid var(--border-color-2);
font-family: monospace;
font-size: var(--font-size);
padding-bottom: var(--base-padding);
padding-left: var(--base-padding);
padding-top: var(--base-padding);
}
#stepByStepOutputPointers {
background-color: var(--line-numbers-background-color);
padding-top: var(--base-padding);
padding-bottom: var(--base-padding);
border-right: 1px solid var(--border-color-2);
border-top: 1px solid var(--border-color-2);
}
#stepByStepPointers {
background-color: var(--line-numbers-background-color);
padding-top: var(--base-padding);
padding-bottom: var(--base-padding);
border-right: 1px solid var(--border-color-2);
}
#stepByStepWrapper {
position: relative;
font-family: monospace;
font-size: var(--font-size);
margin: 0;
padding: 0;
display: grid;
grid-template-columns: 6ch 2ch 1fr;
line-height: var(--line-height);
border-radius: var(--border-radius);
border: 1px solid var(--border-color-2);
/*
box-shadow: 0.1rem 0.1rem 2.1px var(--box-shadow-color);
*/
}
.activeButton {
background-color: var(--active-button-color);
}
.h1 {
color: #22a2c9;
}
.h2 {
color: #ac9739;
}
.newLine {
color: #22a2c9;
}
.pad-top {
padding-top: 1rem;
}
.stepByStepButton {
border: 1px solid black;
border-radius: 0.3rem;
margin-right: 0.3rem;
box-shadow: 0.1rem 0.1rem 2.1px var(--box-shadow-color);
}
JavaScript Source
const c = {
source: `use std::env;
fn main() {
let alfa = env::var("HOME");
match alfa {
Ok(item) => {
println!("got {}", item);
}
Err(error) => {
println!("error {}", error);
}
}
}`,
// Usage (until it's better documented)
// highlights: ['h1|3|4|7'],
// fullCode: true,
// altLines: [
// {
// line: 5,
// text: 'the quick brown fox jumps over the lazy dog',
// },
// ],
// NOTE: Only one hightlight works per line right now
sets: [
{
fullCode: true,
coords: [1, 38, 30, 13],
notes: `<p>This is the full source code example.</p><p>Click through the buttons below to explinations of for each part of the code.</p>`,
},
{
addLines: [1],
coords: [3, 10, 30],
notes: `<p>Start by loading <code>std::env</code> which provides Rust programs with access to the Environmental Variables it runs in</p>`,
},
{
addLines: [3, 13],
coords: [5, 10, 30],
notes: `<p>Create the <code>main</code> function that Rust uses as the entry point for the program</p>`,
},
{
addLines: [4],
coords: [6, 10, 30],
notes: `<p>Create a new immutalbe variable called <code>alfa</code> and bind the value returned by <code>env::var("HOME")</code> to it. That value is a <code>Result</code></p>`,
},
{
addLines: [5, 12],
coords: [7, 10, 30],
notes: `<p>Begin creating the <code>match</code> expression that we'll use to process the <code>Result</code> value that was returned from <code>env::var("HOME")</code></p>`,
},
{
highlights: ['h2, 4, 7, 11', 'h1, 5, 9, 13'],
coords: [7, 10, 30],
notes: `<p>Note that the value <code>match</code> is working on come from the <code>alfa</code> variable</p><p>TODO: See if match transfers ownership</p>`,
},
{
addLines: [6, 8],
coords: [8, 10, 30],
notes: `<p><code>Result</code> values are <code>enums</code> that can contain either an <code>Ok</code> or a <code>Err</code> value. Here we're creating the first arm of the <code>match</code> expression that handles an <code>Ok</code></p>`,
},
{
addLines: [7],
coords: [9, 10, 30],
notes: `<p>When the <code>Result</code> value in <code>alfa</code> contains an <code>Ok</code> the code inside its code block gets executed. In this case we're printing out the value that got passed in via <code>Ok</code></p>`,
},
{
highlights: ['h2, 6, 8, 11', 'h1, 7, 26, 29'],
coords: [9, 10, 30],
notes: `<p>Note the <code>item</code> value we're getting came packeged with the <code>Ok</code> from <code>Result</code></p>`,
},
{
addLines: [9, 11],
coords: [11, 10, 30],
notes: `<p>Next we create the <code>Err</code> arm for the match expression</p>`,
},
{
addLines: [10],
coords: [12, 10, 38],
notes: `<p>Finally we add the code to run if the <code>Result</code> from <code>env::var("HOME")</code> is an <code>Err</code></p>`,
},
{
fullCode: true,
coords: [1, 38, 30, 13],
notes: `<p>Put togehter, the full program looks like this.</p><p>Note: The output for this prototype contain two hard coded lines. The real version will just have one from the actual program</p>`,
},
],
output: ['got /Users/alan', 'this is a stub to check two lines of output'],
}
const s = {}
const addAltLines = () => {
const altData = c.sets[s.currentSet].altLines
if (altData) {
for (let i = 0; i < altData.length; i++) {
s.currentLines[altData[i].line - 1] = altData[i].text
}
}
}
const addCustomHighlights = () => {
// This overwrites any new line highlights
// so specific things can be pointed out
// with alt lines in previous steps
const highlightData = c.sets[s.currentSet].highlights
if (highlightData) {
for (let i = 0; i < highlightData.length; i++) {
const parts = highlightData[i].split(',')
const className = parts[0]
const lineNum = parseInt(parts[1]) - 1
const startChar = parseInt(parts[2]) - 1
const stopChar = parseInt(parts[3])
const sections = [
s.rawLines[lineNum].substring(0, startChar),
`<code class="${className}">`,
s.rawLines[lineNum].substring(startChar, stopChar),
`</code>`,
s.rawLines[lineNum].substring(stopChar),
]
s.currentLines[lineNum] = sections.join('')
// Also update the pointer
window[`stepByStepPointer_${lineNum}`].innerHTML = '‣'
}
}
}
const buildFoundationStructure = () => {
// This is done outside of `makeElement`
// since it needs to be added after the
// other element
const stepByStepWrapperElement = document.createElement('div')
stepByStepWrapperElement.id = 'stepByStepWrapper'
window['step-by-step'].insertAdjacentElement(
'afterend',
stepByStepWrapperElement
)
makeElement('div', 'stepByStepLineNumbers', '', 'stepByStepWrapper')
makeElement('div', 'stepByStepPointers', '', 'stepByStepWrapper')
makeElement('div', 'stepByStepCodeLines', '', 'stepByStepWrapper')
makeElement('div', 'stepByStepOutputNumbers', '', 'stepByStepWrapper')
makeElement('div', 'stepByStepOutputPointers', '', 'stepByStepWrapper')
makeElement('div', 'stepByStepOutputLines', '', 'stepByStepWrapper')
makeElement('div', 'stepByStepNotesSpacer', '', 'stepByStepWrapper')
makeElement('div', 'stepByStepNotes', '', 'stepByStepNotesSpacer')
// TODO Move this under the StepByStepWrapper, probably
const stepByStepButtonWrapperElement = document.createElement('div')
stepByStepButtonWrapperElement.id = 'stepByStepButtonWrapper'
window.stepByStepWrapper.insertAdjacentElement(
'afterend',
stepByStepButtonWrapperElement
)
}
const handleNextButtonClick = () => {
if (s.currentSet < c.sets.length - 1) {
updateEverything(s.currentSet + 1)
}
}
const handleNumberButtonClick = (event) => {
const newIndex = parseInt(event.target.id.split('_')[1])
updateEverything(newIndex)
}
const handlePreviousButtonClick = () => {
if (s.currentSet > 0) {
updateEverything(s.currentSet - 1)
}
}
const highlightNewLines = () => {
const lineCheck = c.sets[s.currentSet].addLines
if (lineCheck) {
for (let i = 0; i < lineCheck.length; i++) {
const lineIndex = lineCheck[i]
s.currentLines[
lineIndex
] = `<code class="newLine">${s.rawLines[lineIndex]}</code>`
}
}
}
const loadInitialLines = () => {
for (let setIndex = 0; setIndex <= s.currentSet; setIndex++) {
const lineSet = c.sets[setIndex].addLines
if (lineSet) {
for (let addIndex = 0; addIndex < lineSet.length; addIndex++) {
const lineIndex = c.sets[setIndex].addLines[addIndex]
s.currentLines[lineIndex] = s.rawLines[lineIndex]
}
}
}
}
const loadRawLines = () => {
s.rawLines = c.source.split('\n')
}
const makeCodeLines = () => {
for (let i = 0; i < totalLines(); i++) {
makeElement(
'pre',
`stepByStepCodeLine_${i}`,
` `,
'stepByStepCodeLines',
null,
null
)
}
}
const makeElement = (
_type,
_id,
_html,
_childOf,
_event,
_function,
_classes
) => {
const newElement = document.createElement(_type)
newElement.id = _id
newElement.innerHTML = _html
window[_childOf].appendChild(newElement)
if (_event !== null) {
newElement.addEventListener(_event, _function)
}
if (_classes) {
newElement.classList.add(_classes)
}
}
const makeAddLineNumbersZeroBased = () => {
// Moves config numbers from human readable to
// zero based index
for (let setsIndex = 0; setsIndex < c.sets.length; setsIndex++) {
const addData = c.sets[setsIndex].addLines
if (addData) {
for (let addIndex = 0; addIndex < addData.length; addIndex++) {
addData[addIndex] -= 1
}
}
}
}
const makeLineNumberRows = () => {
for (let i = 0; i < totalLines(); i++) {
const numberString = i < 9 ? ` 0${i + 1}` : ` ${i + 1}`
makeElement(
'pre',
`stepByStepLineNumber_${i}`,
numberString,
'stepByStepLineNumbers',
null,
null
)
}
}
const makeNextButton = () => {
makeElement(
'button',
'stepByStepNextButton',
'->',
'stepByStepButtonWrapper',
'click',
handleNextButtonClick,
'stepByStepButton'
)
}
const makeNumberButtons = () => {
for (let i = 0; i < c.sets.length; i++) {
let buttonText = i
if (i === 0) {
buttonText = 'Start'
} else if (i === c.sets.length - 1) {
buttonText = 'Complete'
}
makeElement(
'button',
`stepByStepNumberButton_${i}`,
buttonText,
'stepByStepButtonWrapper',
'click',
handleNumberButtonClick,
'stepByStepButton'
)
}
}
const makeOutputLineNumbers = () => {
for (let i = 0; i < c.output.length; i++) {
const theText = i === 0 ? 'out' : ' '
makeElement(
'pre',
`stepByStepOutputLineNumber_${i}`,
theText,
'stepByStepOutputNumbers',
null,
null
)
}
}
const makeOutputLines = () => {
for (let i = 0; i < c.output.length; i++) {
makeElement(
'pre',
`stepByStepOutputLine_${i}`,
' ',
'stepByStepOutputLines',
null,
null
)
}
}
const makeOutputLinePointers = () => {
for (let i = 0; i < c.output.length; i++) {
const theText = i === 0 ? ' ' : ' '
makeElement(
'pre',
`stepByStepOutputPointer_${i}`,
theText,
'stepByStepOutputPointers',
null,
null
)
}
}
const makePointerRows = () => {
for (let i = 0; i < totalLines(); i++) {
makeElement(
'pre',
`stepByStepPointer_${i}`,
` `,
'stepByStepPointers',
null,
null
)
}
}
const makePreviousButton = () => {
makeElement(
'button',
'stepByStepPreviousButton',
'<-',
'stepByStepButtonWrapper',
'click',
handlePreviousButtonClick,
'stepByStepButton'
)
}
const prepCurrentLines = () => {
s.currentLines = []
for (let i = 0; i < s.rawLines.length; i++) {
s.currentLines.push(' ')
}
}
const totalLines = () => {
return s.rawLines.length
}
const updateButtonHighlights = () => {
for (let i = 0; i < c.sets.length; i++) {
if (i === s.currentSet) {
window[`stepByStepNumberButton_${i}`].classList.add('activeButton')
} else {
window[`stepByStepNumberButton_${i}`].classList.remove('activeButton')
}
}
}
const updateCodeLines = () => {
if (c.sets[s.currentSet].fullCode === true) {
for (let i = 0; i < totalLines(); i++) {
window[`stepByStepCodeLine_${i}`].innerHTML = s.rawLines[i]
}
} else {
for (let i = 0; i < totalLines(); i++) {
window[`stepByStepCodeLine_${i}`].innerHTML = s.currentLines[i]
}
}
}
const updateEverything = (setIndex) => {
s.currentSet = setIndex
prepCurrentLines()
loadInitialLines()
highlightNewLines()
addAltLines()
updatePointers()
addCustomHighlights()
updateCodeLines()
updateOutputLines()
updateButtonHighlights()
updateFullHighlights()
updatePositions()
updateNotes()
}
const updateFullHighlights = () => {
for (let i = 0; i < totalLines(); i++) {
if (s.currentSet === c.sets.length - 1 || s.currentSet === 0) {
window[`stepByStepCodeLine_${i}`].classList.add('hljs')
window[`stepByStepCodeLine_${i}`].classList.add('language-rust')
hljs.highlightElement(window[`stepByStepCodeLine_${i}`])
} else {
window[`stepByStepCodeLine_${i}`].classList.remove('hljs')
window[`stepByStepCodeLine_${i}`].classList.remove('language-rust')
}
}
}
const updateHeader = () => {
let headerString = `Step ${s.currentSet}`
if (s.currentSet === 0) {
headerString = `Full Code Sample`
} else if (s.currentSet === s.sets.length - 1) {
headerString = `Final Code Sample`
}
window.stepByStepHeader.innerHTML = headerString
}
const updateNotes = () => {
window.stepByStepNotes.innerHTML = c.sets[s.currentSet].notes
}
const updateOutputLines = () => {
for (let i = 0; i < c.output.length; i++) {
if (s.currentSet === c.sets.length - 1) {
window[`stepByStepOutputLine_${i}`].innerHTML = c.output[i]
} else {
// clear output for moving to previous line sets
window[`stepByStepOutputLine_${i}`].innerHTML = ' '
}
}
}
const updatePointers = () => {
// clear the lines then add in the ones that need it
for (let i = 0; i < totalLines(); i++) {
window[`stepByStepPointer_${i}`].innerHTML = ' '
}
const addData = c.sets[s.currentSet].addLines
if (addData) {
for (let i = 0; i < addData.length; i++) {
window[`stepByStepPointer_${addData[i]}`].innerHTML = '‣'
}
}
}
const updatePositions = () => {
const coords = c.sets[s.currentSet].coords
const theLeft = coords[1] + 9
// this is a bit gross, but it pulls the line
// height in pixels then remove the `px` and
// converts the value into an it that gets used
// as the multiplier for the value coming in
// from the config
const heightMultiplier = parseInt(
window
.getComputedStyle(window.stepByStepCodeLines)
.lineHeight.split('px')[0]
)
const theTop = (coords[0] - 1) * heightMultiplier
window.stepByStepNotesSpacer.style.top = `${theTop}px`
window.stepByStepNotesSpacer.style.left = `${theLeft}ch`
window.stepByStepNotesSpacer.style.width = `${coords[2]}ch`
// set the height if one was passed
if (coords[3]) {
const theHeight = coords[3] * heightMultiplier
window.stepByStepNotesSpacer.style.height = `${theHeight}px`
} else {
window.stepByStepNotesSpacer.style.height = null
}
}
const init = () => {
s.currentSet = 0
makeAddLineNumbersZeroBased()
loadRawLines()
buildFoundationStructure()
makePreviousButton()
makeNumberButtons()
makeNextButton()
makeLineNumberRows()
makeCodeLines()
makePointerRows()
makeOutputLines()
makeOutputLineNumbers()
makeOutputLinePointers()
updateEverything(0)
}
document.addEventListener('DOMContentLoaded', init)