diff options
Diffstat (limited to 'Userland/Applications/Spreadsheet/Spreadsheet.cpp')
-rw-r--r-- | Userland/Applications/Spreadsheet/Spreadsheet.cpp | 734 |
1 files changed, 734 insertions, 0 deletions
diff --git a/Userland/Applications/Spreadsheet/Spreadsheet.cpp b/Userland/Applications/Spreadsheet/Spreadsheet.cpp new file mode 100644 index 0000000000..0d20b22fdd --- /dev/null +++ b/Userland/Applications/Spreadsheet/Spreadsheet.cpp @@ -0,0 +1,734 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Spreadsheet.h" +#include "JSIntegration.h" +#include "Workbook.h" +#include <AK/ByteBuffer.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/Parser.h> +#include <LibJS/Runtime/Function.h> +#include <ctype.h> + +//#define COPY_DEBUG + +namespace Spreadsheet { + +Sheet::Sheet(const 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) +{ + JS::DeferGC defer_gc(m_workbook.interpreter().heap()); + m_global_object = m_workbook.interpreter().heap().allocate_without_global_object<SheetGlobalObject>(*this); + global_object().initialize(); + global_object().put("workbook", m_workbook.workbook_object()); + global_object().put("thisSheet", &global_object()); // Self-reference is unfortunate, but required. + + // Sadly, these have to be evaluated once per sheet. + auto file_or_error = Core::File::open("/res/js/Spreadsheet/runtime.js", Core::IODevice::OpenMode::ReadOnly); + if (!file_or_error.is_error()) { + auto buffer = file_or_error.value()->read_all(); + JS::Parser parser { JS::Lexer(buffer) }; + if (parser.has_errors()) { + warnln("Spreadsheet: Failed to parse runtime code"); + parser.print_errors(); + } else { + interpreter().run(global_object(), parser.parse_program()); + if (auto exc = interpreter().exception()) { + warnln("Spreadsheet: Failed to run runtime code: "); + for (auto& t : exc->trace()) + warnln("{}", t); + interpreter().vm().clear_exception(); + } + } + } +} + +Sheet::~Sheet() +{ +} + +JS::Interpreter& Sheet::interpreter() const +{ + return m_workbook.interpreter(); +} + +size_t Sheet::add_row() +{ + return m_rows++; +} + +static String convert_to_string(size_t value, unsigned base = 26, StringView map = {}) +{ + if (map.is_null()) + map = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + ASSERT(base >= 2 && base <= map.length()); + + // The '8 bits per byte' assumption may need to go? + Array<char, round_up_to_power_of_two(sizeof(size_t) * 8 + 1, 2)> buffer; + size_t i = 0; + do { + buffer[i++] = map[value % base]; + value /= base; + } while (value > 0); + + // NOTE: Weird as this may seem, the thing that comes after 'A' is 'AA', which as a number would be '00' + // to make this work, only the most significant digit has to be in a range of (1..25) as opposed to (0..25), + // but only if it's not the only digit in the string. + if (i > 1) + --buffer[i - 1]; + + for (size_t j = 0; j < i / 2; ++j) + swap(buffer[j], buffer[i - j - 1]); + + return String { ReadonlyBytes(buffer.data(), i) }; +} + +static size_t convert_from_string(StringView str, unsigned base = 26, StringView map = {}) +{ + if (map.is_null()) + map = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + ASSERT(base >= 2 && base <= map.length()); + + size_t value = 0; + for (size_t i = str.length(); i > 0; --i) { + auto digit_value = map.find_first_of(str[i - 1]).value_or(0); + // NOTE: Refer to the note in `convert_to_string()'. + if (i == str.length() && str.length() > 1) + ++digit_value; + value = value * base + digit_value; + } + + return value; +} + +String Sheet::add_column() +{ + auto next_column = convert_to_string(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({}); + } +} + +Sheet::ValueAndException Sheet::evaluate(const StringView& source, Cell* on_behalf_of) +{ + TemporaryChange cell_change { m_current_cell_being_evaluated, on_behalf_of }; + ScopeGuard clear_exception { [&] { interpreter().vm().clear_exception(); } }; + + auto parser = JS::Parser(JS::Lexer(source)); + if (parser.has_errors() || interpreter().exception()) + return { JS::js_undefined(), interpreter().exception() }; + + auto program = parser.parse_program(); + interpreter().run(global_object(), program); + if (interpreter().exception()) { + auto exc = interpreter().exception(); + return { JS::js_undefined(), exc }; + } + + auto value = interpreter().vm().last_value(); + if (value.is_empty()) + return { JS::js_undefined(), {} }; + return { value, {} }; +} + +Cell* Sheet::at(const StringView& name) +{ + auto pos = parse_cell_name(name); + if (pos.has_value()) + return at(pos.value()); + + return nullptr; +} + +Cell* Sheet::at(const Position& position) +{ + auto it = m_cells.find(position); + + if (it == m_cells.end()) + return nullptr; + + return it->value; +} + +Optional<Position> Sheet::parse_cell_name(const StringView& name) +{ + 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 {}; + + return Position { col, row.to_uint().value() }; +} + +Optional<size_t> Sheet::column_index(const StringView& column_name) const +{ + auto index = convert_from_string(column_name); + if (m_columns.size() <= index || m_columns[index] != column_name) + return {}; + + return index; +} + +Optional<String> Sheet::column_arithmetic(const 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. + ASSERT(url.path() == String::formatted("/{}", getpid())); + + return parse_cell_name(url.fragment()); +} + +Position Sheet::offset_relative_to(const Position& base, const Position& offset, const Position& offset_base) const +{ + auto offset_column_it = m_columns.find(offset.column); + auto offset_base_column_it = m_columns.find(offset_base.column); + auto base_column_it = m_columns.find(base.column); + + if (offset_column_it.is_end()) { + dbgln("Column '{}' does not exist!", offset.column); + return base; + } + if (offset_base_column_it.is_end()) { + dbgln("Column '{}' does not exist!", offset.column); + return base; + } + if (base_column_it.is_end()) { + dbgln("Column '{}' does not exist!", offset.column); + return offset; + } + + auto new_column = column(offset_column_it.index() + base_column_it.index() - offset_base_column_it.index()); + auto new_row = offset.row + base.row - offset_base.row; + + return { move(new_column), new_row }; +} + +void Sheet::copy_cells(Vector<Position> from, Vector<Position> to, Optional<Position> resolve_relative_to) +{ + auto copy_to = [&](auto& source_position, Position target_position) { + auto& target_cell = ensure(target_position); + auto* source_cell = at(source_position); + + if (!source_cell) { + target_cell.set_data(""); + return; + } + + target_cell.copy_from(*source_cell); + }; + + if (from.size() == to.size()) { + auto from_it = from.begin(); + // FIXME: Ordering. + for (auto& position : to) + copy_to(*from_it++, position); + + return; + } + + if (to.size() == 1) { + // Resolve each index as relative to the first index offset from the selection. + auto& target = to.first(); + + for (auto& position : from) { +#ifdef COPY_DEBUG + dbg() << "Paste from '" << position.to_url() << "' to '" << target.to_url() << "'"; +#endif + copy_to(position, resolve_relative_to.has_value() ? offset_relative_to(target, position, resolve_relative_to.value()) : target); + } + + return; + } + + if (from.size() == 1) { + // Fill the target selection with the single cell. + auto& source = from.first(); + for (auto& position : to) { +#ifdef COPY_DEBUG + dbg() << "Paste from '" << source.to_url() << "' to '" << position.to_url() << "'"; +#endif + copy_to(source, resolve_relative_to.has_value() ? offset_relative_to(position, source, resolve_relative_to.value()) : position); + } + return; + } + + // Just disallow misaligned copies. + dbgln("Cannot copy {} cells to {} cells", from.size(), to.size()); +} + +RefPtr<Sheet> Sheet::from_json(const JsonObject& object, Workbook& workbook) +{ + auto sheet = adopt(*new Sheet(workbook)); + auto rows = object.get("rows").to_u32(default_row_count); + auto columns = object.get("columns"); + auto name = object.get("name").as_string_or("Sheet"); + + 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 cells = object.get("cells").as_object(); + auto json = sheet->interpreter().global_object().get("JSON"); + auto& parse_function = json.as_object().get("parse").as_function(); + + auto read_format = [](auto& format, const auto& obj) { + if (auto value = obj.get("foreground_color"); value.is_string()) + format.foreground_color = Color::from_string(value.as_string()); + if (auto value = obj.get("background_color"); value.is_string()) + format.background_color = Color::from_string(value.as_string()); + }; + + cells.for_each_member([&](auto& name, JsonValue& value) { + auto position_option = 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").as_string_or("LiteralString") == "LiteralString" ? Cell::LiteralString : Cell::Formula; + + OwnPtr<Cell> cell; + switch (kind) { + case Cell::LiteralString: + cell = make<Cell>(obj.get("value").to_string(), position, *sheet); + break; + case Cell::Formula: { + auto& interpreter = sheet->interpreter(); + auto value = interpreter.vm().call(parse_function, json, JS::js_string(interpreter.heap(), obj.get("value").as_string())); + cell = make<Cell>(obj.get("source").to_string(), move(value), position, *sheet); + break; + } + } + + auto type_name = obj.get_or("type", "Numeric").to_string(); + cell->set_type(type_name); + + auto type_meta = obj.get("type_metadata"); + if (type_meta.is_object()) { + auto& meta_obj = type_meta.as_object(); + auto meta = cell->type_metadata(); + if (auto value = meta_obj.get("length"); value.is_number()) + meta.length = value.to_i32(); + if (auto value = meta_obj.get("format"); value.is_string()) + meta.format = value.as_string(); + read_format(meta.static_format, meta_obj); + + cell->set_type_metadata(move(meta)); + } + + auto conditional_formats = obj.get("conditional_formats"); + 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").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"); + 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() const +{ + Position bound; + for (auto& entry : m_cells) { + 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 'convert_to_string()' +bool Sheet::columns_are_standard() const +{ + for (size_t i = 0; i < m_columns.size(); ++i) { + if (m_columns[i] != convert_to_string(i)) + return false; + } + + return true; +} + +JsonObject Sheet::to_json() const +{ + JsonObject object; + object.set("name", m_name); + + auto save_format = [](const auto& 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(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) { + data.set("source", it.value->data()); + auto json = interpreter().global_object().get("JSON"); + auto stringified = interpreter().vm().call(json.as_object().get("stringify").as_function(), json, it.value->evaluated_data()); + data.set("value", stringified.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); +#if 0 + metadata_object.set("alignment", alignment_to_string(meta.alignment)); +#endif + 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 = convert_from_string(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({ m_columns[j], i }); + if (cell) + row[j] = cell->typed_display(); + } + + data.append(move(row)); + } + + return data; +} + +RefPtr<Sheet> Sheet::from_xsv(const Reader::XSV& xsv, Workbook& workbook) +{ + auto cols = xsv.headers(); + auto rows = xsv.size(); + + auto sheet = adopt(*new Sheet(workbook)); + sheet->m_columns = cols; + 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 { cols[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::PropertyName doc_name { "__documentation" }; + + auto add_docs_from = [&](auto& it, auto& global_object) { + auto value = global_object.get(it.key); + 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)) + return; + + dbgln("Found '{}'", it.key.to_display_string()); + auto doc = value_object.get(doc_name); + if (!doc.is_string()) + return; + + JsonParser parser(doc.to_string_without_side_effects()); + auto doc_object = parser.parse(); + + if (doc_object.has_value()) + 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().global_object().shape().property_table()) + add_docs_from(it, interpreter().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").to_int(0); + auto argnames_value = entry_object.get("argnames"); + 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(", "); + 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(); +} + +} |