Collaborative wiki for a few of us. Fork of lines.love.
-- primitives for saving to file and loading from file
function file_exists(filename)
  local infile = App.open_for_reading(filename)
  if infile then
    infile:close()
    return true
  else
    return false
  end
end

function load_from_disk(State)
  local infile = App.open_for_reading(State.filename)
  State.lines = load_from_file(infile)
  if infile then infile:close() end
end

function load_from_file(infile)
  local result = {}
  if infile then
    local infile_next_line = infile:lines()  -- works with both Lua files and LÖVE Files (https://www.love2d.org/wiki/File)
    while true do
      local line = infile_next_line()
      if line == nil then break end
      if line == '```lines' then  -- inflexible with whitespace since these files are always autogenerated
        table.insert(result, load_drawing(infile_next_line))
      else
        table.insert(result, {mode='text', data=line})
      end
    end
  end
  if #result == 0 then
    table.insert(result, {mode='text', data=''})
  end
  return result
end

function save_to_disk(State)
  -- save the payload
  log(2, 'save_to_disk: '..State.id)
  local outfile = App.open_for_writing(State.filename)
  if not outfile then
    error('failed to write to "'..State.filename..'"')
  end
  for _,line in ipairs(State.lines) do
    if line.mode == 'drawing' then
      store_drawing(outfile, line)
    else
      outfile:write(line.data)
      outfile:write('\n')
    end
  end
  outfile:close()
  -- skip other writes in tests
  if App.run_tests then
    return
  end
  --
  log(2, 'save_to_disk: about to save links for '..State.id)
  save_links(State.id)
  log(2, 'save_to_disk: done saving links for '..State.id)
  -- update recent
  if not State.recent_updated then
    if State.id then
      local f, err = io.open(Directory..'recent', 'a')
      if not f then
        error(err)
      end
      assert(not err, 'failed to save recent')
      f:write(State.id, '\n')
      f:close()
    end
    State.recent_updated = true
  end
  log(2, 'save_to_disk: done updating recent with '..State.id)
end

function load_drawing(infile_next_line)
  local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}}
  while true do
    local line = infile_next_line()
    assert(line, 'drawing in file is incomplete')
    if line == '```' then break end
    local shape = json.decode(line)
    if shape.mode == 'freehand' then
      -- no changes needed
    elseif shape.mode == 'line' or shape.mode == 'manhattan' then
      local name = shape.p1.name
      shape.p1 = Drawing.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600)
      drawing.points[shape.p1].name = name
      name = shape.p2.name
      shape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600)
      drawing.points[shape.p2].name = name
    elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
      for i,p in ipairs(shape.vertices) do
        local name = p.name
        shape.vertices[i] = Drawing.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600)
        drawing.points[shape.vertices[i]].name = name
      end
    elseif shape.mode == 'circle' or shape.mode == 'arc' then
      local name = shape.center.name
      shape.center = Drawing.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600)
      drawing.points[shape.center].name = name
    elseif shape.mode == 'deleted' then
      -- ignore
    else
      assert(false, ('unknown drawing mode %s'):format(shape.mode))
    end
    table.insert(drawing.shapes, shape)
  end
  return drawing
end

function store_drawing(outfile, drawing)
  outfile:write('```lines\n')
  for _,shape in ipairs(drawing.shapes) do
    if shape.mode == 'freehand' then
      outfile:write(json.encode(shape))
      outfile:write('\n')
    elseif shape.mode == 'line' or shape.mode == 'manhattan' then
      local line = json.encode({mode=shape.mode, p1=drawing.points[shape.p1], p2=drawing.points[shape.p2]})
      outfile:write(line)
      outfile:write('\n')
    elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
      local obj = {mode=shape.mode, vertices={}}
      for _,p in ipairs(shape.vertices) do
        table.insert(obj.vertices, drawing.points[p])
      end
      local line = json.encode(obj)
      outfile:write(line)
      outfile:write('\n')
    elseif shape.mode == 'circle' then
      outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius}))
      outfile:write('\n')
    elseif shape.mode == 'arc' then
      outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius, start_angle=shape.start_angle, end_angle=shape.end_angle}))
      outfile:write('\n')
    elseif shape.mode == 'deleted' then
      -- ignore
    else
      assert(false, ('unknown drawing mode %s'):format(shape.mode))
    end
  end
  outfile:write('```\n')
end

function load_links(id)
  print(id)
  local infile = App.open_for_reading(Directory..id..'.json')
  if not infile then
    return {}
  end
  return json.decode(infile:read())
end

