mirror of
https://github.com/RGBCube/serenity
synced 2025-05-20 12:35:07 +00:00

The basic idea is that a global object cannot just come out of nowhere, it must be associated to a realm - so get it from there, if needed. This is to enforce the changes from all the previous commits by not handing out global objects unless you actually have an initialized realm (either stored somewhere, or the VM's current realm).
773 lines
26 KiB
C++
773 lines
26 KiB
C++
/*
|
|
* Copyright (c) 2020-2022, the SerenityOS developers.
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "Spreadsheet.h"
|
|
#include "JSIntegration.h"
|
|
#include "Workbook.h"
|
|
#include <AK/ByteBuffer.h>
|
|
#include <AK/Debug.h>
|
|
#include <AK/GenericLexer.h>
|
|
#include <AK/JsonArray.h>
|
|
#include <AK/JsonObject.h>
|
|
#include <AK/JsonParser.h>
|
|
#include <AK/ScopeGuard.h>
|
|
#include <AK/TemporaryChange.h>
|
|
#include <AK/URL.h>
|
|
#include <LibCore/File.h>
|
|
#include <LibJS/Interpreter.h>
|
|
#include <LibJS/Parser.h>
|
|
#include <LibJS/Runtime/AbstractOperations.h>
|
|
#include <LibJS/Runtime/FunctionObject.h>
|
|
#include <ctype.h>
|
|
#include <unistd.h>
|
|
|
|
namespace Spreadsheet {
|
|
|
|
Sheet::Sheet(StringView name, Workbook& workbook)
|
|
: Sheet(workbook)
|
|
{
|
|
m_name = name;
|
|
|
|
for (size_t i = 0; i < default_row_count; ++i)
|
|
add_row();
|
|
|
|
for (size_t i = 0; i < default_column_count; ++i)
|
|
add_column();
|
|
}
|
|
|
|
Sheet::Sheet(Workbook& workbook)
|
|
: m_workbook(workbook)
|
|
, m_interpreter(JS::Interpreter::create<SheetGlobalObject>(m_workbook.vm(), *this))
|
|
{
|
|
JS::DeferGC defer_gc(m_workbook.vm().heap());
|
|
m_global_object = static_cast<SheetGlobalObject*>(&m_interpreter->realm().global_object());
|
|
global_object().define_direct_property("workbook", m_workbook.workbook_object(), JS::default_attributes);
|
|
global_object().define_direct_property("thisSheet", &global_object(), JS::default_attributes); // Self-reference is unfortunate, but required.
|
|
|
|
// Sadly, these have to be evaluated once per sheet.
|
|
constexpr auto runtime_file_path = "/res/js/Spreadsheet/runtime.js"sv;
|
|
auto file_or_error = Core::File::open(runtime_file_path, Core::OpenMode::ReadOnly);
|
|
if (!file_or_error.is_error()) {
|
|
auto buffer = file_or_error.value()->read_all();
|
|
auto script_or_error = JS::Script::parse(buffer, interpreter().realm(), runtime_file_path);
|
|
if (script_or_error.is_error()) {
|
|
warnln("Spreadsheet: Failed to parse runtime code");
|
|
for (auto& error : script_or_error.error()) {
|
|
// FIXME: This doesn't print hints anymore
|
|
warnln("SyntaxError: {}", error.to_string());
|
|
}
|
|
} else {
|
|
auto result = interpreter().run(script_or_error.value());
|
|
if (result.is_error()) {
|
|
warnln("Spreadsheet: Failed to run runtime code:");
|
|
auto thrown_value = *result.throw_completion().value();
|
|
warn("Threw: {}", thrown_value.to_string_without_side_effects());
|
|
if (thrown_value.is_object() && is<JS::Error>(thrown_value.as_object())) {
|
|
auto& error = static_cast<JS::Error const&>(thrown_value.as_object());
|
|
warnln(" with message '{}'", error.get_without_side_effects(interpreter().vm().names.message));
|
|
for (auto& traceback_frame : error.traceback()) {
|
|
auto& function_name = traceback_frame.function_name;
|
|
auto& source_range = traceback_frame.source_range;
|
|
dbgln(" {} at {}:{}:{}", function_name, source_range.filename, source_range.start.line, source_range.start.column);
|
|
}
|
|
} else {
|
|
warnln();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
JS::Interpreter& Sheet::interpreter() const
|
|
{
|
|
return const_cast<JS::Interpreter&>(*m_interpreter);
|
|
}
|
|
|
|
size_t Sheet::add_row()
|
|
{
|
|
return m_rows++;
|
|
}
|
|
|
|
static Optional<size_t> convert_from_string(StringView str, unsigned base = 26, StringView map = {})
|
|
{
|
|
if (map.is_null())
|
|
map = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"sv;
|
|
|
|
VERIFY(base >= 2 && base <= map.length());
|
|
|
|
size_t value = 0;
|
|
auto const len = str.length();
|
|
for (auto i = 0u; i < len; i++) {
|
|
auto maybe_index = map.find(str[i]);
|
|
if (!maybe_index.has_value())
|
|
return {};
|
|
size_t digit_value = maybe_index.value();
|
|
// NOTE: Refer to the note in `String::bijective_base_from()'.
|
|
if (i == 0 && len > 1)
|
|
++digit_value;
|
|
value += digit_value * AK::pow<float>(base, len - 1 - i);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
String Sheet::add_column()
|
|
{
|
|
auto next_column = String::bijective_base_from(m_columns.size());
|
|
m_columns.append(next_column);
|
|
return next_column;
|
|
}
|
|
|
|
void Sheet::update()
|
|
{
|
|
if (m_should_ignore_updates) {
|
|
m_update_requested = true;
|
|
return;
|
|
}
|
|
m_visited_cells_in_update.clear();
|
|
Vector<Cell&> cells_copy;
|
|
|
|
// Grab a copy as updates might insert cells into the table.
|
|
for (auto& it : m_cells) {
|
|
if (it.value->dirty()) {
|
|
cells_copy.append(*it.value);
|
|
m_workbook.set_dirty(true);
|
|
}
|
|
}
|
|
|
|
for (auto& cell : cells_copy)
|
|
update(cell);
|
|
|
|
m_visited_cells_in_update.clear();
|
|
}
|
|
|
|
void Sheet::update(Cell& cell)
|
|
{
|
|
if (m_should_ignore_updates) {
|
|
m_update_requested = true;
|
|
return;
|
|
}
|
|
if (cell.dirty()) {
|
|
if (has_been_visited(&cell)) {
|
|
// This may be part of an cyclic reference chain,
|
|
// so just ignore it.
|
|
cell.clear_dirty();
|
|
return;
|
|
}
|
|
m_visited_cells_in_update.set(&cell);
|
|
cell.update_data({});
|
|
}
|
|
}
|
|
|
|
JS::ThrowCompletionOr<JS::Value> Sheet::evaluate(StringView source, Cell* on_behalf_of)
|
|
{
|
|
TemporaryChange cell_change { m_current_cell_being_evaluated, on_behalf_of };
|
|
auto name = on_behalf_of ? on_behalf_of->name_for_javascript(*this) : "cell <unknown>"sv;
|
|
auto script_or_error = JS::Script::parse(
|
|
source,
|
|
interpreter().realm(),
|
|
name);
|
|
|
|
if (script_or_error.is_error())
|
|
return interpreter().vm().throw_completion<JS::SyntaxError>(script_or_error.error().first().to_string());
|
|
|
|
return interpreter().run(script_or_error.value());
|
|
}
|
|
|
|
Cell* Sheet::at(StringView name)
|
|
{
|
|
auto pos = parse_cell_name(name);
|
|
if (pos.has_value())
|
|
return at(pos.value());
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Cell* Sheet::at(Position const& position)
|
|
{
|
|
auto it = m_cells.find(position);
|
|
|
|
if (it == m_cells.end())
|
|
return nullptr;
|
|
|
|
return it->value;
|
|
}
|
|
|
|
Optional<Position> Sheet::parse_cell_name(StringView name) const
|
|
{
|
|
GenericLexer lexer(name);
|
|
auto col = lexer.consume_while(isalpha);
|
|
auto row = lexer.consume_while(isdigit);
|
|
|
|
if (!lexer.is_eof() || row.is_empty() || col.is_empty())
|
|
return {};
|
|
|
|
auto it = m_columns.find(col);
|
|
if (it == m_columns.end())
|
|
return {};
|
|
|
|
return Position { it.index(), row.to_uint().value() };
|
|
}
|
|
|
|
Optional<size_t> Sheet::column_index(StringView column_name) const
|
|
{
|
|
auto maybe_index = convert_from_string(column_name);
|
|
if (!maybe_index.has_value())
|
|
return {};
|
|
|
|
auto index = maybe_index.value();
|
|
if (m_columns.size() <= index || m_columns[index] != column_name) {
|
|
auto it = m_columns.find(column_name);
|
|
if (it == m_columns.end())
|
|
return {};
|
|
index = it.index();
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
Optional<String> Sheet::column_arithmetic(StringView column_name, int offset)
|
|
{
|
|
auto maybe_index = column_index(column_name);
|
|
if (!maybe_index.has_value())
|
|
return {};
|
|
|
|
if (offset < 0 && maybe_index.value() < (size_t)(0 - offset))
|
|
return m_columns.first();
|
|
|
|
auto index = maybe_index.value() + offset;
|
|
if (m_columns.size() > index)
|
|
return m_columns[index];
|
|
|
|
for (size_t i = m_columns.size(); i <= index; ++i)
|
|
add_column();
|
|
|
|
return m_columns.last();
|
|
}
|
|
|
|
Cell* Sheet::from_url(const URL& url)
|
|
{
|
|
auto maybe_position = position_from_url(url);
|
|
if (!maybe_position.has_value())
|
|
return nullptr;
|
|
|
|
return at(maybe_position.value());
|
|
}
|
|
|
|
Optional<Position> Sheet::position_from_url(const URL& url) const
|
|
{
|
|
if (!url.is_valid()) {
|
|
dbgln("Invalid url: {}", url.to_string());
|
|
return {};
|
|
}
|
|
|
|
if (url.protocol() != "spreadsheet" || url.host() != "cell") {
|
|
dbgln("Bad url: {}", url.to_string());
|
|
return {};
|
|
}
|
|
|
|
// FIXME: Figure out a way to do this cross-process.
|
|
VERIFY(url.path() == String::formatted("/{}", getpid()));
|
|
|
|
return parse_cell_name(url.fragment());
|
|
}
|
|
|
|
Position Sheet::offset_relative_to(Position const& base, Position const& offset, Position const& offset_base) const
|
|
{
|
|
if (offset.column >= m_columns.size()) {
|
|
dbgln("Column '{}' does not exist!", offset.column);
|
|
return base;
|
|
}
|
|
if (offset_base.column >= m_columns.size()) {
|
|
dbgln("Column '{}' does not exist!", offset_base.column);
|
|
return base;
|
|
}
|
|
if (base.column >= m_columns.size()) {
|
|
dbgln("Column '{}' does not exist!", base.column);
|
|
return offset;
|
|
}
|
|
|
|
auto new_column = offset.column + base.column - offset_base.column;
|
|
auto new_row = offset.row + base.row - offset_base.row;
|
|
|
|
return { new_column, new_row };
|
|
}
|
|
|
|
Vector<CellChange> Sheet::copy_cells(Vector<Position> from, Vector<Position> to, Optional<Position> resolve_relative_to, CopyOperation copy_operation)
|
|
{
|
|
Vector<CellChange> cell_changes;
|
|
// Disallow misaligned copies.
|
|
if (to.size() > 1 && from.size() != to.size()) {
|
|
dbgln("Cannot copy {} cells to {} cells", from.size(), to.size());
|
|
return cell_changes;
|
|
}
|
|
|
|
Vector<Position> target_cells;
|
|
for (auto& position : from)
|
|
target_cells.append(resolve_relative_to.has_value() ? offset_relative_to(to.first(), position, resolve_relative_to.value()) : to.first());
|
|
|
|
auto copy_to = [&](auto& source_position, Position target_position) {
|
|
auto& target_cell = ensure(target_position);
|
|
auto* source_cell = at(source_position);
|
|
auto previous_data = target_cell.data();
|
|
|
|
if (!source_cell) {
|
|
target_cell.set_data("");
|
|
cell_changes.append(CellChange(target_cell, previous_data));
|
|
return;
|
|
}
|
|
|
|
target_cell.copy_from(*source_cell);
|
|
cell_changes.append(CellChange(target_cell, previous_data));
|
|
if (copy_operation == CopyOperation::Cut && !target_cells.contains_slow(source_position)) {
|
|
cell_changes.append(CellChange(*source_cell, source_cell->data()));
|
|
source_cell->set_data("");
|
|
}
|
|
};
|
|
|
|
// Resolve each index as relative to the first index offset from the selection.
|
|
auto& target = to.first();
|
|
|
|
auto top_left_most_position_from = from.first();
|
|
auto bottom_right_most_position_from = from.first();
|
|
for (auto& position : from) {
|
|
if (position.row > bottom_right_most_position_from.row)
|
|
bottom_right_most_position_from = position;
|
|
else if (position.column > bottom_right_most_position_from.column)
|
|
bottom_right_most_position_from = position;
|
|
|
|
if (position.row < top_left_most_position_from.row)
|
|
top_left_most_position_from = position;
|
|
else if (position.column < top_left_most_position_from.column)
|
|
top_left_most_position_from = position;
|
|
}
|
|
|
|
Vector<Position> ordered_from;
|
|
auto current_column = top_left_most_position_from.column;
|
|
auto current_row = top_left_most_position_from.row;
|
|
for ([[maybe_unused]] auto& position : from) {
|
|
for (auto& position : from)
|
|
if (position.row == current_row && position.column == current_column)
|
|
ordered_from.append(position);
|
|
|
|
if (current_column >= bottom_right_most_position_from.column) {
|
|
current_column = top_left_most_position_from.column;
|
|
current_row += 1;
|
|
} else {
|
|
current_column += 1;
|
|
}
|
|
}
|
|
|
|
if (target.row > top_left_most_position_from.row || (target.row >= top_left_most_position_from.row && target.column > top_left_most_position_from.column))
|
|
ordered_from.reverse();
|
|
|
|
for (auto& position : ordered_from) {
|
|
dbgln_if(COPY_DEBUG, "Paste from '{}' to '{}'", position.to_url(*this), target.to_url(*this));
|
|
copy_to(position, resolve_relative_to.has_value() ? offset_relative_to(target, position, resolve_relative_to.value()) : target);
|
|
}
|
|
|
|
return cell_changes;
|
|
}
|
|
|
|
RefPtr<Sheet> Sheet::from_json(JsonObject const& object, Workbook& workbook)
|
|
{
|
|
auto sheet = adopt_ref(*new Sheet(workbook));
|
|
auto rows = object.get("rows"sv).to_u32(default_row_count);
|
|
auto columns = object.get("columns"sv);
|
|
auto name = object.get("name"sv).as_string_or("Sheet");
|
|
if (object.has("cells"sv) && !object.has_object("cells"sv))
|
|
return {};
|
|
|
|
sheet->set_name(name);
|
|
|
|
for (size_t i = 0; i < max(rows, (unsigned)Sheet::default_row_count); ++i)
|
|
sheet->add_row();
|
|
|
|
// FIXME: Better error checking.
|
|
if (columns.is_array()) {
|
|
columns.as_array().for_each([&](auto& value) {
|
|
sheet->m_columns.append(value.as_string());
|
|
return IterationDecision::Continue;
|
|
});
|
|
}
|
|
|
|
if (sheet->m_columns.size() < default_column_count && sheet->columns_are_standard()) {
|
|
for (size_t i = sheet->m_columns.size(); i < default_column_count; ++i)
|
|
sheet->add_column();
|
|
}
|
|
|
|
auto json = sheet->global_object().get_without_side_effects("JSON");
|
|
auto& parse_function = json.as_object().get_without_side_effects("parse").as_function();
|
|
|
|
auto read_format = [](auto& format, auto const& obj) {
|
|
if (auto value = obj.get("foreground_color"sv); value.is_string())
|
|
format.foreground_color = Color::from_string(value.as_string());
|
|
if (auto value = obj.get("background_color"sv); value.is_string())
|
|
format.background_color = Color::from_string(value.as_string());
|
|
};
|
|
|
|
if (object.has_object("cells"sv)) {
|
|
object.get("cells"sv).as_object().for_each_member([&](auto& name, JsonValue const& value) {
|
|
auto position_option = sheet->parse_cell_name(name);
|
|
if (!position_option.has_value())
|
|
return IterationDecision::Continue;
|
|
|
|
auto position = position_option.value();
|
|
auto& obj = value.as_object();
|
|
auto kind = obj.get("kind"sv).as_string_or("LiteralString") == "LiteralString" ? Cell::LiteralString : Cell::Formula;
|
|
|
|
OwnPtr<Cell> cell;
|
|
switch (kind) {
|
|
case Cell::LiteralString:
|
|
cell = make<Cell>(obj.get("value"sv).to_string(), position, *sheet);
|
|
break;
|
|
case Cell::Formula: {
|
|
auto& vm = sheet->interpreter().vm();
|
|
auto value_or_error = JS::call(vm, parse_function, json, JS::js_string(vm, obj.get("value"sv).as_string()));
|
|
if (value_or_error.is_error()) {
|
|
warnln("Failed to load previous value for cell {}, leaving as undefined", position.to_cell_identifier(sheet));
|
|
value_or_error = JS::js_undefined();
|
|
}
|
|
cell = make<Cell>(obj.get("source"sv).to_string(), value_or_error.release_value(), position, *sheet);
|
|
break;
|
|
}
|
|
}
|
|
|
|
auto type_name = obj.has("type"sv) ? obj.get("type"sv).to_string() : "Numeric";
|
|
cell->set_type(type_name);
|
|
|
|
auto type_meta = obj.get("type_metadata"sv);
|
|
if (type_meta.is_object()) {
|
|
auto& meta_obj = type_meta.as_object();
|
|
auto meta = cell->type_metadata();
|
|
if (auto value = meta_obj.get("length"sv); value.is_number())
|
|
meta.length = value.to_i32();
|
|
if (auto value = meta_obj.get("format"sv); value.is_string())
|
|
meta.format = value.as_string();
|
|
if (auto value = meta_obj.get("alignment"sv); value.is_string()) {
|
|
auto alignment = Gfx::text_alignment_from_string(value.as_string());
|
|
if (alignment.has_value())
|
|
meta.alignment = alignment.value();
|
|
}
|
|
read_format(meta.static_format, meta_obj);
|
|
|
|
cell->set_type_metadata(move(meta));
|
|
}
|
|
|
|
auto conditional_formats = obj.get("conditional_formats"sv);
|
|
auto cformats = cell->conditional_formats();
|
|
if (conditional_formats.is_array()) {
|
|
conditional_formats.as_array().for_each([&](const auto& fmt_val) {
|
|
if (!fmt_val.is_object())
|
|
return IterationDecision::Continue;
|
|
|
|
auto& fmt_obj = fmt_val.as_object();
|
|
auto fmt_cond = fmt_obj.get("condition"sv).to_string();
|
|
if (fmt_cond.is_empty())
|
|
return IterationDecision::Continue;
|
|
|
|
ConditionalFormat fmt;
|
|
fmt.condition = move(fmt_cond);
|
|
read_format(fmt, fmt_obj);
|
|
cformats.append(move(fmt));
|
|
|
|
return IterationDecision::Continue;
|
|
});
|
|
cell->set_conditional_formats(move(cformats));
|
|
}
|
|
|
|
auto evaluated_format = obj.get("evaluated_formats"sv);
|
|
if (evaluated_format.is_object()) {
|
|
auto& evaluated_format_obj = evaluated_format.as_object();
|
|
auto& evaluated_fmts = cell->evaluated_formats();
|
|
|
|
read_format(evaluated_fmts, evaluated_format_obj);
|
|
}
|
|
|
|
sheet->m_cells.set(position, cell.release_nonnull());
|
|
return IterationDecision::Continue;
|
|
});
|
|
}
|
|
|
|
return sheet;
|
|
}
|
|
|
|
Position Sheet::written_data_bounds(Optional<size_t> column_index) const
|
|
{
|
|
Position bound;
|
|
for (auto const& entry : m_cells) {
|
|
if (entry.value->data().is_empty())
|
|
continue;
|
|
if (column_index.has_value() && entry.key.column != *column_index)
|
|
continue;
|
|
if (entry.key.row >= bound.row)
|
|
bound.row = entry.key.row;
|
|
if (entry.key.column >= bound.column)
|
|
bound.column = entry.key.column;
|
|
}
|
|
|
|
return bound;
|
|
}
|
|
|
|
/// The sheet is allowed to have nonstandard column names
|
|
/// this checks whether all existing columns are 'standard'
|
|
/// (i.e. as generated by 'String::bijective_base_from()'
|
|
bool Sheet::columns_are_standard() const
|
|
{
|
|
for (size_t i = 0; i < m_columns.size(); ++i) {
|
|
if (m_columns[i] != String::bijective_base_from(i))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
JsonObject Sheet::to_json() const
|
|
{
|
|
JsonObject object;
|
|
object.set("name", m_name);
|
|
|
|
auto save_format = [](auto const& format, auto& obj) {
|
|
if (format.foreground_color.has_value())
|
|
obj.set("foreground_color", format.foreground_color.value().to_string());
|
|
if (format.background_color.has_value())
|
|
obj.set("background_color", format.background_color.value().to_string());
|
|
};
|
|
|
|
auto bottom_right = written_data_bounds();
|
|
|
|
if (!columns_are_standard()) {
|
|
auto columns = JsonArray();
|
|
for (auto& column : m_columns)
|
|
columns.append(column);
|
|
object.set("columns", move(columns));
|
|
}
|
|
object.set("rows", bottom_right.row + 1);
|
|
|
|
JsonObject cells;
|
|
for (auto& it : m_cells) {
|
|
StringBuilder builder;
|
|
builder.append(column(it.key.column));
|
|
builder.appendff("{}", it.key.row);
|
|
auto key = builder.to_string();
|
|
|
|
JsonObject data;
|
|
data.set("kind", it.value->kind() == Cell::Kind::Formula ? "Formula" : "LiteralString");
|
|
if (it.value->kind() == Cell::Formula) {
|
|
auto& vm = interpreter().vm();
|
|
data.set("source", it.value->data());
|
|
auto json = interpreter().realm().global_object().get_without_side_effects("JSON");
|
|
auto stringified_or_error = JS::call(vm, json.as_object().get_without_side_effects("stringify").as_function(), json, it.value->evaluated_data());
|
|
VERIFY(!stringified_or_error.is_error());
|
|
data.set("value", stringified_or_error.release_value().to_string_without_side_effects());
|
|
} else {
|
|
data.set("value", it.value->data());
|
|
}
|
|
|
|
// Set type & meta
|
|
auto& type = it.value->type();
|
|
auto& meta = it.value->type_metadata();
|
|
data.set("type", type.name());
|
|
|
|
JsonObject metadata_object;
|
|
metadata_object.set("length", meta.length);
|
|
metadata_object.set("format", meta.format);
|
|
metadata_object.set("alignment", Gfx::to_string(meta.alignment));
|
|
save_format(meta.static_format, metadata_object);
|
|
|
|
data.set("type_metadata", move(metadata_object));
|
|
|
|
// Set conditional formats
|
|
JsonArray conditional_formats;
|
|
for (auto& fmt : it.value->conditional_formats()) {
|
|
JsonObject fmt_object;
|
|
fmt_object.set("condition", fmt.condition);
|
|
save_format(fmt, fmt_object);
|
|
|
|
conditional_formats.append(move(fmt_object));
|
|
}
|
|
|
|
data.set("conditional_formats", move(conditional_formats));
|
|
|
|
auto& evaluated_formats = it.value->evaluated_formats();
|
|
JsonObject evaluated_formats_obj;
|
|
|
|
save_format(evaluated_formats, evaluated_formats_obj);
|
|
data.set("evaluated_formats", move(evaluated_formats_obj));
|
|
|
|
cells.set(key, move(data));
|
|
}
|
|
object.set("cells", move(cells));
|
|
|
|
return object;
|
|
}
|
|
|
|
Vector<Vector<String>> Sheet::to_xsv() const
|
|
{
|
|
Vector<Vector<String>> data;
|
|
|
|
auto bottom_right = written_data_bounds();
|
|
|
|
// First row = headers.
|
|
size_t column_count = m_columns.size();
|
|
if (columns_are_standard()) {
|
|
column_count = bottom_right.column + 1;
|
|
Vector<String> cols;
|
|
for (size_t i = 0; i < column_count; ++i)
|
|
cols.append(m_columns[i]);
|
|
data.append(move(cols));
|
|
} else {
|
|
data.append(m_columns);
|
|
}
|
|
|
|
for (size_t i = 0; i <= bottom_right.row; ++i) {
|
|
Vector<String> row;
|
|
row.resize(column_count);
|
|
for (size_t j = 0; j < column_count; ++j) {
|
|
auto cell = at({ j, i });
|
|
if (cell) {
|
|
auto result = cell->typed_display();
|
|
if (result.has_value())
|
|
row[j] = result.value();
|
|
}
|
|
}
|
|
|
|
data.append(move(row));
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
RefPtr<Sheet> Sheet::from_xsv(Reader::XSV const& xsv, Workbook& workbook)
|
|
{
|
|
auto cols = xsv.headers();
|
|
auto rows = xsv.size();
|
|
|
|
auto sheet = adopt_ref(*new Sheet(workbook));
|
|
if (xsv.has_explicit_headers()) {
|
|
sheet->m_columns = cols;
|
|
} else {
|
|
sheet->m_columns.ensure_capacity(cols.size());
|
|
for (size_t i = 0; i < cols.size(); ++i)
|
|
sheet->m_columns.append(String::bijective_base_from(i));
|
|
}
|
|
for (size_t i = 0; i < max(rows, Sheet::default_row_count); ++i)
|
|
sheet->add_row();
|
|
if (sheet->columns_are_standard()) {
|
|
for (size_t i = sheet->m_columns.size(); i < Sheet::default_column_count; ++i)
|
|
sheet->add_column();
|
|
}
|
|
|
|
for (auto row : xsv) {
|
|
for (size_t i = 0; i < cols.size(); ++i) {
|
|
auto str = row[i];
|
|
if (str.is_empty())
|
|
continue;
|
|
Position position { i, row.index() };
|
|
auto cell = make<Cell>(str, position, *sheet);
|
|
sheet->m_cells.set(position, move(cell));
|
|
}
|
|
}
|
|
|
|
return sheet;
|
|
}
|
|
|
|
JsonObject Sheet::gather_documentation() const
|
|
{
|
|
JsonObject object;
|
|
const JS::PropertyKey doc_name { "__documentation" };
|
|
|
|
auto add_docs_from = [&](auto& it, auto& global_object) {
|
|
auto value = global_object.get(it.key).release_value();
|
|
if (!value.is_function() && !value.is_object())
|
|
return;
|
|
|
|
auto& value_object = value.is_object() ? value.as_object() : value.as_function();
|
|
if (!value_object.has_own_property(doc_name).release_value())
|
|
return;
|
|
|
|
auto doc = value_object.get(doc_name).release_value();
|
|
if (!doc.is_string())
|
|
return;
|
|
|
|
JsonParser parser(doc.to_string_without_side_effects());
|
|
auto doc_object = parser.parse();
|
|
|
|
if (!doc_object.is_error())
|
|
object.set(it.key.to_display_string(), doc_object.value());
|
|
else
|
|
dbgln("Sheet::gather_documentation(): Failed to parse the documentation for '{}'!", it.key.to_display_string());
|
|
};
|
|
|
|
for (auto& it : interpreter().realm().global_object().shape().property_table())
|
|
add_docs_from(it, interpreter().realm().global_object());
|
|
|
|
for (auto& it : global_object().shape().property_table())
|
|
add_docs_from(it, global_object());
|
|
|
|
m_cached_documentation = move(object);
|
|
return m_cached_documentation.value();
|
|
}
|
|
|
|
String Sheet::generate_inline_documentation_for(StringView function, size_t argument_index)
|
|
{
|
|
if (!m_cached_documentation.has_value())
|
|
gather_documentation();
|
|
|
|
auto& docs = m_cached_documentation.value();
|
|
auto entry = docs.get(function);
|
|
if (entry.is_null() || !entry.is_object())
|
|
return String::formatted("{}(...???{})", function, argument_index);
|
|
|
|
auto& entry_object = entry.as_object();
|
|
size_t argc = entry_object.get("argc"sv).to_int(0);
|
|
auto argnames_value = entry_object.get("argnames"sv);
|
|
if (!argnames_value.is_array())
|
|
return String::formatted("{}(...{}???{})", function, argc, argument_index);
|
|
auto& argnames = argnames_value.as_array();
|
|
StringBuilder builder;
|
|
builder.appendff("{}(", function);
|
|
for (size_t i = 0; i < (size_t)argnames.size(); ++i) {
|
|
if (i != 0 && i < (size_t)argnames.size())
|
|
builder.append(", "sv);
|
|
if (i == argument_index)
|
|
builder.append('<');
|
|
else if (i >= argc)
|
|
builder.append('[');
|
|
builder.append(argnames[i].to_string());
|
|
if (i == argument_index)
|
|
builder.append('>');
|
|
else if (i >= argc)
|
|
builder.append(']');
|
|
}
|
|
|
|
builder.append(')');
|
|
return builder.build();
|
|
}
|
|
|
|
String Position::to_cell_identifier(Sheet const& sheet) const
|
|
{
|
|
return String::formatted("{}{}", sheet.column(column), row);
|
|
}
|
|
|
|
URL Position::to_url(Sheet const& sheet) const
|
|
{
|
|
URL url;
|
|
url.set_protocol("spreadsheet");
|
|
url.set_host("cell");
|
|
url.set_paths({ String::number(getpid()) });
|
|
url.set_fragment(to_cell_identifier(sheet));
|
|
return url;
|
|
}
|
|
|
|
CellChange::CellChange(Cell& cell, String const& previous_data)
|
|
: m_cell(cell)
|
|
, m_previous_data(previous_data)
|
|
{
|
|
m_new_data = cell.data();
|
|
}
|
|
|
|
}
|