Serenity Operating System
at master 426 lines 17 kB view raw
1/* 2 * Copyright (c) 2020-2022, the SerenityOS developers. 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include "JSIntegration.h" 8#include "Spreadsheet.h" 9#include "Workbook.h" 10#include <LibJS/Lexer.h> 11#include <LibJS/Runtime/Error.h> 12#include <LibJS/Runtime/GlobalObject.h> 13#include <LibJS/Runtime/Object.h> 14#include <LibJS/Runtime/Value.h> 15 16namespace Spreadsheet { 17 18Optional<FunctionAndArgumentIndex> get_function_and_argument_index(StringView source) 19{ 20 JS::Lexer lexer { source }; 21 // Track <identifier> <OpenParen>'s, and how many complete expressions are inside the parenthesized expression. 22 Vector<size_t> state; 23 StringView last_name; 24 Vector<StringView> names; 25 size_t open_parens_since_last_commit = 0; 26 size_t open_curlies_and_brackets_since_last_commit = 0; 27 bool previous_was_identifier = false; 28 auto token = lexer.next(); 29 while (token.type() != JS::TokenType::Eof) { 30 switch (token.type()) { 31 case JS::TokenType::Identifier: 32 previous_was_identifier = true; 33 last_name = token.value(); 34 break; 35 case JS::TokenType::ParenOpen: 36 if (!previous_was_identifier) { 37 open_parens_since_last_commit++; 38 break; 39 } 40 previous_was_identifier = false; 41 state.append(0); 42 names.append(last_name); 43 break; 44 case JS::TokenType::ParenClose: 45 previous_was_identifier = false; 46 if (open_parens_since_last_commit == 0) { 47 if (state.is_empty() || names.is_empty()) { 48 // JS Syntax error. 49 break; 50 } 51 state.take_last(); 52 names.take_last(); 53 break; 54 } 55 --open_parens_since_last_commit; 56 break; 57 case JS::TokenType::Comma: 58 previous_was_identifier = false; 59 if (open_parens_since_last_commit == 0 && open_curlies_and_brackets_since_last_commit == 0) { 60 if (!state.is_empty()) 61 state.last()++; 62 break; 63 } 64 break; 65 case JS::TokenType::BracketOpen: 66 previous_was_identifier = false; 67 open_curlies_and_brackets_since_last_commit++; 68 break; 69 case JS::TokenType::BracketClose: 70 previous_was_identifier = false; 71 if (open_curlies_and_brackets_since_last_commit > 0) 72 open_curlies_and_brackets_since_last_commit--; 73 break; 74 case JS::TokenType::CurlyOpen: 75 previous_was_identifier = false; 76 open_curlies_and_brackets_since_last_commit++; 77 break; 78 case JS::TokenType::CurlyClose: 79 previous_was_identifier = false; 80 if (open_curlies_and_brackets_since_last_commit > 0) 81 open_curlies_and_brackets_since_last_commit--; 82 break; 83 default: 84 previous_was_identifier = false; 85 break; 86 } 87 88 token = lexer.next(); 89 } 90 if (!names.is_empty() && !state.is_empty()) 91 return FunctionAndArgumentIndex { names.last(), state.last() }; 92 return {}; 93} 94 95SheetGlobalObject::SheetGlobalObject(JS::Realm& realm, Sheet& sheet) 96 : JS::GlobalObject(realm) 97 , m_sheet(sheet) 98{ 99} 100 101JS::ThrowCompletionOr<bool> SheetGlobalObject::internal_has_property(JS::PropertyKey const& name) const 102{ 103 if (name.is_string()) { 104 if (name.as_string() == "value") 105 return true; 106 if (m_sheet.parse_cell_name(name.as_string()).has_value()) 107 return true; 108 } 109 return Object::internal_has_property(name); 110} 111 112JS::ThrowCompletionOr<JS::Value> SheetGlobalObject::internal_get(const JS::PropertyKey& property_name, JS::Value receiver) const 113{ 114 if (property_name.is_string()) { 115 if (property_name.as_string() == "value") { 116 if (auto cell = m_sheet.current_evaluated_cell()) 117 return cell->js_data(); 118 119 return JS::js_undefined(); 120 } 121 if (auto pos = m_sheet.parse_cell_name(property_name.as_string()); pos.has_value()) { 122 auto& cell = m_sheet.ensure(pos.value()); 123 cell.reference_from(m_sheet.current_evaluated_cell()); 124 return cell.typed_js_data(); 125 } 126 } 127 128 return Base::internal_get(property_name, receiver); 129} 130 131JS::ThrowCompletionOr<bool> SheetGlobalObject::internal_set(const JS::PropertyKey& property_name, JS::Value value, JS::Value receiver) 132{ 133 if (property_name.is_string()) { 134 if (auto pos = m_sheet.parse_cell_name(property_name.as_string()); pos.has_value()) { 135 auto& cell = m_sheet.ensure(pos.value()); 136 if (auto current = m_sheet.current_evaluated_cell()) 137 current->reference_from(&cell); 138 139 cell.set_data(value); // FIXME: This produces un-savable state! 140 return true; 141 } 142 } 143 144 return Base::internal_set(property_name, value, receiver); 145} 146 147JS::ThrowCompletionOr<void> SheetGlobalObject::initialize(JS::Realm& realm) 148{ 149 MUST_OR_THROW_OOM(Base::initialize(realm)); 150 151 u8 attr = JS::Attribute::Configurable | JS::Attribute::Writable | JS::Attribute::Enumerable; 152 define_native_function(realm, "get_real_cell_contents", get_real_cell_contents, 1, attr); 153 define_native_function(realm, "set_real_cell_contents", set_real_cell_contents, 2, attr); 154 define_native_function(realm, "parse_cell_name", parse_cell_name, 1, attr); 155 define_native_function(realm, "current_cell_position", current_cell_position, 0, attr); 156 define_native_function(realm, "column_arithmetic", column_arithmetic, 2, attr); 157 define_native_function(realm, "column_index", column_index, 1, attr); 158 define_native_function(realm, "get_column_bound", get_column_bound, 1, attr); 159 define_native_accessor(realm, "name", get_name, nullptr, attr); 160 161 return {}; 162} 163 164void SheetGlobalObject::visit_edges(Visitor& visitor) 165{ 166 Base::visit_edges(visitor); 167 for (auto& it : m_sheet.cells()) { 168 if (auto opt_thrown_value = it.value->thrown_value(); opt_thrown_value.has_value()) 169 visitor.visit(*opt_thrown_value); 170 171 visitor.visit(it.value->evaluated_data()); 172 } 173} 174 175JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::get_name) 176{ 177 auto* this_object = TRY(vm.this_value().to_object(vm)); 178 179 if (!is<SheetGlobalObject>(this_object)) 180 return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "SheetGlobalObject"); 181 182 auto sheet_object = static_cast<SheetGlobalObject*>(this_object); 183 return JS::PrimitiveString::create(vm, sheet_object->m_sheet.name()); 184} 185 186JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::get_real_cell_contents) 187{ 188 auto* this_object = TRY(vm.this_value().to_object(vm)); 189 190 if (!is<SheetGlobalObject>(this_object)) 191 return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "SheetGlobalObject"); 192 193 auto sheet_object = static_cast<SheetGlobalObject*>(this_object); 194 195 if (vm.argument_count() != 1) 196 return vm.throw_completion<JS::TypeError>("Expected exactly one argument to get_real_cell_contents()"sv); 197 198 auto name_value = vm.argument(0); 199 if (!name_value.is_string()) 200 return vm.throw_completion<JS::TypeError>("Expected a String argument to get_real_cell_contents()"sv); 201 auto position = sheet_object->m_sheet.parse_cell_name(TRY(name_value.as_string().deprecated_string())); 202 if (!position.has_value()) 203 return vm.throw_completion<JS::TypeError>("Invalid cell name"sv); 204 205 auto const* cell = sheet_object->m_sheet.at(position.value()); 206 if (!cell) 207 return JS::js_undefined(); 208 209 if (cell->kind() == Spreadsheet::Cell::Kind::Formula) 210 return JS::PrimitiveString::create(vm, DeprecatedString::formatted("={}", cell->data())); 211 212 return JS::PrimitiveString::create(vm, cell->data()); 213} 214 215JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::set_real_cell_contents) 216{ 217 auto* this_object = TRY(vm.this_value().to_object(vm)); 218 219 if (!is<SheetGlobalObject>(this_object)) 220 return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "SheetGlobalObject"); 221 222 auto sheet_object = static_cast<SheetGlobalObject*>(this_object); 223 224 if (vm.argument_count() != 2) 225 return vm.throw_completion<JS::TypeError>("Expected exactly two arguments to set_real_cell_contents()"sv); 226 227 auto name_value = vm.argument(0); 228 if (!name_value.is_string()) 229 return vm.throw_completion<JS::TypeError>("Expected the first argument of set_real_cell_contents() to be a String"sv); 230 auto position = sheet_object->m_sheet.parse_cell_name(TRY(name_value.as_string().deprecated_string())); 231 if (!position.has_value()) 232 return vm.throw_completion<JS::TypeError>("Invalid cell name"sv); 233 234 auto new_contents_value = vm.argument(1); 235 if (!new_contents_value.is_string()) 236 return vm.throw_completion<JS::TypeError>("Expected the second argument of set_real_cell_contents() to be a String"sv); 237 238 auto& cell = sheet_object->m_sheet.ensure(position.value()); 239 auto new_contents = TRY(new_contents_value.as_string().deprecated_string()); 240 cell.set_data(new_contents); 241 return JS::js_null(); 242} 243 244JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::parse_cell_name) 245{ 246 auto& realm = *vm.current_realm(); 247 248 auto* this_object = TRY(vm.this_value().to_object(vm)); 249 250 if (!is<SheetGlobalObject>(this_object)) 251 return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "SheetGlobalObject"); 252 253 auto sheet_object = static_cast<SheetGlobalObject*>(this_object); 254 255 if (vm.argument_count() != 1) 256 return vm.throw_completion<JS::TypeError>("Expected exactly one argument to parse_cell_name()"sv); 257 auto name_value = vm.argument(0); 258 if (!name_value.is_string()) 259 return vm.throw_completion<JS::TypeError>("Expected a String argument to parse_cell_name()"sv); 260 auto position = sheet_object->m_sheet.parse_cell_name(TRY(name_value.as_string().deprecated_string())); 261 if (!position.has_value()) 262 return JS::js_undefined(); 263 264 auto object = JS::Object::create(realm, realm.intrinsics().object_prototype()); 265 object->define_direct_property("column", JS::PrimitiveString::create(vm, sheet_object->m_sheet.column(position.value().column)), JS::default_attributes); 266 object->define_direct_property("row", JS::Value((unsigned)position.value().row), JS::default_attributes); 267 268 return object; 269} 270 271JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::current_cell_position) 272{ 273 auto& realm = *vm.current_realm(); 274 275 if (vm.argument_count() != 0) 276 return vm.throw_completion<JS::TypeError>("Expected no arguments to current_cell_position()"sv); 277 278 auto* this_object = TRY(vm.this_value().to_object(vm)); 279 280 if (!is<SheetGlobalObject>(this_object)) 281 return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "SheetGlobalObject"); 282 283 auto sheet_object = static_cast<SheetGlobalObject*>(this_object); 284 auto* current_cell = sheet_object->m_sheet.current_evaluated_cell(); 285 if (!current_cell) 286 return JS::js_null(); 287 288 auto position = current_cell->position(); 289 290 auto object = JS::Object::create(realm, realm.intrinsics().object_prototype()); 291 object->define_direct_property("column", JS::PrimitiveString::create(vm, sheet_object->m_sheet.column(position.column)), JS::default_attributes); 292 object->define_direct_property("row", JS::Value((unsigned)position.row), JS::default_attributes); 293 294 return object; 295} 296 297JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::column_index) 298{ 299 if (vm.argument_count() != 1) 300 return vm.throw_completion<JS::TypeError>("Expected exactly one argument to column_index()"sv); 301 302 auto column_name = vm.argument(0); 303 if (!column_name.is_string()) 304 return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "String"); 305 306 auto column_name_str = TRY(column_name.as_string().deprecated_string()); 307 308 auto* this_object = TRY(vm.this_value().to_object(vm)); 309 310 if (!is<SheetGlobalObject>(this_object)) 311 return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "SheetGlobalObject"); 312 313 auto sheet_object = static_cast<SheetGlobalObject*>(this_object); 314 auto& sheet = sheet_object->m_sheet; 315 auto column_index = sheet.column_index(column_name_str); 316 if (!column_index.has_value()) 317 return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("'{}' is not a valid column", column_name_str))); 318 319 return JS::Value((i32)column_index.value()); 320} 321 322JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::column_arithmetic) 323{ 324 if (vm.argument_count() != 2) 325 return vm.throw_completion<JS::TypeError>("Expected exactly two arguments to column_arithmetic()"sv); 326 327 auto column_name = vm.argument(0); 328 if (!column_name.is_string()) 329 return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "String"); 330 331 auto column_name_str = TRY(column_name.as_string().deprecated_string()); 332 333 auto offset = TRY(vm.argument(1).to_number(vm)); 334 auto offset_number = static_cast<i32>(offset.as_double()); 335 336 auto* this_object = TRY(vm.this_value().to_object(vm)); 337 338 if (!is<SheetGlobalObject>(this_object)) 339 return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "SheetGlobalObject"); 340 341 auto sheet_object = static_cast<SheetGlobalObject*>(this_object); 342 auto& sheet = sheet_object->m_sheet; 343 auto new_column = sheet.column_arithmetic(column_name_str, offset_number); 344 if (!new_column.has_value()) 345 return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("'{}' is not a valid column", column_name_str))); 346 347 return JS::PrimitiveString::create(vm, new_column.release_value()); 348} 349 350JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::get_column_bound) 351{ 352 if (vm.argument_count() != 1) 353 return vm.throw_completion<JS::TypeError>("Expected exactly one argument to get_column_bound()"sv); 354 355 auto column_name = vm.argument(0); 356 if (!column_name.is_string()) 357 return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "String"); 358 359 auto column_name_str = TRY(column_name.as_string().deprecated_string()); 360 auto* this_object = TRY(vm.this_value().to_object(vm)); 361 362 if (!is<SheetGlobalObject>(this_object)) 363 return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "SheetGlobalObject"); 364 365 auto sheet_object = static_cast<SheetGlobalObject*>(this_object); 366 auto& sheet = sheet_object->m_sheet; 367 auto maybe_column_index = sheet.column_index(column_name_str); 368 if (!maybe_column_index.has_value()) 369 return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("'{}' is not a valid column", column_name_str))); 370 371 auto bounds = sheet.written_data_bounds(*maybe_column_index); 372 return JS::Value(bounds.row); 373} 374 375WorkbookObject::WorkbookObject(JS::Realm& realm, Workbook& workbook) 376 : JS::Object(ConstructWithPrototypeTag::Tag, *realm.intrinsics().object_prototype()) 377 , m_workbook(workbook) 378{ 379} 380 381JS::ThrowCompletionOr<void> WorkbookObject::initialize(JS::Realm& realm) 382{ 383 MUST_OR_THROW_OOM(Object::initialize(realm)); 384 define_native_function(realm, "sheet", sheet, 1, JS::default_attributes); 385 386 return {}; 387} 388 389void WorkbookObject::visit_edges(Visitor& visitor) 390{ 391 Base::visit_edges(visitor); 392 for (auto& sheet : m_workbook.sheets()) 393 visitor.visit(&sheet->global_object()); 394} 395 396JS_DEFINE_NATIVE_FUNCTION(WorkbookObject::sheet) 397{ 398 if (vm.argument_count() != 1) 399 return vm.throw_completion<JS::TypeError>("Expected exactly one argument to sheet()"sv); 400 auto name_value = vm.argument(0); 401 if (!name_value.is_string() && !name_value.is_number()) 402 return vm.throw_completion<JS::TypeError>("Expected a String or Number argument to sheet()"sv); 403 404 auto* this_object = TRY(vm.this_value().to_object(vm)); 405 406 if (!is<WorkbookObject>(this_object)) 407 return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "WorkbookObject"); 408 409 auto& workbook = static_cast<WorkbookObject*>(this_object)->m_workbook; 410 411 if (name_value.is_string()) { 412 auto name = TRY(name_value.as_string().deprecated_string()); 413 for (auto& sheet : workbook.sheets()) { 414 if (sheet->name() == name) 415 return JS::Value(&sheet->global_object()); 416 } 417 } else { 418 auto index = TRY(name_value.to_length(vm)); 419 if (index < workbook.sheets().size()) 420 return JS::Value(&workbook.sheets()[index]->global_object()); 421 } 422 423 return JS::js_undefined(); 424} 425 426}