function save_links(id)
  local links_filename = Directory..id..'.json'
  local status = App.mkdir(dirname(links_filename))
  assert(status, 'failed to create directory for links')
  log(2, 'save_links: '..id)
  if empty(Links[id]) then
    print_and_log('save_links: no links; getting rid of .json if it exists')
    App.remove(links_filename)
    return
  end
  local outfile = App.open_for_writing(links_filename)
  if not outfile then
    log(2, 'save_links; error')
    error('failed to write to "'..links_filename..'"')
  end
  emit_links_in_json_in_consistent_order(outfile, Links[id])
  outfile:write('\n')
  outfile:close()
  log(2, 'save_links done: '..id)
end

-- for tests
function load_array(a)
  local result = {}
  local next_line = ipairs(a)
  local i,line,drawing = 0, ''
  while true do
    i,line = next_line(a, i)
    if i == nil then break end
--?     print(line)
    if line == '```lines' then  -- inflexible with whitespace since these files are always autogenerated
--?       print('inserting drawing')
      i, drawing = load_drawing_from_array(next_line, a, i)
--?       print('i now', i)
      table.insert(result, drawing)
    else
--?       print('inserting text')
      table.insert(result, {mode='text', data=line})
    end
  end
  if #result == 0 then
    table.insert(result, {mode='text', data=''})
  end
  return result
end

function load_drawing_from_array(iter, a, i)
  local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}}
  local line
  while true do
    i, line = iter(a, i)
    assert(i, 'drawing in array is incomplete')
--?     print(i)
    if line == '```' then break end
    local shape = json.decode(line)
    if shape.mode == 'freehand' then
      -- no changes needed
    elseif shape.mode == 'line' or shape.mode == 'manhattan' then
      local name = shape.p1.name
      shape.p1 = Drawing.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600)
      drawing.points[shape.p1].name = name
      name = shape.p2.name
      shape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600)
      drawing.points[shape.p2].name = name
    elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
      for i,p in ipairs(shape.vertices) do
        local name = p.name
        shape.vertices[i] = Drawing.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600)
        drawing.points[shape.vertices[i]].name = name
      end
    elseif shape.mode == 'circle' or shape.mode == 'arc' then
      local name = shape.center.name
      shape.center = Drawing.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600)
      drawing.points[shape.center].name = name
    elseif shape.mode == 'deleted' then
      -- ignore
    else
      assert(false, ('unknown drawing mode %s'):format(shape.mode))
    end
    table.insert(drawing.shapes, shape)
  end
  return i, drawing
end

-- append to a potentially-nonexistent file
function append_to_file(filename, contents)
  local old_contents = ''
  local f, err = App.open_for_reading(filename)
  if f then
    old_contents = f:read()
    f:close()
  end
  f, err = App.open_for_writing(filename)
  if not f then
    return nil, err
  end
  f:write(old_contents..contents)
  f:close()
  return --[[success]] true, --[[err]] nil
end

function is_absolute_path(path)
  local os_path_separator = package.config:sub(1,1)
  if os_path_separator == '/' then
    -- POSIX systems permit backslashes in filenames
    return path:sub(1,1) == '/'
  elseif os_path_separator == '\\' then
    if path:sub(2,2) == ':' then return true end  -- DOS drive letter followed by volume separator
    local f = path:sub(1,1)
    return f == '/' or f == '\\'
  else
    error('What OS is this? LÖVE reports that the path separator is "'..os_path_separator..'"')
  end
end

function is_relative_path(path)
  return not is_absolute_path(path)
end

function dirname(path)
  local os_path_separator = package.config:sub(1,1)
  if os_path_separator == '/' then
    -- POSIX systems permit backslashes in filenames
    return path:match('.*/') or './'
  elseif os_path_separator == '\\' then
    return path:match('.*[/\\]') or './'
  else
    error('What OS is this? LÖVE reports that the path separator is "'..os_path_separator..'"')
  end
end

function test_dirname()
  check_eq(dirname('a/b'), 'a/', 'F - test_dirname')
  check_eq(dirname('x'), './', 'F - test_dirname/current')
end

function basename(path)
  local os_path_separator = package.config:sub(1,1)
  if os_path_separator == '/' then
    -- POSIX systems permit backslashes in filenames
    return string.gsub(path, ".*/(.*)", "%1")
  elseif os_path_separator == '\\' then
    return string.gsub(path, ".*[/\\](.*)", "%1")
  else
    error('What OS is this? LÖVE reports that the path separator is "'..os_path_separator..'"')
  end
end

function empty(h)
  for _,_ in pairs(h) do
    return false
  end
  return true
end