mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-26 01:20:25 +00:00
525f22d018
Note that js_rope_string() has been folded into this, the old name was misleading - it would not always create a rope string, only if both sides are not empty strings. Use a three-argument create() overload instead.
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_deprecated_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;
|
|
}
|
|
|
|
DeprecatedString Sheet::add_column()
|
|
{
|
|
auto next_column = DeprecatedString::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_deprecated_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<DeprecatedString> 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_deprecated_string());
|
|
return {};
|
|
}
|
|
|
|
if (url.scheme() != "spreadsheet" || url.host() != "cell") {
|
|
dbgln("Bad url: {}", url.to_deprecated_string());
|
|
return {};
|
|
}
|
|
|
|
// FIXME: Figure out a way to do this cross-process.
|
|
VERIFY(url.path() == DeprecatedString::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_deprecated_string(), position, *sheet);
|
|
break;
|
|
case Cell::Formula: {
|
|
auto& vm = sheet->interpreter().vm();
|
|
auto value_or_error = JS::call(vm, parse_function, json, JS::PrimitiveString::create(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_deprecated_string(), value_or_error.release_value(), position, *sheet);
|
|
break;
|
|
}
|
|
}
|
|
|
|
auto type_name = obj.has("type"sv) ? obj.get("type"sv).to_deprecated_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_deprecated_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] != DeprecatedString::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_deprecated_string());
|
|
if (format.background_color.has_value())
|
|
obj.set("background_color", format.background_color.value().to_deprecated_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_deprecated_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<DeprecatedString>> Sheet::to_xsv() const
|
|
{
|
|
Vector<Vector<DeprecatedString>> 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<DeprecatedString> 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<DeprecatedString> 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(DeprecatedString::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();
|
|
}
|
|
|
|
DeprecatedString 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 DeprecatedString::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 DeprecatedString::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_deprecated_string());
|
|
if (i == argument_index)
|
|
builder.append('>');
|
|
else if (i >= argc)
|
|
builder.append(']');
|
|
}
|
|
|
|
builder.append(')');
|
|
return builder.build();
|
|
}
|
|
|
|
DeprecatedString Position::to_cell_identifier(Sheet const& sheet) const
|
|
{
|
|
return DeprecatedString::formatted("{}{}", sheet.column(column), row);
|
|
}
|
|
|
|
URL Position::to_url(Sheet const& sheet) const
|
|
{
|
|
URL url;
|
|
url.set_scheme("spreadsheet");
|
|
url.set_host("cell");
|
|
url.set_paths({ DeprecatedString::number(getpid()) });
|
|
url.set_fragment(to_cell_identifier(sheet));
|
|
return url;
|
|
}
|
|
|
|
CellChange::CellChange(Cell& cell, DeprecatedString const& previous_data)
|
|
: m_cell(cell)
|
|
, m_previous_data(previous_data)
|
|
{
|
|
m_new_data = cell.data();
|
|
}
|
|
|
|
}
|