Serenity Operating System
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}