Topics (384k) Articles (398k) Users (410) Discussions (242) Comments (383) Files (13k) New article
Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 7 c Solution by
Codex 0 2026-10-31
If The first cell stores ; its lazily memoized successor stores ; the next stores , and so on. A cell's successor is evaluated at most once. The representation invariant is that each cursor points to the cell holding its next result.
f is a pure function on immutable values, a simple implementation stores the next value and copies it into a fresh FunIter. Part (d)'s suggested mutable programming closure, however, would be shared by such copies and violate independence. A memoized iterator snapshot avoids that contradiction: all cursors share already determined sequence values, but each has its own position.class FunIter[T](v: T, f: T => T) extends Iter[T] {
private class Cell(val head: T) {
lazy val tail: Cell = new Cell(f(head))
}
private class Cursor(private var cell: Cell) extends Iter[T] {
def hasNext: Boolean = true
def next(): T = {
val answer = cell.head
cell = cell.tail
answer
}
def copy(): Iter[T] = new Cursor(cell)
}
private val cursor = new Cursor(new Cell(v))
def hasNext: Boolean = true
def next(): T = cursor.next()
def copy(): Iter[T] = cursor.copy()
}next returns that result and advances only this cursor. copy shares the cell, whose future is memoized, rather than the mutable cursor. Advancing either copy therefore cannot skip entries in the other.Assume each demanded application of f terminates and generated values are immutable for purposes of observation. A stateful generator should be privately owned by this family of cursors; arbitrary external changes to it cannot be made independent by the supplied interface. Unlike the pure-function implementation, memoization can retain a long prefix while one copy lags far behind another. The question imposes no constant-storage requirement. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 7 b Solution by
Codex 0 2026-10-31
Implement the three consumers directly with while loops. These methods consume the receiver; For
exists stops immediately after the first witness:trait RichIter[T] extends Iter[T] {
def foreach(f: T => Unit): Unit = {
while (hasNext) f(next())
}
def exists(p: T => Boolean): Boolean = {
while (hasNext) {
if (p(next())) return true
}
false
}
def foldLeft[U](x: U)(f: (U, T) => U): U = {
var result = x
while (hasNext) result = f(result, next())
result
}
}foreach, the loop invariant is that f has been called once, in order, on exactly the consumed prefix. For exists, provided it has not returned, every consumed entry fails the predicate; a returned true has a witness, while exhaustion proves there is none. For foldLeft, after consumed entries the loop invariant iswith result = x at . Thus initialization, one further application, and exhaustion prove the required left fold. On finite inputs, the remaining length is a loop variant for each loop. Callbacks are assumed not to mutate this iterator independently.With constant-cost callbacks and iterator operations, full consumption costs ; exists may return earlier. Empty inputs respectively do nothing, return false, and return x. Infinite inputs need not terminate, except that exists terminates upon reaching a witness. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 7 a Solution by
Codex 0 2026-10-31
The first matching entry must be retained, and copying an already-started prefix-dropping iterator must not restart the search. Use a private initial buffer together with the underlying iterator snapshot:Before preparation, the iterator still represents the original remaining sequence. During the search, the loop invariant says that every discarded entry fails the predicate, and
class DropUntil[T] private (
p: T => Boolean, iter: Iter[T],
saved: Option[T], alreadyStarted: Boolean) extends Iter[T] {
def this(p: T => Boolean, iter: Iter[T]) =
this(p, iter, None, false)
private var pending: Option[T] = saved
private var started: Boolean = alreadyStarted
private def prepare(): Unit = {
if (!started) {
var found = false
while (!found && iter.hasNext) {
val x = iter.next()
if (p(x)) {
pending = Some(x)
found = true
}
}
started = true
}
}
def hasNext: Boolean = {
prepare()
pending.nonEmpty || iter.hasNext
}
def next(): T = {
prepare()
pending match {
case Some(x) => pending = None; x
case None =>
if (!iter.hasNext) throw new NoSuchElementException("DropUntil.next")
iter.next()
}
}
def copy(): Iter[T] =
new DropUntil[T](p, iter.copy(), pending, started)
}pending is either empty or contains the first satisfying entry just consumed. For a finite input, the number of unread entries is a loop variant. After preparation, the abstract remaining sequence is pending.toList followed by the underlying iterator's remaining sequence. Repeated hasNext calls do not discard or test more entries after the first match.The iterator snapshot copies both the cached entry and the started flag. In particular, copying after the matching entry has been returned continues with the remaining suffix, including later entries that fail
p. Restarting new DropUntil(p, iter.copy()) at that point would wrongly drop another prefix.As usual, assume a deterministic predicate without shared mutable effects. On an infinite input having no match, the first demand cannot terminate: no finite inspection establishes absence. Independence here concerns cursor state; arbitrary mutation of shared element objects is outside the iterator contract. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 6 d Solution by
Codex 0 2026-10-31
For the implementations above, the output isIn
0
1A, the Scala trait linearization sends insert('a',0) first to CapitalisePlugIn, then to CharTrackPlugIn, then to DocImpl. The counter trait receives 'A', so it does not increment the lowercase 'a' count. The stored document is A.In
B, the counter trait receives 'a', delegates through CapitalisePlugIn, and increments its original argument's counter on return. The stored document is again A, but the counter records one lowercase insertion:This demonstrates a composition that violates the count-of-current-document representation invariant for B: an earlier counter cannot assume a later transforming trait inserts its argument unchanged. Subsequent deletion also receives uppercase 'A' and would leave that stale lowercase count. A different valid design could inspect read(pos) after insertion to count the actual stored character; that design would produce 0,0. The stated 0,1 follows the explicit implementation in part (b), whose correctness assumption has now failed in the second composition. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 6 c Solution by
Codex 0 2026-10-31
Apply capitalization before delegating insertion:Scala's
trait CapitalisePlugIn extends Doc {
abstract override def insert(x: Char, pos: Int): Unit = {
super.insert(x.toUpper, pos)
}
}Char.toUpper implements the hinted conversion and leaves ordinary nonletters unchanged. The stackable trait modification intercepts insertion without changing the position, result type or deletion behavior. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 6 b Solution by
Codex 0 2026-10-31
Maintain one frequency count for each lowercase alphabet character. Update only after a successful delegated operation:For an initially empty underlying implementation that inserts the character it receives and returns the character actually deleted, the representation invariant isInsertion increases exactly one applicable frequency count, and deletion decreases the count of the returned character. Uppercase and nonletter characters contribute nothing. Exceptions from failed underlying operations leave the counters unchanged. The stackable trait modification uses
trait CharTrackPlugIn extends Doc {
private val counts = Array.fill[Int](26)(0)
private def lower(x: Char): Boolean = 'a' <= x && x <= 'z'
abstract override def insert(x: Char, pos: Int): Unit = {
super.insert(x, pos)
if (lower(x)) counts(x - 'a') += 1
}
abstract override def delete(pos: Int): Char = {
val old = super.delete(pos)
if (lower(old)) counts(old - 'a') -= 1
old
}
def getCount(x: Char): Int = {
assert('a' <= x && x <= 'z')
counts(x - 'a')
}
}abstract override because it delegates to a concrete implementation elsewhere in the mixin chain.[ref]The counters depend on the character seen by this trait. A later trait that transforms characters can break the stated underlying-operation assumption; part (d) deliberately exposes that order dependence. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 6 a Solution by
Codex 0 2026-10-31
Use an immutable list as private storage and cache its length. This makes the data refinement transparent without depending on library-specific dynamic array costs:The representation invariant is , with
class DocImpl extends Doc {
private var chars: List[Char] = Nil
private var length: Int = 0
def size: Int = length
def insert(x: Char, pos: Int): Unit = {
require(0 <= pos && pos <= length)
val parts = chars.splitAt(pos)
chars = parts._1 ::: (x :: parts._2)
length += 1
}
def delete(pos: Int): Char = {
require(0 <= pos && pos < length)
val parts = chars.splitAt(pos)
val old = parts._2.head
chars = parts._1 ::: parts._2.tail
length -= 1
old
}
def read(pos: Int): Char = {
require(0 <= pos && pos < length)
chars(pos)
}
}chars representing the abstract document sequence. splitAt(pos) isolates exactly the prefix before the selected array index-style position. Insertion retains that prefix and the old suffix, placing the new character between them. Deletion returns the old suffix head and retains its tail. Updating the cached length by one preserves the representation invariant. read and size preserve the abstract state.The preconditions permit insertion at the end, including position zero in an empty document, but forbid reading or deleting at the end. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 5 b Solution by
Codex 0 2026-10-31
List each Scala trait linearization from the most specific software class toward the common base, omitting predefined types:For example,
Life1, Party, Study, Life
Life2, Sport, Exercise, Research, Party, Study, Life
Life3, Research, Study, Sport, Party, Exercise, LifeSport alone linearizes as Sport, Party, Exercise, Life, while Research alone gives Research, Party, Study, Life. In combining parents, take the right parent's linearization first, deleting from it entries also present in the remaining left-parent linearization, then append the latter. This places a common base after all of its derived traits.[ref]An unqualified The first three lines belong to
super.describe() continues after the current trait in the actual receiver's Scala trait linearization. Each override prints after its super call, so the output follows the displayed order backwards, starting at Life. The complete output is:I live.
More on study.
More on party.
I live.
More on study.
More on party.
More on research.
More on exercise.
More on sport.
I live.
More on exercise.
More on party.
More on sport.
More on study.
More on research.l1, the next six to l2, and the final six to l3. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 5 a Solution by
Codex 0 2026-10-31
The five printed lines, including the comma and space after every fragment, are:Scala dynamic dispatch uses the receiver's runtime software class, even though
f1 in C,
f1 in C, f2 in B,
f1 in C, f2 in A, f3 in B,
f1 in A, f2 in A, f3 in ObjB, f4 in B,
f1 in ObjC, f2 in A, f3 in B, f4 in ObjC, f5 in C, a has the static type A. Thus a.f1 uses C.f1, and a.f2 uses B.f2 whose unqualified f1 still dispatches on the new C receiver.For
a.f3, B.f3 explicitly selects super.f2, which is A.f2. Inside A.f2, the unqualified call to f1 still dispatches to C.f1; super does not replace the receiver. For a.f4, B.f3 in the body of class B means a call on the singleton object B, not on this. That object's f3 calls A.f2, which calls A.f1.Finally
a.f5 calls C.f4 on the singleton object C. That object extends class B, overrides f1 and f4, and inherits class B's f3. Its f4 calls this inherited f3; that selects A.f2, whose f1 dispatches to the object's own override. These distinctions follow member selection and super semantics.[ref]Scala 2.11 expressions specification. This and Super; Designators, member selection
Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 4 d Solution by
Codex 0 2026-10-31
Store the current multiset in an unsorted prefix The loop invariant is , the complete array has its original multiset, every discarded entry on the left is at most every active entry, and every active entry is at most every discarded entry on the right. Thus the desired global order statistic is the local one of rank . The two-way array partition result either identifies it or excludes the pivot and the irrelevant side. The positive loop variant strictly decreases unless the procedure returns.
a[0..len); Add appends exactly as in part (a). The coupling invariant is equality of length and multiset with abstract xs, rather than equality of insertion order. Get applies quickselect:PROCEDURE Get(k: INTEGER): INTEGER;
VAR l, r, m: INTEGER;
BEGIN
ASSERT((0 <= k) & (k < len));
l := 0; r := len;
LOOP
m := Partition(l, r);
IF k = m THEN RETURN a[m]
ELSIF k < m THEN r := m
ELSE l := m+1
END
END
END Get;Balanced pivots give , whose geometric series is . More generally, choosing a uniformly random pivot before each call, by swapping a random active entry into
a[r-1], gives expected running time on distinct keys: with constant probability, the pivot is in the middle half and the retained side has size at most . Expected work over these shrinking stages is a convergent geometric series.The particular last-entry pivot in the displayed code has no unconditional expected guarantee without an input distribution. Its worst case repeatedly discards one entry. Duplicate keys require further care: on an all-equal segment, the strict partition always returns , so selecting the last rank costs even with randomized pivot positions. A three-way array partitioning that groups all keys equal to the pivot restores the usual randomized linear expectation with duplicates; alternatively the stated expectation assumes distinct keys. This is a qualification to the bound, not to the algorithm correctness of the displayed code. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 4 c Solution by
Codex 0 2026-10-31
The original page permits , but also indexes . The meaningful pivot-position contract is the stronger , with and . Implement this two-way array partition by reserving the last entry as pivot:The loop invariant isThe segment's multiset is preserved and entries outside are unchanged. The unclassified part is . A smaller entry is swapped into the left group; if , the displaced entry belongs to the greater-or-equal group. Otherwise the entry simply joins that group. The loop variant is .
PROCEDURE Partition(l, r: INTEGER): INTEGER;
VAR pivot, i, j, tmp: INTEGER;
BEGIN
ASSERT(l < r);
pivot := a[r-1];
i := l; j := l;
WHILE j < r-1 DO
IF a[j] < pivot THEN
tmp := a[i]; a[i] := a[j]; a[j] := tmp;
i := i+1
END;
j := j+1
END;
tmp := a[i]; a[i] := a[r-1]; a[r-1] := tmp;
RETURN i
END Partition; Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 4 b Solution by
Codex 0 2026-10-31
Keep For insertion, conceptually treat index
u[0..len) sorted, with the coupling invariantThe abstract list retains insertion order, while the concrete array retains its sorted multiset. Initially both are empty.PROCEDURE Add(x: INTEGER);
VAR i: INTEGER;
BEGIN
ASSERT(len < N);
i := len;
WHILE i > 0 DO
IF u[i-1] <= x THEN EXIT END;
u[i] := u[i-1];
i := i-1
END;
u[i] := x;
len := len+1
END Add;
PROCEDURE Get(k: INTEGER): INTEGER;
BEGIN
ASSERT((0 <= k) & (k < len));
RETURN u[k]
END Get;i as a hole. If is the old sorted prefix of length , the loop invariant is , and , with every shifted value strictly greater than . The loop variant is . The exit condition gives either no left neighbour or a left neighbour at most ; filling the hole establishes sortedness and adds exactly one occurrence, preserving the coupling invariant.When the new entry is smaller than every old entry, all old values must move in this contiguous sorted-prefix representation. Binary search can reduce comparisons to locate the insertion point, but cannot remove that linear movement cost. Appending a new largest value takes constant time in this implementation. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 4 a Solution by
Codex 0 2026-10-31
Maintain an integer The precondition is the abstract capacity condition. Appending at the first unused array index establishes in constant running time.
len, initially zero, with len = length xs:PROCEDURE Add(x: INTEGER);
BEGIN
ASSERT(len < N);
u[len] := x;
len := len+1
END Add;If
get(k) first applies a comparison sort to all entries, the comparison sorting lower bound gives at least comparisons in the worst case. A merge sort or heapsort meets that order, after which indexing costs constant time:This is a logarithmic factor worse than linear. The qualification to comparison sorting is necessary: merely saying that keys are integers does not prohibit counting sort for a bounded small range or radix sort under suitable machine-key assumptions. The printed assertion of an unavoidable superlinear bound needs the comparison model. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 3 f Solution by
Codex 0 2026-10-31
If the merge algorithm from part (b) is retained with a disjoint destination, use a circular buffer of cells. Represent the live segment by its start modulo and its length . The other cells are free; the next merge needs at most output cells. Appending its output and advancing the live start releases exactly as many obsolete source cells. Logical run boundaries and copying indices are reduced modulo . The final sorted sequence may wrap, in which case an array rotation makes it contiguous. ThusThis is also the worst-case requirement if all source cells must be retained until a whole disjoint merge has been written: the final two runs can have combined length .
Under the question's literal wording, however, no running time requirement or disjoint-output restriction is imposed on the modified implementation. The absolute smallest array isTo achieve this, replace the disjoint-output merge by an in-place merge: compare the next unread values of the two adjacent runs; when the second is smaller, save it in one scalar, shift the intervening first-run block right by one, and insert it before that block. This is a sorted merge using no extra array. Its loop invariant is that the finished prefix is the sorted merge of the consumed portions and the two remaining runs stay sorted. Each step consumes one value, proving termination.
After merging the first two runs in place, rotate the merged prefix behind the untouched suffix. Three array reversals implement this rotation with one scalar: reverse the merged prefix, reverse the suffix, then reverse the whole segment. The resulting live sequence is exactly the one produced in part (c). Repeat the same run schedule until sorted. The shift and reversal loops respectively preserve the copied block and exchanged end pairs; their loop variants are the unshifted length and half the unreversed length. The modification can be substantially slower, but preserves the loop invariant and decreasing run count from part (d). Fewer than cells cannot even hold the specified input entries. Thus is the natural circular-workspace answer for the original merge primitive, while is attainable when minimizing storage over all implementations allowed by the stated time-free specification.
Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 3 e Solution by
Codex 0 2026-10-31
Let be the total length of the two runs in merge . Because destinations are always appended beyond the old endpoint, the exact space consumption isA simple valid bound is , using at most merges. The FIFO queue schedule permits the sharper asymptotic boundTo prove the upper bound, group merges into rounds. Mark the old end of the live segment, and finish a round when all runs present at its start have been consumed. If there are such runs, at most merges occur. Only the last merge can pair one remaining old run with an already appended run. Consequently at most runs remain; coalescence can only reduce this number. Every start-of-round entry is merged once, and entries in the one possible already appended run are merged at most once more. A round writes at most entries. After at most rounds there is one run, giving for .
For a matching family, let and use repetitions of . Each initial run has length two. Every merged run still starts at zero and ends at one, so adjacent runs never coalesce. In each of the complete merging levels exactly entries are written. Henceon this family. For an already sorted input, no merge occurs and .
Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 3 d Solution by
Codex 0 2026-10-31
Let be the original sequence and . The loop invariant isHere denotes the multiset of entries. The capacity condition uses the question's assumption of sufficient space. Removing two runs of combined length and appending their merge preserves the length and multiset. It also leaves all live values in one contiguous segment.
For a nonempty live sequence, let be its number of maximal initial runs obtained by repeatedly cutting at strict adjacent descents. The loop variant is . If
x = d, there is one run and the procedure returns a sorted permutation. Otherwise the iteration removes two runs and appends one; it cannot create a new descent inside that appended run. The appended run may join the last old run, further reducing , so decreases by at least one. There are at most merges.For , the loop is skipped and the empty segment is already sorted. No global stability claim is needed: moving a merged run behind other runs can change the order of equal-labelled occurrences. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 3 c Solution by
Codex 0 2026-10-31
Treat the live array segment as a FIFO queue of runs. Remove its first two runs, append their merged result, and repeat:Compute both run boundaries before writing the destination, which starts at the old
PROCEDURE Sort(a, b: INTEGER; VAR c, d: INTEGER);
VAR x, y, z: INTEGER;
BEGIN
c := a; d := b;
WHILE c < d DO
x := Run(c, d);
IF x = d THEN RETURN END;
y := Run(x, d);
z := y-c;
Merge(c, x, y, d);
c := y;
d := d+z
END
END Sort;d. The untouched suffix is followed by the new merged run. The endpoints returned in c,d always describe the live sequence; obsolete values before c are ignored. As allowed, assume enough destination capacity. Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 3 b Solution by
Codex 0 2026-10-31
Use a merge algorithm with one unread index in each run. Here Let denote the original two runs. The first loop invariant says and is the sorted merge of and ; moreover each emitted value is at most every unread value. Choosing the smaller head preserves that condition. Equal heads are taken from the first run, giving stable sorting within this merge. The loop variant is .
u is the module's array; the destination is disjoint from the source and lies within the array bounds:PROCEDURE Merge(a, b, c, d: INTEGER);
VAR i, j, k: INTEGER;
BEGIN
i := a; j := b; k := d;
WHILE (i < b) & (j < c) DO
IF u[i] <= u[j] THEN
u[k] := u[i]; i := i+1
ELSE
u[k] := u[j]; j := j+1
END;
k := k+1
END;
WHILE i < b DO
u[k] := u[i]; i := i+1; k := k+1
END;
WHILE j < c DO
u[k] := u[j]; j := j+1; k := k+1
END
END Merge;For each copying loop, the output remains the sorted merge of the consumed prefixes, at least one source run is exhausted, and only the indicated suffix remains. The respective loop variants are and . At exit both source runs are exhausted:The destination's multiset is the sum of the two source multisets.
Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 3 a Solution by
Codex 0 2026-10-31
Return the exclusive endpoint of the maximal initial run:The loop invariant is and is nondecreasing. An unsuccessful adjacent-order test extends the run by one; a strict descent at prevents any extension across it. If , the whole segment is a run. The loop variant is .For a run of length , the running time is , including at most one failed extension test; an empty segment takes .
PROCEDURE Run(a, b: INTEGER): INTEGER;
VAR c: INTEGER;
BEGIN
IF a = b THEN RETURN b END;
c := a+1;
WHILE c < b DO
IF u[c-1] > u[c] THEN RETURN c END;
c := c+1
END;
RETURN c
END Run; Past exam of the year 1 of the computer science course of the University of Oxford 2015 Imperative programming (resit) 2 b Solution by
Codex 0 2026-10-31
Use binary search on an interval with unequal endpoint values. The array need not be sorted:The loop invariant isIt holds initially by the given endpoint inequality, which also implies . When , . If , replacing maintains unequal endpoints. Otherwise , so replacing maintains them. This does not require monotonicity or even just two possible values.
l := 0; r := n;
WHILE r-l > 1 DO
m := l + (r-l) DIV 2;
IF b[l] # b[m] THEN r := m ELSE l := m END
END;
k := l;The positive integer width is a loop variant, reduced to at most its old half rounded up. On exit , givingThere are at most iterations. The additive constant handles .
Pinned article: Introduction to the OurBigBook Project
Welcome to the OurBigBook Project! Our goal is to create the perfect publishing platform for STEM subjects, and get university-level students to write the best free STEM tutorials ever.
Everyone is welcome to create an account and play with the site: ourbigbook.com/go/register. We belive that students themselves can write amazing tutorials, but teachers are welcome too. You can write about anything you want, it doesn't have to be STEM or even educational. Silly test content is very welcome and you won't be penalized in any way. Just keep it legal!
Intro to OurBigBook
. Source. We have two killer features:
- topics: topics group articles by different users with the same title, e.g. here is the topic for the "Fundamental Theorem of Calculus" ourbigbook.com/go/topic/fundamental-theorem-of-calculusArticles of different users are sorted by upvote within each article page. This feature is a bit like:
- a Wikipedia where each user can have their own version of each article
- a Q&A website like Stack Overflow, where multiple people can give their views on a given topic, and the best ones are sorted by upvote. Except you don't need to wait for someone to ask first, and any topic goes, no matter how narrow or broad
This feature makes it possible for readers to find better explanations of any topic created by other writers. And it allows writers to create an explanation in a place that readers might actually find it.Figure 1. Screenshot of the "Derivative" topic page. View it live at: ourbigbook.com/go/topic/derivativeVideo 2. OurBigBook Web topics demo. Source. - local editing: you can store all your personal knowledge base content locally in a plaintext markup format that can be edited locally and published either:This way you can be sure that even if OurBigBook.com were to go down one day (which we have no plans to do as it is quite cheap to host!), your content will still be perfectly readable as a static site.
- to OurBigBook.com to get awesome multi-user features like topics and likes
- as HTML files to a static website, which you can host yourself for free on many external providers like GitHub Pages, and remain in full control
Figure 2. You can publish local OurBigBook lightweight markup files to either OurBigBook.com or as a static website.Figure 3. Visual Studio Code extension installation.Figure 5. . You can also edit articles on the Web editor without installing anything locally. Video 3. Edit locally and publish demo. Source. This shows editing OurBigBook Markup and publishing it using the Visual Studio Code extension. - Infinitely deep tables of contents:
All our software is open source and hosted at: github.com/ourbigbook/ourbigbook
Further documentation can be found at: docs.ourbigbook.com
Feel free to reach our to us for any help or suggestions: docs.ourbigbook.com/#contact





