From 81daf6f387c4682e51faa856ea961ed232e60194 Mon Sep 17 00:00:00 2001 From: "G. Gibson" Date: Tue, 16 Nov 2021 08:58:51 -0800 Subject: [PATCH] Initial Commit --- LICENSE.txt | 31 + README.md | 11 +- gxg/core/alias.rb | 7 + gxg/core/alpha.rb | 810 ++ gxg/core/argf.rb | 573 ++ gxg/core/array.rb | 2324 +++++ gxg/core/array_mirror.rb | 96 + gxg/core/atomic.rb | 43 + gxg/core/autoload.rb | 119 + gxg/core/backtrace.rb | 163 + gxg/core/basic_object.rb | 162 + gxg/core/binding.rb | 116 + gxg/core/block_environment.rb | 183 + gxg/core/byte_array.rb | 127 + gxg/core/call_site.rb | 76 + gxg/core/capi.rb | 166 + gxg/core/channel.rb | 40 + gxg/core/character.rb | 55 + gxg/core/class.rb | 67 + gxg/core/code_db.rb | 41 + gxg/core/code_loader.rb | 348 + gxg/core/compact_lookup_table.rb | 33 + gxg/core/comparable.rb | 63 + gxg/core/compiled_code.rb | 726 ++ gxg/core/complex.rb | 340 + gxg/core/complexifier.rb | 59 + gxg/core/configuration.rb | 34 + gxg/core/console.rb | 31 + gxg/core/constant_cache.rb | 28 + gxg/core/constant_table.rb | 97 + gxg/core/continuation.rb | 43 + gxg/core/ctype.rb | 90 + gxg/core/delegated_method.rb | 41 + gxg/core/deprecations.rb | 29 + gxg/core/dir.rb | 269 + gxg/core/dir_glob.rb | 460 + gxg/core/encoding.rb | 570 ++ gxg/core/enumerable.rb | 864 ++ gxg/core/enumerable_helper.rb | 13 + gxg/core/enumerator.rb | 500 + gxg/core/env.rb | 294 + gxg/core/errno.rb | 42 + gxg/core/exception.rb | 578 ++ gxg/core/executable.rb | 44 + gxg/core/false.rb | 23 + gxg/core/ffi.rb | 528 ++ gxg/core/fiber.rb | 73 + gxg/core/file.rb | 1289 +++ gxg/core/file_test.rb | 119 + gxg/core/float.rb | 602 ++ gxg/core/fsevent.rb | 33 + gxg/core/gc.rb | 137 + gxg/core/global.rb | 159 + gxg/core/hash.rb | 1004 ++ gxg/core/hook.rb | 23 + gxg/core/identity_map.rb | 307 + gxg/core/immediate.rb | 31 + gxg/core/integer.rb | 1580 ++++ gxg/core/io.rb | 3609 +++++++ gxg/core/iseq.rb | 36 + gxg/core/jit.rb | 32 + gxg/core/kernel.rb | 1142 +++ gxg/core/lexical_scope.rb | 181 + gxg/core/library.rb | 319 + gxg/core/load_order.txt | 130 + gxg/core/loader.rb | 872 ++ gxg/core/location.rb | 167 + gxg/core/logger.rb | 99 + gxg/core/lookup_table.rb | 134 + gxg/core/lru_cache.rb | 147 + gxg/core/main.rb | 35 + gxg/core/marshal.rb | 1227 +++ gxg/core/match_data.rb | 208 + gxg/core/math.rb | 250 + gxg/core/method.rb | 362 + gxg/core/method_equality.rb | 18 + gxg/core/method_table.rb | 143 + gxg/core/metrics.rb | 32 + gxg/core/mirror.rb | 48 + gxg/core/missing_method.rb | 37 + gxg/core/module.rb | 797 ++ gxg/core/mutex.rb | 82 + gxg/core/native_method.rb | 86 + gxg/core/nil.rb | 53 + gxg/core/numeric.rb | 289 + gxg/core/numeric_mirror.rb | 74 + gxg/core/object_space.rb | 88 + gxg/core/options.rb | 254 + gxg/core/pack.rb | 27 + gxg/core/peg.rb | 347 + gxg/core/pointer.rb | 468 + gxg/core/pointer_accessors.rb | 838 ++ gxg/core/prediction.rb | 14 + gxg/core/proc.rb | 276 + gxg/core/proc_mirror.rb | 37 + gxg/core/process.rb | 794 ++ gxg/core/process_mirror.rb | 256 + gxg/core/random.rb | 188 + gxg/core/range.rb | 318 + gxg/core/range_mirror.rb | 61 + gxg/core/rational.rb | 383 + gxg/core/rationalizer.rb | 52 + gxg/core/rbconfig.rb | 277 + gxg/core/regexp.rb | 594 ++ gxg/core/respond_to_cache.rb | 22 + gxg/core/rubinius.rb | 349 + gxg/core/ruby_constants.rb | 14 + gxg/core/runtime.rb | 175 + gxg/core/signal.rb | 71 + gxg/core/splitter.rb | 157 + gxg/core/sprinter.rb | 1134 +++ gxg/core/stat.rb | 298 + gxg/core/stats.rb | 108 + gxg/core/string.rb | 2691 ++++++ gxg/core/string_mirror.rb | 76 + gxg/core/struct.rb | 264 + gxg/core/symbol.rb | 162 + gxg/core/thread.rb | 454 + gxg/core/thread_group.rb | 39 + gxg/core/thread_group_mirror.rb | 12 + gxg/core/thread_mirror.rb | 54 + gxg/core/throw_catch.rb | 23 + gxg/core/thunk.rb | 43 + gxg/core/time.rb | 520 + gxg/core/true.rb | 25 + gxg/core/tuple.rb | 355 + gxg/core/type.rb | 794 ++ gxg/core/undefined.rb | 7 + gxg/core/variable_scope.rb | 236 + gxg/core/vm.rb | 34 + gxg/core/weakref.rb | 43 + gxg/core/zed.rb | 1916 ++++ gxg/deps/chronic.rb | 3448 +++++++ gxg/deps/mimemagic.rb | 2043 ++++ gxg/deps/rexml/attlistdecl.rb | 63 + gxg/deps/rexml/attribute.rb | 192 + gxg/deps/rexml/cdata.rb | 68 + gxg/deps/rexml/child.rb | 97 + gxg/deps/rexml/comment.rb | 80 + gxg/deps/rexml/doctype.rb | 270 + gxg/deps/rexml/document.rb | 291 + gxg/deps/rexml/dtd/attlistdecl.rb | 11 + gxg/deps/rexml/dtd/dtd.rb | 47 + gxg/deps/rexml/dtd/elementdecl.rb | 18 + gxg/deps/rexml/dtd/entitydecl.rb | 57 + gxg/deps/rexml/dtd/notationdecl.rb | 40 + gxg/deps/rexml/element.rb | 1265 +++ gxg/deps/rexml/encoding.rb | 51 + gxg/deps/rexml/entity.rb | 174 + gxg/deps/rexml/formatters/default.rb | 112 + gxg/deps/rexml/formatters/pretty.rb | 142 + gxg/deps/rexml/formatters/transitive.rb | 58 + gxg/deps/rexml/functions.rb | 421 + gxg/deps/rexml/instruction.rb | 71 + gxg/deps/rexml/light/node.rb | 196 + gxg/deps/rexml/namespace.rb | 48 + gxg/deps/rexml/node.rb | 76 + gxg/deps/rexml/output.rb | 30 + gxg/deps/rexml/parent.rb | 166 + gxg/deps/rexml/parseexception.rb | 52 + gxg/deps/rexml/parsers/baseparser.rb | 533 ++ gxg/deps/rexml/parsers/lightparser.rb | 59 + gxg/deps/rexml/parsers/pullparser.rb | 197 + gxg/deps/rexml/parsers/sax2parser.rb | 273 + gxg/deps/rexml/parsers/streamparser.rb | 61 + gxg/deps/rexml/parsers/treeparser.rb | 101 + gxg/deps/rexml/parsers/ultralightparser.rb | 57 + gxg/deps/rexml/parsers/xpathparser.rb | 657 ++ gxg/deps/rexml/quickpath.rb | 266 + gxg/deps/rexml/rexml.rb | 32 + gxg/deps/rexml/sax2listener.rb | 98 + gxg/deps/rexml/security.rb | 28 + gxg/deps/rexml/source.rb | 297 + gxg/deps/rexml/streamlistener.rb | 93 + gxg/deps/rexml/syncenumerator.rb | 33 + gxg/deps/rexml/text.rb | 430 + gxg/deps/rexml/undefinednamespaceexception.rb | 9 + gxg/deps/rexml/validation/relaxng.rb | 539 ++ gxg/deps/rexml/validation/validation.rb | 144 + .../rexml/validation/validationexception.rb | 10 + gxg/deps/rexml/xmldecl.rb | 116 + gxg/deps/rexml/xmltokens.rb | 85 + gxg/deps/rexml/xpath.rb | 81 + gxg/deps/rexml/xpath_parser.rb | 704 ++ gxg/deps/xmlsimple.rb | 1040 ++ gxg/standard/English.rb | 184 + gxg/standard/abbrev.rb | 132 + gxg/standard/cmath.rb | 435 + gxg/standard/csv.rb | 2355 +++++ gxg/standard/expect.rb | 71 + gxg/standard/find.rb | 88 + gxg/standard/getoptlong.rb | 613 ++ gxg/standard/io/console/size.rb | 23 + gxg/standard/monitor.rb | 315 + gxg/standard/mutex_m.rb | 109 + gxg/standard/net/ftp.rb | 1493 +++ gxg/standard/net/http.rb | 1646 ++++ gxg/standard/net/http/backward.rb | 26 + gxg/standard/net/http/exceptions.rb | 26 + gxg/standard/net/http/generic_request.rb | 338 + gxg/standard/net/http/header.rb | 494 + gxg/standard/net/http/proxy_delta.rb | 17 + gxg/standard/net/http/request.rb | 21 + gxg/standard/net/http/requests.rb | 123 + gxg/standard/net/http/response.rb | 419 + gxg/standard/net/http/responses.rb | 299 + gxg/standard/net/http/status.rb | 83 + gxg/standard/net/https.rb | 23 + gxg/standard/net/imap.rb | 3724 ++++++++ gxg/standard/net/pop.rb | 1022 ++ gxg/standard/net/protocol.rb | 442 + gxg/standard/net/smtp.rb | 1078 +++ gxg/standard/openssl/bn.rb | 40 + gxg/standard/openssl/buffering.rb | 455 + gxg/standard/openssl/cipher.rb | 67 + gxg/standard/openssl/config.rb | 474 + gxg/standard/openssl/digest.rb | 75 + gxg/standard/openssl/pkcs5.rb | 22 + gxg/standard/openssl/pkey.rb | 25 + gxg/standard/openssl/ssl.rb | 504 + gxg/standard/openssl/x509.rb | 215 + gxg/standard/optionparser.rb | 2 + gxg/standard/optparse.rb | 2161 +++++ gxg/standard/optparse/ac.rb | 51 + gxg/standard/optparse/date.rb | 18 + gxg/standard/optparse/kwargs.rb | 17 + gxg/standard/optparse/shellwords.rb | 7 + gxg/standard/optparse/time.rb | 11 + gxg/standard/optparse/uri.rb | 7 + gxg/standard/optparse/version.rb | 71 + gxg/standard/prettyprint.rb | 556 ++ gxg/standard/prime.rb | 467 + gxg/standard/profiler.rb | 149 + gxg/standard/racc/parser.rb | 623 ++ gxg/standard/resolv-replace.rb | 76 + gxg/standard/resolv.rb | 2856 ++++++ gxg/standard/rinda/rinda.rb | 327 + gxg/standard/rinda/ring.rb | 484 + gxg/standard/rinda/tuplespace.rb | 641 ++ gxg/standard/scanf.rb | 776 ++ gxg/standard/shellwords.rb | 231 + gxg/standard/sync.rb | 329 + gxg/standard/syslog/logger.rb | 209 + gxg/standard/thwait.rb | 140 + gxg/standard/timeout.rb | 130 + gxg/standard/tracer.rb | 286 + gxg/standard/tsort.rb | 452 + gxg/standard/unicode_normalize/normalize.rb | 175 + gxg/standard/unicode_normalize/tables.rb | 1170 +++ gxg/standard/uri/common.rb | 750 ++ gxg/standard/uri/ftp.rb | 267 + gxg/standard/uri/generic.rb | 1563 +++ gxg/standard/uri/http.rb | 88 + gxg/standard/uri/https.rb | 23 + gxg/standard/uri/ldap.rb | 261 + gxg/standard/uri/ldaps.rb | 21 + gxg/standard/uri/mailto.rb | 294 + gxg/standard/uri/rfc2396_parser.rb | 544 ++ gxg/standard/uri/rfc3986_parser.rb | 125 + gxg/todo/uuidtools/uuidtools.rb | 753 ++ gxg/todo/xmlsimple/xmlsimple.rb | 1040 ++ gxg/web/gxg.rb | 301 + gxg/web/gxg_applications.rb | 519 + gxg/web/gxg_augments.rb | 1270 +++ gxg/web/gxg_comm.rb | 1081 +++ gxg/web/gxg_database.rb | 2319 +++++ gxg/web/gxg_datetime.rb | 3193 +++++++ gxg/web/gxg_elements.rb | 1887 ++++ gxg/web/gxg_events.rb | 567 ++ gxg/web/gxg_ferro.rb | 8418 +++++++++++++++++ gxg/web/gxg_foundation.rb | 2130 +++++ gxg/web/gxg_gui.rb | 1281 +++ gxg/web/gxg_gui_old.rb | 2349 +++++ gxg/web/gxg_iac.rb | 483 + gxg/web/gxg_layout.rb | 140 + gxg/web/gxg_libraries.rb | 207 + gxg/web/gxg_pre_foundation.rb | 1111 +++ gxg/web/gxg_uri.rb | 3786 ++++++++ gxg/web/gxg_vdom.rb | 2673 ++++++ gxg/web/gxg_windows.rb | 2283 +++++ gxg/web/gxg_xhr.rb | 741 ++ gxg_web.rb | 101 + update_web.sh | 5 + 283 files changed, 130672 insertions(+), 1 deletion(-) create mode 100644 LICENSE.txt create mode 100644 gxg/core/alias.rb create mode 100644 gxg/core/alpha.rb create mode 100644 gxg/core/argf.rb create mode 100644 gxg/core/array.rb create mode 100644 gxg/core/array_mirror.rb create mode 100644 gxg/core/atomic.rb create mode 100644 gxg/core/autoload.rb create mode 100644 gxg/core/backtrace.rb create mode 100644 gxg/core/basic_object.rb create mode 100644 gxg/core/binding.rb create mode 100644 gxg/core/block_environment.rb create mode 100644 gxg/core/byte_array.rb create mode 100644 gxg/core/call_site.rb create mode 100644 gxg/core/capi.rb create mode 100644 gxg/core/channel.rb create mode 100644 gxg/core/character.rb create mode 100644 gxg/core/class.rb create mode 100644 gxg/core/code_db.rb create mode 100644 gxg/core/code_loader.rb create mode 100644 gxg/core/compact_lookup_table.rb create mode 100644 gxg/core/comparable.rb create mode 100644 gxg/core/compiled_code.rb create mode 100644 gxg/core/complex.rb create mode 100644 gxg/core/complexifier.rb create mode 100644 gxg/core/configuration.rb create mode 100644 gxg/core/console.rb create mode 100644 gxg/core/constant_cache.rb create mode 100644 gxg/core/constant_table.rb create mode 100644 gxg/core/continuation.rb create mode 100644 gxg/core/ctype.rb create mode 100644 gxg/core/delegated_method.rb create mode 100644 gxg/core/deprecations.rb create mode 100644 gxg/core/dir.rb create mode 100644 gxg/core/dir_glob.rb create mode 100644 gxg/core/encoding.rb create mode 100644 gxg/core/enumerable.rb create mode 100644 gxg/core/enumerable_helper.rb create mode 100644 gxg/core/enumerator.rb create mode 100644 gxg/core/env.rb create mode 100644 gxg/core/errno.rb create mode 100644 gxg/core/exception.rb create mode 100644 gxg/core/executable.rb create mode 100644 gxg/core/false.rb create mode 100644 gxg/core/ffi.rb create mode 100644 gxg/core/fiber.rb create mode 100644 gxg/core/file.rb create mode 100644 gxg/core/file_test.rb create mode 100644 gxg/core/float.rb create mode 100644 gxg/core/fsevent.rb create mode 100644 gxg/core/gc.rb create mode 100644 gxg/core/global.rb create mode 100644 gxg/core/hash.rb create mode 100644 gxg/core/hook.rb create mode 100644 gxg/core/identity_map.rb create mode 100644 gxg/core/immediate.rb create mode 100644 gxg/core/integer.rb create mode 100644 gxg/core/io.rb create mode 100644 gxg/core/iseq.rb create mode 100644 gxg/core/jit.rb create mode 100644 gxg/core/kernel.rb create mode 100644 gxg/core/lexical_scope.rb create mode 100644 gxg/core/library.rb create mode 100644 gxg/core/load_order.txt create mode 100644 gxg/core/loader.rb create mode 100644 gxg/core/location.rb create mode 100644 gxg/core/logger.rb create mode 100644 gxg/core/lookup_table.rb create mode 100644 gxg/core/lru_cache.rb create mode 100644 gxg/core/main.rb create mode 100644 gxg/core/marshal.rb create mode 100644 gxg/core/match_data.rb create mode 100644 gxg/core/math.rb create mode 100644 gxg/core/method.rb create mode 100644 gxg/core/method_equality.rb create mode 100644 gxg/core/method_table.rb create mode 100644 gxg/core/metrics.rb create mode 100644 gxg/core/mirror.rb create mode 100644 gxg/core/missing_method.rb create mode 100644 gxg/core/module.rb create mode 100644 gxg/core/mutex.rb create mode 100644 gxg/core/native_method.rb create mode 100644 gxg/core/nil.rb create mode 100644 gxg/core/numeric.rb create mode 100644 gxg/core/numeric_mirror.rb create mode 100644 gxg/core/object_space.rb create mode 100644 gxg/core/options.rb create mode 100644 gxg/core/pack.rb create mode 100644 gxg/core/peg.rb create mode 100644 gxg/core/pointer.rb create mode 100644 gxg/core/pointer_accessors.rb create mode 100644 gxg/core/prediction.rb create mode 100644 gxg/core/proc.rb create mode 100644 gxg/core/proc_mirror.rb create mode 100644 gxg/core/process.rb create mode 100644 gxg/core/process_mirror.rb create mode 100644 gxg/core/random.rb create mode 100644 gxg/core/range.rb create mode 100644 gxg/core/range_mirror.rb create mode 100644 gxg/core/rational.rb create mode 100644 gxg/core/rationalizer.rb create mode 100644 gxg/core/rbconfig.rb create mode 100644 gxg/core/regexp.rb create mode 100644 gxg/core/respond_to_cache.rb create mode 100644 gxg/core/rubinius.rb create mode 100644 gxg/core/ruby_constants.rb create mode 100644 gxg/core/runtime.rb create mode 100644 gxg/core/signal.rb create mode 100644 gxg/core/splitter.rb create mode 100644 gxg/core/sprinter.rb create mode 100644 gxg/core/stat.rb create mode 100644 gxg/core/stats.rb create mode 100644 gxg/core/string.rb create mode 100644 gxg/core/string_mirror.rb create mode 100644 gxg/core/struct.rb create mode 100644 gxg/core/symbol.rb create mode 100644 gxg/core/thread.rb create mode 100644 gxg/core/thread_group.rb create mode 100644 gxg/core/thread_group_mirror.rb create mode 100644 gxg/core/thread_mirror.rb create mode 100644 gxg/core/throw_catch.rb create mode 100644 gxg/core/thunk.rb create mode 100644 gxg/core/time.rb create mode 100644 gxg/core/true.rb create mode 100644 gxg/core/tuple.rb create mode 100644 gxg/core/type.rb create mode 100644 gxg/core/undefined.rb create mode 100644 gxg/core/variable_scope.rb create mode 100644 gxg/core/vm.rb create mode 100644 gxg/core/weakref.rb create mode 100644 gxg/core/zed.rb create mode 100644 gxg/deps/chronic.rb create mode 100644 gxg/deps/mimemagic.rb create mode 100644 gxg/deps/rexml/attlistdecl.rb create mode 100644 gxg/deps/rexml/attribute.rb create mode 100644 gxg/deps/rexml/cdata.rb create mode 100644 gxg/deps/rexml/child.rb create mode 100644 gxg/deps/rexml/comment.rb create mode 100644 gxg/deps/rexml/doctype.rb create mode 100644 gxg/deps/rexml/document.rb create mode 100644 gxg/deps/rexml/dtd/attlistdecl.rb create mode 100644 gxg/deps/rexml/dtd/dtd.rb create mode 100644 gxg/deps/rexml/dtd/elementdecl.rb create mode 100644 gxg/deps/rexml/dtd/entitydecl.rb create mode 100644 gxg/deps/rexml/dtd/notationdecl.rb create mode 100644 gxg/deps/rexml/element.rb create mode 100644 gxg/deps/rexml/encoding.rb create mode 100644 gxg/deps/rexml/entity.rb create mode 100644 gxg/deps/rexml/formatters/default.rb create mode 100644 gxg/deps/rexml/formatters/pretty.rb create mode 100644 gxg/deps/rexml/formatters/transitive.rb create mode 100644 gxg/deps/rexml/functions.rb create mode 100644 gxg/deps/rexml/instruction.rb create mode 100644 gxg/deps/rexml/light/node.rb create mode 100644 gxg/deps/rexml/namespace.rb create mode 100644 gxg/deps/rexml/node.rb create mode 100644 gxg/deps/rexml/output.rb create mode 100644 gxg/deps/rexml/parent.rb create mode 100644 gxg/deps/rexml/parseexception.rb create mode 100644 gxg/deps/rexml/parsers/baseparser.rb create mode 100644 gxg/deps/rexml/parsers/lightparser.rb create mode 100644 gxg/deps/rexml/parsers/pullparser.rb create mode 100644 gxg/deps/rexml/parsers/sax2parser.rb create mode 100644 gxg/deps/rexml/parsers/streamparser.rb create mode 100644 gxg/deps/rexml/parsers/treeparser.rb create mode 100644 gxg/deps/rexml/parsers/ultralightparser.rb create mode 100644 gxg/deps/rexml/parsers/xpathparser.rb create mode 100644 gxg/deps/rexml/quickpath.rb create mode 100644 gxg/deps/rexml/rexml.rb create mode 100644 gxg/deps/rexml/sax2listener.rb create mode 100644 gxg/deps/rexml/security.rb create mode 100644 gxg/deps/rexml/source.rb create mode 100644 gxg/deps/rexml/streamlistener.rb create mode 100644 gxg/deps/rexml/syncenumerator.rb create mode 100644 gxg/deps/rexml/text.rb create mode 100644 gxg/deps/rexml/undefinednamespaceexception.rb create mode 100644 gxg/deps/rexml/validation/relaxng.rb create mode 100644 gxg/deps/rexml/validation/validation.rb create mode 100644 gxg/deps/rexml/validation/validationexception.rb create mode 100644 gxg/deps/rexml/xmldecl.rb create mode 100644 gxg/deps/rexml/xmltokens.rb create mode 100644 gxg/deps/rexml/xpath.rb create mode 100644 gxg/deps/rexml/xpath_parser.rb create mode 100644 gxg/deps/xmlsimple.rb create mode 100644 gxg/standard/English.rb create mode 100644 gxg/standard/abbrev.rb create mode 100644 gxg/standard/cmath.rb create mode 100644 gxg/standard/csv.rb create mode 100644 gxg/standard/expect.rb create mode 100644 gxg/standard/find.rb create mode 100644 gxg/standard/getoptlong.rb create mode 100644 gxg/standard/io/console/size.rb create mode 100644 gxg/standard/monitor.rb create mode 100644 gxg/standard/mutex_m.rb create mode 100644 gxg/standard/net/ftp.rb create mode 100644 gxg/standard/net/http.rb create mode 100644 gxg/standard/net/http/backward.rb create mode 100644 gxg/standard/net/http/exceptions.rb create mode 100644 gxg/standard/net/http/generic_request.rb create mode 100644 gxg/standard/net/http/header.rb create mode 100644 gxg/standard/net/http/proxy_delta.rb create mode 100644 gxg/standard/net/http/request.rb create mode 100644 gxg/standard/net/http/requests.rb create mode 100644 gxg/standard/net/http/response.rb create mode 100644 gxg/standard/net/http/responses.rb create mode 100644 gxg/standard/net/http/status.rb create mode 100644 gxg/standard/net/https.rb create mode 100644 gxg/standard/net/imap.rb create mode 100644 gxg/standard/net/pop.rb create mode 100644 gxg/standard/net/protocol.rb create mode 100644 gxg/standard/net/smtp.rb create mode 100644 gxg/standard/openssl/bn.rb create mode 100644 gxg/standard/openssl/buffering.rb create mode 100644 gxg/standard/openssl/cipher.rb create mode 100644 gxg/standard/openssl/config.rb create mode 100644 gxg/standard/openssl/digest.rb create mode 100644 gxg/standard/openssl/pkcs5.rb create mode 100644 gxg/standard/openssl/pkey.rb create mode 100644 gxg/standard/openssl/ssl.rb create mode 100644 gxg/standard/openssl/x509.rb create mode 100644 gxg/standard/optionparser.rb create mode 100644 gxg/standard/optparse.rb create mode 100644 gxg/standard/optparse/ac.rb create mode 100644 gxg/standard/optparse/date.rb create mode 100644 gxg/standard/optparse/kwargs.rb create mode 100644 gxg/standard/optparse/shellwords.rb create mode 100644 gxg/standard/optparse/time.rb create mode 100644 gxg/standard/optparse/uri.rb create mode 100644 gxg/standard/optparse/version.rb create mode 100644 gxg/standard/prettyprint.rb create mode 100644 gxg/standard/prime.rb create mode 100644 gxg/standard/profiler.rb create mode 100644 gxg/standard/racc/parser.rb create mode 100644 gxg/standard/resolv-replace.rb create mode 100644 gxg/standard/resolv.rb create mode 100644 gxg/standard/rinda/rinda.rb create mode 100644 gxg/standard/rinda/ring.rb create mode 100644 gxg/standard/rinda/tuplespace.rb create mode 100644 gxg/standard/scanf.rb create mode 100644 gxg/standard/shellwords.rb create mode 100644 gxg/standard/sync.rb create mode 100644 gxg/standard/syslog/logger.rb create mode 100644 gxg/standard/thwait.rb create mode 100644 gxg/standard/timeout.rb create mode 100644 gxg/standard/tracer.rb create mode 100644 gxg/standard/tsort.rb create mode 100644 gxg/standard/unicode_normalize/normalize.rb create mode 100644 gxg/standard/unicode_normalize/tables.rb create mode 100644 gxg/standard/uri/common.rb create mode 100644 gxg/standard/uri/ftp.rb create mode 100644 gxg/standard/uri/generic.rb create mode 100644 gxg/standard/uri/http.rb create mode 100644 gxg/standard/uri/https.rb create mode 100644 gxg/standard/uri/ldap.rb create mode 100644 gxg/standard/uri/ldaps.rb create mode 100644 gxg/standard/uri/mailto.rb create mode 100644 gxg/standard/uri/rfc2396_parser.rb create mode 100644 gxg/standard/uri/rfc3986_parser.rb create mode 100644 gxg/todo/uuidtools/uuidtools.rb create mode 100644 gxg/todo/xmlsimple/xmlsimple.rb create mode 100644 gxg/web/gxg.rb create mode 100644 gxg/web/gxg_applications.rb create mode 100644 gxg/web/gxg_augments.rb create mode 100644 gxg/web/gxg_comm.rb create mode 100644 gxg/web/gxg_database.rb create mode 100644 gxg/web/gxg_datetime.rb create mode 100644 gxg/web/gxg_elements.rb create mode 100644 gxg/web/gxg_events.rb create mode 100644 gxg/web/gxg_ferro.rb create mode 100644 gxg/web/gxg_foundation.rb create mode 100644 gxg/web/gxg_gui.rb create mode 100644 gxg/web/gxg_gui_old.rb create mode 100644 gxg/web/gxg_iac.rb create mode 100644 gxg/web/gxg_layout.rb create mode 100644 gxg/web/gxg_libraries.rb create mode 100644 gxg/web/gxg_pre_foundation.rb create mode 100644 gxg/web/gxg_uri.rb create mode 100644 gxg/web/gxg_vdom.rb create mode 100644 gxg/web/gxg_windows.rb create mode 100644 gxg/web/gxg_xhr.rb create mode 100644 gxg_web.rb create mode 100755 update_web.sh diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..14a7bcb --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,31 @@ +GxG Web Client Copyright (2021) (COPYRIGHT G. GIBSON)(“Licensor”) + +Hippocratic License Version Number: 2.1. + +Purpose. The purpose of this License is for the Licensor named above to permit the Licensee (as defined below) broad permission, if consistent with Human Rights Laws and Human Rights Principles (as each is defined below), to use and work with the Software (as defined below) within the full scope of Licensor’s copyright and patent rights, if any, in the Software, while ensuring attribution and protecting the Licensor from liability. + +Permission and Conditions. The Licensor grants permission by this license (“License”), free of charge, to the extent of Licensor’s rights under applicable copyright and patent law, to any person or entity (the “Licensee”) obtaining a copy of this software and associated documentation files (the “Software”), to do everything with the Software that would otherwise infringe (i) the Licensor’s copyright in the Software or (ii) any patent claims to the Software that the Licensor can license or becomes able to license, subject to all of the following terms and conditions: + +Acceptance. This License is automatically offered to every person and entity subject to its terms and conditions. Licensee accepts this License and agrees to its terms and conditions by taking any action with the Software that, absent this License, would infringe any intellectual property right held by Licensor. + +Notice. Licensee must ensure that everyone who gets a copy of any part of this Software from Licensee, with or without changes, also receives the License and the above copyright notice (and if included by the Licensor, patent, trademark and attribution notice). Licensee must cause any modified versions of the Software to carry prominent notices stating that Licensee changed the Software. For clarity, although Licensee is free to create modifications of the Software and distribute only the modified portion created by Licensee with additional or different terms, the portion of the Software not modified must be distributed pursuant to this License. If anyone notifies Licensee in writing that Licensee has not complied with this Notice section, Licensee can keep this License by taking all practical steps to comply within 30 days after the notice. If Licensee does not do so, Licensee’s License (and all rights licensed hereunder) shall end immediately. + +Compliance with Human Rights Principles and Human Rights Laws. + +Human Rights Principles. + +(a) Licensee is advised to consult the articles of the United Nations Universal Declaration of Human Rights and the United Nations Global Compact that define recognized principles of international human rights (the “Human Rights Principles”). Licensee shall use the Software in a manner consistent with Human Rights Principles. + +(b) Unless the Licensor and Licensee agree otherwise, any dispute, controversy, or claim arising out of or relating to (i) Section 1(a) regarding Human Rights Principles, including the breach of Section 1(a), termination of this License for breach of the Human Rights Principles, or invalidity of Section 1(a) or (ii) a determination of whether any Law is consistent or in conflict with Human Rights Principles pursuant to Section 2, below, shall be settled by arbitration in accordance with the Hague Rules on Business and Human Rights Arbitration (the “Rules”); provided, however, that Licensee may elect not to participate in such arbitration, in which event this License (and all rights licensed hereunder) shall end immediately. The number of arbitrators shall be one unless the Rules require otherwise. + +Unless both the Licensor and Licensee agree to the contrary: (1) All documents and information concerning the arbitration shall be public and may be disclosed by any party; (2) The repository referred to under Article 43 of the Rules shall make available to the public in a timely manner all documents concerning the arbitration which are communicated to it, including all submissions of the parties, all evidence admitted into the record of the proceedings, all transcripts or other recordings of hearings and all orders, decisions and awards of the arbitral tribunal, subject only to the arbitral tribunal’s powers to take such measures as may be necessary to safeguard the integrity of the arbitral process pursuant to Articles 18, 33, 41 and 42 of the Rules; and (3) Article 26(6) of the Rules shall not apply. + +Human Rights Laws. The Software shall not be used by any person or entity for any systems, activities, or other uses that violate any Human Rights Laws. “Human Rights Laws” means any applicable laws, regulations, or rules (collectively, “Laws”) that protect human, civil, labor, privacy, political, environmental, security, economic, due process, or similar rights; provided, however, that such Laws are consistent and not in conflict with Human Rights Principles (a dispute over the consistency or a conflict between Laws and Human Rights Principles shall be determined by arbitration as stated above). Where the Human Rights Laws of more than one jurisdiction are applicable or in conflict with respect to the use of the Software, the Human Rights Laws that are most protective of the individuals or groups harmed shall apply. + +Indemnity. Licensee shall hold harmless and indemnify Licensor (and any other contributor) against all losses, damages, liabilities, deficiencies, claims, actions, judgments, settlements, interest, awards, penalties, fines, costs, or expenses of whatever kind, including Licensor’s reasonable attorneys’ fees, arising out of or relating to Licensee’s use of the Software in violation of Human Rights Laws or Human Rights Principles. + +Failure to Comply. Any failure of Licensee to act according to the terms and conditions of this License is both a breach of the License and an infringement of the intellectual property rights of the Licensor (subject to exceptions under Laws, e.g., fair use). In the event of a breach or infringement, the terms and conditions of this License may be enforced by Licensor under the Laws of any jurisdiction to which Licensee is subject. Licensee also agrees that the Licensor may enforce the terms and conditions of this License against Licensee through specific performance (or similar remedy under Laws) to the extent permitted by Laws. For clarity, except in the event of a breach of this License, infringement, or as otherwise stated in this License, Licensor may not terminate this License with Licensee. + +Enforceability and Interpretation. If any term or provision of this License is determined to be invalid, illegal, or unenforceable by a court of competent jurisdiction, then such invalidity, illegality, or unenforceability shall not affect any other term or provision of this License or invalidate or render unenforceable such term or provision in any other jurisdiction; provided, however, subject to a court modification pursuant to the immediately following sentence, if any term or provision of this License pertaining to Human Rights Laws or Human Rights Principles is deemed invalid, illegal, or unenforceable against Licensee by a court of competent jurisdiction, all rights in the Software granted to Licensee shall be deemed null and void as between Licensor and Licensee. Upon a determination that any term or provision is invalid, illegal, or unenforceable, to the extent permitted by Laws, the court may modify this License to affect the original purpose that the Software be used in compliance with Human Rights Principles and Human Rights Laws as closely as possible. The language in this License shall be interpreted as to its fair meaning and not strictly for or against any party. + +Disclaimer. TO THE FULL EXTENT ALLOWED BY LAW, THIS SOFTWARE COMES “AS IS,” WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED, AND LICENSOR AND ANY OTHER CONTRIBUTOR SHALL NOT BE LIABLE TO ANYONE FOR ANY DAMAGES OR OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THIS LICENSE, UNDER ANY KIND OF LEGAL CLAIM. diff --git a/README.md b/README.md index 843c3d6..4484deb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ -# gxg-web-client GxG Web Client + +Installation: + +sudo gem install opal + +ln -s /path/to/opal/stdlib opal-stdlib + +Building: + +./update_web.sh diff --git a/gxg/core/alias.rb b/gxg/core/alias.rb new file mode 100644 index 0000000..9050710 --- /dev/null +++ b/gxg/core/alias.rb @@ -0,0 +1,7 @@ +module Rubinius + class Alias + attr_reader :original_exec + attr_reader :original_module + attr_reader :original_name + end +end diff --git a/gxg/core/alpha.rb b/gxg/core/alpha.rb new file mode 100644 index 0000000..b25a456 --- /dev/null +++ b/gxg/core/alpha.rb @@ -0,0 +1,810 @@ +# This is the beginning of loading Ruby code. At this point, the VM is +# bootstrapped and the fundamental data structures, primitive functions and +# the basic classes and objects are available. +# +# The classes, modules, and methods defined here provide basic functionality +# needed to load the bootstrap directory. By the end of this file, the +# following methods are available: +# +# attr_reader :sym +# attr_writer :sym +# attr_accessor :sym +# +# private :sym +# protected :sym +# +# module_function :sym +# +# These forms should be used in the rest of the core library. In core/zed.rb, +# more complete forms of these methods are provided for user code. +# +# NOTE: The order of these definitions is important. Do not +# change it without consultation. + +class NilClass + def nil? + true + end + + def ! + true + end + + def !=(other) + other.nil? ? false : true + end + + def ==(other) + other.nil? ? true : false + end + + # alias_method is not available yet + def equal?(other) + other.nil? ? true : false + end +end + +# This class encapsulates primitives that involve the VM itself rather than +# something in Ruby-land. +# +# See core/vm.rb +# +class Rubinius::VM + + # Write given string to stderr. + # + # Used to support error reporting where IO is not reliable yet. + # + def self.write_error(str) + Rubinius.primitive :vm_write_error + raise PrimitiveFailure, "Rubinius::VM.write_error primitive failed" + end + + # Prints Ruby backtrace at point of call. + # + def self.show_backtrace + Rubinius.primitive :vm_show_backtrace + raise PrimitiveFailure, "Rubinius::VM.show_backtrace primitive failed" + end + + # Reset the method cache globally for given method name. + # + def self.reset_method_cache(mod, sym) + Rubinius.primitive :vm_reset_method_cache + raise PrimitiveFailure, "Rubinius::VM.reset_method_cache primitive failed" + end +end + + +class Object + # Prints basic information about the object to stdout. + # + def __show__ + Rubinius.primitive :object_show + "" + end +end + + +class Class + + # Allocate memory for an instance of the class without initialization. + # + # The object returned is valid to use, but its #initialize method has not + # been called. In almost all cases, .new is the correct method to use + # instead. + # + # See .new + # + def allocate + Rubinius.primitive :class_allocate + raise RuntimeError, "Class#allocate primitive failed on #{self.inspect}" + end + + # Allocate and initialize an instance of the class. + # + # Default implementation: merely allocates the instance, and then calls the + # #initialize method on the object with the given arguments and block, if + # provided. + # + # See .allocate + # + def new(*args) + obj = allocate() + + Rubinius.asm(args, obj) do |args, obj| + run obj + run args + push_block + send_with_splat :initialize, 0, true + # no pop here, as .asm blocks imply a pop as they're not + # allowed to leak a stack value + end + + obj + end +end + +module Kernel + def nil? + false + end + + # Return the Class object this object is an instance of. + # + # Note that this method must always be called with an explicit receiver, + # since class by itself is a keyword. + # + def class + Rubinius.primitive :object_class + raise PrimitiveFailure, "Kernel#class primitive failed." + end + + # Return the class object of self. + # + # This is the same as #class, but named to always be available. + def __class__ + Rubinius.primitive :object_class + raise PrimitiveFailure, "Kernel#class primitive failed." + end + + # String representation of an object. + # + # By default, the representation is the name of the object's class preceded + # by a # to indicate the object is an instance thereof. + # + def to_s + "#<#{self.class.name}>" + end + + # :internal: + # + # Lowest-level implementation of raise, used internally by core library code + # until a more sophisticated version is loaded. + # + # Redefined later. + # + def raise(cls, str="", junk=nil) + Rubinius::VM.write_error "Fatal error loading core library:\n " + Rubinius::VM.write_error str + Rubinius::VM.write_error "\n" + Rubinius::VM.show_backtrace + Process.exit! 1 + end + + # Returns true if the given Class is either the class or superclass of the + # object or, when given a Module, if the Module has been included in + # object's class or one of its superclasses. Returns false otherwise. + # + # If the argument is not a Class or Module, a TypeError is raised. + # + def kind_of?(cls) + Rubinius.primitive :object_kind_of + raise TypeError, "Kernel#kind_of? requires a Class or Module argument" + end + + # Hook method invoked when object is sent a message it cannot handle. + # + # The default implementation will merely raise a NoMethodError with + # information about the message. + # + # This method may be overridden, and is often used to provide dynamic + # behaviour. An overriding version should call super if it fails to resolve + # the message. This practice ensures that the default version will be called + # if all else fails. + # + def method_missing(meth, *args) + raise NoMethodError, "Unable to send '#{meth}' on '#{self}' (#{self.class})" + end + + # :internal: + # + # Backend method for Object#dup and Object#clone. + # + # Redefined in core/kernel.rb + # + def initialize_copy(other) + end + + # Generic shallow copy of object. + # + # Copies instance variables, but does not recursively copy the objects they + # reference. Copies taintedness. + # + # In contrast to .clone, .dup can be considered as creating a new object of + # the same class and populating it with data from the object. + # + # If class-specific behaviour is desired, the class should define + # #initialize_copy and implement the behaviour there. #initialize_copy will + # automatically be called on the new object - the copy - with the original + # object as argument if defined. + # + def dup + copy = Rubinius::Type.object_class(self).allocate + + Rubinius.invoke_primitive :object_copy_object, copy, self + + Rubinius::Type.object_initialize_dup self, copy + copy + end + + # Direct shallow copy of object. + # + # Copies instance variables, but does not recursively copy the objects they + # reference. Copies taintedness and frozenness. + # + # In contrast to .dup, .clone can be considered to actually clone the + # existing object, including its internal state and any singleton methods. + # + # If class-specific behaviour is desired, the class should define + # #initialize_copy and implement the behaviour there. #initialize_copy will + # automatically be called on the new object - the copy - with the original + # object as argument if defined. + # + def clone + # Do not implement in terms of dup. It breaks rails. + # + copy = Rubinius::Type.object_class(self).allocate + + Rubinius.invoke_primitive :object_copy_object, copy, self + Rubinius.invoke_primitive :object_copy_singleton_class, copy, self + + Rubinius::Type.object_initialize_clone self, copy + + copy.freeze if frozen? + copy + end +end + + +# Module for internals. +# +# See core/rubinius.rb +# +module Rubinius + class Mirror + def self.subject=(klass) + @subject = klass + end + + def self.subject + @subject + end + end + + module Runtime + def self.dup_as_array(obj) + Rubinius.primitive :array_dup_as_array + raise PrimitiveFailure, "Rubinius::Runtime.dup_as_array primitive failed" + end + end + + # Executable abstraction for accessors. + # + class AccessVariable + + # Specialised allocation. + # + def self.allocate + Rubinius.primitive :accessvariable_allocate + raise PrimitiveFailure, "AccessVariable.allocate primitive failed" + end + + # Set up the executable. + # + # Name of variable provided without leading @, the second parameter should + # be true if the attr is writable. + # + def initialize(variable, write) + @primitive = nil + @serial = 0 + @name = "@#{variable}".to_sym + @write = write + end + + # Get the arity for this reader or writr + # + def arity + @write ? 1 : 0 + end + + # Create a getter for named instance var, without leading @. + # + def self.get_ivar(name) + new(name, false) + end + + # Create a setter for named instance var, without leading @. + # + def self.set_ivar(name) + new(name, true) + end + end + + # Simplified lookup table. + # + # See core/lookup_table.rb. + # + class LookupTable + + # Retrieve value for given key. + # + def [](key) + Rubinius.primitive :lookuptable_aref + raise PrimitiveFailure, "LookupTable#[] primitive failed" + end + + # Store value under key. + # + def []=(key, val) + Rubinius.primitive :lookuptable_store + raise PrimitiveFailure, "LookupTable#[]= primitive failed" + end + end + + # Constant table for storing methods. + # + # See core/constant_table.rb + # + class ConstantTable + # Perform lookup for constant name. + # + def lookup(name) + Rubinius.primitive :constant_table_lookup + raise PrimitiveFailure, "ConstantTable#lookup primitive failed" + end + + # Store Constant under name, with given visibility. + # + def store(name, constant, visibility) + Rubinius.primitive :constant_table_store + raise PrimitiveFailure, "ConstantTable#store primitive failed" + end + end + + # Lookup table for storing methods. + # + # See core/method_table.rb + # + class MethodTable + # Perform lookup for method name. + # + def lookup(name) + Rubinius.primitive :methodtable_lookup + raise PrimitiveFailure, "MethodTable#lookup primitive failed" + end + + # Store Executable under name, with given visibility. + # + def store(name, method_id, exec, scope, serial, visibility) + Rubinius.primitive :methodtable_store + raise PrimitiveFailure, "MethodTable#store primitive failed" + end + + # Make an alias from +original_name+ in +original_mod+ to +name+ with + # visibility +vis+ + def alias(name, visibility, original_name, original_exec, original_mod) + Rubinius.primitive :methodtable_alias + raise PrimitiveFailure, "MethodTable#alias primitive failed" + end + end +end + +class Symbol + # Produce String representation of this Symbol. + # + def to_s + Rubinius.primitive :symbol_to_s + raise PrimitiveFailure, "Symbol#to_s primitive failed" + end + + # For completeness, returns self. + # + def to_sym + self + end +end + +class String + # Returns the Symbol corresponding to self, creating the + # symbol if it did not previously exist. See Symbol#id2name. + # + # "Koala".intern #=> :Koala + # s = 'cat'.to_sym #=> :cat + # s == :cat #=> true + # s = '@cat'.to_sym #=> :@cat + # s == :@cat #=> true + # + # This can also be used to create symbols that cannot be represented using the + # :xxx notation. + # + # 'cat and dog'.to_sym #=> :"cat and dog" + #-- + # TODO: Add taintedness-check + #++ + def to_sym + Rubinius.primitive :symbol_lookup + raise PrimitiveFailure, "String#to_sym primitive failed: #{self.dump}" + end + + # For completeness, returns self. + # + def to_s + self + end +end + +module Process + # Terminate with given status code. + # + def self.exit(code=0) + case code + when true + code = 0 + when false + code = 1 + else + code = Rubinius::Type.coerce_to code, Integer, :to_int + end + + raise SystemExit.new(code) + end + + def self.exit!(code=1) + Rubinius.primitive :vm_exit + + case code + when true + exit! 0 + when false + exit! 1 + else + exit! Rubinius::Type.coerce_to(code, Integer, :to_int) + end + end +end + +class Module + def method_table ; @method_table ; end + def constant_table ; @constant_table ; end + def name ; @module_name.to_s ; end + def origin ; @origin ; end + + # Specialised allocator. + # + def self.allocate + Rubinius.primitive :module_allocate + raise PrimitiveFailure, "Module.allocate primitive failed" + end + + # :internal: + # + # Hook called when a constant cannot be located. + # + # Default implementation 'raises', but we don't use #raise to prevent + # infinite recursion. + # + # Redefined in core/module.rb + # + def const_missing(name) + Rubinius::VM.write_error "Missing or uninitialized constant: #{name.to_s}" + end + + # Set Module's direct superclass. + # + # The corresponding 'getter' #superclass method is defined in class.rb, + # because it is more complex than a mere accessor + # + def superclass=(other) + Rubinius.check_frozen + @superclass = other + end + + # Module's stored superclass. + # + # This may be either an included Module or an inherited Class. + # + def direct_superclass + @superclass + end + + # :internal: + # + # Perform actual work for including a Module in this one. + # + # Redefined in core/module.rb. + # + def append_features(mod) + im = Rubinius::IncludedModule.new(self) + im.attach_to mod + end + + # Hook method called on Module when another Module is .include'd into it. + # + # Override for module-specific behaviour. + # + def included(mod); end + + # :internal: + # + # Basic version of .include used while loading core library code. + # + # Redefined in core/module.rb. + # + def include(mod) + mod.append_features(self) + Rubinius.privately do + mod.included self + end + self + end + + # :internal: + # + # Basic version used while loading core library code. + # + # Redefined in core/module.rb. + # + def attr_reader(name) + Rubinius::VM.reset_method_cache self, name + meth = Rubinius::AccessVariable.get_ivar name + @method_table.store name, nil, meth, nil, 0, :public + nil + end + + def attr_reader_specific(name, method_name) + Rubinius::VM.reset_method_cache self, method_name + meth = Rubinius::AccessVariable.get_ivar name + @method_table.store method_name, nil, meth, nil, 0, :public + nil + end + + # :internal: + # + # Basic version used while loading core library code. + # + # Redefined in core/module.rb. + # + def attr_writer(name) + meth = Rubinius::AccessVariable.set_ivar name + writer_name = "#{name}=".to_sym + Rubinius::VM.reset_method_cache self, writer_name + @method_table.store writer_name, nil, meth, nil, 0, :public + nil + end + + # :internal: + # + # Basic version used while loading core library code. + # + # Redefined in core/module.rb. + # + def attr_accessor(name) + attr_reader(name) + attr_writer(name) + nil + end + + # :internal: + # + # Basic version used while loading core library code. Cannot be used as a + # toggle, and only takes a single method name. + # + # Redefined in core/module.rb. + # + def public(name) + if entry = @method_table.lookup(name) + entry.visibility = :public + end + end + + # :internal: + # + # Basic version used while loading core library code. Cannot be used as a + # toggle, and only takes a single method name. + # + # Redefined in core/module.rb. + # + def private(name) + if entry = @method_table.lookup(name) + entry.visibility = :private + end + end + + # :internal: + # + # Basic version used while loading core library code. Cannot be used as a + # toggle, and only takes a single method name. + # + # Redefined in core/module.rb. + # + def protected(name) + if entry = @method_table.lookup(name) + entry.visibility = :protected + end + end + + # :internal: + # + # Basic version used while loading core library code. Creates a copy of + # current method and stores it under the new name. The two are independent. + # + # Redefined in core/module.rb. + # + def alias_method(new_name, current_name) + Rubinius::VM.reset_method_cache self, new_name + + # If we're aliasing a method we contain, just reference it directly, no + # need for the alias wrapper + if entry = @method_table.lookup(current_name) + @method_table.store new_name, entry.method_id, entry.get_method, + entry.scope, entry.serial, entry.visibility + else + mod = direct_superclass() + while mod + entry = mod.method_table.lookup(current_name) + break if entry + mod = mod.direct_superclass + end + + unless entry + raise NoMethodError, "No method '#{current_name}' to alias to '#{new_name}'" + end + + @method_table.alias new_name, entry.visibility, current_name, + entry.get_method, mod + end + end + + # :internal: + # + # Basic version used while loading core library code. Only takes a single + # method name. + # + # Redefined in core/module.rb. + # + def module_function(name) + if entry = @method_table.lookup(name) + sc = class << self; self; end + Rubinius::VM.reset_method_cache sc, name + sc.method_table.store name, entry.method_id, entry.method, + entry.scope, entry.serial, :public + private name + end + end + + def custom_call_site(name) + if entry = @method_table.lookup(name) + entry.get_method.custom_call_site + end + end + + def undef_method(name) + Rubinius::VM.reset_method_cache self, name + @method_table.store name, nil, nil, nil, 0, :undef + name + end +end + +module Rubinius + class AccessVariable + attr_reader :name + end + + # Visibility handling for MethodTables. + # + # See core/method_table.rb + # + class MethodTable::Bucket + attr_accessor :visibility + attr_accessor :method_id + attr_accessor :method + attr_accessor :scope + attr_accessor :serial + + def get_method + Rubinius.primitive :methodtable_bucket_get_method + raise PrimitiveFailure, "MethodTable::Bucket#get_method primitive failed" + end + + def public? + @visibility == :public + end + + def private? + @visibility == :private + end + + def protected? + @visibility == :protected + end + end + + # :internal: + # + # Internal representation of a Module's inclusion in another. + # + # Abstracts the injection of the included Module into the ancestor hierarchy + # for method- and constant lookup in a roughly transparent fashion. + # + # This class is known to the VM. + # + class IncludedModule < Module + attr_reader :superclass + attr_reader :module + + # :internal: + # + # Specialised allocator. + # + def self.allocate + Rubinius.primitive :included_module_allocate + raise PrimitiveFailure, "IncludedModule.allocate primitive failed" + end + + # :internal: + # + # Created referencing the Module that is being included. + # + def initialize(mod) + @method_table = mod.method_table + @constant_table = mod.constant_table + @module = mod + end + + def module + @module + end + + # :internal: + # + # Inject self inbetween class and its previous direct superclass. + # + def attach_to(cls) + @superclass = cls.direct_superclass + cls.superclass = self + self + end + + # :internal: + # + # Name of the included Module. + # + def name + @module.name + end + + # :internal: + # + # String representation of the included Module. + # + def to_s + @module.to_s + end + + def inspect + "#" + end + + # Returns true if +other+ is the same object as self or if +other+ is the + # module this IncludedModule is for. + # + def ==(other) + if other.kind_of? IncludedModule + super || other.module == @module + else + super || other == @module + end + end + end +end + +class Object + include Kernel +end + +module Enumerable; end +class Integer; end +class IO; end +class Numeric; end +class String; end diff --git a/gxg/core/argf.rb b/gxg/core/argf.rb new file mode 100644 index 0000000..f2606ab --- /dev/null +++ b/gxg/core/argf.rb @@ -0,0 +1,573 @@ +module Rubinius + + # :internal: + # + # The virtual concatenation file of the files given on command line (or + # from $stdin if no files were given.) + # + # The only instance hereof is the object referred to by ARGF. + # + # @see ARGF + # + class ARGFClass + include Enumerable + + # :internal: + # + # Create a stateless ARGF. + # + # The actual setup is done on the fly: + # + # @see #advance! + # + def initialize + @lineno = 0 + @advance = true + @init = false + @use_stdin_only = false + @encoding_args = nil + end + + private :initialize + + # + # Set stream into binary mode. + # + # Stream is set into binary mode, i.e. 8-bit ASCII. + # Once set, the binary mode cannot be undone. Returns + # self. + # + def binmode + @binmode = true + @external = Encoding::ASCII_8BIT + self + end + + def binmode? + @binmode + end + + # + # Close stream. + # + def close + advance! + @stream.close + @advance = true unless @use_stdin_only + @lineno = 0 + @binmode = false + @external = nil + self + end + + # + # True if the stream is closed. + # + def closed? + advance! + @stream.closed? + end + + def default_value + "".encode(encoding) + end + + # + # Linewise iteration. + # + # Yields one line from stream at a time, as given by + # #gets. An Enumerator is returned if no block is + # provided. Returns nil if no content, self otherwise. + # + # @see #gets. + # + def each_line(sep=$/) + return to_enum :each_line, sep unless block_given? + return nil unless advance! + + while line = gets(sep) + yield line + end + self + end + alias_method :lines, :each_line + alias_method :each, :each_line + + # + # Bytewise iteration. + # + # Yields one byte at a time from stream, an Integer + # as given by #getc. An Enumerator is returned if no + # block is provided. Returns self. + # + # @see #getc + # + def each_byte + return to_enum :each_byte unless block_given? + while ch = getbyte() + yield ch + end + self + end + alias_method :bytes, :each_byte + + # + # Character-wise iteration. + # + # Yields one character at a time from stream. An + # Enumerator is returned if no block is provided. + # Returns self. + # + # The characters yielded are gotten from #getc. + # + # @see #getc + # + def each_char + return to_enum :each_char unless block_given? + while c = getc() + yield c.chr + end + self + end + alias_method :chars, :each_char + + def each_codepoint + return to_enum :each_codepoint unless block_given? + + while c = getc + yield c.ord + end + + self + end + alias_method :codepoints, :each_codepoint + + def encoding + @external || Encoding.default_external + end + + # + # Query whether stream is at end-of-file. + # + # True if there is a stream and it is in EOF + # status. + # + def eof? + @stream and @stream.eof? + end + alias_method :eof, :eof? + + # + # File descriptor number for stream. + # + # Returns a file descriptor number for the stream being + # read out of. + # + # @todo Check correctness, does this imply there may be + # multiple FDs and if so, is this correct? --rue + # + def fileno + raise ArgumentError, "No stream" unless advance! + @stream.fileno + end + alias_method :to_i, :fileno + + # + # File path currently in use. + # + # Path to file from which read currently is + # occurring, or an indication that the stream + # is STDIN. + # + def filename + advance! + @filename + end + alias_method :path, :filename + + # + # Current stream object. + # + # This may change during the course of execution, + # but is the current one! + # + def file + advance! + @stream + end + + def getbyte + while true + return nil unless advance! + if val = @stream.getbyte + return val + end + + return nil if @use_stdin_only + @stream.close unless @stream.closed? + @advance = true + end + end + + # + # Return one character from stream. + # + # If a character cannot be returned and we are + # reading from a file, the stream is closed. + # + def getc + while true + return nil unless advance! + if val = @stream.getc + return val + end + + return nil if @use_stdin_only + @stream.close unless @stream.closed? + @advance = true + end + end + + # + # Return next line of text from stream. + # + # If a line cannot be returned and we are + # reading from a file, the stream is closed. + # + # The mechanism does track the line numbers, + # and updates $. accordingly. + # + def gets(sep=$/) + while true + return nil unless advance! + line = @stream.gets(sep) + + unless line + return nil if @use_stdin_only + @stream.close unless @stream.closed? + @advance = true + next + end + + @lineno += 1 + $. = @lineno + return line + end + end + + # + # Return current line number. + # + # Line numbers are maintained when using the linewise + # access methods. + # + # @see #gets + # @see #each_line + # + attr_reader :lineno + + # + # Set current line number. + # + # Also sets $. accordingly. + # + # @todo Should this be public? --rue + # + def lineno=(val) + $. = @lineno = val + end + + # + # Return stream position for seeking etc. + # + # @see IO#pos. + # + def pos + raise ArgumentError, "no stream" unless advance! + @stream.tell + end + alias_method :tell, :pos + + # + # Set stream position to a previously obtained position. + # + # @see IO#pos= + # + def pos=(position) + raise ArgumentError, "no stream" unless advance! + @stream.pos = position + end + + # + # Read a byte from stream. + # + # Similar to #getc, but raises an EOFError if + # EOF has been reached. + # + # @see #getc + # + def readbyte + advance! + + if val = getc() + return val + end + + raise EOFError, "ARGF at end" + end + alias_method :readchar, :readbyte + + # + # Read number of bytes or all, optionally into buffer. + # + # If number of bytes is not given or is nil, tries to read + # all of the stream, which is then closed. If the number is + # specified, then at most that many bytes will be read. + # + # A buffer responding to #<< may be provided as the second + # argument. The data read is pushed into it. If no buffer + # is provided, as by default, a String with the data is + # returned instead. + # + def read(bytes=nil, output=nil) + # The user might try to pass in nil, so we have to check here + output ||= default_value + output.clear + + if bytes + bytes_left = bytes + + until bytes_left == 0 + return output unless advance! + + if res = @stream.read(bytes_left) + output << res + bytes_left -= res.size + else + break if @use_stdin_only + @stream.close unless @stream.closed? + @advance = true + end + + end + + return output + end + + while advance! + output << @stream.read + + break if @use_stdin_only + @stream.close unless @stream.closed? + @advance = true + end + + output + end + + def read_nonblock(maxlen, output = nil, exception: true) + output ||= default_value + + unless advance! + output.clear + raise EOFError, "ARGF at end" + end + + begin + out = @stream.read_nonblock(maxlen, output, exception: exception) + + return out if out == :wait_readable + rescue EOFError => e + raise e if @use_stdin_only + + @stream.close + @advance = true + advance! or raise e + end + + return output + end + + # + # Read next line of text. + # + # As #gets, but an EOFError is raised if the stream + # is at EOF. + # + # @see #gets + # + def readline(sep=$/) + raise EOFError, "ARGF at end" unless advance! + + if line = gets(sep) + return line + end + + raise EOFError, "ARGF at end" + end + + # + # Read all lines from stream. + # + # Reads all lines into an Array using #gets and + # returns the Array. + # + # @see #gets + # + def readlines(sep=$/) + return [] unless advance! + + lines = [] + while line = gets(sep) + lines << line + end + + lines + end + + alias_method :to_a, :readlines + + def readpartial(maxlen, output=nil) + output ||= default_value + + unless advance! + output.clear + raise EOFError, "ARGF at end" + end + + begin + @stream.readpartial(maxlen, output) + rescue EOFError => e + raise e if @use_stdin_only + + @stream.close + @advance = true + advance! or raise e + end + + return output + end + + # + # Rewind the stream to its beginning. + # + # Line number is updated accordingly. + # + # @todo Is this correct, only current stream is rewound? --rue + # + def rewind + raise ArgumentError, "no stream to rewind" unless advance! + @lineno -= @stream.lineno + @stream.rewind + end + + # + # Seek into a previous position in the stream. + # + # @see IO#seek. + # + def seek(*args) + raise ArgumentError, "no stream" unless advance! + @stream.seek(*args) + end + + def set_encoding(*args) + @encoding_args = args + if @stream and !@stream.closed? + @stream.set_encoding *args + end + end + + # + # Close file stream and return self. + # + # STDIN is not closed if being used, otherwise the + # stream gets closed. Returns self. + # + def skip + return self if @use_stdin_only + @stream.close unless @stream.closed? + @advance = true + self + end + + def stream(file) + stream = file == "-" ? STDIN : File.open(file, "r", :external_encoding => encoding) + + if @encoding_args + stream.set_encoding *@encoding_args + elsif encoding + stream.set_encoding encoding + end + + stream + end + + # + # Return IO object for current stream. + # + # @see IO#to_io + # + def to_io + advance! + @stream.to_io + end + + # + # Returns "ARGF" as the string representation of this object. + # + def to_s + "ARGF" + end + + + # Internals + + # + # Main processing. + # + # If not initialised yet, sets the object up on either + # first of provided file names or STDIN. + # + # Does nothing further or later if using STDIN, but if + # there are further file names in ARGV, tries to open + # the next one as the current stream. + # + def advance! + return true unless @advance + + unless @init + + if ARGV.empty? + @advance = false + @stream = STDIN + @filename = "-" + @use_stdin_only = true + return true + end + @init = true + end + + File.unlink(@backup_filename) if @backup_filename && $-i == "" + + return false if @use_stdin_only || ARGV.empty? + + @advance = false + + file = ARGV.shift + @stream = stream(file) + @filename = file + + if $-i && @stream != STDIN + backup_extension = $-i == "" ? ".bak" : $-i + @backup_filename = "#{@filename}#{backup_extension}" + File.rename(@filename, @backup_filename) + @stream = File.open(@backup_filename, "r") + $stdout = File.open(@filename, "w") + end + + return true + end + private :advance! + end +end diff --git a/gxg/core/array.rb b/gxg/core/array.rb new file mode 100644 index 0000000..16696ab --- /dev/null +++ b/gxg/core/array.rb @@ -0,0 +1,2324 @@ +class Array + include Enumerable + + def self.allocate + Rubinius.primitive :array_allocate + raise PrimitiveFailure, "Array.allocate primitive failed" + end + + def size + @total + end + + alias_method :length, :size + + def new_range(start, count) + Rubinius.primitive :array_new_range + raise PrimitiveFailure, "Array.new_range primitive failed" + end + + def new_reserved(count) + Rubinius.primitive :array_new_reserved + raise PrimitiveFailure, "Array.new_reserved primitive failed" + end + + def []=(idx, ent) + Rubinius.check_frozen + + if idx >= @tuple.fields + new_tuple = Rubinius::Tuple.new(idx + 10) + new_tuple.copy_from @tuple, @start, @total, 0 + @tuple = new_tuple + end + + @tuple.put @start + idx, ent + if idx >= @total - 1 + @total = idx + 1 + end + return ent + end + + # Returns the element at the given index. If the + # index is negative, counts from the end of the + # Array. If the index is out of range, nil is + # returned. Slightly faster than +Array#[]+ + def at(idx) + Rubinius.primitive :array_aref + idx = Rubinius::Type.coerce_to_collection_index idx + + total = @start + @total + + if idx < 0 + idx += total + else + idx += @start + end + + if idx >= @start and idx < total + return @tuple.at idx + end + end + + # Passes each element in the Array to the given block + # and returns self. + def each + return to_enum(:each) { size } unless block_given? + + i = @start + total = i + @total + tuple = @tuple + + while i < total + yield tuple.at(i) + i += 1 + end + + self + end + + # Creates a new Array from the return values of passing + # each element in self to the supplied block. + def map + return to_enum(:map) { size } unless block_given? + out = Array.new size + + i = @start + total = i + @total + src = @tuple + + dest = Rubinius::Mirror.reflect(out).tuple + + j = 0 + while i < total + dest[j] = yield src[i] + i += 1 + j += 1 + end + + out + end + + # Replaces each element in self with the return value + # of passing that element to the supplied block. + def map! + return to_enum(:map!) { size } unless block_given? + + Rubinius.check_frozen + + i = @start + total = i + @total + tuple = @tuple + + while i < total + tuple[i] = yield tuple.at(i) + i += 1 + end + + self + end + + def to_tuple + tuple = Rubinius::Tuple.new @total + tuple.copy_from @tuple, @start, @total, 0 + tuple + end + + # The flow control for many of these methods is + # pretty evil due to how MRI works. There is + # also a lot of duplication of code due to very + # subtle processing differences and, in some + # cases, to avoid mutual dependency. Apologies. + + + def self.[](*args) + ary = allocate + ary.replace args + ary + end + + # Try to convert obj into an array, using to_ary method. + # Returns converted array or nil if obj cannot be converted + # for any reason. This method is to check if an argument is an array. + def self.try_convert(obj) + Rubinius::Type.try_convert obj, Array, :to_ary + end + + def initialize(size_or_array=undefined, obj=undefined) + Rubinius.check_frozen + + if undefined.equal?(size_or_array) + unless @total == 0 + @total = @start = 0 + @tuple = Rubinius::Tuple.new 8 + end + + return self + end + + if undefined.equal?(obj) + obj = nil + ary = nil + if size_or_array.kind_of? Integer + # Do nothing, fall through to later case. + elsif size_or_array.kind_of? Array + ary = size_or_array + elsif Rubinius::Type.object_respond_to_ary?(size_or_array) + ary = Rubinius::Type.coerce_to size_or_array, Array, :to_ary + end + + if ary + m = Rubinius::Mirror::Array.reflect ary + @tuple = m.tuple.dup + @start = m.start + @total = m.total + + return self + end + end + + size = Rubinius::Type.coerce_to_collection_length size_or_array + raise ArgumentError, "size must be positive" if size < 0 + raise ArgumentError, "size must be <= #{Fixnum::MAX}" if size > Fixnum::MAX + + if block_given? + @tuple = Rubinius::Tuple.new size + @total = i = 0 + while i < size + @tuple.put i, yield(i) + @total = i += 1 + end + else + @total = size + @tuple = Rubinius::Tuple.pattern size, obj + end + + self + end + + private :initialize + + # Replaces contents of self with contents of other, + # adjusting size as needed. + def replace(other) + Rubinius.check_frozen + + other = Rubinius::Type.coerce_to other, Array, :to_ary + + m = Rubinius::Mirror::Array.reflect other + @tuple = m.tuple.dup + @total = m.total + @start = m.start + + Rubinius::Type.infect(self, other) + self + end + + alias_method :initialize_copy, :replace + private :initialize_copy + + def [](arg1, arg2=nil) + case arg1 + + # This is split out from the generic case and put first because + # it is by far the most common case and we want to deal with it + # immediately, even at the expensive of duplicate code with the + # generic case below. In other words, don't refactor this unless + # you preserve the same or better performance. + when Fixnum + start_idx = arg1 + + # Convert negative indices + start_idx += @total if start_idx < 0 + + if arg2 + count = Rubinius::Type.coerce_to_collection_index arg2 + else + return nil if start_idx >= @total + + begin + return @tuple.at(@start + start_idx) + + # Tuple#at raises this if the index is negative or + # past the end. This is faster than checking explicitly + # since this is an exceptional case anyway. + rescue Rubinius::ObjectBoundsExceededError + return nil + end + end + when Range + start_idx = Rubinius::Type.coerce_to_collection_index arg1.begin + # Convert negative indices + start_idx += @total if start_idx < 0 + + # Check here because we must detect this boundary + # before we check the right index boundary cases + return nil if start_idx < 0 or start_idx > @total + + right_idx = Rubinius::Type.coerce_to_collection_index arg1.end + right_idx += @total if right_idx < 0 + right_idx -= 1 if arg1.exclude_end? + + return new_range(0, 0) if right_idx < start_idx + + count = right_idx - start_idx + 1 + + # Slower, less common generic coercion case. + else + start_idx = Rubinius::Type.coerce_to_collection_index arg1 + + # Convert negative indices + start_idx += @total if start_idx < 0 + + if arg2 + count = Rubinius::Type.coerce_to_collection_index arg2 + else + return nil if start_idx >= @total + + begin + return @tuple.at(@start + start_idx) + + # Tuple#at raises this if the index is negative or + # past the end. This is faster than checking explicitly + # since this is an exceptional case anyway. + rescue Rubinius::ObjectBoundsExceededError + return nil + end + end + end + + # No need to go further + return nil if count < 0 + + # Check start boundaries + if start_idx >= @total + # Odd MRI boundary case + return new_range(0, 0) if start_idx == @total + return nil + end + + return nil if start_idx < 0 + + # Check count boundaries + if start_idx + count > @total + count = @total - start_idx + end + + # Construct the subrange + return new_range(@start + start_idx, count) + end + + alias_method :slice, :[] + + def <<(obj) + set_index(@total, obj) + self + end + + alias_method :__append__, :<< + + def *(multiplier) + if multiplier.respond_to? :to_str + return join(multiplier) + + else + multiplier = Rubinius::Type.coerce_to_collection_index multiplier + + raise ArgumentError, "Count cannot be negative" if multiplier < 0 + + case @total + when 0 + # Edge case + out = self.class.allocate + Rubinius::Type.infect(out, self) + return out + when 1 + # Easy case + tuple = Rubinius::Tuple.pattern multiplier, at(0) + out = self.class.allocate + m = Rubinius::Mirror::Array.reflect out + m.tuple = tuple + m.total = multiplier + Rubinius::Type.infect(out, self) + return out + end + + new_total = multiplier * @total + new_tuple = Rubinius::Tuple.new(new_total) + + out = self.class.allocate + m = Rubinius::Mirror::Array.reflect out + m.tuple = new_tuple + m.total = new_total + Rubinius::Type.infect(out, self) + + offset = 0 + while offset < new_total + new_tuple.copy_from @tuple, @start, @total, offset + offset += @total + end + + out + end + end + + def &(other) + other = Rubinius::Type.coerce_to other, Array, :to_ary + + array = [] + im = Rubinius::IdentityMap.from other + + each { |x| array << x if im.delete x } + + array + end + + def |(other) + other = Rubinius::Type.coerce_to other, Array, :to_ary + + im = Rubinius::IdentityMap.from self, other + im.to_array + end + + def +(other) + other = Rubinius::Type.coerce_to other, Array, :to_ary + Array.new(self).concat(other) + end + + def -(other) + other = Rubinius::Type.coerce_to other, Array, :to_ary + + array = [] + im = Rubinius::IdentityMap.from other + + each { |x| array << x unless im.include? x } + + array + end + + def <=>(other) + other = Rubinius::Type.check_convert_type other, Array, :to_ary + return 0 if equal? other + return nil if other.nil? + + total = Rubinius::Mirror::Array.reflect(other).total + + Thread.detect_recursion self, other do + i = 0 + count = total < @total ? total : @total + + while i < count + order = self[i] <=> other[i] + return order unless order == 0 + + i += 1 + end + end + + # subtle: if we are recursing on that pair, then let's + # no go any further down into that pair; + # any difference will be found elsewhere if need be + @total <=> total + end + + def ==(other) + return true if equal?(other) + + unless other.kind_of?(Array) + return false unless other.respond_to?(:to_ary) + return other == self + end + + return false unless size == other.size + + Thread.detect_recursion(self, other) do + mirror = Rubinius::Mirror::Array.reflect(other) + + self_tuple = @tuple + other_tuple = mirror.tuple + + self_idx = @start + other_idx = mirror.start + + total = self_idx + @total + + while self_idx < total + return false unless self_tuple[self_idx] == other_tuple[other_idx] + + self_idx += 1 + other_idx += 1 + end + end + + true + end + + def assoc(obj) + each do |x| + if x.kind_of? Array and x.first == obj + return x + end + end + + nil + end + + def bsearch(&block) + return to_enum :bsearch unless block + + i = bsearch_index(&block) + return unless i + + m = Rubinius::Mirror::Array.reflect self + tuple = m.tuple + + tuple.at(i) + end + + def bsearch_index + return to_enum :bsearch_index unless block_given? + + m = Rubinius::Mirror::Array.reflect self + + tuple = m.tuple + + min = start = m.start + max = total = start + m.total + + last_true = nil + i = start + m.total / 2 + + while max >= min and i >= start and i < total + x = yield tuple.at(i) + + return i if x == 0 + + case x + when Numeric + if x > 0 + min = i + 1 + else + max = i - 1 + end + when true + last_true = i + max = i - 1 + when false, nil + min = i + 1 + else + raise TypeError, "block must return Numeric or boolean" + end + + i = min + (max - min) / 2 + end + + return i if max > min + return last_true if last_true + + nil + end + + def clear + Rubinius.check_frozen + + @tuple = Rubinius::Tuple.new(1) + @total = 0 + @start = 0 + self + end + + def combination(num) + num = Rubinius::Type.coerce_to_collection_index num + + unless block_given? + return to_enum(:combination, num) do + Rubinius::Mirror::Array.reflect(self).combination_size(num) + end + end + + if num == 0 + yield [] + elsif num == 1 + each do |i| + yield [i] + end + elsif num == size + yield self.dup + elsif num >= 0 && num < size + stack = Rubinius::Tuple.pattern num + 1, 0 + chosen = Rubinius::Tuple.new num + lev = 0 + done = false + stack[0] = -1 + until done + chosen[lev] = self.at(stack[lev+1]) + while lev < num - 1 + lev += 1 + chosen[lev] = self.at(stack[lev+1] = stack[lev] + 1) + end + yield chosen.to_a + lev += 1 + begin + done = lev == 0 + stack[lev] += 1 + lev -= 1 + end while stack[lev+1] + num == size + lev + 1 + end + end + self + end + + def compact + out = dup + out.untaint if out.tainted? + + Array.new(out.compact! || out) + end + + def compact! + Rubinius.check_frozen + + if (deleted = @tuple.delete(@start, @total, nil)) > 0 + @total -= deleted + reallocate_shrink() + return self + else + return nil + end + end + + def concat(other) + Rubinius.primitive :array_concat + + other = Rubinius::Type.coerce_to(other, Array, :to_ary) + Rubinius.check_frozen + + return self if other.empty? + + concat other + end + + def count(item = undefined) + seq = 0 + if !undefined.equal?(item) + each { |o| seq += 1 if item == o } + elsif block_given? + each { |o| seq += 1 if yield(o) } + else + return @total + end + seq + end + + def cycle(n=nil) + unless block_given? + return to_enum(:cycle, n) do + Rubinius::EnumerableHelper.cycle_size(size, n) + end + end + + return nil if empty? + + # Don't use nil? because, historically, lame code has overridden that method + if n.equal? nil + while true + each { |x| yield x } + end + else + n = Rubinius::Type.coerce_to_collection_index n + n.times do + each { |x| yield x } + end + end + nil + end + + def delete(obj) + key = undefined + i = @start + total = i + @total + tuple = @tuple + + while i < total + element = tuple.at i + if element == obj + # We MUST check frozen here, not at the top, because MRI + # requires that #delete not raise unless an element would + # be deleted. + Rubinius.check_frozen + tuple.put i, key + last_matched_element = element + end + i += 1 + end + + deleted = @tuple.delete @start, @total, key + if deleted > 0 + @total -= deleted + reallocate_shrink() + return last_matched_element + end + + if block_given? + yield + else + nil + end + end + + def delete_at(idx) + Rubinius.check_frozen + + idx = Rubinius::Type.coerce_to_collection_index idx + + # Flip to positive and weed out out of bounds + idx += @total if idx < 0 + return nil if idx < 0 or idx >= @total + + # Grab the object and adjust the indices for the rest + obj = @tuple.at(@start + idx) + + # Shift style. + if idx == 0 + @tuple.put @start, nil + @start += 1 + else + @tuple.copy_from(@tuple, @start+idx+1, @total-idx-1, @start+idx) + @tuple.put(@start + @total - 1, nil) + end + + @total -= 1 + obj + end + + def delete_if + return to_enum(:delete_if) { size } unless block_given? + + Rubinius.check_frozen + + return self if empty? + + i = pos = @start + total = i + @total + tuple = @tuple + + while i < total + x = tuple.at i + unless yield x + # Ok, keep the value, so stick it back into the array at + # the insert position + tuple.put pos, x + pos += 1 + end + + i += 1 + end + + @total = pos - @start + + self + end + + def dig(index, *remaining_indeces) + item = self[index] + return item if remaining_indeces.empty? || item.nil? + + raise TypeError, "#{item.class} does not have #dig method" unless item.respond_to?(:dig) + + item.dig(*remaining_indeces) + end + + def each_index + return to_enum(:each_index) { size } unless block_given? + + i = 0 + total = @total + + while i < total + yield i + i += 1 + end + + self + end + + # WARNING: This method does no boundary checking. It is expected that + # the caller handle that, eg #slice! + def delete_range(index, del_length) + # optimize for fast removal.. + reg_start = index + del_length + reg_length = @total - reg_start + + if reg_start <= @total + # If we're removing from the front, also reset @start to better + # use the Tuple + if index == 0 + # Use a shift start optimization if we're only removing one + # element and the shift started isn't already huge. + if del_length == 1 + @tuple.put @start, nil + @start += 1 + else + @tuple.copy_from @tuple, reg_start + @start, reg_length, 0 + @start = 0 + end + else + @tuple.copy_from @tuple, reg_start + @start, reg_length, + @start + index + end + + # TODO we leave the old references in the Tuple, we should + # probably clear them out though. + @total -= del_length + end + end + + private :delete_range + + def eql?(other) + return true if equal? other + return false unless other.kind_of?(Array) + return false if @total != other.size + + Thread.detect_recursion self, other do + i = 0 + each do |x| + return false unless x.eql? other[i] + i += 1 + end + end + + true + end + + def empty? + @total == 0 + end + + def fetch(idx, default=undefined) + orig = idx + idx = Rubinius::Type.coerce_to_collection_index idx + + idx += @total if idx < 0 + + if idx < 0 or idx >= @total + if block_given? + return yield(orig) + end + + return default unless undefined.equal?(default) + + raise IndexError, "index #{idx} out of bounds" + end + + at(idx) + end + + def fill(a=undefined, b=undefined, c=undefined) + Rubinius.check_frozen + + if block_given? + unless undefined.equal?(c) + raise ArgumentError, "wrong number of arguments" + end + one = a + two = b + else + if undefined.equal?(a) + raise ArgumentError, "wrong number of arguments" + end + obj = a + one = b + two = c + end + + if one.kind_of? Range + raise TypeError, "length invalid with range" unless undefined.equal?(two) + + left = Rubinius::Type.coerce_to_collection_length one.begin + left += size if left < 0 + raise RangeError, "#{one.inspect} out of range" if left < 0 + + right = Rubinius::Type.coerce_to_collection_length one.end + right += size if right < 0 + right += 1 unless one.exclude_end? + return self if right <= left # Nothing to modify + + elsif one and !undefined.equal?(one) + left = Rubinius::Type.coerce_to_collection_length one + left += size if left < 0 + left = 0 if left < 0 + + if two and !undefined.equal?(two) + begin + right = Rubinius::Type.coerce_to_collection_length two + rescue TypeError + raise ArgumentError, "second argument must be a Fixnum" + end + + return self if right == 0 + right += left + else + right = size + end + else + left = 0 + right = size + end + + total = @start + right + + if right > @total + reallocate total + @total = right + end + + # Must be after the potential call to reallocate, since + # reallocate might change @tuple + tuple = @tuple + + i = @start + left + + if block_given? + while i < total + tuple.put i, yield(i-@start) + i += 1 + end + else + while i < total + tuple.put i, obj + i += 1 + end + end + + self + end + + def first(n = undefined) + return at(0) if undefined.equal?(n) + + n = Rubinius::Type.coerce_to_collection_index n + raise ArgumentError, "Size must be positive" if n < 0 + + Array.new self[0, n] + end + + def flatten(level=-1) + level = Rubinius::Type.coerce_to_collection_index level + return self.dup if level == 0 + + out = new_reserved size + recursively_flatten(self, out, level) + Rubinius::Type.infect(out, self) + out + end + + def flatten!(level=-1) + Rubinius.check_frozen + + level = Rubinius::Type.coerce_to_collection_index level + return nil if level == 0 + + out = new_reserved size + if recursively_flatten(self, out, level) + replace(out) + return self + end + + nil + end + + def hash + hash_val = size + mask = Fixnum::MAX >> 1 + + # This is duplicated and manually inlined code from Thread for performance + # reasons. Before refactoring it, please benchmark it and compare your + # refactoring against the original. + + id = object_id + objects = Thread.current.recursive_objects + + # If there is already an our version running... + if objects.key? :__detect_outermost_recursion__ + + # If we've seen self, unwind back to the outer version + if objects.key? id + raise Thread::InnerRecursionDetected + end + + # .. or compute the hash value like normal + begin + objects[id] = true + + each { |x| hash_val = ((hash_val & mask) << 1) ^ x.hash } + ensure + objects.delete id + end + + return hash_val + else + # Otherwise, we're the outermost version of this code.. + begin + objects[:__detect_outermost_recursion__] = true + objects[id] = true + + each { |x| hash_val = ((hash_val & mask) << 1) ^ x.hash } + + # An inner version will raise to return back here, indicating that + # the whole structure is recursive. In which case, abondon most of + # the work and return a simple hash value. + rescue Thread::InnerRecursionDetected + return size + ensure + objects.delete :__detect_outermost_recursion__ + objects.delete id + end + end + + return hash_val + end + + def include?(obj) + + # This explicit loop is for performance only. Preferably, + # this method would be implemented as: + # + # each { |x| return true if x == obj } + # + # but the JIT will currently not inline the block into the + # method that calls #include? which causes #include? to + # execute about 3x slower. Since this is a very commonly + # used method, this manual performance optimization is used. + # Ideally, this will be removed when the JIT can handle the + # block used here. + + i = @start + total = i + @total + tuple = @tuple + + while i < total + return true if tuple.at(i) == obj + i += 1 + end + + false + end + + def find_index(obj=undefined) + super + end + + alias_method :index, :find_index + + def insert(idx, *items) + Rubinius.check_frozen + + return self if items.length == 0 + + # Adjust the index for correct insertion + idx = Rubinius::Type.coerce_to_collection_index idx + idx += (@total + 1) if idx < 0 # Negatives add AFTER the element + raise IndexError, "#{idx} out of bounds" if idx < 0 + + self[idx, 0] = items # Cheat + self + end + + def inspect + return "[]".force_encoding(Encoding::US_ASCII) if @total == 0 + comma = ", " + result = "[" + + return "[...]" if Thread.detect_recursion self do + each_with_index do |element, index| + temp = Rubinius::Type.coerce_inspect element + result.force_encoding(temp.encoding) if index == 0 + result << temp << comma + end + end + + Rubinius::Type.infect(result, self) + result.shorten!(2) + result << "]" + result + end + + alias_method :to_s, :inspect + + def join(sep=nil) + return "".force_encoding(Encoding::US_ASCII) if @total == 0 + + out = "" + raise ArgumentError, "recursive array join" if Thread.detect_recursion self do + sep = sep.nil? ? $, : StringValue(sep) + + # We've manually unwound the first loop entry for performance + # reasons. + x = @tuple[@start] + + if str = String.try_convert(x) + x = str + elsif ary = Array.try_convert(x) + x = ary.join(sep) + else + x = x.to_s + end + + out.force_encoding(x.encoding) + out << x + + total = @start + size() + i = @start + 1 + + while i < total + out << sep if sep + + x = @tuple[i] + + if str = String.try_convert(x) + x = str + elsif ary = Array.try_convert(x) + x = ary.join(sep) + else + x = x.to_s + end + + out << x + i += 1 + end + end + + Rubinius::Type.infect(out, self) + end + + def keep_if(&block) + return to_enum(:keep_if) { size } unless block_given? + + Rubinius.check_frozen + + replace select(&block) + end + + def last(n=undefined) + if undefined.equal?(n) + return at(-1) + elsif size < 1 + return [] + end + + n = Rubinius::Type.coerce_to_collection_index n + return [] if n == 0 + + raise ArgumentError, "count must be positive" if n < 0 + + n = size if n > size + Array.new self[-n..-1] + end + + alias_method :collect, :map + + alias_method :collect!, :map! + + def nitems + sum = 0 + each { |elem| sum += 1 unless elem.equal? nil } + sum + end + + def pack(directives) + Rubinius.primitive :array_pack + + unless directives.kind_of? String + return pack(StringValue(directives)) + end + + raise ArgumentError, "invalid directives string: #{directives}" + end + + def permutation(num=undefined, &block) + unless block_given? + return to_enum(:permutation, num) do + Rubinius::Mirror::Array.reflect(self).permutation_size(num) + end + end + + if undefined.equal? num + num = @total + else + num = Rubinius::Type.coerce_to_collection_index num + end + + if num < 0 || @total < num + # no permutations, yield nothing + elsif num == 0 + # exactly one permutation: the zero-length array + yield [] + elsif num == 1 + # this is a special, easy case + each { |val| yield [val] } + else + # this is the general case + perm = Array.new(num) + used = Array.new(@total, false) + + if block + # offensive (both definitions) copy. + offensive = dup + Rubinius.privately do + offensive.__permute__(num, perm, 0, used, &block) + end + else + __permute__(num, perm, 0, used, &block) + end + end + + self + end + + def __permute__(num, perm, index, used, &block) + # Recursively compute permutations of r elements of the set [0..n-1]. + # When we have a complete permutation of array indexes, copy the values + # at those indexes into a new array and yield that array. + # + # num: the number of elements in each permutation + # perm: the array (of size num) that we're filling in + # index: what index we're filling in now + # used: an array of booleans: whether a given index is already used + # + # Note: not as efficient as could be for big num. + @total.times do |i| + unless used[i] + perm[index] = i + if index < num-1 + used[i] = true + __permute__(num, perm, index+1, used, &block) + used[i] = false + else + yield values_at(*perm) + end + end + end + end + private :__permute__ + + def pop(many=undefined) + Rubinius.check_frozen + + if undefined.equal?(many) + return nil if @total == 0 + + @total -= 1 + index = @start + @total + + elem = @tuple.at(index) + @tuple.put index, nil + + elem + else + many = Rubinius::Type.coerce_to_collection_index many + raise ArgumentError, "negative array size" if many < 0 + + first = @total - many + first = 0 if first < 0 + + out = Array.new self[first, many] + + if many > @total + @total = 0 + else + @total -= many + end + + return out + end + end + + # Implementation notes: We build a block that will generate all the + # combinations by building it up successively using "inject" and starting + # with one responsible to append the values. + def product(*args) + args.map! { |x| Rubinius::Type.coerce_to(x, Array, :to_ary) } + + # Check the result size will fit in an Array. + sum = args.inject(size) { |n, x| n * x.size } + + if sum > Fixnum::MAX + raise RangeError, "product result is too large" + end + + # TODO rewrite this to not use a tree of Proc objects. + + # to get the results in the same order as in MRI, vary the last argument first + args.reverse! + + result = [] + args.push self + + outer_lambda = args.inject(result.method(:push)) do |trigger, values| + lambda do |partial| + values.each do |val| + trigger.call(partial.dup << val) + end + end + end + + outer_lambda.call([]) + + if block_given? + result.each { |v| yield(v) } + self + else + result + end + end + + def push(*args) + Rubinius.check_frozen + + return self if args.empty? + + concat args + end + alias_method :append, :push + + def rassoc(obj) + each do |elem| + if elem.kind_of? Array and elem.at(1) == obj + return elem + end + end + + nil + end + + def reject(&block) + return to_enum(:reject) { size } unless block_given? + Array.new(self).delete_if(&block) + end + + def reject!(&block) + return to_enum(:reject!) { size } unless block_given? + + Rubinius.check_frozen + + was = size() + delete_if(&block) + + return nil if was == size() + self + end + + def repeated_combination(combination_size, &block) + combination_size = combination_size.to_i + unless block_given? + return to_enum(:repeated_combination, combination_size) do + Rubinius::Mirror::Array.reflect(self).repeated_combination_size(combination_size) + end + end + + if combination_size < 0 + # yield nothing + else + Rubinius.privately do + dup.compile_repeated_combinations(combination_size, [], 0, combination_size, &block) + end + end + + return self + end + + def compile_repeated_combinations(combination_size, place, index, depth, &block) + if depth > 0 + (length - index).times do |i| + place[combination_size-depth] = index + i + compile_repeated_combinations(combination_size,place,index + i,depth-1, &block) + end + else + yield place.map { |element| self[element] } + end + end + + private :compile_repeated_combinations + + def repeated_permutation(combination_size, &block) + combination_size = combination_size.to_i + unless block_given? + return to_enum(:repeated_permutation, combination_size) do + Rubinius::Mirror::Array.reflect(self).repeated_permutation_size(combination_size) + end + end + + if combination_size < 0 + # yield nothing + elsif combination_size == 0 + yield [] + else + Rubinius.privately do + dup.compile_repeated_permutations(combination_size, [], 0, &block) + end + end + + return self + end + + def compile_repeated_permutations(combination_size, place, index, &block) + length.times do |i| + place[index] = i + if index < (combination_size-1) + compile_repeated_permutations(combination_size, place, index + 1, &block) + else + yield place.map { |element| self[element] } + end + end + end + + private :compile_repeated_permutations + + def reverse + Array.new dup.reverse! + end + + def reverse! + Rubinius.check_frozen + + return self unless @total > 1 + + @tuple.reverse! @start, @total + + return self + end + + def reverse_each + return to_enum(:reverse_each) { size } unless block_given? + + stop = @start - 1 + i = stop + @total + tuple = @tuple + + while i > stop + yield tuple.at(i) + i -= 1 + end + + self + end + + def rindex(obj=undefined) + if undefined.equal?(obj) + return to_enum(:rindex, obj) unless block_given? + + i = @total - 1 + while i >= 0 + return i if yield @tuple.at(@start + i) + + # Compensate for the array being modified by the block + i = @total if i > @total + + i -= 1 + end + else + stop = @start - 1 + i = stop + @total + tuple = @tuple + + while i > stop + return i - @start if tuple.at(i) == obj + i -= 1 + end + end + nil + end + + def rotate(n=1) + n = Rubinius::Type.coerce_to_collection_index n + return Array.new(self) if length == 1 + return [] if empty? + + ary = Array.new(self) + idx = n % ary.size + + ary[idx..-1].concat ary[0...idx] + end + + def rotate!(cnt=1) + Rubinius.check_frozen + + return self if length == 0 || length == 1 + + ary = rotate(cnt) + replace ary + end + + class SampleRandom + def initialize(rng) + @rng = rng + end + + private :initialize + + def rand(size) + random = Rubinius::Type.coerce_to_collection_index @rng.rand(size) + raise RangeError, "random value must be >= 0" if random < 0 + raise RangeError, "random value must be less than Array size" unless random < size + + random + end + end + + def sample(count=undefined, options=undefined) + return at Kernel.rand(size) if undefined.equal? count + + if undefined.equal? options + if o = Rubinius::Type.check_convert_type(count, Hash, :to_hash) + options = o + count = nil + else + options = nil + count = Rubinius::Type.coerce_to_collection_index count + end + else + count = Rubinius::Type.coerce_to_collection_index count + options = Rubinius::Type.coerce_to options, Hash, :to_hash + end + + if count and count < 0 + raise ArgumentError, "count must be greater than 0" + end + + rng = options[:random] if options + if rng and rng.respond_to? :rand + rng = SampleRandom.new rng + else + rng = Kernel + end + + return at rng.rand(size) unless count + + count = size if count > size + + case count + when 0 + return [] + when 1 + return [at(rng.rand(size))] + when 2 + i = rng.rand(size) + j = rng.rand(size) + if i == j + j = i == 0 ? i + 1 : i - 1 + end + return [at(i), at(j)] + else + if size / count > 3 + abandon = false + + result = Array.new count + i = 1 + + result[0] = rng.rand(size) + while i < count + k = rng.rand(size) + + j = 0 + while j < i + if k == result[j] + abandon = true + break + end + j += 1 + end + + break if abandon + + result[i] = k + + i += 1 + end + + unless abandon + i = 0 + while i < count + result[i] = at result[i] + i += 1 + end + + return result + end + end + + result = Array.new self + tuple = Rubinius::Mirror::Array.reflect(result).tuple + + count.times { |i| tuple.swap i, rng.rand(size - i) + i } + + return count == size ? result : result[0, count] + end + end + + def select(&block) + return to_enum(:select) { size } unless block_given? + + values = [] + + i = @start + total = i + @total + tuple = @tuple + + while i < total + elem = tuple.at(i) + values << elem if yield elem + i += 1 + end + + values + end + + def select!(&block) + return to_enum(:select!) { size } unless block_given? + + Rubinius.check_frozen + + ary = select(&block) + replace ary unless size == ary.size + end + + def set_index(index, ent, fin=undefined) + Rubinius.primitive :array_aset + + Rubinius.check_frozen + + ins_length = nil + unless undefined.equal? fin + ins_length = Rubinius::Type.coerce_to_collection_index ent + ent = fin # 2nd arg (ins_length) is the optional one! + end + + # Normalise Ranges + if index.kind_of? Range + if ins_length + raise ArgumentError, "Second argument invalid with a range" + end + + last = Rubinius::Type.coerce_to_collection_index index.last + last += @total if last < 0 + last += 1 unless index.exclude_end? + + index = Rubinius::Type.coerce_to_collection_index index.first + + if index < 0 + index += @total + raise RangeError, "Range begin #{index-@total} out of bounds" if index < 0 + end + + # m..n, m > n allowed + last = index if index > last + + ins_length = last - index + else + index = Rubinius::Type.coerce_to_collection_index index + + if index < 0 + index += @total + raise IndexError,"Index #{index-@total} out of bounds" if index < 0 + end + end + + if ins_length + # ins_length < 0 not allowed + raise IndexError, "Negative length #{ins_length}" if ins_length < 0 + + # MRI seems to be forgiving here! + space = @total - index + if ins_length > space + ins_length = space > 0 ? space : 0 + end + + replace_count = 0 + + if ent.kind_of? Array + replacement = ent + replace_count = replacement.size + replacement = replacement.first if replace_count == 1 + elsif ent.respond_to? :to_ary + replacement = ent.to_ary + replace_count = replacement.size + replacement = replacement.first if replace_count == 1 + else + replacement = ent + replace_count = 1 + end + + new_total = (index > @total) ? index : @total + if replace_count > ins_length + new_total += replace_count - ins_length + elsif replace_count < ins_length + new_total -= ins_length - replace_count + end + + if new_total > @tuple.size - @start + # Expand the size just like #<< does. + # MRI uses a straight realloc here to the exact size, but + # realloc can easily include bumper data so it's pretty fast. + # We simply compensate by using the same logic to reduce + # having to copy data. + new_tuple = Rubinius::Tuple.new(new_total + @tuple.size / 2) + + new_tuple.copy_from(@tuple, @start, index < @total ? index : @total, 0) + + case replace_count + when 1 + new_tuple[index] = replacement + when 0 + # nothing + else + m = Rubinius::Mirror::Array.reflect replacement + new_tuple.copy_from m.tuple, m.start, replace_count, index + end + + if index < @total + new_tuple.copy_from(@tuple, @start + index + ins_length, + @total - index - ins_length, + index + replace_count) + end + @start = 0 + @tuple = new_tuple + @total = new_total + else + # Move the elements to the right + if index < @total + right_start = @start + index + ins_length + right_len = @total - index - ins_length + + @tuple.copy_from(@tuple, right_start, right_len, + @start + index + replace_count) + end + + case replace_count + when 1 + @tuple[@start + index] = replacement + when 0 + # nothing + else + m = Rubinius::Mirror::Array.reflect replacement + @tuple.copy_from m.tuple, m.start, replace_count, @start + index + end + + @total = new_total + end + + return ent + else + nt = @start + index + 1 + reallocate(nt) if @tuple.size < nt + + @tuple.put @start + index, ent + if index >= @total - 1 + @total = index + 1 + end + return ent + end + end + + alias_method :[]=, :set_index + + private :set_index + + def shift(n=undefined) + Rubinius.check_frozen + + if undefined.equal?(n) + return nil if @total == 0 + obj = @tuple.at @start + @tuple.put @start, nil + @start += 1 + @total -= 1 + + obj + else + n = Rubinius::Type.coerce_to_collection_index n + raise ArgumentError, "negative array size" if n < 0 + + Array.new slice!(0, n) + end + end + + def shuffle(options = undefined) + return dup.shuffle!(options) if instance_of? Array + Array.new(self).shuffle!(options) + end + + def shuffle!(options = undefined) + Rubinius.check_frozen + + random_generator = Kernel + + unless undefined.equal? options + options = Rubinius::Type.coerce_to options, Hash, :to_hash + random_generator = options[:random] if options[:random].respond_to?(:rand) + end + + size.times do |i| + r = i + random_generator.rand(size - i).to_int + raise RangeError, "random number too big #{r - i}" if r < 0 || r >= size + @tuple.swap(@start + i, @start + r) + end + self + end + + def slice!(start, length=undefined) + Rubinius.check_frozen + + if undefined.equal? length + if start.kind_of? Range + range = start + out = self[range] + + range_start = Rubinius::Type.coerce_to_collection_index range.begin + if range_start < 0 + range_start = range_start + @total + end + + range_end = Rubinius::Type.coerce_to_collection_index range.end + if range_end < 0 + range_end = range_end + @total + elsif range_end >= @total + range_end = @total - 1 + range_end += 1 if range.exclude_end? + end + + range_length = range_end - range_start + range_length += 1 unless range.exclude_end? + range_end -= 1 if range.exclude_end? + + if range_start < @total && range_start >= 0 && range_end < @total && range_end >= 0 && range_length > 0 + delete_range(range_start, range_length) + end + else + # make sure that negative values are not passed through to the + # []= assignment + start = Rubinius::Type.coerce_to_collection_index start + start = start + @total if start < 0 + + # This is to match the MRI behaviour of not extending the array + # with nil when specifying an index greater than the length + # of the array. + return out unless start >= 0 and start < @total + + out = @tuple.at start + @start + + # Check for shift style. + if start == 0 + @tuple.put @start, nil + @total -= 1 + @start += 1 + else + delete_range(start, 1) + end + end + else + start = Rubinius::Type.coerce_to_collection_index start + length = Rubinius::Type.coerce_to_collection_length length + return nil if length < 0 + + out = self[start, length] + + if start < 0 + start = @total + start + end + if start + length > @total + length = @total - start + end + + if start < @total && start >= 0 + delete_range(start, length) + end + end + + out + end + + def drop(n) + n = Rubinius::Type.coerce_to_collection_index n + raise ArgumentError, "attempt to drop negative size" if n < 0 + + return [] if @total == 0 + + new_size = @total - n + return [] if new_size <= 0 + + new_range @start + n, new_size + end + + def sort(&block) + Array.new dup.sort_inplace(&block) + end + + def sort_by!(&block) + Rubinius.check_frozen + + return to_enum(:sort_by!) { size } unless block_given? + + replace sort_by(&block) + end + + # Sorts this Array in-place. See #sort. + # + # The threshold for choosing between Insertion sort and Mergesort + # is 13, as determined by a bit of quick tests. + # + # For results and methodology, see the commit message. + def sort_inplace(&block) + Rubinius.check_frozen + + return self unless @total > 1 + + if (@total - @start) < 13 + if block + isort_block! @start, (@start + @total), block + else + isort! @start, (@start + @total) + end + else + if block + mergesort_block! block + else + mergesort! + end + end + + self + end + + protected :sort_inplace + + # Make a public alias. Only use sort_inplace internally to avoid + # subclass overrides. + + # Alias used to avoid subclass overrides + alias_method :sort!, :sort_inplace + public :sort! + + def to_a + if self.instance_of? Array + self + else + Array.new(self) + end + end + + def to_ary + self + end + + def to_h + super + end + + def transpose + return [] if empty? + + out = [] + max = nil + + each do |ary| + ary = Rubinius::Type.coerce_to ary, Array, :to_ary + max ||= ary.size + + # Catches too-large as well as too-small (for which #fetch would suffice) + raise IndexError, "All arrays must be same length" if ary.size != max + + ary.size.times do |i| + entry = (out[i] ||= []) + entry << ary.at(i) + end + end + + out + end + + def uniq(&block) + dup.uniq!(&block) or dup + end + + def uniq!(&block) + Rubinius.check_frozen + + if block_given? + im = Rubinius::IdentityMap.from(self, &block) + else + im = Rubinius::IdentityMap.from(self) + end + return if im.size == size + + m = Rubinius::Mirror::Array.reflect im.to_array + @tuple = m.tuple + @start = m.start + @total = m.total + + self + end + + def unshift(*values) + Rubinius.check_frozen + + return self if values.empty? + + m = Rubinius::Mirror::Array.reflect values + + if @start > values.size + # fit the new values in between 0 and @start if possible + @start -= values.size + @tuple.copy_from(m.tuple, 0, values.size, @start) + else + new_tuple = Rubinius::Tuple.new @total + values.size + new_tuple.copy_from m.tuple, 0, values.size, 0 + new_tuple.copy_from @tuple, @start, @total, values.size + @start = 0 + @tuple = new_tuple + end + + @total += values.size + self + end + alias_method :prepend, :unshift + + def values_at(*args) + out = [] + + args.each do |elem| + # Cannot use #[] because of subtly different errors + if elem.kind_of? Range + finish = Rubinius::Type.coerce_to_collection_index elem.last + start = Rubinius::Type.coerce_to_collection_index elem.first + + start += @total if start < 0 + next if start < 0 + + finish += @total if finish < 0 + finish -= 1 if elem.exclude_end? + + next if finish < start + + start.upto(finish) { |i| out << at(i) } + + else + i = Rubinius::Type.coerce_to_collection_index elem + out << at(i) + end + end + + out + end + + def zip(*others) + out = Array.new(size) { [] } + others = others.map do |other| + if other.respond_to?(:to_ary) + other.to_ary + else + other.to_enum :each + end + end + + size.times do |i| + slot = out.at(i) + slot << @tuple.at(@start + i) + others.each do |other| + slot << case other + when Array + other.at i + else + begin + other.next + rescue StopIteration + nil + end + end + end + end + + if block_given? + out.each { |ary| yield ary } + return nil + end + + out + end + + # Reallocates the internal Tuple to accommodate at least given size + def reallocate(at_least) + return if at_least < @tuple.size + + new_total = @tuple.size * 2 + + if new_total < at_least + new_total = at_least + end + + new_tuple = Rubinius::Tuple.new new_total + new_tuple.copy_from @tuple, @start, @total, 0 + + @start = 0 + @tuple = new_tuple + end + + private :reallocate + + def reallocate_shrink + new_total = @tuple.size + return if @total > (new_total / 3) + + # halve the tuple size until the total > 1/3 the size of the total + begin + new_total /= 2 + end while @total < (new_total / 6) + + new_tuple = Rubinius::Tuple.new(new_total) + # position values in the middle somewhere + new_start = (new_total - @total)/2 + new_tuple.copy_from @tuple, @start, @total, new_start + + @start = new_start + @tuple = new_tuple + end + + private :reallocate_shrink + + # Helper to recurse through flattening since the method + # is not allowed to recurse itself. Detects recursive structures. + def recursively_flatten(array, out, max_levels = -1) + modified = false + + # Strict equality since < 0 means 'infinite' + if max_levels == 0 + out.concat(array) + return false + end + + max_levels -= 1 + recursion = Thread.detect_recursion(array) do + m = Rubinius::Mirror::Array.reflect array + + i = m.start + total = i + m.total + tuple = m.tuple + + while i < total + o = tuple.at i + + if Rubinius::Type.object_kind_of? o, Array + modified = true + recursively_flatten o, out, max_levels + elsif Rubinius::Type.object_respond_to? o, :to_ary + ary = o.__send__ :to_ary + if nil.equal? ary + out << o + else + modified = true + recursively_flatten ary, out, max_levels + end + elsif ary = Rubinius::Type.execute_check_convert_type(o, Array, :to_ary) + modified = true + recursively_flatten ary, out, max_levels + else + out << o + end + + i += 1 + end + end + + raise ArgumentError, "tried to flatten recursive array" if recursion + modified + end + + private :recursively_flatten + + # Non-recursive sort using a temporary tuple for scratch storage. + # This is a hybrid mergesort; it's hybrid because for short runs under + # 8 elements long we use insertion sort and then merge those sorted + # runs back together. + def mergesort! + width = 7 + @scratch = Rubinius::Tuple.new @tuple.size + + # do a pre-loop to create a bunch of short sorted runs; isort on these + # 7-element sublists is more efficient than doing merge sort on 1-element + # sublists + left = @start + finish = @total + @start + while left < finish + right = left + width + right = right < finish ? right : finish + last = left + (2 * width) + last = last < finish ? last : finish + + isort!(left, right) + isort!(right, last) + + left += 2 * width + end + + # now just merge together those sorted lists from the prior loop + width = 7 + while width < @total + left = @start + while left < finish + right = left + width + right = right < finish ? right : finish + last = left + (2 * width) + last = last < finish ? last : finish + + bottom_up_merge(left, right, last) + left += 2 * width + end + + @tuple, @scratch = @scratch, @tuple + width *= 2 + end + + @scratch = nil + self + end + private :mergesort! + + def bottom_up_merge(left, right, last) + left_index = left + right_index = right + i = left + + while i < last + if left_index < right && (right_index >= last || (@tuple.at(left_index) <=> @tuple.at(right_index)) <= 0) + @scratch[i] = @tuple.at(left_index) + left_index += 1 + else + @scratch[i] = @tuple.at(right_index) + right_index += 1 + end + + i += 1 + end + end + private :bottom_up_merge + + def mergesort_block!(block) + width = 7 + @scratch = Rubinius::Tuple.new @tuple.size + + left = @start + finish = @total + @start + while left < finish + right = left + width + right = right < finish ? right : finish + last = left + (2 * width) + last = last < finish ? last : finish + + isort_block!(left, right, block) + isort_block!(right, last, block) + + left += 2 * width + end + + width = 7 + while width < @total + left = @start + while left < finish + right = left + width + right = right < finish ? right : finish + last = left + (2 * width) + last = last < finish ? last : finish + + bottom_up_merge_block(left, right, last, block) + left += 2 * width + end + + @tuple, @scratch = @scratch, @tuple + width *= 2 + end + + @scratch = nil + self + end + private :mergesort_block! + + def bottom_up_merge_block(left, right, last, block) + left_index = left + right_index = right + i = left + + while i < last + if left_index < right && (right_index >= last || block.call(@tuple.at(left_index), @tuple.at(right_index)) <= 0) + @scratch[i] = @tuple.at(left_index) + left_index += 1 + else + @scratch[i] = @tuple.at(right_index) + right_index += 1 + end + + i += 1 + end + end + private :bottom_up_merge_block + + # Insertion sort in-place between the given indexes. + def isort!(left, right) + i = left + 1 + + tup = @tuple + + while i < right + j = i + + while j > left + jp = j - 1 + el1 = tup.at(jp) + el2 = tup.at(j) + + unless cmp = (el1 <=> el2) + raise ArgumentError, "comparison of #{el1.inspect} with #{el2.inspect} failed (#{j})" + end + + break unless cmp > 0 + + tup.put(j, el1) + tup.put(jp, el2) + + j = jp + end + + i += 1 + end + end + private :isort! + + # Insertion sort in-place between the given indexes using a block. + def isort_block!(left, right, block) + i = left + 1 + + while i < right + j = i + + while j > left + block_result = block.call(@tuple.at(j - 1), @tuple.at(j)) + + if block_result.nil? + raise ArgumentError, 'block returned nil' + elsif block_result > 0 + @tuple.swap(j, (j - 1)) + j -= 1 + else + break + end + end + + i += 1 + end + end + private :isort_block! + + # Move to compiler runtime + def __rescue_match__(exception) + each { |x| return true if x === exception } + false + end +end diff --git a/gxg/core/array_mirror.rb b/gxg/core/array_mirror.rb new file mode 100644 index 0000000..8566f23 --- /dev/null +++ b/gxg/core/array_mirror.rb @@ -0,0 +1,96 @@ +module Rubinius + class Mirror + class Array < Mirror + self.subject = ::Array + + def self.reflect(object) + m = super(object) + + if Rubinius::Type.object_kind_of? m.object, ::Array + m + elsif ary = Rubinius::Type.try_convert(m.object, ::Array, :to_ary) + super(ary) + else + message = "expected Array, given #{Rubinius::Type.object_class(object)}" + raise TypeError, message + end + end + + # TODO: implement mirror_attribute :name + def total + Rubinius.invoke_primitive :object_get_ivar, @object, :@total + end + + def total=(value) + Rubinius.invoke_primitive :object_set_ivar, @object, :@total, value + end + + def tuple + Rubinius.invoke_primitive :object_get_ivar, @object, :@tuple + end + + def tuple=(value) + Rubinius.invoke_primitive :object_set_ivar, @object, :@tuple, value + end + + def start + Rubinius.invoke_primitive :object_get_ivar, @object, :@start + end + + def start=(value) + Rubinius.invoke_primitive :object_set_ivar, @object, :@start, value + end + + # Implementation based on the binomial_coefficient + # implemented on MRI array.c. + def binomial_coefficient(comb, size) + comb = size-comb if (comb > size-comb) + return 0 if comb < 0 + descending_factorial(size, comb) / descending_factorial(comb, comb) + end + + # Implementation based on the rb_ary_combination_size + # implemented on MRI array.c. + def combination_size(num) + binomial_coefficient(num, @object.size) + end + + # Implementation based on the descending_factorial + # implemented on MRI array.c. + # Comment from there: + # Returns the product of from, from-1, ..., from - how_many + 1. + # http://en.wikipedia.org/wiki/Pochhammer_symbol + def descending_factorial(from, how_many) + cnt = how_many >= 0 ? 1 : 0 + while (how_many) > 0 + cnt = cnt*(from) + from -= 1 + how_many -= 1 + end + cnt + end + + # Implementation based on the rb_ary_permutation_size + # implemented on MRI array.c. + def permutation_size(num) + n = @object.size + if undefined.equal? num + k = @object.size + else + k = Rubinius::Type.coerce_to_collection_index num + end + descending_factorial(n, k) + end + + def repeated_combination_size(combination_size) + return 1 if combination_size == 0 + return binomial_coefficient(combination_size, @object.size + combination_size - 1) + end + + def repeated_permutation_size(combination_size) + return 0 if combination_size < 0 + @object.size ** combination_size + end + end + end +end diff --git a/gxg/core/atomic.rb b/gxg/core/atomic.rb new file mode 100644 index 0000000..0386759 --- /dev/null +++ b/gxg/core/atomic.rb @@ -0,0 +1,43 @@ +module Rubinius + class AtomicReference + def initialize(val=nil) + set(val) unless val.nil? + end + + private :initialize + + def marshal_dump + get + end + + def marshal_load(val) + set(val) + end + + def get + Rubinius.primitive :atomic_get + raise PrimitiveFailure, "Rubinius::AtomicReference#get primitive failed" + end + + alias_method :value, :get + + def set(val) + Rubinius.primitive :atomic_set + raise PrimitiveFailure, "Rubinius::AtomicReference#set primitive failed" + end + + alias_method :value=, :set + + def compare_and_set(old, new) + Rubinius.primitive :atomic_compare_and_set + raise PrimitiveFailure, "Rubinius::AtomicReference#compare_and_set primitive failed" + end + + def get_and_set(new) + while true + val = get + return val if compare_and_set(val, new) + end + end + end +end diff --git a/gxg/core/autoload.rb b/gxg/core/autoload.rb new file mode 100644 index 0000000..ff7ac06 --- /dev/null +++ b/gxg/core/autoload.rb @@ -0,0 +1,119 @@ +## +# Used to implement Module#autoload. + +class Autoload + attr_reader :name + attr_reader :scope + attr_reader :path + attr_reader :constant + + attr_accessor :loaded + attr_accessor :loading + + def self.allocate + Rubinius.primitive :autoload_allocate + raise PrimtiveFailure, "Autoload.allocate primitive failed" + end + + def initialize(name, scope, path) + @name = name + @scope = scope + @path = path + @constant = undefined + @loading = false + @loaded = false + end + + private :initialize + + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} name=#{@name} scope=#{@scope}" \ + " path=#{@path} constant=#{@constant} loading=#{@loading} loaded=#{@loaded}>" + end + + alias_method :to_s, :inspect + + def loading? + Rubinius.synchronize(self) { @loading } + end + + ## + # + # Change the file to autoload. Used by Module#autoload + def set_path(path) + @path = path + @loading = false + end + + ## + # When any code that finds a constant sees an instance of Autoload as its match, + # it calls this method on us + def call(under, honor_require=false) + return constant unless undefined.equal? constant + + if resolve or not honor_require + find_const under + end + end + + def resolve + # The protocol that MRI defines for resolving an Autoload instance + # requires that the constant table entry be removed _during_ the load so + # that Module#const_defined? returns false and defined?() returns nil for + # the constant that triggered the load. + + Rubinius.synchronize(self) do + unless @loaded or @loading + @loading = true + + result = Rubinius::CodeLoader.new(@path).require + + @loaded = true if result + @loading = false + + result + end + end + end + + def find_const(under) + current = under + constant = undefined + + while current + if entry = current.constant_table.lookup(name) + constant = entry.constant + if constant.equal? self + if undefined.equal?(constant.constant) + unless Object.constant_table.lookup(name) + return under.const_missing(name) + end + else + entry.constant = constant.constant + return constant.constant + end + end + return constant + end + + current = current.direct_superclass + end + + if instance_of?(Module) + if entry = Object.constant_table.lookup(name) + constant = entry.constant + if constant.equal? self + if undefined.equal?(constant.constant) + return under.const_missing(name) + else + entry.constant = constant.constant + return constant.constant + end + end + return constant + end + end + + under.const_missing(name) + end +end diff --git a/gxg/core/backtrace.rb b/gxg/core/backtrace.rb new file mode 100644 index 0000000..5989286 --- /dev/null +++ b/gxg/core/backtrace.rb @@ -0,0 +1,163 @@ +## +# Contains all logic for gathering and displaying backtraces. + +class Rubinius::Backtrace + include Enumerable + + MAX_WIDTH_PERCENTAGE = 45 + MIN_WIDTH = 20 + + # If passed nil, we assume someone forgot to create a backtrace + # in the VM. + def self.backtrace(locations) + locations ? new(locations) : new([Rubinius::Location::Missing.new]) + end + + def initialize(locations, width = Rubinius::TERMINAL_WIDTH, min_width = MIN_WIDTH) + color_config = Rubinius::Config["rbx.colorize_backtraces"] + if ENV['RBX_NOCOLOR'] or color_config == "no" or color_config == "NO" + @colorize = false + else + @colorize = true + end + + @locations = locations + @bold = "\033[0;1m" + @inline_effect = "\033[0;4m" + + @width = width + @min_width = min_width + + @mri_backtrace = nil + end + + private :initialize + + def [](index) + @locations[index] + end + + def show(sep="\n", show_color=true) + show_color = false unless @colorize + clear = show_color ? "\033[0m" : "" + + max = 0 + lines = [] + last_method = nil + last_line = nil + last_name = nil + times = 0 + + @locations.reverse_each do |loc| + if loc.name == last_name and loc.method == last_method \ + and loc.line == last_line + times += 1 + else + lines.last[-1] = times if lines.size > 0 + last_method = loc.method + last_line = loc.line + last_name = loc.name + + receiver_name = loc.describe_receiver + + if loc.is_block + method_name = "{ } in #{loc.describe_method}" + else + method_name = loc.describe_method + end + + size = method_name.size + 4 # additional space needed for {#, .} + max = size if size > max + + lines << [receiver_name, method_name, loc, 1] + times = 0 + end + end + + cwd = Dir.getwd + + str = "" + lines.each do |receiver_name, method_name, location, rec_times| + next unless location + + full_name = "#{method_name} #{receiver_name[-1]} " + + if show_color + receiver_method = "#{@bold}#{full_name}#{receiver_name[0..-2]}#{clear}" + else + receiver_method = "#{full_name}#{receiver_name[0..-2]}" + end + + spaces = max - full_name.size + spaces = 0 if spaces < 0 + + if show_color and location.inlined? + start = " #{' ' * spaces}#{receiver_method} #{@inline_effect}at#{clear}#{color} " + else + start = " #{' ' * spaces}#{receiver_method} at " + end + + # start.size without the escapes + start_size = 1 + spaces + receiver_method.size + 4 + + pos = location.position cwd + + pos << " (+#{location.ip})" + + if rec_times > 1 + pos << " (#{rec_times} times)" + end + + if location.is_jit and $DEBUG + pos << " (jit)" + end + + str << " #{start}#{pos}" + + str << sep + end + + return str + end + + def join(sep) + show + end + + alias_method :to_s, :show + + def each + @locations.each { |f| yield f } + self + end + + def self.detect_backtrace(obj) + if bt = obj.instance_variable_get(:@rbx_backtrace) + return bt if bt.same_mri_backtrace?(obj) + end + + return nil + end + + def to_mri + return @mri_backtrace if @mri_backtrace + + @mri_backtrace = @locations.map do |loc| + "#{loc.position}:in `#{loc.describe_method}'" + end + + # A little extra magic, so we can carry the backtrace along and reuse it + + @mri_backtrace.instance_variable_set(:@rbx_backtrace, self) + + return @mri_backtrace + end + + def same_mri_backtrace?(obj) + currently = @locations.map do |loc| + "#{loc.position}:in `#{loc.describe_method}'" + end + + currently == obj + end +end diff --git a/gxg/core/basic_object.rb b/gxg/core/basic_object.rb new file mode 100644 index 0000000..209ecf6 --- /dev/null +++ b/gxg/core/basic_object.rb @@ -0,0 +1,162 @@ +class BasicObject + def initialize + # do nothing + end + private :initialize + + def equal?(other) + Rubinius.primitive :object_equal + raise ::PrimitiveFailure, "BasicObject#equal? primitive failed" + end + + alias_method :==, :equal? + + def ! + Rubinius.invoke_primitive(:object_equal, self, false) || + Rubinius.invoke_primitive(:object_equal, self, nil) ? true : false + end + + def !=(other) + self == other ? false : true + end + + # Send message to object with given arguments. + # + # Ignores visibility of method, and may therefore be used to + # invoke protected or private methods. + # + # As denoted by the double-underscore, this method must not + # be removed or redefined by user code. + # + def __send__(message, *args) + Rubinius.primitive :object_send + raise ::PrimitiveFailure, "BasicObject#__send__ primitive failed" + end + + def __id__ + Rubinius.primitive :object_id + raise ::PrimitiveFailure, "BasicObject#__id__ primitive failed" + end + + def method_missing(meth, *args) + error = ::NoMethodError + .new("Unable to send '#{meth}' on instance of BasicObject", receiver: self) + + ::Kernel.raise(error) + end + private :method_missing + + def singleton_method_added(name) end + private :singleton_method_added + + def singleton_method_removed(name) end + private :singleton_method_removed + + def singleton_method_undefined(name) end + private :singleton_method_undefined + + ## + # :call-seq: + # obj.instance_eval(string [, filename [, lineno]] ) => obj + # obj.instance_eval { | | block } => obj + # + # Evaluates a string containing Ruby source code, or the given block, within + # the context of the receiver +obj+. In order to set the context, the + # variable +self+ is set to +obj+ while the code is executing, giving the + # code access to +obj+'s instance variables. In the version of + # #instance_eval that takes a +String+, the optional second and third + # parameters supply a filename and starting line number that are used when + # reporting compilation errors. + # + # class Klass + # def initialize + # @secret = 99 + # end + # end + # k = Klass.new + # k.instance_eval { @secret } #=> 99 + + def instance_eval(string=nil, filename="(eval)", line=1, &prc) + if ::ImmediateValue === self + sc = nil + else + sc = ::Rubinius::Type.object_singleton_class(self) + end + + if prc + if string + raise ::ArgumentError, 'cannot pass both a block and a string to evaluate' + end + + return prc.ruby_method.call(self) if prc.ruby_method + + # Return a copy of the BlockEnvironment with the receiver set to self + env = prc.block + + if sc + lexical_scope = env.repoint_scope sc + else + lexical_scope = env.disable_scope! + end + + return env.call_under(self, lexical_scope, true, self) + elsif string + string = ::Kernel.StringValue(string) + + lexical_scope = ::Rubinius::LexicalScope.of_sender + + if sc + lexical_scope = ::Rubinius::LexicalScope.new(sc, lexical_scope) + else + lexical_scope = lexical_scope.using_disabled_scope + end + + binding = ::Binding.setup(::Rubinius::VariableScope.of_sender, + ::Rubinius::CompiledCode.of_sender, + lexical_scope) + + c = ::Rubinius::ToolSets::Runtime::Compiler + be = c.construct_block string, binding, filename, line + + be.call_on_instance(self) + else + raise ::ArgumentError, 'block not supplied' + end + end + + ## + # :call-seq: + # obj.instance_exec(arg, ...) { |var,...| block } => obj + # + # Executes the given block within the context of the receiver +obj+. In + # order to set the context, the variable +self+ is set to +obj+ while the + # code is executing, giving the code access to +obj+'s instance variables. + # + # Arguments are passed as block parameters. + # + # class Klass + # def initialize + # @secret = 99 + # end + # end + # + # k = Klass.new + # k.instance_exec(5) { |x| @secret+x } #=> 104 + + def instance_exec(*args, &prc) + raise ::LocalJumpError, "Missing block" unless block_given? + env = prc.block + + return prc.ruby_method.call(*args) if prc.ruby_method + + lexical_scope = env.lexical_scope + if ::ImmediateValue === self + lexical_scope = lexical_scope.using_disabled_scope + else + sc = ::Rubinius::Type.object_singleton_class(self) + lexical_scope = lexical_scope.using_current_as(sc) + end + + return env.call_under(self, lexical_scope, true, *args) + end +end diff --git a/gxg/core/binding.rb b/gxg/core/binding.rb new file mode 100644 index 0000000..de4dbfb --- /dev/null +++ b/gxg/core/binding.rb @@ -0,0 +1,116 @@ +class Binding + attr_accessor :variables + attr_accessor :compiled_code + attr_accessor :lexical_scope + attr_accessor :proc_environment + attr_accessor :location + attr_accessor :receiver + + def from_proc? + @proc_environment + end + + def self.self_context(recv, variables) + Rubinius::Type.object_equal(recv, Kernel) ? recv : variables.self + end + private :self_context + + # Create a new Binding object. MRI does not allow .new to be called, so + # we used .setup(). Any code can use this as they wish, provided they have + # all the right arguments. + # + # +variables+ is a Rubinius::VariableScope object + # +code+ is a Rubinius::CompiledCode object + # +lexical_scope+ is a Rubinius::LexicalScope object + # + # See Kernel#binding in core/eval.rb for a simple example of creating a + # Binding object. + # + def self.setup(variables, code, lexical_scope, recv=nil, location=nil) + bind = allocate() + + bind.receiver = self_context(recv, variables) + bind.variables = variables + bind.compiled_code = code + bind.lexical_scope = lexical_scope + bind.location = location + + return bind + end + + def line_number + if proc_environment + proc_environment.line + else + location ? location.line : 1 + end + end + + # Evaluates the Ruby expression(s) in string, in the binding‘s context. + # If the optional filename and lineno parameters are present, + # they will be used when reporting syntax errors. + def eval(expr, filename=nil, lineno=nil) + lineno ||= filename ? 1 : line_number + + Kernel.eval(expr, self, filename, lineno) + end + + def local_variables + variables.local_variables + end + + def local_variable_set(name, value) + unless name.is_a?(Symbol) + name = Rubinius::Type.coerce_to(name, String, :to_str).to_sym + end + + vars = variables + + # If a local variable is defined in a parent scope we should update the + # variable in said scope and all child scopes, instead of _only_ setting it + # in the current scope. + while vars + meth = vars.method + + if meth.local_names.include?(name) + return vars.set_local(meth.local_slot(name), value) + elsif vars.eval_local_defined?(name) + return vars.set_eval_local(name, value) + end + + vars = vars.parent + end + + variables.set_eval_local(name, value) + end + + def local_variable_get(name) + unless name.is_a?(Symbol) + name = Rubinius::Type.coerce_to(name, String, :to_str).to_sym + end + + vars = variables + + while vars + meth = vars.method + + if meth.local_names.include?(name) + return vars.locals[meth.local_slot(name)] + elsif vars.eval_local_defined?(name) + return vars.get_eval_local(name) + end + + vars = vars.parent + end + + raise NameErrror, "local variable #{name.inspect} not defined for #{inspect}" + end + + def local_variable_defined?(name) + unless name.is_a?(Symbol) + name = Rubinius::Type.coerce_to(name, String, :to_str).to_sym + end + + variables.local_defined?(name) + end +end diff --git a/gxg/core/block_environment.rb b/gxg/core/block_environment.rb new file mode 100644 index 0000000..7f85f20 --- /dev/null +++ b/gxg/core/block_environment.rb @@ -0,0 +1,183 @@ +## +# Describes the environment a block was created in. BlockEnvironment is used +# to create a BlockContext. + +module Rubinius + class BlockEnvironment + attr_reader :scope + attr_reader :top_scope + attr_reader :module + + # The CompiledCode that implements the code for the block + attr_reader :compiled_code + attr_reader :lexical_scope + + attr_accessor :proc_environment + + def self.allocate + Rubinius.primitive :blockenvironment_allocate + raise PrimitiveFailure, "BlockEnvironment.allocate primitive failed" + end + + def call(*args) + Rubinius.primitive :block_call + raise PrimitiveFailure, "BlockEnvironment#call primitive failed" + end + + def call_under(recv, lexical_scope, visibility_scope, *args) + Rubinius.primitive :block_call_under + raise PrimitiveFailure, "BlockEnvironment#call_under primitive failed" + end + + def self.of_sender + Rubinius.primitive :block_env_of_sender + raise PrimitiveFailure, "BlockEnvironment#of_sender primitive failed" + end + + class AsMethod < Executable + attr_reader :block_env + + def self.new(block_env) + Rubinius.primitive :block_as_method_create + raise PrimitiveFailure, "BlockEnvironment::AsMethod.new primitive failed" + end + + def ==(other) + # Given the following code: + # class Foo + # p = Proc.new { :cool } + # define_method :a, p + # define_method :a, p + # end + # foo = Foo.new + # foo.method(:a) + # foo.method(:b) + # + # The Method instances for :a and :b have different + # BlockEnvironments as define_method dups the BE + # when given a Proc. + # + # The methods are equal if the BEs are equal. + return false unless other.kind_of? AsMethod + + @block_env == other.block_env + end + + def parameters + @block_env.parameters + end + + def arity + @block_env.compiled_code.arity + end + + def local_names + @block_env.compiled_code.local_names + end + + def required_args + @block_env.compiled_code.required_args + end + + def total_args + @block_env.compiled_code.total_args + end + + def post_args + @block_env.compiled_code.post_args + end + + def splat + @block_env.compiled_code.splat + end + + def block_index + @block_env.compiled_code.block_index + end + + def file + @block_env.file + end + + def defined_line + @block_env.line + end + + def scope + @block_env.scope + end + end + + def from_proc? + @proc_environment + end + + def under_context(scope, code) + @top_scope = scope + @scope = scope + @compiled_code = code + @lexical_scope = code.scope + @module = code.scope.module + + return self + end + + def change_name(name) + @compiled_code = @compiled_code.change_name name + end + + def repoint_scope(where) + @lexical_scope.using_current_as where + end + + def disable_scope! + @lexical_scope.using_disabled_scope + end + + def to_binding + Binding.setup @scope, @compiled_code, @lexical_scope + end + + def make_independent + @top_scope = @top_scope.dup + @scope = @scope.dup + end + + def call_on_instance(obj, *args) + call_under obj, @lexical_scope, false, *args + end + + def arity + @compiled_code.arity + end + + def parameters + @compiled_code.parameters + end + + def file + @compiled_code.file + end + + def line + @compiled_code.defined_line + end + + def source_location + [file.to_s, line] + end + + def ==(other) + return false unless other.kind_of? BlockEnvironment + + @top_scope == other.top_scope and + @scope == other.scope and + @module == other.module and + @compiled_code.equivalent_body?(other.compiled_code) + end + + def inspect + "#<#{self.class.name}:0x#{self.object_id.to_s(16)} scope=#{@scope.inspect} top_scope=#{@top_scope.inspect} module=#{@module.inspect} compiled_code=#{@compiled_code.inspect} lexical_scope=#{@lexical_scope.inspect}>" + end + end +end diff --git a/gxg/core/byte_array.rb b/gxg/core/byte_array.rb new file mode 100644 index 0000000..da0b9f5 --- /dev/null +++ b/gxg/core/byte_array.rb @@ -0,0 +1,127 @@ +## +# An array of bytes, used as a low-level data store for implementing various +# other classes. +module Rubinius + class ByteArray + def self.allocate + raise TypeError, "ByteArray cannot be created via allocate()" + end + + def self.allocate_sized(cnt) + Rubinius.primitive :bytearray_allocate + raise PrimitiveFailure, "ByteArray#allocate primitive failed" + end + + def self.new(cnt) + obj = allocate_sized cnt + Rubinius.asm(obj) do |obj| + run obj + send :initialize, 0, true + end + + obj + end + + def fetch_bytes(start, count) + Rubinius.primitive :bytearray_fetch_bytes + raise PrimitiveFailure, "ByteArray#fetch_bytes primitive failed" + end + + def move_bytes(start, count, dest) + Rubinius.primitive :bytearray_move_bytes + raise ArgumentError, "ByteArray#move_bytes primitive failed" + end + + def get_byte(index) + Rubinius.primitive :bytearray_get_byte + raise PrimitiveFailure, "ByteArray#get_byte primitive failed" + end + + alias_method :[], :get_byte + + def set_byte(index, value) + Rubinius.primitive :bytearray_set_byte + raise PrimitiveFailure, "ByteArray#set_byte primitive failed" + end + + alias_method :[]=, :set_byte + + def compare_bytes(other, a, b) + Rubinius.primitive :bytearray_compare_bytes + raise PrimitiveFailure, "ByteArray#compare_bytes primitive failed" + end + + def size + Rubinius.primitive :bytearray_size + raise PrimitiveFailure, "ByteArray#size primitive failed" + end + + def dup + obj = Rubinius::Type.object_class(self).allocate_sized(self.size) + + Rubinius.invoke_primitive :object_copy_object, obj, self + + Rubinius.privately do + obj.initialize_copy self + end + + return obj + end + + ## + # Searches for +pattern+ in the ByteArray. Returns the number + # of characters from the front of the ByteArray to the end + # of the pattern if a match is found. Returns Qnil if a match + # is not found. Starts searching at index +start+. + def locate(pattern, start, max) + Rubinius.primitive :bytearray_locate + raise PrimitiveFailure, "ByteArray#locate primitive failed" + end + + # Return a new ByteArray by taking the bytes from +string+ and +self+ + # together. + def prepend(string) + Rubinius.primitive :bytearray_prepend + + if string.kind_of? String + raise PrimitiveFailure, "ByteArray#prepend failed" + else + prepend(StringValue(string)) + end + end + + def reverse(start, total) + Rubinius.primitive :bytearray_reverse + raise PrimitiveFailure, "ByteArray#reverse primitive failed" + end + + # TODO: encoding + def character_at_index(index) + "" << self[index] + end + + def each + i = 0 + max = size() + + while i < max + yield get_byte(i) + i += 1 + end + end + + def inspect + "#<#{self.class}:0x#{object_id.to_s(16)} #{size} bytes>" + end + + def <=>(other) + compare_bytes other, size, other.size + end + + # Sets the first character to be an ASCII capitalize letter + # if it's an ASCII lower case letter + def first_capitalize! + self[0] = self[0].toupper + end + end +end diff --git a/gxg/core/call_site.rb b/gxg/core/call_site.rb new file mode 100644 index 0000000..1ea7c1d --- /dev/null +++ b/gxg/core/call_site.rb @@ -0,0 +1,76 @@ +module Rubinius + class CallSite + attr_reader :name + attr_reader :cache + + def ip + Rubinius.primitive :call_site_ip + raise PrimitiveFailure, "CallSite#ip primitive failed" + end + + def size + Rubinius.primitive :call_site_size + raise PrimitiveFailure, "CallSite#size primitive failed" + end + + def invokes + Rubinius.primitive :call_site_invokes + raise PrimitiveFailure, "CallSite#invokes primitive failed" + end + + def hits + Rubinius.primitive :call_site_hits + raise PrimitiveFailure, "CallSite#hits primitive failed" + end + + def misses + Rubinius.primitive :call_site_misses + raise PrimitiveFailure, "CallSite#misses primitive failed" + end + + def evictions + Rubinius.primitive :call_site_evictions + raise PrimitiveFailure, "CallSite#evictions primitive failed" + end + + def rotations + Rubinius.primitive :call_site_rotations + raise PrimitiveFailure, "CallSite#rotations primitive failed" + end + + def dead_list_size + Rubinius.primitive :call_site_dead_list_size + raise PrimitiveFailure, "CallSite#dead_list_size primitive failed" + end + + def cache_entries + Rubinius.primitive :call_site_cache_entries + raise PrimitiveFailure, "CallSite#caches primitive failed" + end + + def reset + Rubinius.primitive :call_site_reset + raise PrimitiveFailure, "CallSite#reset primitive failed" + end + + def inspect + str = "#<#{self.class.name}:0x#{self.object_id.to_s(16)} name=#{@name} ip=#{ip} " \ + "size=#{size} invokes=#{invokes} hits=#{hits} misses=#{misses} " \ + "evictions=#{evictions} rotations=#{rotations} dead_list=#{dead_list_size}" + + cache_entries.each do |entry| + str << " #" + end + + str << ">" + end + + alias_method :to_s, :inspect + end +end diff --git a/gxg/core/capi.rb b/gxg/core/capi.rb new file mode 100644 index 0000000..f0be712 --- /dev/null +++ b/gxg/core/capi.rb @@ -0,0 +1,166 @@ +module Rubinius + # CAPI contains utility methods used by capi. + module CAPI + def self.coerce(x, y, error=false) + begin + ary = y.coerce x + rescue + if error + raise TypeError, "#{y.inspect} can't be coerced into #{x.class}" + else + return false + end + end + + unless ary.kind_of? Array and ary.size == 2 + if error + raise TypeError, "coerce must return [x, y]" + else + return false + end + end + + return ary + end + + # C-API method corresponding to rb_num_coerce_bin + def self.rb_num_coerce_bin(x, y, op) + a, b = coerce x, y, true + + a.__send__ op, b + end + + # C-API method corresponding to rb_num_coerce_cmp + def self.rb_num_coerce_cmp(x, y, op) + ary = coerce x, y + + return ary.first.__send__ op, ary.last if ary + end + + # C-API method corresponding to rb_num_coerce_relop + def self.rb_num_coerce_relop(x, y, op) + ary = coerce x, y, false + + if ary + rel = ary.first.__send__ op, ary.last + return rel unless rel.equal? nil + end + + raise ArgumentError, "comparison of #{x.class} with #{y.class} failed" + end + + def self.sprintf(format, args) + ::Rubinius::Sprinter.get(format).call(*args) + end + + def self.rb_str_encode(str, enc, flags, opts) + # TODO: set flags from opts + if str.encoding == enc + str.dup + else + ec = Encoding::Converter.new str.encoding, enc, flags + ec.replacement = opts[:replace] if opts + + ec.convert str + end + end + + def self.rb_str_conv_enc_opts(str, from, to, flags, opts) + return str if to.nil? + return str if from == to + + if (to.ascii_compatible? and str.ascii_only?) or to == Encoding::ASCII_8BIT + if str.encoding != to + return str.dup.force_encoding(to) + else + return str + end + end + + # TODO: set flags from opts + ec = Encoding::Converter.new from, to, flags + ec.replacement = opts[:replace] if opts + + result = "" + status = ec.primitive_convert str.dup, result, 0, nil, flags + + return status == :finished ? result : str + end + + def self.rb_integer_pack(value, words, numwords, wordsize, nails, flags) + digits = INTEGER_PACK_DIGITS + numbytes = numwords * wordsize + nibble_size = numbytes * 2 + + nibbles = value.abs.to_s(16) + nibbles = nibbles[-nibble_size..-1] if nibble_size < nibbles.size + + complement = value < 0 && (flags & INTEGER_PACK_2COMP) != 0 + + if (ENDIAN == :little and (flags & INTEGER_PACK_BIG_ENDIAN) != 0) or + (ENDIAN == :big and (flags & INTEGER_PACK_LITTLE_ENDIAN) != 0) + c_index = index = numbytes - 1 + incr = -1 + else + c_index = index = 0 + incr = 1 + end + + i = nibbles.size - 1 + stop = nibbles.size & 1 + + while i > stop + high = digits[nibbles.getbyte(i - 1)] + low = digits[nibbles.getbyte(i)] + byte = (high << 4) | low + + byte ^= 0xff if complement + words.setbyte index, byte + + index += incr + i -= 2 + end + + if stop == 1 + byte = digits[nibbles.getbyte(0)] + byte ^= 0xff if complement + words.setbyte index, byte + end + + if complement + c_stop = c_index == 0 ? numbytes : -1 + + while c_index != c_stop + byte = words.getbyte(c_index) + 1 + words.setbyte(c_index, byte) + + break if byte <= 0xff + + c_index += incr + end + end + + overflow = 2**(numwords * wordsize * 8) + + if value < 0 + if complement + return -2 if value < -overflow + else + return -2 if value <= -overflow + end + return -1 + else + return 2 if value >= overflow + return 1 + end + end + + def self.rb_block_proc(env) + Rubinius::Mirror::Proc.from_block ::Proc, env + end + + def self.rb_obj_method_arity(obj, sym) + obj.method(sym).arity + end + end +end diff --git a/gxg/core/channel.rb b/gxg/core/channel.rb new file mode 100644 index 0000000..c4c7924 --- /dev/null +++ b/gxg/core/channel.rb @@ -0,0 +1,40 @@ +# Channel is a FIFO. If #receive is called and no value is available, the +# Thread will sleep until a value is available. If #receive_timeout is called +# and no value is availale, the Thread will sleep until a value is available +# or duration time has elapsed. If #try_receive is called, it returns +# immediately with a value or nil. It's impossible to know whether nil was in +# the channel in the case that the return value is nil. +module Rubinius + class Channel + def self.allocate + Rubinius.primitive :channel_new + raise PrimitiveFailure, "Channel.allocate primitive failed" + end + + def send(obj) + Rubinius.primitive :channel_send + raise PrimitiveFailure, "Channel#send primitive failed" + end + + alias_method :<<, :send + + def receive + Rubinius.primitive :channel_receive + raise PrimitiveFailure, "Channel#receive primitive failed" + end + + def receive_timeout(duration) + Rubinius.primitive :channel_receive_timeout + raise PrimitiveFailure, "Channel#receive_timeout primitive failed" + end + + def try_receive + Rubinius.primitive :channel_try_receive + raise PrimitiveFailure, "Channel#try_receive primitive failed" + end + + def inspect + "#" + end + end +end diff --git a/gxg/core/character.rb b/gxg/core/character.rb new file mode 100644 index 0000000..46462e9 --- /dev/null +++ b/gxg/core/character.rb @@ -0,0 +1,55 @@ +module Rubinius + class Character < ::String + def self.__allocate__ + Rubinius.primitive :character_allocate + raise PrimitiveFailure, "Rubinius::Character.allocate primitive failed" + end + + def self.allocate + chr = __allocate__ + chr.__data__ = Rubinius::ByteArray.allocate_sized(1) + chr.num_bytes = 0 + chr + end + + def alphabetical? + Rubinius.primitive :character_alphabetical_p + raise PrimitiveFailure, "Rubinius::Character#alphabetical? primitive failed" + end + + def ascii? + Rubinius.primitive :character_ascii_p + raise PrimitiveFailure, "Rubinius::Character#ascii? primitive failed" + end + + def digit? + Rubinius.primitive :character_digit_p + raise PrimitiveFailure, "Rubinius::Character#digit? primitive failed" + end + + def lower? + Rubinius.primitive :character_lower_p + raise PrimitiveFailure, "Rubinius::Character#lower? primitive failed" + end + + def printable? + Rubinius.primitive :character_printable_p + raise PrimitiveFailure, "Rubinius::Character#printable? primitive failed" + end + + def punctuation? + Rubinius.primitive :character_punctuation_p + raise PrimitiveFailure, "Rubinius::Character#punctuation? primitive failed" + end + + def space? + Rubinius.primitive :character_space_p + raise PrimitiveFailure, "Rubinius::Character#space? primitive failed" + end + + def upper? + Rubinius.primitive :character_upper_p + raise PrimitiveFailure, "Rubinius::Character#upper? primitive failed" + end + end +end diff --git a/gxg/core/class.rb b/gxg/core/class.rb new file mode 100644 index 0000000..b739e8c --- /dev/null +++ b/gxg/core/class.rb @@ -0,0 +1,67 @@ +class Class + def self.allocate + Rubinius.primitive :class_s_allocate + raise PrimitiveFailure, "Class.allocate primitive failed" + end + + def set_superclass(sup) + Rubinius.primitive :class_set_superclass + kind = Rubinius::Type.object_class(sup) + raise TypeError, "Class.set_superclass: argument must be a Class (#{kind} given)" + end + + private :set_superclass + + def initialize(sclass=Object, name=nil, under=nil) + raise TypeError, "already initialized class" if @instance_type + raise TypeError, "can't make subclass of Class" if Class.equal?(sclass) + + set_superclass sclass + + # Things (rails) depend on the fact that a normal class is in the constant + # table and have a name BEFORE inherited is run. + under.const_set name, self if under + + if sclass + Rubinius.privately do + sclass.inherited self + end + end + super() + end + private :initialize + + ## + # Specialized initialize_copy because Class needs additional protection + def initialize_copy(other) + # The code in super (Module#initialize_copy) will duplicate + # the method table. Therefore do this check here to see whether + # we've already initialized this class before continuing. + unless @method_table == other.method_table + raise TypeError, "already initialized class" + end + + @module_name = nil + super + end + + private :initialize_copy + + def inherited(name) + end + private :inherited + + def superclass + cls = direct_superclass + unless cls + return nil if self == BasicObject + raise TypeError, "uninitialized class" + end + + while cls and cls.kind_of? Rubinius::IncludedModule + cls = cls.direct_superclass + end + + return cls + end +end diff --git a/gxg/core/code_db.rb b/gxg/core/code_db.rb new file mode 100644 index 0000000..e6c58a2 --- /dev/null +++ b/gxg/core/code_db.rb @@ -0,0 +1,41 @@ +module Rubinius + class CodeDB + attr_accessor :loaded_features + attr_accessor :load_path + + class << self + # CodeDB.open is the constructor + private :new + end + + def self.open(path) + Rubinius.primitive :code_db_open + raise PrimitiveFailure, "Rubinius::CodeDB.open primitive failed" + end + + def load_db + Rubinius.primitive :code_db_load_db + raise PrimitiveFailure, "Rubinius::CodeDB#load_db primitive failed" + end + + def close + Rubinius.primitive :code_db_close + raise PrimitiveFailure, "Rubinius::CodeDB#close primitive failed" + end + + def load(id) + Rubinius.primitive :code_db_load + raise PrimitiveFailure, "Rubinius::CodeDB#load primitive failed" + end + + def load_feature(stem, ext, reload, record) + Rubinius.primitive :code_db_load_feature + raise PrimitiveFailure, "Rubinius::CodeDB#load_feature primitive failed" + end + + def store(code, stem, path, feature, record) + Rubinius.primitive :code_db_store + raise PrimitiveFailure, "Rubinius::CodeDB#store primitive failed" + end + end +end diff --git a/gxg/core/code_loader.rb b/gxg/core/code_loader.rb new file mode 100644 index 0000000..85fdffe --- /dev/null +++ b/gxg/core/code_loader.rb @@ -0,0 +1,348 @@ +module Rubinius + class CodeLoader + def self.registry + Rubinius.synchronize(self) { @registry ||= [] } + end + + def self.loading?(path) + feature = nil + + resource = Feature.new path + if resource.loadable? or resource.search + feature = resource.feature + end + + unless feature + resource = Library.new path + if resource.loadable? or resource.search + feature = resource.feature + end + end + + if feature + Rubinius.synchronize(registry) do + return true if registry.include? feature + end + end + + false + end + + class Resource + attr_reader :feature + + def initialize(stem, ext) + stem = Type.coerce_to_path stem + stem = "#{stem}#{ext}" unless stem.suffix? ext + + @relative = false + + if stem[0] == ?~ + @prefix = "" + stem = File.expand_path stem + elsif stem[0] == ?/ + @prefix = "" + elsif stem[0] == ?. and (stem[1] == ?/ or + (stem[1] == ?. and stem[2] == ?/)) + @relative = true + @prefix = Dir.pwd + else + @prefix = nil + end + + @stem = stem + @feature = nil + @record = true + end + + private :initialize + + def resolve + return true if loadable? + return true if search + return false + end + + def search + return false if @relative + + $LOAD_PATH.each do |dir| + if dir[-1] == ?/ + @prefix = dir[0...-1] + else + @prefix = dir + end + + return true if loadable? + end + + return false + end + + def loadable? + feature = "#{@prefix}/#{@stem}" + + stat = File::Stat.stat feature + + if stat and stat.file? and stat.readable? + @feature = File.expand_path feature + return true + else + return false + end + end + + def unload_feature + Rubinius.synchronize($LOADED_FEATURES) do + $LOADED_FEATURES.delete_if { |f| f == @feature } + end + end + end + + class Source < Resource + def initialize(stem, ext) + super stem, ext + + @code = nil + end + + private :initialize + + def load + if @code + @feature = @code.file.to_s + else + @code = ToolSets::Runtime::Compiler.compile_file @feature + CoreDB.store @code, @stem, @prefix, @feature, @record + end + + run_script + + return true + end + + def make_script + script = @code.create_script @wrap + script.file_path = script.data_path = @code.file.to_s + script + end + + def run_script + make_script + Rubinius.run_script @code + rescue Object + unload_feature + raise + end + end + + class Feature < Source + def initialize(stem) + super stem, CodeLoader.source_extension + end + + private :initialize + + def resolve + @code = CoreDB.load_feature @stem, CodeLoader.source_extension, false, @record + + return true if @code or false.equal? @code + return true if loadable? + return true if search + + return false + end + + def load + return false if false.equal? @code + + super() + end + end + + class CodeModule < Source + def initialize(stem, wrap) + super stem, "" + + @wrap = wrap + @record = false + end + + private :initialize + + def resolve + @code = CoreDB.load_feature @stem, CodeLoader.source_extension, true, @record + + return true if @code + return true if loadable? + return true if search + + @prefix = Dir.pwd + return true if loadable? + + return false + end + end + + class Script < Source + def initialize(stem) + super File.expand_path(stem), "" + + @file = stem + @record = false + end + + private :initialize + + def resolve + @code = CoreDB.load_feature @stem, "", true, @record + + return true if @code + return true if loadable? + return false + end + + def make_script + script = super() + script.file_path = @file + script.make_main! + end + end + + class Library < Resource + def initialize(stem) + if stem.suffix? ".so" + super stem[0...-3], CodeLoader.library_extension + else + super stem, CodeLoader.library_extension + end + end + + private :initialize + + def load + CoreDB.store nil, @stem, @prefix, @feature, @record + + name = File.basename @feature, CodeLoader.library_extension + NativeMethod.load_extension(@feature, name) + + return true + rescue Object + unload_feature + raise + end + end + + def self.source_extension + ".rb" + end + + def self.library_extension + LIBSUFFIX + end + + # Loads rubygems using the bootstrap standard library files. + def self.load_rubygems + require "rubygems" + end + + # Loads the pre-compiled bytecode compiler. Sets up paths needed by the + # compiler to find dependencies like the parser. + def self.load_compiler + begin + require "rubinius/code/toolset" + + ToolSets.create :runtime do + begin + require "rubinius/code/melbourne" + rescue LoadError + STDERR.puts "Melbourne failed to load, Ruby source parsing disabled" + end + require "rubinius/code/processor" + require "rubinius/code/compiler" + require "rubinius/code/ast" + end + rescue Object => e + raise LoadError, "Unable to load the bytecode compiler", e + end + end + + def initialize(stem) + @stem = stem + end + + private :initialize + + def load_script + resource = Script.new @stem + + load_error unless resource.resolve + resource.load + end + + def load(wrap) + resource = CodeModule.new @stem, wrap + + load_error unless resource.resolve + resource.load + end + + def require + resource = Feature.new @stem + if resource.resolve + register_file resource.feature + return resource.load + end + + resource = Library.new @stem + if resource.resolve + register_file resource.feature + return resource.load + end + + load_error + ensure + unregister_file resource.feature if resource + end + + def require_relative(scope) + script = scope.current_script + if script + if script.data_path + path = File.dirname(File.realdirpath(script.data_path)) + else + path = Dir.pwd + end + + @stem = File.expand_path(@stem, path) + + require + else + raise LoadError.new "unable to get relative path" + end + end + + def register_file(path) + registry = self.class.registry + + Rubinius.synchronize(registry) do + registry << path + end + end + + def unregister_file(path) + registry = self.class.registry + + Rubinius.synchronize(registry) do + registry.delete_if { |x| x == path } + end + end + + def load_error + # Some versions of Rails override LoadError.new but do NOT override + # LoadError.exception, so we must use LoadError.new here to match MRI + # behavior. + raise LoadError.new("no such file to load -- #{@stem}", path: @stem) + end + end +end diff --git a/gxg/core/compact_lookup_table.rb b/gxg/core/compact_lookup_table.rb new file mode 100644 index 0000000..0d3821a --- /dev/null +++ b/gxg/core/compact_lookup_table.rb @@ -0,0 +1,33 @@ +module Rubinius + class CompactLookupTable + def self.allocate + Rubinius.primitive :compactlookuptable_allocate + raise PrimitiveFailure, "CompactLookupTable.allocate primitive failed" + end + + def [](key) + Rubinius.primitive :compactlookuptable_fetch + raise PrimitiveFailure, "CompactLookupTable#[] primitive failed" + end + + def []=(key, val) + Rubinius.primitive :compactlookuptable_store + raise PrimitiveFailure, "CompactLookupTable#[]= primitive failed" + end + + def key?(key) + Rubinius.primitive :compactlookuptable_has_key + raise PrimitiveFailure, "CompactLookupTable#key? primitive failed" + end + + def keys + Rubinius.primitive :compactlookuptable_keys + raise PrimitiveFailure, "CompactLookupTable#keys primitive failed" + end + + def values + Rubinius.primitive :compactlookuptable_values + raise PrimitiveFailure, "CompactLookupTable#keys primitive failed" + end + end +end diff --git a/gxg/core/comparable.rb b/gxg/core/comparable.rb new file mode 100644 index 0000000..c1ca3e2 --- /dev/null +++ b/gxg/core/comparable.rb @@ -0,0 +1,63 @@ +module Comparable + def ==(other) + return true if equal?(other) + + return false if Thread.detect_recursion(self, other) do + unless comp = (self <=> other) + return false + end + + return Comparable.compare_int(comp) == 0 + end + end + + def >(other) + unless comp = (self <=> other) + raise ArgumentError, "comparison of #{self.class} with #{other.class}" + end + + Comparable.compare_int(comp) > 0 + end + + def >=(other) + unless comp = (self <=> other) + raise ArgumentError, "comparison of #{self.class} with #{other.class}" + end + + Comparable.compare_int(comp) >= 0 + end + + def <(other) + unless comp = (self <=> other) + raise ArgumentError, "comparison of #{self.class} with #{other.class}" + end + + Comparable.compare_int(comp) < 0 + end + + def <=(other) + unless comp = (self <=> other) + raise ArgumentError, "comparison of #{self.class} with #{other.class}" + end + + Comparable.compare_int(comp) <= 0 + end + + def between?(min, max) + # This could be more elegant, but we need to use <= and => on self to match + # MRI. + return false if self < min + return false if self > max + return true + end + + # A version of MRI's rb_cmpint (sort of) + def self.compare_int(int) + return int if int.kind_of? Fixnum + + return 1 if int > 0 + return -1 if int < 0 + return 0 + end +end + diff --git a/gxg/core/compiled_code.rb b/gxg/core/compiled_code.rb new file mode 100644 index 0000000..4228899 --- /dev/null +++ b/gxg/core/compiled_code.rb @@ -0,0 +1,726 @@ +module Rubinius + class CompiledCode < Executable + def self.allocate + Rubinius.primitive :compiledcode_allocate + raise PrimitiveFailure, "CompiledCode.allocate primitive failed" + end + + def dup + Rubinius.primitive :compiledcode_dup + raise PrimitiveFailure, "CompiledCode#dup primitive failed" + end + + def call_sites + Rubinius.primitive :compiledcode_call_sites + raise PrimitiveFailure, "CompiledCode#call_sites primitive failed" + end + + def code_id + @code_id || stamp_id + end + + def constant_caches + Rubinius.primitive :compiledcode_constant_caches + raise PrimitiveFailure, "CompiledCode#constant_caches primitive failed" + end + + # Return the CompiledCode for caller of the method that called + # .of_sender. + # + # For example: + # + # def g + # f_code = Rubinius::CompiledCode.of_sender + # end + # + # def f + # g + # end + # + # f_code is the CompiledCode of f as requested by g. + # + def self.of_sender + Rubinius.primitive :compiledcode_of_sender + raise PrimitiveFailure, "CompiledCode.of_sender primitive failed" + end + + def jitted? + Rubinius.primitive :compiledcode_jitted_p + raise PrimitiveFailure, "CompiledCode#jitted? primitive failed" + end + + def sample_count + Rubinius.primitive :compiledcode_sample_count + raise PrimitiveFailure, "CompiledCode#sample_count primitive failed" + end + + # Returns the CompiledCode object for the currently executing Ruby + # method. For example: + # + # def m + # p Rubinius::CompiledCode.current.name + # end + # + def self.current + Rubinius.primitive :compiledcode_current + raise PrimitiveFailure, "CompiledCode.current primitive failed" + end + + ## + # Any CompiledCode with this value in it's serial slot is expected to be + # the default, core library version. + KernelMethodSerial = 47 + + ## + # Ivars: instance_variables, primitive, serial, name, iseq, stack_size, + # local_count, required_args, total_args, splat, literals, exceptions, + # lines, file, compiled, scope + ## + # This is runtime hints, added to the method by the VM to indicate how it's + # being used. + + attr_accessor :name + attr_accessor :hints # added by the VM to indicate how it's being used. + attr_accessor :metadata # [Tuple] extra data + attr_accessor :name # [Symbol] name of the method + attr_accessor :iseq # [Tuple] instructions to execute + attr_accessor :stack_size # [Integer] size of stack at compile time + attr_accessor :local_count # [Integer] number of local vars + attr_accessor :required_args # [Integer] number of required args + attr_accessor :post_args # [Integer] number of args after splat + attr_accessor :total_args # [Integer] number of total args + attr_accessor :splat # [Integer] POSITION of the splat arg + attr_accessor :literals # [Tuple] tuple of the literals + attr_accessor :lines # [Tuple] tuple of the lines where its found + attr_accessor :file # [Symbol] the file where this comes from + attr_accessor :local_names # [Array] names of the local vars + attr_accessor :scope # [LexicalScope] scope for looking up constants + attr_accessor :keywords # [Tuple] pairs of Symbol name, required flag + attr_accessor :arity # [Integer] number of arguments, negative if variadic. + attr_accessor :registers # [Integer] number of registers used. + + ## + # Compare this method with +other+. Instead of bugging out if +other+ + # isn't a {CompiledCode}, this returns +false+ immediately unless + # we're comparing two apples, AKA, {CompiledCode}s. The methods have + # to be the exact same in implementation, but their scoping (location) + # can differ. + # + # For instance: + # + # (module A; def m; 5; end; end) == (def m; 5; end) + # + # and + # + # def monkey; 5; end + # + # and + # + # module B + # def monkey; 5; end + # end + # + # would all be the same, despite their access to different ivars and scopes + # (which {CompiledCode}s DO keep track of) + # + # @todo Make example (in method documentation) match reality + # @param [Rubinius::CompiledCode] other the other part to compare + # @param [Boolean] + # + def ==(other) + return false unless other.kind_of? CompiledCode + equivalent_body?(other) and + @lines == other.lines and + @name == other.name + end + + def equivalent_body?(other) + @tags == other.tags and + @primitive == other.primitive and + @iseq == other.iseq and + @stack_size == other.stack_size and + @local_count == other.local_count and + @required_args == other.required_args and + @total_args == other.total_args and + @splat == other.splat and + block_index == other.block_index and + @literals == other.literals and + @file == other.file and + @local_names == other.local_names + end + + ## + # Returns the index of local +name+ or nil if there is no local. + # + def local_slot(name) + slot = 0 + while slot < @local_count + return slot if @local_names[slot] == name + slot += 1 + end + end + + ## + # Stores the index of the block argument in the metadata storage. + # + # @param [Fixnum] position + # + def block_index=(position) + if position + add_metadata(:block_index, position) + end + end + + ## + # @return [Fixnum|NilClass] + # + def block_index + return get_metadata(:block_index) + end + + ## + # Return a human readable interpretation of this method. + # + # @return [String] + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} name=#{@name} file=#{@file} line=#{defined_line}>" + end + + alias_method :to_s, :inspect + + ## + # Make the method change its scope so that it can act as though + # it's from somewhere else. You can pass in a method and +self+ + # will borrow its scope. + # + # @param [#scope] other the other method that has a scope we can borrow + def inherit_scope(other) + @scope = other.scope + end + + ## + # Set a breakpoint on +ip+. +obj+ can be any object. When the breakpoint + # is hit +obj+ is sent to the debugger so it can distinguish which breakpoint + # this is. + # + def set_breakpoint(ip, obj) + Rubinius.primitive :compiledcode_set_breakpoint + raise ArgumentError, "Unable to set breakpoint on #{inspect} at invalid bytecode address #{ip}" + end + + ## + # Erase a breakpoint at +ip+ + # + def clear_breakpoint(ip) + Rubinius.primitive :compiledcode_clear_breakpoint + raise ArgumentError, "Unable to clear breakpoint on #{inspect} at invalid bytecode address #{ip}" + end + + ## + # Indicate if there is a breakpoint set at +ip+ + # + def breakpoint?(ip) + Rubinius.primitive :compiledcode_is_breakpoint + raise ArgumentError, "Unable to retrieve breakpoint status on #{inspect} at bytecode address #{ip}" + end + + def stamp_id + Rubinius.primitive :compiledcode_stamp_id + raise ArgumentError, "CompiledCode#stamp_id primitive failed" + end + + class Script + attr_accessor :compiled_code + attr_accessor :file_path + attr_accessor :data_path + attr_accessor :eval_source + + def initialize(method, path=nil, for_eval=false) + @compiled_code = method + @file_path = path + @for_eval = for_eval + @eval_source = nil + @main = false + end + + private :initialize + + def eval? + @for_eval + end + + def make_main! + @main = true + end + + def main? + @main + end + end + + # Creates the Script instance for a toplevel compiled method. + def create_script(wrap=false) + script = CompiledCode::Script.new(self) + + # Setup the scoping. + cs = LexicalScope.new(Object) + cs.script = script + + if wrap + @scope = LexicalScope.new(Module.new, cs) + else + @scope = cs + end + + sc = Rubinius::Type.object_singleton_class(MAIN) + VM.reset_method_cache sc, :__script__ + sc.method_table.store :__script__, nil, self, @scope, 0, :public + + script + end + + def active_path + if @scope and script = @scope.current_script + if fp = script.file_path + return fp.dup + end + end + + @file.to_s + end + + def eval_source + if @scope and script = @scope.current_script + return script.eval_source + end + + return nil + end + + ## + # Return the line of source code at +ip+. + # + # @param [Fixnum] ip + # @return [Fixnum] line + def line_from_ip(ip) + return -1 unless @lines + return 0 if @lines.size < 2 + + low = 0 + high = @lines.size / 2 - 1 + + while low <= high + # the chance that we're going from a fixnum to a bignum + # here is low, but we still try to prevent that. + mid = low + ((high - low) / 2) + + line_index = mid * 2 + 1 + + if ip < @lines.at(line_index - 1) + high = mid - 1 + elsif ip >= @lines.at(line_index + 1) + low = mid + 1 + else + return @lines.at(line_index) + end + end + + @lines.at(@lines.size - 2) + end + + ## + # Returns the address (IP) of the first instruction in this CompiledCode + # that is on the specified line, or the address of the first instruction on + # the next code line after the specified line if there are no instructions + # on the requested line. + # This method only looks at instructions within the current CompiledCode; + # see #locate_line for an alternate method that also searches inside the child + # CompiledCodes. + # + # Optionally only consider ip's greater than +start+ + # + # @return [Fixnum] the address of the first instruction + # OR nil if there is no ip for the given line + def first_ip_on_line(line, start=nil) + i = 1 + total = @lines.size + + while i < total + cur_line = @lines.at(i) + if cur_line >= line + ip = @lines.at(i-1) + + if !start or ip > start + # matched the definition line, return 0 + return 0 if ip == -1 + return ip + end + end + + i += 2 + end + + nil + end + + ## + # The first line where instructions are located. + # + # @return [Fixnum] + def first_line + line_from_ip(0) + end + + ## + # Indicate the line in the source code that this + # was defined on. + # + def defined_line + # Detect a -1 ip, which indicates a definition entry. + return @lines[1] if @lines[0] == -1 + first_line + end + + ## + # + # Given all CompiledCodes in the system, find one that + # was defined in file +file+ and encompasses +line+ + # + def self.locate(file, line=nil) + file = StringValue file + + if line + line = Integer line + elsif m = /\A(.*):(\d+)\Z/.match(file) + file = m[1] + line = m[2].to_i + end + + ary = [] + ObjectSpace.find_object([:kind_of, Rubinius::CompiledCode], ary) + + methods = ary.find_all do |x| + x.scope and path = x.scope.absolute_active_path and \ + path.suffix?(file) + end + + return methods unless line + + methods.find_all { |x| x.first_ip_on_line(line) } + end + + ## + # Is this actually a block of code? + # + # @return [Boolean] + def is_block? + get_metadata(:for_block) + end + + def for_eval? + get_metadata(:for_eval) + end + + def for_module_body? + get_metadata(:for_module_body) + end + + def describe + str = "method #{@name}: #{@total_args} arg(s), #{@required_args} required" + if @splat + str << ", splatted." + end + str + end + + ## + # Convenience method to return an array of the child CompiledCodes from + # this CompiledCode's literals. + # + # @return [Tuple] + def child_methods + literals.select { |lit| lit.kind_of? CompiledCode } + end + + def change_name(name) + code = dup + code.name = name + + lits = Tuple.new(code.literals.size) + code.literals.each_with_index do |lit, idx| + if lit.kind_of? CompiledCode and lit.is_block? + lit = lit.change_name name + end + + lits[idx] = lit + end + + code.literals = lits + return code + end + + ## + # Locates the CompiledCode and instruction address (IP) of the first + # instruction on the specified line. This method recursively examines child + # compiled methods until an exact match for the searched line is found. + # It returns both the matching CompiledCode and the IP of the first + # instruction on the requested line, or nil if no match for the specified line + # is found. + # + # @return [(Rubinius::CompiledCode, Fixnum), NilClass] returns + # nil if nothing is found, else an array of size 2 containing the method + # the line was found in and the IP pointing there. + def locate_line(line) + i = 1 + total = @lines.size + while i < total + cur_line = @lines.at(i) + if cur_line == line + ip = @lines.at(i-1) + return nil if ip < 0 + return [self, ip] + elsif cur_line > line + break + end + + i += 2 + end + + # Didn't find line in this CM, so check if a contained + # CM encompasses the line searched for + child_methods.each do |child| + if res = child.locate_line(line) + return res + end + end + + # No child method is a match - fail + return nil + end + + ## + # Decodes the instruction sequence that is represented by this compileed + # method. Delegates to InstructionSequence to do the instruction decoding, + # but then converts opcode literal arguments to their actual values by looking + # them up in the literals tuple. + # Takes an optional bytecodes argument representing the bytecode that is to + # be decoded using this CompiledCode's locals and literals. This is provided + # for use by the debugger, where the bytecode sequence to be decoded may not + # exactly match the bytecode currently held by the CompiledCode, typically + # as a result of substituting yield_debugger instructions into the bytecode. + def decode(bytecodes = @iseq) + decoder = Rubinius::InstructionDecoder.new(bytecodes) + stream = decoder.decode(false) + ip = 0 + + stream.map do |inst| + instruct = Instruction.new(inst, self, ip) + ip += instruct.size + instruct + end + end + + def add_metadata(key, val) + raise TypeError, "key must be a symbol" unless key.kind_of? Symbol + + case val + when true, false, Symbol, Fixnum, String + # ok + else + raise TypeError, "invalid type of value" + end + + @metadata ||= nil # to deal with MRI seeing @metadata as not set + + unless @metadata + @metadata = Tuple.new(2) + @metadata[0] = key + @metadata[1] = val + return val + end + + i = 0 + fin = @metadata.size + + while i < fin + if @metadata[i] == key + @metadata[i + 1] = val + return val + end + + i += 2 + end + + tup = Tuple.new(fin + 2) + tup.copy_from @metadata, 0, fin, 0 + tup[fin] = key + tup[fin + 1] = val + + @metadata = tup + + return val + end + + def get_metadata(key) + return nil unless @metadata.kind_of? Tuple + + i = 0 + while i < @metadata.size + if @metadata[i] == key + return @metadata[i + 1] + end + + i += 2 + end + + return nil + end + + ## + # For Method#parameters + def parameters + params = [] + + return params unless respond_to?(:local_names) + + m = required_args - post_args + o = m + total_args - required_args + p = o + post_args + p += 1 if splat + + local_names.each_with_index do |name, i| + if i < m + params << parameter(:req, name) + elsif i < o + params << [:opt, name] + elsif splat == i + params << parameter(:rest, name) + elsif i < p + params << parameter(:req, name) + elsif block_index == i + params << [:block, name] + end + end + + params + end + + def parameter(kind, param) + array = [kind] + array << param unless param == :* or param[0, 2] == "_:" + array + end + private :parameter + + ## + # Represents virtual machine's CPU instruction. + # Instructions are organized into instruction + # sequences known as iSeq, forming body + # of CompiledCodes. + # + # To generate VM opcodes documentation + # use rake doc:vm task. + class Instruction + class Association + def initialize(index) + @index = index + end + + private :initialize + + def inspect + "literals[#{@index}]" + end + end + + class Location + FORMAT = "%04d:" + + def initialize(location) + @location = location + end + + private :initialize + + def inspect + FORMAT % @location + end + end + + def initialize(inst, code, ip) + @instruction = inst[0] + @args = inst[1..-1] + @comment = nil + + @args.each_index do |i| + case @instruction.args[i] + when :literal + @args[i] = code.literals[@args[i]] + when :local + # TODO: Blocks should be able to retrieve local names as well, + # but need access to method corresponding to home context + if code.local_names and !code.is_block? + @comment = code.local_names[args[i]].to_s + end + when :association + @args[i] = Association.new(args[i]) + when :location + @args[i] = Location.new(args[i]) + end + end + + @compiled_code = code + @ip = ip + end + + private :initialize + + # Instruction pointer + attr_reader :ip + + ## + # Return the line that this instruction is on in the method + # + def line + @compiled_code.line_from_ip(ip) + end + + ## + # Returns the OpCode object + # + attr_reader :instruction + + ## + # Returns the symbol representing the opcode for this instruction. + # + def opcode + @instruction.opcode + end + + ## + # Returns an array of 0 to 2 arguments, depending on the opcode. + # + attr_reader :args + + ## + # Returns a Fixnum indicating how wide the instruction takes up + # in the instruction stream + # + def size + @args.size + 1 + end + + ## + # A nice human readable interpretation of this set of instructions + def to_s + str = "#{Location::FORMAT} %-27s" % [@ip, opcode] + str << @args.map{ |a| a.inspect }.join(', ') + if @comment + str << " # #{@comment}" + end + + return str + end + end + end +end diff --git a/gxg/core/complex.rb b/gxg/core/complex.rb new file mode 100644 index 0000000..437f2bf --- /dev/null +++ b/gxg/core/complex.rb @@ -0,0 +1,340 @@ +# +# complex.rb - +# $Release Version: 0.5 $ +# $Revision: 1.3 $ +# $Date: 1998/07/08 10:05:28 $ +# by Keiju ISHITSUKA(SHL Japan Inc.) +# + +class Complex < Numeric + + undef_method :% + undef_method :< + undef_method :<= + undef_method :<=> + undef_method :> + undef_method :>= + undef_method :between? + undef_method :div + undef_method :divmod + undef_method :floor + undef_method :ceil + undef_method :modulo + undef_method :remainder + undef_method :round + undef_method :step + undef_method :truncate + undef_method :i + undef_method :positive? + undef_method :negative? + + def self.convert(real, imag = undefined) + if real.equal?(nil) || imag.equal?(nil) + raise TypeError, "cannot convert nil into Complex" + end + + real = real.to_c if real.kind_of?(String) + imag = imag.to_c if imag.kind_of?(String) + + if real.kind_of?(Complex) && !real.imag.kind_of?(Float) && real.imag == 0 + real = real.real + end + + if imag.kind_of?(Complex) && !imag.imag.kind_of?(Float) && imag.imag == 0 + imag = imag.real + end + + if real.kind_of?(Complex) && !imag.kind_of?(Float) && imag == 0 + return real + end + + if undefined.equal?(imag) + if real.kind_of?(Numeric) && !real.real? + return real + elsif !real.kind_of?(Numeric) + return Rubinius::Type.coerce_to(real, Complex, :to_c) + else + imag = 0 + end + elsif real.kind_of?(Numeric) && imag.kind_of?(Numeric) && (!real.real? || !imag.real?) + return real + imag * Complex.new(0, 1) + end + + return real if Rubinius.mathn_loaded? && imag.equal?(0) + rect(real, imag) + end + + def Complex.generic?(other) # :nodoc: + other.kind_of?(Integer) or + other.kind_of?(Float) or + (defined?(Rational) and other.kind_of?(Rational)) + end + + def Complex.rect(real, imag=0) + raise TypeError, 'not a real' unless check_real?(real) && check_real?(imag) + new(real, imag) + end + + def Complex.polar(r, theta=0) + raise TypeError, 'not a real' unless check_real?(r) && check_real?(theta) + + Complex(r*Math.cos(theta), r*Math.sin(theta)) + end + + def Complex.check_real?(obj) + obj.kind_of?(Numeric) && obj.real? + end + + def initialize(a, b = 0) + @real = a + @imag = b + end + + private :initialize + + def -@ + Complex(-real, -imag) + end + + def +(other) + if other.kind_of?(Complex) + Complex(real + other.real, imag + other.imag) + elsif other.kind_of?(Numeric) && other.real? + Complex(real + other, imag) + else + redo_coerced(:+, other) + end + end + + def -(other) + if other.kind_of?(Complex) + Complex(real - other.real, imag - other.imag) + elsif other.kind_of?(Numeric) && other.real? + Complex(real - other, imag) + else + redo_coerced(:-, other) + end + end + + def *(other) + if other.kind_of?(Complex) + Complex(real * other.real - imag * other.imag, + real * other.imag + imag * other.real) + elsif other.kind_of?(Numeric) && other.real? + Complex(real * other, imag * other) + else + redo_coerced(:*, other) + end + end + + def divide(other) + if other.kind_of?(Complex) + self * other.conjugate / other.abs2 + elsif other.kind_of?(Numeric) && other.real? + Complex(real.quo(other), imag.quo(other)) + else + redo_coerced(:quo, other) + end + end + + alias_method :/, :divide + alias_method :quo, :divide + + def ** (other) + if !other.kind_of?(Float) && other == 0 + return Complex(1) + end + if other.kind_of?(Complex) + r, theta = polar + ore = other.real + oim = other.imag + nr = Math.exp(ore*Math.log(r) - oim * theta) + ntheta = theta*ore + oim*Math.log(r) + Complex.polar(nr, ntheta) + elsif other.kind_of?(Integer) + if other > 0 + x = self + z = x + n = other - 1 + while n != 0 + while (div, mod = n.divmod(2) + mod == 0) + x = Complex(x.real*x.real - x.imag*x.imag, 2*x.real*x.imag) + n = div + end + z *= x + n -= 1 + end + z + else + if defined? Rational + (Rational.new(1, 1) / self) ** -other + else + self ** Float(other) + end + end + elsif Complex.generic?(other) + r, theta = polar + Complex.polar(r**other, theta*other) + else + x, y = other.coerce(self) + x**y + end + end + + def abs + Math.hypot(@real, @imag) + end + + alias_method :magnitude, :abs + + def abs2 + @real*@real + @imag*@imag + end + + def arg + Math.atan2(@imag, @real) + end + alias_method :angle, :arg + alias_method :phase, :arg + + def polar + [abs, arg] + end + + def conjugate + Complex(@real, -@imag) + end + alias_method :conj, :conjugate + + def ==(other) + if other.kind_of?(Complex) + real == other.real && imag == other.imag + elsif other.kind_of?(Numeric) && other.real? + real == other && imag == 0 + else + other == self + end + end + + def eql?(other) + other.kind_of?(Complex) and + imag.class == other.imag.class and + real.class == other.real.class and + self == other + end + + def coerce(other) + if other.kind_of?(Numeric) && other.real? + [Complex.new(other, 0), self] + elsif other.kind_of?(Complex) + [other, self] + else + raise TypeError, "#{other.class} can't be coerced into Complex" + end + end + + def denominator + @real.denominator.lcm(@imag.denominator) + end + + def numerator + cd = denominator + Complex(@real.numerator*(cd/@real.denominator), + @imag.numerator*(cd/@imag.denominator)) + end + + def real? + false + end + + def rect + [@real, @imag] + end + + alias_method :rectangular, :rect + + def to_f + raise RangeError, "can't convert #{self} into Float" unless !imag.kind_of?(Float) && imag == 0 + real.to_f + end + + def to_i + raise RangeError, "can't convert #{self} into Integer" unless !imag.kind_of?(Float) && imag == 0 + real.to_i + end + + def to_r + raise RangeError, "can't' convert #{self} into Rational" unless !imag.kind_of?(Float) && imag == 0 + real.to_r + end + + def rationalize(eps = nil) + raise RangeError, "can't' convert #{self} into Rational" unless !imag.kind_of?(Float) && imag == 0 + real.rationalize(eps) + end + + def to_s + result = real.to_s + + if imag.kind_of?(Float) ? !imag.nan? && imag.signbit? : imag < 0 + result << "-" + else + result << "+" + end + + imag_s = imag.abs.to_s + result << imag_s + + unless imag_s[-1] =~ /\d/ + result << "*" + end + + result << "i" + result + end + + def hash + @real.hash ^ @imag.hash + end + + def inspect + "(#{to_s})" + end + + def fdiv(other) + raise TypeError, "#{other.class} can't be coerced into Complex" unless other.is_a?(Numeric) + + # FIXME + self / other + end + + def marshal_dump + ary = [real, imag] + instance_variables.each do |ivar| + ary.instance_variable_set(ivar, instance_variable_get(ivar)) + end + ary + end + + private :marshal_dump + + def marshal_load(ary) + @real, @imag = ary + ary.instance_variables.each do |ivar| + instance_variable_set(ivar, ary.instance_variable_get(ivar)) + end + self + end + + class << self + private :convert + private :check_real? + + alias_method :rectangular, :rect + end + + attr_reader :real + attr_reader :imag + alias_method :imaginary, :imag +end diff --git a/gxg/core/complexifier.rb b/gxg/core/complexifier.rb new file mode 100644 index 0000000..85ab30c --- /dev/null +++ b/gxg/core/complexifier.rb @@ -0,0 +1,59 @@ +class String + class Complexifier + def initialize(value) + @value = value + end + + private :initialize + + def convert + if m = PATTERN0.match(@value) + sr = m[1] + si = m[2] + re = m.post_match + po = true + elsif m = PATTERN1.match(@value) + sr = nil + si = (m[1] || "") + (m[2] || "1") + re = m.post_match + po = false + elsif m = PATTERN2.match(@value) + sr = m[1] + si = m[2] ? m[3] + (m[4] || "1") : nil + re = m.post_match + po = false + else + return Complex.new(0, 0) + end + + r = 0 + i = 0 + + if sr + if sr.include?("/") + r = sr.to_r + elsif sr.match(/[.eE]/) + r = sr.to_f + else + r = sr.to_i + end + end + + if si + if si.include?("/") + i = si.to_r + elsif si.match(/[.eE]/) + i = si.to_f + else + i = si.to_i + end + end + + if po + Complex.polar(r, i) + else + Complex.rect(r, i) + end + end + end +end diff --git a/gxg/core/configuration.rb b/gxg/core/configuration.rb new file mode 100644 index 0000000..6d0252b --- /dev/null +++ b/gxg/core/configuration.rb @@ -0,0 +1,34 @@ +module Rubinius + class Configuration + def initialize + end + + private :initialize + + def get_variable(name) + Rubinius.primitive :vm_get_config_item + raise PrimitiveFailure, "Rubinius::Configuration#get_variable primitive failed" + end + + def get_section(section) + Rubinius.primitive :vm_get_config_section + raise PrimitiveFailure, "Rubinius::Configuration#get_section primitive failed" + end + + def section(section) + ary = get_section(section) + i = 0 + while i < ary.size + tup = ary[i] + yield tup[0], tup[1] + i += 1 + end + end + + def [](name) + get_variable(name) + end + + alias_method :get, :[] + end +end diff --git a/gxg/core/console.rb b/gxg/core/console.rb new file mode 100644 index 0000000..e214a1b --- /dev/null +++ b/gxg/core/console.rb @@ -0,0 +1,31 @@ +module Rubinius + module Console + class Server + attr_reader :inbox + attr_reader :outbox + attr_reader :thread + + def initialize + @inbox = Channel.new + @outbox = Channel.new + + @thread = Thread.new do + source = @inbox.receive + break unless source.size + + @outbox.send evaluate(source) + end + end + + private :initialize + + def evaluate(source) + begin + eval(source, TOPLEVEL_BINDING, "(console)", 1).inspect + rescue Object => e + e.message + e.backtrace.join("\n") + end + end + end + end +end diff --git a/gxg/core/constant_cache.rb b/gxg/core/constant_cache.rb new file mode 100644 index 0000000..6df26de --- /dev/null +++ b/gxg/core/constant_cache.rb @@ -0,0 +1,28 @@ +module Rubinius + class ConstantCache + attr_reader :name + attr_reader :value + attr_reader :module + attr_reader :lexical_scope + attr_reader :executable + + def ip + Rubinius.primitive :constant_cache_ip + raise PrimitiveFailure, "CallSite#ip primitive failed" + end + + def serial + Rubinius.primitive :constant_cache_serial + raise PrimitiveFailure, "CallSite#serial primitive failed" + end + + def location + "#{@executable.file}:#{@executable.line_from_ip(ip)}" + end + + def inspect + "#<#{self.class.name}:0x#{self.object_id.to_s(16)} #{location}##{@name} constant=#{@value} module=#{@module} lexical_scope=#{@lexical_scope}>" + end + end +end + diff --git a/gxg/core/constant_table.rb b/gxg/core/constant_table.rb new file mode 100644 index 0000000..aee2c4a --- /dev/null +++ b/gxg/core/constant_table.rb @@ -0,0 +1,97 @@ +module Rubinius + class ConstantTable + class Bucket + attr_reader :name + attr_accessor :constant + attr_accessor :visibility + attr_reader :next + + def private? + @visibility == :private + end + + def inspect + "#<#{self.class}:0x#{object_id.to_s(16)} name=#{@name.inspect} constant=#{@constant.inspect} visibility=#{visibility.inspect}>" + end + end + + attr_reader :bins + + def size + @entries + end + + alias_method :length, :size + + def self.allocate + Rubinius.primitive :constant_table_allocate + raise PrimitiveFailure, "ConstantTable.allocate primitive failed" + end + + def initialize(hash=nil) + return unless hash + hash.each do |k, v| + self[k] = v + end + end + + private :initialize + + def duplicate + Rubinius.primitive :constant_table_duplicate + raise PrimitiveFailure, "ConstantTable#duplicate primitive failed" + end + + def dup + copy = duplicate + Rubinius.privately do + copy.initialize_copy self + end + copy + end + + def name?(name) + Rubinius.primitive :constant_table_has_name + raise PrimitiveFailure, "ConstantTable#name? primitive failed" + end + + alias_method :has_name?, :name? + alias_method :include?, :name? + alias_method :member?, :name? + + def keys + Rubinius.primitive :constant_table_keys + raise PrimitiveFailure, "ConstantTable#keys primitive failed" + end + + def delete(name) + Rubinius.primitive :constant_table_delete + raise PrimitiveFailure, "ConstantTable#delete primitive failed" + end + + def each + raise LocalJumpError, "no block given" unless block_given? + + vals = values + + i = 0 + max = @bins + + while i < max + if entry = vals.at(i) + while entry + yield entry.name, entry.constant, entry.visibility + entry = entry.next + end + end + i += 1 + end + self + end + + def values + Rubinius.primitive :constant_table_values + raise PrimitiveFailure, "ConstantTable#values primitive failed" + end + end +end diff --git a/gxg/core/continuation.rb b/gxg/core/continuation.rb new file mode 100644 index 0000000..c0436c5 --- /dev/null +++ b/gxg/core/continuation.rb @@ -0,0 +1,43 @@ +class Continuation +# def self.create(task=nil) +# cont = self.allocate +# cont.setup(task) +# cont +# end +# +# def setup(task=nil) +# @task = task +# @value = nil +# end +# +# def call(*value) +# task = @task.dup +# if value.empty? +# @value = nil +# elsif value.size == 1 +# @value = value.pop +# else +# @value = value +# end +# Rubinius::Task.current = task +# end +# +# alias_method :[], :call +end + +module Kernel + def callcc + raise NotImplementedError, "call/cc is not implemented!" +# cont = Continuation.create +# # Task#dup appears as though it returns nil in the dup'd +# # task, kinda like fork(). +# task = Rubinius::Task.current.dup +# if task +# cont.task = task +# yield cont +# else +# return cont.value +# end + end + module_function :callcc +end diff --git a/gxg/core/ctype.rb b/gxg/core/ctype.rb new file mode 100644 index 0000000..c73314f --- /dev/null +++ b/gxg/core/ctype.rb @@ -0,0 +1,90 @@ +## +# Mixin containing byte classification methods. +#-- +# isspace, islower, ... are not in MRI core library. + +module Rubinius::CType + # The character literals (?x) are Fixnums in 1.8 and Strings in 1.9 + # so we use literal values instead so this is 1.8/1.9 compatible. + + # \n \r \t \f \v \a \b \e + def self.isctrl(num) + (num >= 7 and num <= 13) or num == 27 + end + + # ' ' \n \t \r \f \v + def self.isspace(num) + num == 32 or (num >= 9 and num <= 13) + end + + def self.isupper(num) + num >= 65 and num <= 90 + end + + def self.islower(num) + num >= 97 and num <= 122 + end + + def self.isdigit(num) + num >= 48 and num <= 57 + end + + def self.isalnum(num) + islower(num) or isupper(num) or isdigit(num) + end + + def self.toupper!(num) + num - 32 + end + + def self.toupper(num) + islower(num) ? toupper!(num) : num + end + + def self.tolower!(num) + num + 32 + end + + def self.tolower(num) + isupper(num) ? tolower!(num) : num + end + + def self.toprint(num) + # The character literals (?x) are Fixnums in 1.8 and Strings in 1.9 + # so we use literal values instead so this is 1.8/1.9 compatible. + case num + when 7; '\a' + when 8; '\b' + when 9; '\t' + when 10; '\n' + when 11; '\v' + when 12; '\f' + when 13; '\r' + when 27; '\e' + when 34; '\"' + when 35; Rubinius::Tuple['#$', '\#$', '#@', '\#@', '#{', '\#{', '#', '#'] + when 92; '\\\\' + else + if num < 32 || num > 126 + unprintable_chr(num) + else + num.chr + end + end + end + + def self.unprintable_chr(num) + str = "\\x00" + str.modify! + m = Rubinius::Mirror.reflect str + + c = num.to_s(16).upcase + m.copy_from c, 0, c.size, 4-c.size + + str + end + + def toprint + Printed[self] + end +end diff --git a/gxg/core/delegated_method.rb b/gxg/core/delegated_method.rb new file mode 100644 index 0000000..deafc91 --- /dev/null +++ b/gxg/core/delegated_method.rb @@ -0,0 +1,41 @@ +## +# DelegatedMethod is used to implement Module#define_method + +module Rubinius + class DelegatedMethod < Executable + def initialize(name, method, receiver, pass_self=false) + @name = name + @method = method + @receiver = receiver + @pass_self = pass_self + end + + private :initialize + + attr_reader :name + attr_reader :receiver + + def call(called_object, called_method, *args, &block) + args.unshift called_object if @pass_self + @receiver.__send__(@method, *args, &block) + end + + alias_method :activate, :call + + def arity + @receiver.arity + end + + def parameters + @receiver.parameters + end + + def source_location + @receiver.source_location + end + + def scope + nil + end + end +end diff --git a/gxg/core/deprecations.rb b/gxg/core/deprecations.rb new file mode 100644 index 0000000..e902f4e --- /dev/null +++ b/gxg/core/deprecations.rb @@ -0,0 +1,29 @@ +module Rubinius + DEPRECATIONS = { + # "Description" => "Alternative" + "Rubinius::Executable#primitive is deprecated." => + "Use Rubinius::Executable#tags instead.", + "Non-UTF-8 encodings for String objects are deprecated." => + "Use String objects encoded as UTF-8.", + "Encoding objects other than UTF-8 are deprecated." => + "Use ByteString (when available) to passed text data with non-UTF-8 encodings through a program. Use transcoding facilities at IO boundaries to read from or write to non-UTF-8 encodings. Use File.transcode to transcode a file from one encoding to another.", + "Mutating a String object is deprecated." => + "Use ByteString for binary data or MutableString for text (when available).", + "Insertion ordering of Hash is deprecated." => + "Use OrderedHash when insertion ordering is desired.", + "OpenSSL is deprecated." => + "Use the core cryptography and TLS libraries when available.", + "The Ruby stdlib is deprecated." => + "Use core libraries (eg Socket) or alternative gems when available.", + "Rubygems and Bundler are deprecated." => + "Use the IPFS library facility when available.", + "Use of the identifier 'type' at script scope is deprecated." => + "Use an identifier not named 'type' at script scope.", + "Use of the identifier 'import' at script, class/module, method, block, or function scope is deprecated." => + "Use an identifier not named 'import' at script, class/module, method, block, or function scope.", + "Defining a new method without removing an existing method definition for that name is deprecated." => + "Defining a new method with the same name as an existing method will define multiple methods. Use `remove_method` followed by `def` to replace a method.", + "Fixnum and Bignum classes are deprecated" => + "Use Integer instead of Fixnum or Bignum", + } +end diff --git a/gxg/core/dir.rb b/gxg/core/dir.rb new file mode 100644 index 0000000..b64e0e2 --- /dev/null +++ b/gxg/core/dir.rb @@ -0,0 +1,269 @@ +class Dir + include Enumerable + + def self.allocate + Rubinius.primitive :dir_allocate + raise PrimitiveFailure, "Dir.allocate primitive failed" + end + + def initialize(path, options=undefined) + path = Rubinius::Type.coerce_to_path path + + if options.equal? undefined + enc = nil + else + options = Rubinius::Type.coerce_to options, Hash, :to_hash + enc = options[:encoding] + enc = Rubinius::Type.coerce_to_encoding enc if enc + end + + Rubinius.invoke_primitive :dir_open, self, path, enc + end + + private :initialize + + def close + Rubinius.primitive :dir_close + raise PrimitiveFailure, "Dir#close primitive failed" + end + + def closed? + Rubinius.primitive :dir_closed_p + raise PrimitiveFailure, "Dir#closed? primitive failed" + end + + def read + entry = Rubinius.invoke_primitive :dir_read, self + return unless entry + + if Encoding.default_external == Encoding::US_ASCII && !entry.valid_encoding? + entry.force_encoding Encoding::ASCII_8BIT + return entry + end + + enc = Encoding.default_internal + enc ? entry.encode(enc) : entry + end + + def control(kind, pos) + Rubinius.primitive :dir_control + raise PrimitiveFailure, "Dir#__control__ primitive failed" + end + + private :control + + def self.open(path, options=undefined) + dir = new path, options + if block_given? + begin + value = yield dir + ensure + dir.close + end + + return value + else + return dir + end + end + + def self.entries(path, options=undefined) + ret = [] + + open(path, options) do |dir| + while s = dir.read + ret << s + end + end + + ret + end + + def self.exist?(path) + PrivateFile.directory?(path) + end + + class << self + alias_method :exists?, :exist? + end + + def self.home(user=nil) + PrivateFile.expand_path("~#{user}") + end + + def self.[](*patterns) + if patterns.size == 1 + pattern = Rubinius::Type.coerce_to_path(patterns[0]) + return [] if pattern.empty? + patterns = glob_split pattern + end + + glob patterns + end + + def self.glob(pattern, flags=0, &block) + if pattern.kind_of? Array + patterns = pattern + else + pattern = Rubinius::Type.coerce_to_path pattern + + return [] if pattern.empty? + + patterns = glob_split pattern + end + + matches = [] + index = 0 + + patterns.each do |pat| + pat = Rubinius::Type.coerce_to_path pat + enc = Rubinius::Type.ascii_compatible_encoding pat + Dir::Glob.glob pat, flags, matches + + total = matches.size + while index < total + Rubinius::Type.encode_string matches[index], enc + index += 1 + end + end + + if block + matches.each(&block) + return nil + end + + return matches + end + + def self.glob_split(pattern) + result = [] + start = 0 + m = Rubinius::Mirror.reflect pattern + while idx = m.find_string("\0", start) + result << pattern.byteslice(start, idx) + start = idx + 1 + end + result << pattern.byteslice(start, pattern.bytesize) + end + + def self.foreach(path) + return to_enum(:foreach, path) unless block_given? + + open(path) do |dir| + while s = dir.read + yield s + end + end + + nil + end + + def self.join_path(p1, p2, dirsep) + "#{p1}#{dirsep ? '/' : ''}#{p2}" + end + + def self.chdir(path = ENV['HOME']) + path = Rubinius::Type.coerce_to_path path + + if block_given? + original_path = self.getwd + error = FFI::Platform::POSIX.chdir path + Errno.handle(path) if error != 0 + + begin + value = yield path + ensure + error = FFI::Platform::POSIX.chdir original_path + Errno.handle(original_path) if error != 0 + end + + return value + else + error = FFI::Platform::POSIX.chdir path + Errno.handle path if error != 0 + error + end + end + + def self.mkdir(path, mode = 0777) + error = FFI::Platform::POSIX.mkdir(Rubinius::Type.coerce_to_path(path), mode) + Errno.handle path if error != 0 + error + end + + def self.rmdir(path) + error = FFI::Platform::POSIX.rmdir(Rubinius::Type.coerce_to_path(path)) + Errno.handle path if error != 0 + error + end + + def self.getwd + buf = String.pattern Rubinius::PATH_MAX, 0 + wd = FFI::Platform::POSIX.getcwd(buf, buf.length) + Errno.handle unless wd + Rubinius::Type.external_string wd + end + + def self.chroot(path) + ret = FFI::Platform::POSIX.chroot Rubinius::Type.coerce_to_path(path) + Errno.handle path if ret != 0 + ret + end + + def each + return to_enum unless block_given? + + while s = read + yield s + end + + self + end + + def fileno + Rubinius.primitive :dir_fileno + raise PrimitiveFailure, "Dir#fileno primitive failed" + end + + attr_reader :path + + alias_method :to_path, :path + + SeekKind = 0 + RewindKind = 1 + TellKind = 2 + + def pos + control TellKind, 0 + end + + alias_method :tell, :pos + + def pos=(position) + seek(position) + + position + end + + def seek(position) + control SeekKind, position + + self + end + + def rewind + control RewindKind, 0 + + self + end + + def inspect + "#<#{self.class}:#{object_id.to_s(16)} @path=#{@path}>" + end + + class << self + alias_method :pwd, :getwd + alias_method :delete, :rmdir + alias_method :unlink, :rmdir + end +end diff --git a/gxg/core/dir_glob.rb b/gxg/core/dir_glob.rb new file mode 100644 index 0000000..f9d86d4 --- /dev/null +++ b/gxg/core/dir_glob.rb @@ -0,0 +1,460 @@ +class Dir + module Glob + class Node + def initialize(nxt, flags) + @flags = flags + @next = nxt + @separator = nil + end + + private :initialize + + attr_writer :separator + + def separator + @separator || "/" + end + + def path_join(parent, ent) + return ent unless parent + if parent == "/" + "/#{ent}" + else + "#{parent}#{separator}#{ent}" + end + end + end + + class ConstantDirectory < Node + def initialize(nxt, flags, dir) + super nxt, flags + @dir = dir + end + + private :initialize + + def call(env, path) + full = path_join(path, @dir) + + # Don't check if full exists. It just costs us time + # and the downstream node will be able to check properly. + @next.call env, full + end + end + + class ConstantEntry < Node + def initialize(nxt, flags, name) + super nxt, flags + @name = name + end + + private :initialize + + def call(env, parent) + path = path_join(parent, @name) + + if File.exist? path + env.matches << path + end + end + end + + class RootDirectory < Node + def call(env, path) + @next.call env, "/" + end + end + + class RecursiveDirectories < Node + def call(env, start) + return if !start || !File.exist?(start) + + # Even though the recursive entry is zero width + # in this case, its left separator is still the + # dominant one, so we fix things up to use it. + switched = @next.dup + switched.separator = @separator + switched.call env, start + + stack = [start] + + allow_dots = ((@flags & File::FNM_DOTMATCH) != 0) + + until stack.empty? + path = stack.pop + begin + dir = Dir.new(path) + rescue Errno::ENOTDIR + next + end + + while ent = dir.read + next if ent == "." || ent == ".." + full = path_join(path, ent) + stat = File::Stat.lstat full + + if stat.directory? and (allow_dots or ent.getbyte(0) != 46) # ?. + stack << full + @next.call env, full + end + end + dir.close + end + end + end + + class StartRecursiveDirectories < Node + def call(env, start) + raise "invalid usage" if start + + # Even though the recursive entry is zero width + # in this case, its left separator is still the + # dominant one, so we fix things up to use it. + if @separator + switched = @next.dup + switched.separator = @separator + switched.call env, start + else + @next.call env, start + end + + stack = [] + + allow_dots = ((@flags & File::FNM_DOTMATCH) != 0) + + dir = Dir.new(".") + while ent = dir.read + next if ent == "." || ent == ".." + stat = File::Stat.lstat ent + + if stat.directory? and (allow_dots or ent.getbyte(0) != 46) # ?. + stack << ent + @next.call env, ent + end + end + dir.close + + until stack.empty? + path = stack.pop + dir = Dir.new(path) + while ent = dir.read + next if ent == "." || ent == ".." + full = path_join(path, ent) + stat = File::Stat.lstat full + + if stat.directory? and ent.getbyte(0) != 46 # ?. + stack << full + @next.call env, full + end + end + dir.close + end + end + end + + class Match < Node + def initialize(nxt, flags, glob) + super nxt, flags + @glob = glob || "" + end + + private :initialize + + def match?(str) + File.fnmatch @glob, str, @flags + end + end + + class DirectoryMatch < Match + def initialize(nxt, flags, glob) + super + + @glob.gsub! "**", "*" + end + + private :initialize + + def call(env, path) + return if path and !File.exist?("#{path}/.") + + dir = Dir.new(path ? path : ".") + while ent = dir.read + if match? ent + full = path_join(path, ent) + + if File.directory? full + @next.call env, full + end + end + end + dir.close + end + end + + class EntryMatch < Match + def call(env, path) + return if path and !File.exist?("#{path}/.") + + begin + dir = Dir.new(path ? path : ".") + rescue SystemCallError + return + end + + while ent = dir.read + if match? ent + env.matches << path_join(path, ent) + end + end + dir.close + end + end + + class DirectoriesOnly < Node + def call(env, path) + if path and File.exist?("#{path}/.") + env.matches << "#{path}/" + end + end + end + + class Environment + attr_reader :matches + + def initialize(matches=[]) + @matches = matches + end + + private :initialize + end + + def self.path_split(str) + start = 0 + ret = [] + + last_match = nil + + while match = %r!/+!.match_from(str, start) + cur_start, cur_end = match.full + ret << str.byteslice(start, cur_start - start) + ret << str.byteslice(cur_start, cur_end - cur_start) + + start = cur_end + + last_match = match + end + + if last_match + ret << last_match.post_match + else + ret << str + end + + # Trim from end + if !ret.empty? + while s = ret.last and s.empty? + ret.pop + end + end + + ret + end + + def self.single_compile(glob, flags=0) + parts = path_split(glob) + + if glob.getbyte(-1) == 47 # ?/ + last = DirectoriesOnly.new nil, flags + else + file = parts.pop + if /^[a-zA-Z0-9._]+$/.match(file) + last = ConstantEntry.new nil, flags, file + else + last = EntryMatch.new nil, flags, file + end + end + + until parts.empty? + last.separator = parts.pop + dir = parts.pop + + if dir == "**" + if parts.empty? + last = StartRecursiveDirectories.new last, flags + else + last = RecursiveDirectories.new last, flags + end + elsif /^[^\*\?\]]+$/.match(dir) + while /^[^\*\?\]]+$/.match(parts[-2]) + next_sep = parts.pop + next_sect = parts.pop + + dir = next_sect << next_sep << dir + end + + last = ConstantDirectory.new last, flags, dir + elsif !dir.empty? + last = DirectoryMatch.new last, flags, dir + end + end + + if glob.getbyte(0) == 47 # ?/ + last = RootDirectory.new last, flags + end + + last + end + + def self.run(node, matches=[]) + env = Environment.new(matches) + node.call env, nil + env.matches + end + + def self.glob_cache + @glob_cache + end + + def self.glob(pattern, flags, matches=[]) + # Rubygems uses Dir[] as a glorified File.exist? to check for multiple + # extensions. So we went ahead and sped up that specific case. + + if flags == 0 and + m = /^([a-zA-Z0-9_.\/\s]*[a-zA-Z0-9_.])(?:\{([^{}\/\*\?]*)\})?$/.match(pattern) + # no meta characters, so this is a glorified + # File.exist? check. We allow for a brace expansion + # only as a suffix. + + if braces = m[2] + stem = m[1] + + braces.split(",").each do |s| + path = "#{stem}#{s}" + if File.exist? path + matches << path + end + end + + # Split strips an empty closing part, so we need to add it back in + if braces.getbyte(-1) == 44 # ?, + matches << stem if File.exist? stem + end + else + matches << pattern if File.exist?(pattern) + end + + return matches + end + + ec_key = nil + + if gc = @glob_cache + ec_key = [pattern, flags] + if patterns = gc.retrieve(ec_key) + patterns.each do |node| + run node, matches + end + + return matches + end + end + + if pattern.include? "{" + patterns = compile(pattern, flags) + + gc.set ec_key, patterns if ec_key + + patterns.each do |node| + run node, matches + end + elsif node = single_compile(pattern, flags) + gc.set ec_key, [node] if ec_key + + run node, matches + else + matches + end + end + + def self.compile(pattern, flags=0, patterns=[]) + escape = (flags & File::FNM_NOESCAPE) == 0 + + rbrace = nil + lbrace = nil + + # Do a quick search for a { to start the search better + i = pattern.index("{") + + # If there was a { found, then search + if i + nest = 0 + + while i < pattern.size + char = pattern[i] + + if char == "{" + lbrace = i if nest == 0 + nest += 1 + end + + if char == "}" + nest -= 1 + end + + if nest == 0 + rbrace = i + break + end + + if char == "\\" and escape + i += 1 + end + + i += 1 + end + end + + # There was a full {} expression detected, expand each part of it + # recursively. + if lbrace and rbrace + pos = lbrace + front = pattern[0...lbrace] + back = pattern[(rbrace + 1)..-1] + + while pos < rbrace + nest = 0 + pos += 1 + last = pos + + while pos < rbrace and not (pattern[pos] == "," and nest == 0) + nest += 1 if pattern[pos] == "{" + nest -= 1 if pattern[pos] == "}" + + if pattern[pos] == "\\" and escape + pos += 1 + break if pos == rbrace + end + + pos += 1 + end + + brace_pattern = "#{front}#{pattern[last...pos]}#{back}" + + compile brace_pattern, flags, patterns + end + + # No braces found, match the pattern normally + else + # Don't use .glob here because this code can detect properly + # if a { is a brace or a just a normal character, but .glob can't. + # if .glob is used and there is a { as a normal character, it will + # recurse forever. + if node = single_compile(pattern, flags) + patterns << node + end + end + + patterns + end + end +end diff --git a/gxg/core/encoding.rb b/gxg/core/encoding.rb new file mode 100644 index 0000000..5c9d94c --- /dev/null +++ b/gxg/core/encoding.rb @@ -0,0 +1,570 @@ +class EncodingError < StandardError +end + +class Encoding + class UndefinedConversionError < EncodingError + attr_accessor :source_encoding_name + attr_accessor :destination_encoding_name + attr_accessor :source_encoding + attr_accessor :destination_encoding + attr_accessor :error_char + + private :source_encoding_name= + private :destination_encoding_name= + private :source_encoding= + private :destination_encoding= + private :error_char= + end + + class InvalidByteSequenceError < EncodingError + attr_accessor :source_encoding_name + attr_accessor :destination_encoding_name + attr_accessor :source_encoding + attr_accessor :destination_encoding + attr_accessor :error_bytes + attr_accessor :readagain_bytes + attr_writer :incomplete_input + + private :source_encoding_name= + private :destination_encoding_name= + private :source_encoding= + private :destination_encoding= + private :error_bytes= + private :readagain_bytes= + private :incomplete_input= + + def initialize(message="") + super(message) + + @incomplete_input = nil + end + + private :initialize + + def incomplete_input? + @incomplete_input + end + end + + class ConverterNotFoundError < EncodingError + end + + class CompatibilityError < EncodingError + end + + class Transcoding + attr_accessor :source + attr_accessor :target + + def inspect + "#<#{super} #{source} to #{target}" + end + end + + class Converter + attr_accessor :source_encoding + attr_accessor :destination_encoding + attr_reader :replacement + attr_reader :options + + def self.allocate + Rubinius.primitive :encoding_converter_allocate + raise PrimitiveFailure, "Encoding::Converter.allocate primitive failed" + end + + def self.asciicompat_encoding(string_or_encoding) + encoding = Rubinius::Type.try_convert_to_encoding string_or_encoding + + return if not encoding or undefined.equal? encoding + return if encoding.ascii_compatible? + + transcoding = TranscodingMap[encoding.name.upcase] + return unless transcoding and transcoding.size == 1 + + Encoding.find transcoding.keys.first.to_s + end + + def self.search_convpath(from, to, options=undefined) + new(from, to, options).convpath + end + + def initialize(from, to, options=undefined) + @source_encoding = Rubinius::Type.coerce_to_encoding from + @destination_encoding = Rubinius::Type.coerce_to_encoding to + + if options.kind_of? Fixnum + @options = options + elsif !undefined.equal? options + options = Rubinius::Type.coerce_to options, Hash, :to_hash + + @options = 0 + unless options.empty? + @options |= INVALID_REPLACE if options[:invalid] == :replace + @options |= UNDEF_REPLACE if options[:undef] == :replace + + if options[:newline] == :universal or options[:universal_newline] + @options |= UNIVERSAL_NEWLINE_DECORATOR + end + + if options[:newline] == :crlf or options[:crlf_newline] + @options |= CRLF_NEWLINE_DECORATOR + end + + if options[:newline] == :cr or options[:cr_newline] + @options |= CR_NEWLINE_DECORATOR + end + + @options |= XML_TEXT_DECORATOR if options[:xml] == :text + if options[:xml] == :attr + @options |= XML_ATTR_CONTENT_DECORATOR + @options |= XML_ATTR_QUOTE_DECORATOR + end + + replacement = options[:replace] + end + else + @options = 0 + end + + source_name = @source_encoding.name.upcase.to_sym + dest_name = @destination_encoding.name.upcase.to_sym + + unless source_name == dest_name + @convpath, @converters = TranscodingPath[source_name, dest_name] + end + + unless @convpath + conversion = "(#{@source_encoding.name} to #{@destination_encoding.name})" + msg = "code converter not found #{conversion}" + raise ConverterNotFoundError, msg + end + + if @options & (INVALID_REPLACE | UNDEF_REPLACE | UNDEF_HEX_CHARREF) + if replacement.nil? + if @destination_encoding == Encoding::UTF_8 + @replacement = "\xef\xbf\xbd".force_encoding(Encoding::UTF_8) + else + @replacement = "?".force_encoding(Encoding::US_ASCII) + end + else + @replacement = Rubinius::Type.coerce_to replacement, String, :to_str + end + + replacement_encoding_name = @replacement.encoding.name.upcase + @replacement_converters = [] + + @convpath.each do |enc| + name = enc.to_s.upcase + next if name == replacement_encoding_name + + _, converters = TranscodingPath[replacement_encoding_name, enc] + @replacement_converters << name << converters + end + end + end + + private :initialize + + def convert(str) + str = StringValue(str) + + dest = "" + status = primitive_convert str.dup, dest, nil, nil, @options | PARTIAL_INPUT + + if status == :invalid_byte_sequence or + status == :undefined_conversion or + status == :incomplete_input + raise last_error + end + + if status == :finished + raise ArgumentError, "converter already finished" + end + + if status != :source_buffer_empty + raise RuntimeError, "unexpected result of Encoding::Converter#primitive_convert: #{status}" + end + + dest + end + + def primitive_convert(source, target, offset=nil, size=nil, options=0) + source = StringValue(source) if source + target = StringValue(target) + + if offset.nil? + offset = target.bytesize + else + offset = Rubinius::Type.coerce_to offset, Fixnum, :to_int + end + + if size.nil? + size = -1 + else + size = Rubinius::Type.coerce_to size, Fixnum, :to_int + + if size < 0 + raise ArgumentError, "byte size is negative" + end + end + + if offset < 0 + raise ArgumentError, "byte offset is negative" + end + + if offset > target.bytesize + raise ArgumentError, "byte offset is greater than destination buffer size" + end + + if !options.kind_of? Fixnum + opts = Rubinius::Type.coerce_to options, Hash, :to_hash + + options = 0 + options |= PARTIAL_INPUT if opts[:partial_input] + options |= AFTER_OUTPUT if opts[:after_output] + end + + if primitive_errinfo.first == :invalid_byte_sequence + source.prepend putback + end + + Rubinius.invoke_primitive(:encoding_converter_primitive_convert, + self, source, target, offset, size, options) + end + + def putback(maxbytes=nil) + Rubinius.primitive :encoding_converter_putback + + putback maxbytes + end + + def finish + dest = "" + status = primitive_convert nil, dest + + if status == :invalid_byte_sequence or + status == :undefined_conversion or + status == :incomplete_input + raise last_error + end + + if status != :finished + raise RuntimeError, "unexpected result of Encoding::Converter#finish: #{status}" + end + + dest + end + + def last_error + error = Rubinius.invoke_primitive :encoding_converter_last_error, self + return if error.nil? + + result = error[:result] + error_bytes = error[:error_bytes] + error_bytes_msg = error_bytes.dump + source_encoding_name = error[:source_encoding_name] + destination_encoding_name = error[:destination_encoding_name] + + case result + when :invalid_byte_sequence + read_again_string = error[:read_again_string] + if read_again_string + msg = "#{error_bytes_msg} followed by #{read_again_string.dump} on #{source_encoding_name}" + else + msg = "#{error_bytes_msg} on #{source_encoding_name}" + end + + exc = InvalidByteSequenceError.new msg + when :incomplete_input + msg = "incomplete #{error_bytes_msg} on #{source_encoding_name}" + + exc = InvalidByteSequenceError.new msg + when :undefined_conversion + error_char = error_bytes + if codepoint = error[:codepoint] + error_bytes_msg = "U+%04X" % codepoint + end + + if source_encoding_name.to_sym == @source_encoding.name and + destination_encoding_name.to_sym == @destination_encoding.name + msg = "#{error_bytes_msg} from #{source_encoding_name} to #{destination_encoding_name}" + else + msg = "#{error_bytes_msg} to #{destination_encoding_name} in conversion from #{source_encoding_name}" + transcoder = @converters.first + msg << " to #{transcoder.target}" + end + + exc = UndefinedConversionError.new msg + end + + Rubinius.privately do + exc.source_encoding_name = source_encoding_name + src = Rubinius::Type.try_convert_to_encoding source_encoding_name + exc.source_encoding = src unless undefined.equal? src + + exc.destination_encoding_name = destination_encoding_name + dst = Rubinius::Type.try_convert_to_encoding destination_encoding_name + exc.destination_encoding = dst unless undefined.equal? dst + + if error_char + error_char.force_encoding src unless undefined.equal? src + exc.error_char = error_char + end + + if result == :invalid_byte_sequence or result == :incomplete_input + exc.error_bytes = error_bytes.force_encoding Encoding::ASCII_8BIT + + if bytes = error[:read_again_bytes] + exc.readagain_bytes = bytes.force_encoding Encoding::ASCII_8BIT + end + end + + if result == :invalid_byte_sequence + exc.incomplete_input = false + elsif result == :incomplete_input + exc.incomplete_input = true + end + end + + exc + end + + def primitive_errinfo + Rubinius.primitive :encoding_converter_primitive_errinfo + raise PrimitiveFailure, "Encoding::Converter#primitive_errinfo primitive failed" + end + + def convpath + path = [] + a = 0 + b = @convpath.size - 1 + + while a < b + path << [Encoding.find(@convpath[a].to_s), Encoding.find(@convpath[a + 1].to_s)] + a += 1 + end + + path << "xml_text_escape" if @options & XML_TEXT_DECORATOR != 0 + path << "xml_attr_content_escape" if @options & XML_ATTR_CONTENT_DECORATOR != 0 + path << "xml_attr_quote" if @options & XML_ATTR_QUOTE_DECORATOR != 0 + path << "universal_newline" if @options & UNIVERSAL_NEWLINE_DECORATOR != 0 + path << "crlf_newline" if @options & CRLF_NEWLINE_DECORATOR != 0 + path << "cr_newline" if @options & CR_NEWLINE_DECORATOR != 0 + + path + end + + def inspect + "#" + end + + def replacement=(str) + str = StringValue(str) + + @replacement = str.encode(@destination_encoding) + end + + class TranscodingPath + def self.paths + @paths + end + + def self.default_transcoders? + @transcoders_count == TranscodingMap.size + end + + def self.[](source, target) + key = "[#{source}, #{target}]" + + path, converters = paths[key] + + unless path + return unless path = search(source, target) + paths[key] = [path] + end + + unless converters + converters = get_converters path + paths[key][1] = converters + end + + return path, converters + end + + def self.search(source, target) + if entry = TranscodingMap[source] + if entry[target] + return [source, target] + else + visited = { source => true } + search = { [source] => entry } + + until search.empty? + path, table = search.shift + + table.each do |key, _| + next if visited.key? key + next unless entry = TranscodingMap[key] + + return path << key << target if entry[target] + + unless visited.key? key + search[path.dup << key] = entry + visited[key] = true + end + end + end + end + end + end + + def self.get_converters(path) + converters = [] + total = path.size - 1 + i = 0 + + while i < total + entry = TranscodingMap[path[i]][path[i + 1]] + + if entry.kind_of? String + lib = "#{Rubinius::ENC_PATH}/#{entry}" + Rubinius::NativeMethod.load_extension lib, entry + + entry = TranscodingMap[path[i]][path[i + 1]] + end + + converters << entry + + i += 1 + end + + converters + end + end + end + + attr_reader :name + attr_reader :dummy + + alias_method :to_s, :name + alias_method :dummy?, :dummy + + def self.compatible?(a, b) + Rubinius.primitive :encoding_compatible_p + raise PrimitiveFailure, "Encoding.compatible? primitive failed" + end + + def replicate(name) + Rubinius.primitive :encoding_replicate + raise PrimitiveFailure, "Encoding#replicate primitive failed" + end + + def ascii_compatible? + Rubinius.primitive :encoding_ascii_compatible_p + raise PrimitiveFailure, "Encoding#ascii_compatible? primitive failed" + end + + def self.aliases + aliases = {} + EncodingMap.each do |n, r| + index = r.last + next unless index + + aname = r.first + aliases[aname] = EncodingList[index].name if aname + end + + aliases + end + + def self.set_alias_index(name, obj) + key = name.upcase.to_sym + + case obj + when Encoding + source_name = obj.name + when nil + EncodingMap[key][1] = nil + return + else + source_name = StringValue(obj) + end + + entry = EncodingMap[source_name.upcase.to_sym] + raise ArgumentError, "unknown encoding name - #{source_name}" unless entry + index = entry.last + + EncodingMap[key][1] = index + end + + class << self + private :set_alias_index + end + + def self.default_external + if undefined.equal? @default_external + @default_external = find "external" + end + @default_external + end + + def self.default_external=(enc) + raise ArgumentError, "default external encoding cannot be nil" if enc.nil? + + set_alias_index "external", enc + set_alias_index "filesystem", enc + @default_external = undefined + end + + def self.default_internal + # Rubinius internal encoding is always UTF-8 + end + + def self.default_internal=(enc) + Rubinius::Logger.system.warn "Encoding.default_internal= is deprecated. Rubinius internal encoding is always UTF-8" + end + + def self.find(name) + enc = Rubinius::Type.try_convert_to_encoding name + return enc unless undefined.equal? enc + + raise ArgumentError, "unknown encoding name - #{name}" + end + + def self.list + EncodingList + end + + def self.locale_charmap + LocaleCharmap + end + + def self.name_list + EncodingMap.map do |n, r| + index = r.last + r.first or (index and EncodingList[index].name) + end + end + + def inspect + "#" + end + + def names + entry = EncodingMap[name.upcase.to_sym] + names = [name] + EncodingMap.each do |k, r| + aname = r.first + names << aname if aname and r.last == entry.last + end + names + end + + def _dump(depth) + name + end + + def self._load(name) + find name + end +end diff --git a/gxg/core/enumerable.rb b/gxg/core/enumerable.rb new file mode 100644 index 0000000..39e0146 --- /dev/null +++ b/gxg/core/enumerable.rb @@ -0,0 +1,864 @@ +# Just to save you 10 seconds, the reason we always use #each to extract +# elements instead of something simpler is because Enumerable can not assume +# any other methods than #each. If needed, class-specific versions of any of +# these methods can be written *in those classes* to override these. + +module Enumerable + def chunk + raise ArgumentError, "no block given" unless block_given? + ::Enumerator.new do |yielder| + previous = nil + accumulate = [] + each do |element| + key = yield(element) + if key.nil? || (key.is_a?(Symbol) && key.to_s[0, 1] == "_") + yielder.yield [previous, accumulate] unless accumulate.empty? + accumulate = [] + previous = nil + case key + when nil, :_separator + when :_alone + yielder.yield [key, [element]] + else + raise RuntimeError, "symbols beginning with an underscore are reserved" + end + else + if previous.nil? || previous == key + accumulate << element + else + yielder.yield [previous, accumulate] unless accumulate.empty? + accumulate = [element] + end + previous = key + end + end + yielder.yield [previous, accumulate] unless accumulate.empty? + end + end + + def collect + if block_given? + ary = [] + each do |*element| + ary << yield(*element) + end + ary + else + to_enum(:collect) { enumerator_size } + end + end + + alias_method :map, :collect + + def count(item = undefined) + seq = 0 + if !undefined.equal?(item) + each do + element = Rubinius.single_block_arg + seq += 1 if item == element + end + elsif block_given? + each { |element| seq += 1 if yield(element) } + else + each { seq += 1 } + end + seq + end + + def each_entry(*pass) + return to_enum(:each_entry, *pass) { enumerator_size } unless block_given? + each(*pass) do + element = Rubinius.single_block_arg + yield element + end + self + end + + def each_with_object(memo) + return to_enum(:each_with_object, memo) { enumerator_size } unless block_given? + each do + element = Rubinius.single_block_arg + yield element, memo + end + memo + end + + alias_method :with_object, :each_with_object + + def flat_map + return to_enum(:flat_map) { enumerator_size } unless block_given? + + array = [] + each do |*element| + result = yield(*element) + + value = Rubinius::Type.try_convert(result, Array, :to_ary) || result + + if value.kind_of? Array + array.concat value + else + array.push value + end + end + + array + end + + alias_method :collect_concat, :flat_map + + def lazy + Enumerator::Lazy.new(self, enumerator_size) do |yielder, *args| + yielder.<<(*args) + end + end + + def enumerator_size + Rubinius::Type.object_respond_to?(self, :size) ? size : nil + end + private :enumerator_size + + def group_by + return to_enum(:group_by) { enumerator_size } unless block_given? + + h = {} + each do + element = Rubinius.single_block_arg + key = yield(element) + if h.key?(key) + h[key] << element + else + h[key] = [element] + end + end + Rubinius::Type.infect h, self + h + end + + def slice_before(pattern = undefined, &block) + pattern_given = !(undefined.equal? pattern) + + raise ArgumentError, "cannot pass both pattern and block" if pattern_given && block_given? + raise ArgumentError, "a pattern or a block must be provided" if !pattern_given && !block_given? + + block = Proc.new{ |elem| pattern === elem } if pattern_given + + Enumerator.new do |yielder| + accumulator = nil + each do + element = Rubinius.single_block_arg + start_new = block.yield(element) + if start_new + yielder.yield accumulator if accumulator + accumulator = [element] + else + accumulator ||= [] + accumulator << element + end + end + yielder.yield accumulator if accumulator + end + end + + def slice_after(pattern = undefined, &block) + pattern_given = !undefined.equal?(pattern) + + raise ArgumentError, "cannot pass both pattern and block" if pattern_given && block_given? + raise ArgumentError, "a pattern or a block must be provided" if !pattern_given && !block_given? + + block = Proc.new { |elem| pattern === elem } if pattern_given + + Enumerator.new do |yielder| + accumulator = nil + each do + element = Rubinius.single_block_arg + end_chunk = block.yield(element) + accumulator ||= [] + if end_chunk + accumulator << element + yielder.yield accumulator + accumulator = nil + else + accumulator << element + end + end + yielder.yield accumulator if accumulator + end + end + + def slice_when(&block) + raise ArgumentError, "wrong number of arguments (0 for 1)" unless block_given? + + Enumerator.new do |enum| + ary = nil + last_after = nil + each_cons(2) do |before, after| + last_after = after + match = block.call before, after + + ary ||= [] + if match + ary << before + enum.yield ary + ary = [] + else + ary << before + end + end + + unless ary.nil? + ary << last_after + enum.yield ary + end + end + end + + def chunk_while(&block) + raise ArgumentError, "no block given" unless block_given? + + slice_when { |before, after| !(yield before, after) } + end + + def to_a(*arg) + ary = [] + each(*arg) do + element = Rubinius.single_block_arg + ary << element + nil + end + Rubinius::Type.infect ary, self + ary + end + alias_method :entries, :to_a + + def to_h(*arg) + h = {} + each_with_index(*arg) do |element, i| + unless element.respond_to?(:to_ary) + raise TypeError, "wrong element type #{element.class} at #{i} (expected array)" + end + + ary = element.to_ary + if ary.size != 2 + raise ArgumentError, "wrong array length at #{i} (expected 2, was #{ary.size})" + end + + h[ary[0]] = ary[1] + end + h + end + + def zip(*args) + args.map! do |a| + if a.respond_to? :to_ary + a.to_ary + else + a.to_enum(:each) + end + end + + results = [] + i = 0 + each do + element = Rubinius.single_block_arg + entry = args.inject([element]) do |ary, a| + ary << case a + when Array + a[i] + else + begin + a.next + rescue StopIteration + nil + end + end + end + + yield entry if block_given? + + results << entry + i += 1 + end + + return nil if block_given? + results + end + + def each_with_index(*args) + return to_enum(:each_with_index, *args) { enumerator_size } unless block_given? + + idx = 0 + each(*args) do + element = Rubinius.single_block_arg + yield element, idx + idx += 1 + end + + self + end + + def grep(pattern) + ary = [] + + each do + element = Rubinius.single_block_arg + if pattern === element + Regexp.set_block_last_match + if block_given? + ary << yield(element) + else + ary << element + end + end + end + + ary + end + + def grep_v(pattern) + ary = [] + + each do + element = Rubinius.single_block_arg + unless pattern === element + if block_given? + ary << yield(element) + else + ary << element + end + end + end + + ary + end + + def sort(&prc) + ary = to_a + ary.frozen? ? ary.sort(&prc) : ary.sort!(&prc) + end + + class SortedElement + def initialize(val, sort_id) + @value, @sort_id = val, sort_id + end + + private :initialize + + attr_reader :value + attr_reader :sort_id + + def <=>(other) + @sort_id <=> other.sort_id + end + end + + def sort_by + return to_enum(:sort_by) { enumerator_size } unless block_given? + + # Transform each value to a tuple with the value and it's sort by value + sort_values = map do + element = Rubinius.single_block_arg + SortedElement.new(element, yield(element)) + end.to_a + + # Now sort the tuple according to the sort by value + sort_values.sort! + + # Now strip of the tuple leaving the original value + sort_values.map! { |ary| ary.value } + end + + def inject(initial=undefined, sym=undefined) + if !block_given? or !undefined.equal?(sym) + if undefined.equal?(sym) + sym = initial + initial = undefined + end + + # Do the sym version + + sym = sym.to_sym + + each do + element = Rubinius.single_block_arg + if undefined.equal? initial + initial = element + else + initial = initial.__send__(sym, element) + end + end + + # Block version + else + each do + element = Rubinius.single_block_arg + if undefined.equal? initial + initial = element + else + initial = yield(initial, element) + end + end + end + + undefined.equal?(initial) ? nil : initial + end + alias_method :reduce, :inject + + def all? + if block_given? + each { |*element| return false unless yield(*element) } + else + each { return false unless Rubinius.single_block_arg } + end + true + end + + def any? + if block_given? + each { |*element| return true if yield(*element) } + else + each { return true if Rubinius.single_block_arg } + end + false + end + + def cycle(many=nil) + unless block_given? + return to_enum(:cycle, many) do + Rubinius::EnumerableHelper.cycle_size(enumerator_size, many) + end + end + + if many + many = Rubinius::Type.coerce_to_collection_index many + return nil if many <= 0 + else + many = nil + end + + cache = [] + each do + element = Rubinius.single_block_arg + cache << element + yield element + end + + return nil if cache.empty? + + if many + i = 0 + many -= 1 + while i < many + cache.each { |element| yield element } + i += 1 + end + else + while true + cache.each { |element| yield element } + end + end + + nil + end + + def drop(n) + n = Rubinius::Type.coerce_to_collection_index n + raise ArgumentError, "attempt to drop negative size" if n < 0 + + ary = to_a + return [] if n > ary.size + ary[n...ary.size] + end + + def drop_while + return to_enum(:drop_while) unless block_given? + + ary = [] + dropping = true + each do + element = Rubinius.single_block_arg + ary << element unless dropping &&= yield(element) + end + + ary + end + + def each_cons(num) + n = Rubinius::Type.coerce_to_collection_index num + raise ArgumentError, "invalid size: #{n}" if n <= 0 + + unless block_given? + return to_enum(:each_cons, num) do + enum_size = enumerator_size + if enum_size.nil? + nil + elsif enum_size == 0 || enum_size < n + 0 + else + enum_size - n + 1 + end + end + end + + array = [] + each do + element = Rubinius.single_block_arg + array << element + array.shift if array.size > n + yield array.dup if array.size == n + end + nil + end + + def each_slice(slice_size) + n = Rubinius::Type.coerce_to_collection_index slice_size + raise ArgumentError, "invalid slice size: #{n}" if n <= 0 + + unless block_given? + return to_enum(:each_slice, slice_size) do + enum_size = enumerator_size + enum_size.nil? ? nil : (enum_size.to_f / n).ceil + end + end + + a = [] + each do + element = Rubinius.single_block_arg + a << element + if a.length == n + yield a + a = [] + end + end + + yield a unless a.empty? + nil + end + + def find(ifnone=nil) + return to_enum(:find, ifnone) unless block_given? + + each do + element = Rubinius.single_block_arg + return element if yield(element) + end + + ifnone.call if ifnone + end + + alias_method :detect, :find + + def find_all + return to_enum(:find_all) { enumerator_size } unless block_given? + + ary = [] + each do + element = Rubinius.single_block_arg + ary << element if yield(element) + end + ary + end + + alias_method :select, :find_all + + def find_index(value=undefined) + if undefined.equal? value + return to_enum(:find_index) unless block_given? + + i = 0 + each do |*args| + return i if yield(*args) + i += 1 + end + else + i = 0 + each do + e = Rubinius.single_block_arg + return i if e == value + i += 1 + end + end + nil + end + + def first(n=undefined) + return __take__(n) unless undefined.equal?(n) + each do + return Rubinius.single_block_arg + end + nil + end + + def min(n=nil) + if n.nil? + min = undefined + each do + element = Rubinius.single_block_arg + if undefined.equal? min + min = element + else + comp = block_given? ? yield(element, min) : element <=> min + unless comp + raise ArgumentError, "comparison of #{element.class} with #{min} failed" + end + + if Comparable.compare_int(comp) < 0 + min = element + end + end + end + + undefined.equal?(min) ? nil : min + else + if block_given? + sort { |a, b| yield a, b }.take n + else + sort.take n + end + end + end + + def max(n=nil) + if n.nil? + max = undefined + each do + element = Rubinius.single_block_arg + if undefined.equal? max + max = element + else + comp = block_given? ? yield(element, max) : element <=> max + unless comp + raise ArgumentError, "comparison of #{element.class} with #{max} failed" + end + + if Comparable.compare_int(comp) > 0 + max = element + end + end + end + + undefined.equal?(max) ? nil : max + else + if block_given? + sort { |a, b| yield a, b }.reverse.take n + else + sort.reverse.take n + end + end + end + + def max_by(n=nil) + return to_enum(:max_by, n) { enumerator_size } unless block_given? + + if n.nil? + max_element = nil + max_result = undefined + + each do + element = Rubinius.single_block_arg + result = yield element + + if undefined.equal?(max_result) or \ + Rubinius::Type.coerce_to_comparison(max_result, result) < 0 + max_element = element + max_result = result + end + end + + max_element + else + sort_by { |element| yield element }.reverse.take n + end + end + + def min_by(n=nil) + return to_enum(:min_by, n) { enumerator_size } unless block_given? + + if n.nil? + min_element = nil + min_result = undefined + + each do + element = Rubinius.single_block_arg + result = yield element + + if undefined.equal?(min_result) or \ + Rubinius::Type.coerce_to_comparison(min_result, result) > 0 + min_element = element + min_result = result + end + end + + min_element + else + sort_by { |element| yield element }.take n + end + end + + def self.sort_proc + @sort_proc ||= Proc.new do |a, b| + unless ret = a <=> b + raise ArgumentError, "Improper spaceship value" + end + ret + end + end + + def minmax(&block) + block = Enumerable.sort_proc unless block + first_time = true + min, max = nil + + each do + element = Rubinius.single_block_arg + if first_time + min = max = element + first_time = false + else + unless min_cmp = block.call(min, element) + raise ArgumentError, "comparison failed" + end + min = element if min_cmp > 0 + + unless max_cmp = block.call(max, element) + raise ArgumentError, "comparison failed" + end + + max = element if max_cmp < 0 + end + end + [min, max] + end + + def minmax_by(&block) + return to_enum(:minmax_by) { enumerator_size } unless block_given? + + min_element = nil + min_result = undefined + + max_element = nil + max_result = undefined + + each do + element = Rubinius.single_block_arg + result = yield element + + if undefined.equal?(min_result) or \ + Rubinius::Type.coerce_to_comparison(min_result, result) > 0 + min_element = element + min_result = result + end + + if undefined.equal?(max_result) or \ + Rubinius::Type.coerce_to_comparison(max_result, result) < 0 + max_element = element + max_result = result + end + end + + [min_element, max_element] + end + + def none? + if block_given? + each { |*element| return false if yield(*element) } + else + each { return false if Rubinius.single_block_arg } + end + + return true + end + + def one? + found_one = false + + if block_given? + each do |*element| + if yield(*element) + return false if found_one + found_one = true + end + end + else + each do + if Rubinius.single_block_arg + return false if found_one + found_one = true + end + end + end + + found_one + end + + def partition + return to_enum(:partition) { enumerator_size } unless block_given? + + left = [] + right = [] + each do + element = Rubinius.single_block_arg + yield(element) ? left.push(element) : right.push(element) + end + + return [left, right] + end + + def reject + return to_enum(:reject) { enumerator_size } unless block_given? + + ary = [] + each do + element = Rubinius.single_block_arg + ary << element unless yield(element) + end + + ary + end + + def reverse_each(&block) + return to_enum(:reverse_each) { enumerator_size } unless block_given? + + # There is no other way then to convert to an array first... see 1.9's source. + to_a.reverse_each(&block) + self + end + + def take(n) + n = Rubinius::Type.coerce_to_collection_index n + raise ArgumentError, "attempt to take negative size: #{n}" if n < 0 + + array = [] + + unless n <= 0 + each do + element = Rubinius.single_block_arg + array << element + break if array.size >= n + end + end + + array + end + alias_method :__take__, :take + private :__take__ + + def take_while + return to_enum(:take_while) unless block_given? + + array = [] + each do |element| + return array unless yield(element) + array << element + end + + array + end + + def include?(obj) + each { return true if Rubinius.single_block_arg == obj } + false + end + + alias_method :member?, :include? + +end diff --git a/gxg/core/enumerable_helper.rb b/gxg/core/enumerable_helper.rb new file mode 100644 index 0000000..9e3f68f --- /dev/null +++ b/gxg/core/enumerable_helper.rb @@ -0,0 +1,13 @@ +module Rubinius + module EnumerableHelper + def self.cycle_size(enum_size, many) + if many + many = Rubinius::Type.coerce_to_collection_index many + many = 0 if many < 0 + (_size = enum_size).nil? ? nil : _size * many + else + enum_size.nil? ? nil : Float::INFINITY + end + end + end +end diff --git a/gxg/core/enumerator.rb b/gxg/core/enumerator.rb new file mode 100644 index 0000000..88e4219 --- /dev/null +++ b/gxg/core/enumerator.rb @@ -0,0 +1,500 @@ +class Enumerator + include Enumerable + + attr_writer :args + private :args= + + attr_writer :size + private :size= + + attr_reader :generator + protected :generator + + attr_reader :lookahead + protected :lookahead + + def initialize_enumerator(receiver, size, method_name, *method_args) + @object = receiver + @size = size + @iter = method_name + @args = method_args + @generator = nil + @lookahead = [] + @feedvalue = nil + + self + end + private :initialize_enumerator + + def initialize(receiver_or_size=undefined, method_name=:each, *method_args, &block) + size = nil + + if block_given? + unless undefined.equal? receiver_or_size + size = if receiver_or_size.nil? || receiver_or_size == Float::INFINITY || receiver_or_size.respond_to?(:call) + receiver_or_size + else + Rubinius::Type.coerce_to receiver_or_size, Integer, :to_int + end + end + + receiver = Generator.new(&block) + else + if undefined.equal? receiver_or_size + raise ArgumentError, "Enumerator#initialize requires a block when called without arguments" + end + + receiver = receiver_or_size + end + + method_name = Rubinius::Type.coerce_to method_name, Symbol, :to_sym + + initialize_enumerator receiver, size, method_name, *method_args + + self + end + private :initialize + + def each(*args) + enumerator = self + new_args = @args + + unless args.empty? + enumerator = dup + new_args = @args.empty? ? args : (@args + args) + end + + Rubinius.privately do + enumerator.args = new_args + end + + if block_given? + Rubinius.privately do + enumerator.each_with_block do |*yield_args| + yield(*yield_args) + end + end + else + enumerator + end + end + + def each_with_block + @object.__send__ @iter, *@args do |*args| + ret = yield(*args) + unless @feedvalue.nil? + ret = @feedvalue + @feedvalue = nil + end + ret + end + end + private :each_with_block + + def each_with_index + return to_enum(:each_with_index) { size } unless block_given? + + idx = 0 + + each do + o = Rubinius.single_block_arg + val = yield(o, idx) + idx += 1 + val + end + end + + def next + return @lookahead.shift unless @lookahead.empty? + + @generator ||= Iterator.new self + + begin + return @generator.next if @generator.next? + rescue StopIteration + end + + exception = StopIteration.new "iteration reached end" + Rubinius.privately do + exception.result = @generator.result + end + + raise exception + end + + def next_values + Array(self.next) + end + + def peek + return @lookahead.first unless @lookahead.empty? + item = self.next + @lookahead << item + item + end + + def peek_values + Array(self.peek).dup + end + + def rewind + @object.rewind if @object.respond_to? :rewind + @generator.rewind if @generator + @lookahead = [] + @feedvalue = nil + self + end + + def size + @size.respond_to?(:call) ? @size.call : @size + end + + def with_index(offset=0) + if offset + offset = Rubinius::Type.coerce_to offset, Integer, :to_int + else + offset = 0 + end + + return to_enum(:with_index, offset) { size } unless block_given? + + each do + o = Rubinius.single_block_arg + val = yield(o, offset) + offset += 1 + val + end + end + + def feed(val) + raise TypeError, "Feed value already set" unless @feedvalue.nil? + @feedvalue = val + nil + end + + def dup + raise TypeError, "Can't copy iteration state" if @generator&.instance_variable_get(:@fiber) + super + end + + def initialize_copy(other) + @generator = other.generator&.dup + @lookahead = other.lookahead.dup + super + end + private :initialize_copy + + class Yielder + def initialize(&block) + raise LocalJumpError, "Expected a block to be given" unless block_given? + + @proc = block + + self + end + private :initialize + + def yield(*args) + @proc.call *args + end + + def <<(*args) + self.yield(*args) + + self + end + end + + class Generator + include Enumerable + + def initialize(&block) + raise LocalJumpError, "Expected a block to be given" unless block_given? + + @proc = block + + self + end + private :initialize + + def each(*args) + enclosed_yield = Proc.new { |*enclosed_args| yield *enclosed_args } + + @proc.call Yielder.new(&enclosed_yield), *args + end + end + + class Lazy < self + class StopLazyError < Exception; end + + def initialize(receiver, size=nil) + raise ArgumentError, "Lazy#initialize requires a block" unless block_given? + Rubinius.check_frozen + + super(size) do |yielder, *each_args| + begin + receiver.each(*each_args) do |*args| + yield yielder, *args + end + rescue Exception + nil + end + end + + self + end + private :initialize + + def to_enum(method_name=:each, *method_args, &block) + size = block_given? ? block : nil + ret = Lazy.allocate + + Rubinius.privately do + ret.initialize_enumerator self, size, method_name, *method_args + end + + ret + end + alias_method :enum_for, :to_enum + + def lazy + self + end + + alias_method :force, :to_a + + def take(n) + n = Rubinius::Type.coerce_to n, Integer, :to_int + raise ArgumentError, "attempt to take negative size" if n < 0 + + current_size = enumerator_size + set_size = if current_size.kind_of?(Numeric) + n < current_size ? n : current_size + else + current_size + end + + return to_enum(:cycle, 0).lazy if n.zero? + + taken = 0 + Lazy.new(self, set_size) do |yielder, *args| + if taken < n + yielder.yield(*args) + taken += 1 + raise StopLazyError unless taken < n + else + raise StopLazyError + end + end + end + + def drop(n) + n = Rubinius::Type.coerce_to n, Integer, :to_int + raise ArgumentError, "attempt to drop negative size" if n < 0 + + current_size = enumerator_size + set_size = if current_size.kind_of?(Integer) + n < current_size ? current_size - n : 0 + else + current_size + end + + dropped = 0 + Lazy.new(self, set_size) do |yielder, *args| + if dropped < n + dropped += 1 + else + yielder.yield(*args) + end + end + end + + def take_while + raise ArgumentError, "Lazy#take_while requires a block" unless block_given? + + Lazy.new(self, nil) do |yielder, *args| + if yield(*args) + yielder.yield(*args) + else + raise StopLazyError + end + end + end + + def drop_while + raise ArgumentError, "Lazy#drop_while requires a block" unless block_given? + + succeeding = true + Lazy.new(self, nil) do |yielder, *args| + if succeeding + unless yield(*args) + succeeding = false + yielder.yield(*args) + end + else + yielder.yield(*args) + end + end + end + + def select + raise ArgumentError, 'Lazy#{select,find_all} requires a block' unless block_given? + + Lazy.new(self, nil) do |yielder, *args| + val = args.length >= 2 ? args : args.first + yielder.yield(*args) if yield(val) + end + end + alias_method :find_all, :select + + def reject + raise ArgumentError, "Lazy#reject requires a block" unless block_given? + + Lazy.new(self, nil) do |yielder, *args| + val = args.length >= 2 ? args : args.first + yielder.yield(*args) unless yield(val) + end + end + + def grep(pattern) + Lazy.new(self, nil) do |yielder, *args| + val = args.length >= 2 ? args : args.first + + if pattern === val + Regexp.set_block_last_match + + if block_given? + yielder.yield yield(val) + else + yielder.yield val + end + end + end + end + + def grep_v(pattern) + Lazy.new(self, nil) do |yielder, *args| + val = args.length >= 2 ? args : args.first + + unless pattern === val + if block_given? + yielder.yield yield(val) + else + yielder.yield val + end + end + end + end + + def map + raise ArgumentError, 'Lazy#{map,collect} requires a block' unless block_given? + + Lazy.new(self, enumerator_size) do |yielder, *args| + yielder.yield yield(*args) + end + end + alias_method :collect, :map + + def collect_concat + raise ArgumentError, 'Lazy#{collect_concat,flat_map} requires a block' unless block_given? + + Lazy.new(self, nil) do |yielder, *args| + yield_ret = yield(*args) + + if Rubinius::Type.object_respond_to?(yield_ret, :force) && + Rubinius::Type.object_respond_to?(yield_ret, :each) + yield_ret.each do |v| + yielder.yield v + end + else + array = Rubinius::Type.check_convert_type yield_ret, Array, :to_ary + if array + array.each do |v| + yielder.yield v + end + else + yielder.yield yield_ret + end + end + end + end + alias_method :flat_map, :collect_concat + + def zip(*lists) + return super(*lists) { |entry| yield entry } if block_given? + + lists.map! do |list| + array = Rubinius::Type.check_convert_type list, Array, :to_ary + + case + when array + array + when Rubinius::Type.object_respond_to?(list, :each) + list.to_enum :each + else + raise TypeError, "wrong argument type #{list.class} (must respond to :each)" + end + end + + index = 0 + Lazy.new(self, enumerator_size) do |yielder, *args| + val = args.length >= 2 ? args : args.first + rests = lists.map { |list| + case list + when Array + list[index] + else + begin + list.next + rescue StopIteration + nil + end + end + } + yielder.yield [val, *rests] + index += 1 + end + end + end + + class Iterator + STACK_SIZE = 1_048_576 + + attr_reader :result + + def initialize(obj) + @object = obj + rewind + end + + private :initialize + + def next? + !@done + end + + def next + reset unless @fiber + + val = @fiber.resume + + raise StopIteration, "iteration has ended" if @done + + return val + end + + def rewind + @fiber = nil + @done = false + end + + def reset + @done = false + @fiber = Fiber.new stack_size: STACK_SIZE do + obj = @object + @result = obj.each { |*val| Fiber.yield *val } + @done = true + end + end + end +end diff --git a/gxg/core/env.rb b/gxg/core/env.rb new file mode 100644 index 0000000..837c30d --- /dev/null +++ b/gxg/core/env.rb @@ -0,0 +1,294 @@ +module Rubinius + class EnvironmentVariables + include Enumerable + + def [](key) + value = getenv(StringValue(key)) + if value + value = set_encoding value + value.freeze + end + value + end + + def []=(key, value) + key = StringValue(key) + if value.nil? + unsetenv(key) + else + if setenv(key, StringValue(value), 1) != 0 + Errno.handle("setenv") + end + end + value + end + + alias_method :store, :[]= + + def each_key + return to_enum(:each_key) { size } unless block_given? + + each { |k, v| yield k } + end + + def each_value + return to_enum(:each_value) { size } unless block_given? + + each { |k, v| yield v } + end + + def each + return to_enum(:each) { size } unless block_given? + + env = environ() + ptr_size = FFI.type_size FFI.find_type(:pointer) + + offset = 0 + cur = env + offset + + until cur.read_pointer.null? + entry = cur.read_pointer.read_string + key, value = entry.split '=', 2 + value.taint if value + key.taint if key + + key = set_encoding key + value = set_encoding value + + yield key, value + + offset += ptr_size + cur = env + offset + end + + self + end + + alias_method :each_pair, :each + + def delete(key) + existing_value = self[key] + if existing_value + self[key] = nil + elsif block_given? + yield key + end + existing_value + end + + def delete_if(&block) + return to_enum(:delete_if) { size } unless block_given? + reject!(&block) + self + end + + def include?(key) + !self[key].nil? + end + + alias_method :has_key?, :include? + alias_method :key?, :include? + + # More efficient than using the one from Enumerable + alias_method :member?, :include? + + def fetch(key, absent=undefined) + if block_given? and !undefined.equal?(absent) + warn "block supersedes default value argument" + end + + if value = self[key] + return value + end + + if block_given? + return yield(key) + elsif undefined.equal?(absent) + raise KeyError, "key not found" + end + + return absent + end + + def to_s + "ENV" + end + + def inspect + to_hash.inspect + end + + def reject(&block) + to_hash.reject(&block) + end + + def reject! + return to_enum(:reject!) { size } unless block_given? + + # Avoid deleting from environ while iterating because the + # OS can handle that in a million different bad ways. + + keys = [] + each { |k, v| keys << k if yield(k, v) } + keys.each { |k| delete k } + + keys.empty? ? nil : self + end + + def clear + # Avoid deleting from environ while iterating because the + # OS can handle that in a million different bad ways. + + keys = [] + each { |k, v| keys << k } + keys.each { |k| delete k } + + self + end + + def has_value?(value) + each { |k, v| return true if v == value } + return false + end + + alias_method :value?, :has_value? + + def values_at(*params) + params.map{ |k| self[k] } + end + + def index(value) + each do |k, v| + return k if v == value + end + nil + end + + def invert + to_hash.invert + end + + def key(value) + index(value) + end + + def keys + keys = [] + each { |k, v| keys << k } + keys + end + + def values + vals = [] + each { |k, v| vals << v } + vals + end + + def empty? + each { return false } + return true + end + + def length + sz = 0 + each { |k, v| sz += 1 } + sz + end + + alias_method :size, :length + + def rehash + # No need to do anything, our keys are always strings + end + + def replace(other) + clear + other.each { |k, v| self[k] = v } + end + + def select(&blk) + return to_enum { size } unless block_given? + to_hash.select(&blk) + end + + def shift + env = environ() + + offset = 0 + cur = env + offset + + ptr = cur.read_pointer + return nil unless ptr + + key, value = ptr.read_string.split "=", 2 + + return nil unless key + + key.taint if key + value.taint if value + + delete key + + key = set_encoding key + value = set_encoding value + + return [key, value] + end + + def to_a + ary = [] + each { |k, v| ary << [k, v] } + ary + end + + def to_hash + hsh = {} + each { |k, v| hsh[k] = v } + hsh + end + + alias_method :to_h, :to_hash + + def update(other) + if block_given? + other.each { |k, v| self[k] = yield(k, self[k], v) } + else + other.each { |k, v| self[k] = v } + end + end + + def keep_if(&block) + return to_enum(:keep_if) { size } unless block_given? + select!(&block) + self + end + + def select! + return to_enum(:select!) { size } unless block_given? + reject! { |k, v| !yield(k, v) } + end + + def assoc(key) + key = StringValue(key) + value = self[key] + value ? [key, value] : nil + end + + def rassoc(value) + value = StringValue(value) + key = index(value) + key ? [key, value] : nil + end + + def set_encoding(value) + return unless value.kind_of? String + if Encoding.default_internal && value.ascii_only? + value.encode Encoding.default_internal, Encoding.find("locale") + else + value.force_encoding Encoding.find("locale") + end + end + + private :set_encoding + end +end diff --git a/gxg/core/errno.rb b/gxg/core/errno.rb new file mode 100644 index 0000000..2de96ac --- /dev/null +++ b/gxg/core/errno.rb @@ -0,0 +1,42 @@ +## +# Interface to the C errno integer. + +module Errno + ## + # Raises the appropriate SystemCallError exception with +additional+ as the + # message. Equivalent to MRI's rb_sys_fail(). + # + # Unlike rb_sys_fail(), handle does not raise an exception if errno is 0. + + def self.handle(additional = nil) + err = errno + return if err == 0 + + raise SystemCallError.new(additional, err) + end + + def self.errno + FFI.errno + end + + def self.eql?(code) + FFI.errno == code + end + + def self.raise_waitreadable(message=nil) + raise IO::EAGAINWaitReadable, message + end + + def self.raise_waitwritable(message=nil) + raise IO::EAGAINWaitWritable, message + end + + def self.raise_eagain(message=nil) + raise_errno(message, Errno::EAGAIN::Errno) + end + + def self.raise_errno(message, errno) + raise SystemCallError.new(message, errno) + end + private :raise_errno +end diff --git a/gxg/core/exception.rb b/gxg/core/exception.rb new file mode 100644 index 0000000..ebe9b53 --- /dev/null +++ b/gxg/core/exception.rb @@ -0,0 +1,578 @@ +class Exception + attr_accessor :locations + attr_accessor :cause + attr_accessor :custom_backtrace + + def initialize(message = nil) + @reason_message = message + @locations = nil + @backtrace = nil + @custom_backtrace = nil + end + + private :initialize + + def capture_backtrace!(offset=1) + @locations = Rubinius::VM.backtrace offset + end + + def ==(other) + other.instance_of?(__class__) && + message == other.message && + backtrace == other.backtrace + end + + def to_s + if @reason_message + @reason_message.to_s + else + self.class.to_s + end + end + + # This is here rather than in yaml.rb because it contains "private" + # information, ie, the list of ivars. Putting it over in the yaml + # source means it's easy to forget about. + def to_yaml_properties + list = super + list.delete :@backtrace + list.delete :@custom_backtrace + return list + end + + def message + @reason_message + end + + # Needed to properly implement #exception, which must clone and call + # #initialize again, BUT not a subclasses initialize. + alias_method :__initialize__, :initialize + + def backtrace + return @custom_backtrace if @custom_backtrace + + if backtrace? + awesome_backtrace.to_mri + else + nil + end + end + + # Indicates if the Exception has a backtrace set + def backtrace? + (@backtrace || @locations) ? true : false + end + + def awesome_backtrace + @backtrace ||= Rubinius::Backtrace.backtrace(@locations) + end + + def render(header="An exception occurred", io=STDERR, color=true) + exception = self + exceptions = [] + + while exception + exceptions.unshift exception + exception = exception.cause + end + + while exception = exceptions.shift + if custom_trace = exception.custom_backtrace + io.puts "User-defined backtrace:", "\n" + + custom_trace.reverse_each { |line| io.puts line } + + io.puts "\nBacktrace:", "\n" + end + + io.puts exception.awesome_backtrace.show("\n", color) + + io.puts "\n#{exception.message} (#{exception.class})" + + io.puts "\nCausing:", "\n" unless exceptions.empty? + end + + message_lines = message.to_s.split("\n") + + io.puts header + io.puts + io.puts " #{message_lines.shift} (#{self.class})" + + message_lines.each do |line| + io.puts " #{line}" + end + end + + def set_backtrace(bt) + if bt.kind_of? Rubinius::Backtrace + @backtrace = bt + else + # See if we stashed a Backtrace object away, and use it. + if hidden_bt = Rubinius::Backtrace.detect_backtrace(bt) + @backtrace = hidden_bt + else + type_error = TypeError.new "backtrace must be Array of String" + case bt + when Array + if bt.all? { |s| s.kind_of? String } + @custom_backtrace = bt + else + raise type_error + end + when String + @custom_backtrace = [bt] + when nil + @custom_backtrace = nil + else + raise type_error + end + end + end + end + + # This is important, because I subclass can just override #to_s and calling + # #message will call it. Using an alias doesn't achieve that. + def message + to_s + end + + def inspect + s = self.to_s + if s.empty? + self.class.name + else + "#<#{self.class.name}: #{s}>" + end + end + + class << self + alias_method :exception, :new + end + + def exception(message=nil) + if message + unless message.equal? self + # As strange as this might seem, this IS actually the protocol + # that MRI implements for this. The explicit call to + # Exception#initialize (via __initialize__) is exactly what MRI + # does. + e = clone + Rubinius.privately { e.__initialize__(message) } + return e + end + end + + self + end + + def location + [context.file.to_s, context.line] + end +end + +class PrimitiveFailure < Exception +end + +class ScriptError < Exception +end + +class StandardError < Exception +end + +class SignalException < Exception +end + +class NoMemoryError < Exception +end + +class ZeroDivisionError < StandardError +end + +class ArgumentError < StandardError + def to_s + if @given and @expected + if @method_name + "method '#{@method_name}': given #{@given}, expected #{@expected}" + else + "given #{@given}, expected #{@expected}" + end + else + super + end + end +end + +class UncaughtThrowError < ArgumentError + attr_accessor :tag +end + +class IndexError < StandardError +end + +class StopIteration < IndexError +end + +class RangeError < StandardError +end + +class FloatDomainError < RangeError +end + +class LocalJumpError < StandardError +end + +class NameError < StandardError + attr_reader :name + + def initialize(*args, receiver: nil) + super(args.shift) + + @name = args.shift + @receiver = receiver + end + + private :initialize + + def receiver + if @receiver + @receiver + else + raise ArgumentError, 'no receiver is available' + end + end +end + +class NoMethodError < NameError + attr_reader :name + attr_reader :args + + def initialize(*arguments, **options) + super(arguments.shift, **options) + @name = arguments.shift + @args = arguments.shift + end + + private :initialize +end + +class RuntimeError < StandardError +end + +class SecurityError < StandardError +end + +class ThreadError < StandardError +end + +class FiberError < StandardError +end + +class TypeError < StandardError +end + +class FloatDomainError < RangeError +end + +class RegexpError < StandardError +end + +class LoadError < ScriptError + attr_accessor :path + + def initialize(message=nil, path: nil) + super message + @path = path + end + + private :initialize + + class InvalidExtensionError < LoadError + end + + class MRIExtensionError < InvalidExtensionError + end +end + +class NotImplementedError < ScriptError +end + +class Interrupt < SignalException + def initialize(*args) + super(args.shift) + @name = args.shift + end + + private :initialize +end + +class IOError < StandardError +end + +class EOFError < IOError +end + +class LocalJumpError < StandardError +end + +class SyntaxError < ScriptError + attr_accessor :column + attr_accessor :line + attr_accessor :file + attr_accessor :code + + def self.from(message, column, line, code, file) + exc = new message + exc.file = file + exc.line = line + exc.column = column + exc.code = code + exc + end + + def reason + @reason_message + end +end + +class SystemExit < Exception + + ## + # Process exit status if this exception is raised + + attr_reader :status + + ## + # Creates a SystemExit exception with optional status and message. If the + # status is omitted, Process::EXIT_SUCCESS is used. + #-- + # *args is used to simulate optional prepended argument like MRI + + def initialize(first=nil, *args) + if first.kind_of?(Fixnum) + status = first + super(*args) + else + status = Process::EXIT_SUCCESS + super + end + + @status = status + end + + private :initialize + + ## + # Returns true is exiting successfully, false if not. A successful exit is + # one with a status equal to 0 (zero). Any other status is considered a + # unsuccessful exit. + + def success? + status == Process::EXIT_SUCCESS + end + +end + + +class SystemCallError < StandardError + + attr_reader :errno + + def self.errno_error(message, errno, location) + Rubinius.primitive :exception_errno_error + raise PrimitiveFailure, "SystemCallError.errno_error failed" + end + + # We use .new here because when errno is set, we attempt to + # lookup and return a subclass of SystemCallError, specificly, + # one of the Errno subclasses. + def self.new(*args) + # This method is used 2 completely different ways. One is when it's called + # on SystemCallError, in which case it tries to construct a Errno subclass + # or makes a generic instead of itself. + # + # Otherwise it's called on a Errno subclass and just helps setup + # a instance of the subclass + if self.equal? SystemCallError + case args.size + when 1 + if args.first.kind_of?(Fixnum) + errno = args.first + message = nil + else + errno = nil + message = StringValue(args.first) + end + location = nil + when 2 + message, errno = args + location = nil + when 3 + message, errno, location = args + else + raise ArgumentError, "wrong number of arguments (#{args.size} for 1..3)" + end + + # If it corresponds to a known Errno class, create and return it now + if errno && error = SystemCallError.errno_error(message, errno, location) + return error + else + return super(message, errno, location) + end + else + case args.size + when 0 + message = nil + location = nil + when 1 + message = StringValue(args.first) + location = nil + when 2 + message, location = args + else + raise ArgumentError, "wrong number of arguments (#{args.size} for 0..2)" + end + + if defined?(self::Errno) && self::Errno.kind_of?(Fixnum) + errno = self::Errno + error = SystemCallError.errno_error(message, self::Errno, location) + if error && error.class.equal?(self) + return error + end + end + + error = allocate + Rubinius::Unsafe.set_class error, self + Rubinius.privately { error.initialize(*args) } + return error + end + end + + # Must do this here because we have a unique new and otherwise .exception will + # call Exception.new because of the alias in Exception. + class << self + alias_method :exception, :new + end + + # Use splat args here so that arity returns -1 to match MRI. + def initialize(*args) + kls = self.class + message, errno, location = args + @errno = errno + + msg = "unknown error" + msg << " @ #{StringValue(location)}" if location + msg << " - #{StringValue(message)}" if message + super(msg) + end + + private :initialize +end + +class KeyError < IndexError +end + +class SignalException < Exception + + attr_reader :signo + attr_reader :signm + + def initialize(signo = nil, signm = nil) + # MRI overrides this behavior just for SignalException itself + # but not for anything that inherits from it, therefore we + # need this ugly check to make sure it works as intented. + return super(signo) unless self.class == SignalException + if signo.is_a? Integer + unless @signm = Signal::Numbers[signo] + raise ArgumentError, "invalid signal number #{signo}" + end + @signo = signo + @signm = signm || "SIG#{@signm}" + elsif signo + if signm + raise ArgumentError, "wrong number of arguments (2 for 1)" + end + signm = signo + if signo.kind_of?(Symbol) + signm = signm.to_s + else + signm = StringValue(signm) + end + signm = signm[3..-1] if signm.prefix? "SIG" + unless @signo = Signal::Names[signm] + raise ArgumentError, "invalid signal name #{signm}" + end + @signm = "SIG#{signm}" + end + super(@signm) + end + + private :initialize +end + +class StopIteration + attr_accessor :result + private :result= +end + +## +# Base class for various exceptions raised in the VM. + +class Rubinius::MachineException < Exception +end + +class Rubinius::ConcurrentUpdateError < Rubinius::MachineException +end + +class Rubinius::VMException < Exception +end + +## +# Raised in the VM when an assertion fails. + +class Rubinius::AssertionError < Rubinius::VMException +end + +## +# Raised in the VM when attempting to read/write outside +# the bounds of an object. + +class Rubinius::ObjectBoundsExceededError < Rubinius::VMException +end + +# Defined by the VM itself +class Rubinius::InvalidBytecode < Rubinius::Internal + attr_reader :compiled_code + attr_reader :ip + + def message + if @compiled_code + if @ip and @ip >= 0 + "#{super} - at #{@compiled_code.name}+#{@ip}" + else + "#{super} - method #{@compiled_code.name}" + end + else + super + end + end +end + +class InterpreterError < Exception + +end + +class DeadlockError < Exception + +end + +# MRI has an Exception class named "fatal" that is raised +# by the rb_fatal function. The class is not accessible from +# ruby because the name is begins with a lower-case letter. +# Also, the exception cannot be rescued. +# +# To support rb_fatal in the C-API, Rubinius provides the +# following FatalError class. If it clashes with code in +# the wild, we can rename it. + +class FatalError < Exception +end diff --git a/gxg/core/executable.rb b/gxg/core/executable.rb new file mode 100644 index 0000000..80454ce --- /dev/null +++ b/gxg/core/executable.rb @@ -0,0 +1,44 @@ +## +# Parent class of everything that can live in a MethodTable. +# +# When calling an Executable, first the primitive slot is checked and executed +# if it exists. If execution of the primitive is successful, the value is put +# on the stack, and execution of the method is finished. If unsuccessful or +# absent (only in the case of a CompiledCode), the ruby code code +# in the CompiledCode is run. +module Rubinius + class Executable + attr_accessor :serial + attr_accessor :primitive + attr_accessor :tags + + def self.allocate + Rubinius.primitive :executable_allocate + raise PrimitiveFailure, "Executable.allocate primitive failed" + end + + def invoke(name, mod, recv, args, block) + Rubinius.primitive :executable_invoke + raise PrimitiveFailure, "Executable#invoke primitive failed" + end + + def custom_call_site + Rubinius.primitive :executable_set_custom_call_site + raise PrimitiveFailure, "Executable#set_custom_call_site primitive failed" + end + + ## + # Allows instances of Executable and its subclasses + # to duck-type as CompiledCode::Visibility since + # they can both be entries in a MethodTable. + def public? + true + end + + def private? + false + end + + alias_method :protected?, :private? + end +end diff --git a/gxg/core/false.rb b/gxg/core/false.rb new file mode 100644 index 0000000..cbaa8a8 --- /dev/null +++ b/gxg/core/false.rb @@ -0,0 +1,23 @@ +class FalseClass + def &(other) + false + end + + def ^(other) + !!other + end + + def to_s + "false" + end + + alias_method :|, :^ + + def inspect + # Call #to_s rather than alias it so that people can change #to_s if they + # wish. Yes, sounds dumb. + to_s + end +end + +FALSE = false diff --git a/gxg/core/ffi.rb b/gxg/core/ffi.rb new file mode 100644 index 0000000..8629eb2 --- /dev/null +++ b/gxg/core/ffi.rb @@ -0,0 +1,528 @@ +## +# A Foreign Function Interface used to bind C libraries to ruby. + +module Rubinius + module FFI + + # Specialised error classes + class TypeError < RuntimeError; end + + class NotFoundError < RuntimeError; end + + class << self + def add_typedef(current, add) + if current.kind_of? Fixnum + code = current + else + code = FFI::TypeDefs[current] + raise TypeError, "Unable to resolve type '#{current}'" unless code + end + + FFI::TypeDefs[add] = code + end + + def find_type(name) + code = FFI::TypeDefs[name] + raise TypeError, "Unable to resolve type '#{name}'" unless code + return code + end + + ## + # Given a +type+ as a number, indicate how many bytes that type + # takes up on this platform. + + def type_size(type) + Rubinius.primitive :nativefunction_type_size + + case type + when Symbol + return type_size(find_type(type)) + when Rubinius::NativeFunction + return type_size(TYPE_PTR) + when FFI::Enum + return type_size(TYPE_ENUM) + end + + raise PrimitiveFailure, "FFI.type_size primitive failed: #{type}" + end + + def size_to_type(size) + if sz = TypeSizes[size] + return sz + end + + # Be like C, use int as the default type size. + return :int + end + + def config(name) + Rubinius::Config["rbx.platform.#{name}"] + end + + def config_hash(name) + vals = {} + section = "rbx.platform.#{name}." + Rubinius::Config.section section do |key, value| + vals[key.substring(section.size, key.length)] = value + end + vals + end + + def errno + FFI::Platform::POSIX.errno + end + + # Convenience method for determining if a function call succeeded or failed + def call_failed?(return_code) + return_code == -1 + end + end + + # Represents a C enum. + class Enum + attr_reader :tag + attr_reader :kv_map + + def initialize(info, tag=nil) + @tag = tag + @kv_map = Hash.new + + unless info.nil? + last_cst = nil + value = 0 + info.each do |i| + case i + when Symbol + @kv_map[i] = value + last_cst = i + value += 1 + when Integer + @kv_map[last_cst] = i + value = i + 1 + end + end + end + end + + private :initialize + + def inspect + "#<%s:0x%x %s=>%s>" % [self.class, self.object_id, @tag, @kv_map.inspect] + end + + def anonym? + @tag.nil? + end + + def values + @kv_map.values + end + + def symbols + @kv_map.keys + end + + def [](sym) + @kv_map[sym] + end + + def symbol(value) + key, val = @kv_map.detect { |key, val| val == value } + key + end + end + + # It's a class to be compat with the ffi gem. + class Type + class Array + def initialize(element_type, size, impl_class=nil) + @element_type = element_type + @size = size + @implementation = impl_class + end + + private :initialize + + attr_reader :element_type + attr_reader :size + attr_reader :implementation + end + + class StructByValue + def initialize(struct) + @implementation = struct + end + + private :initialize + + attr_reader :implementation + end + + Struct = StructByValue + + CHAR = TYPE_CHAR + UCHAR = TYPE_UCHAR + BOOL = TYPE_BOOL + SHORT = TYPE_SHORT + USHORT = TYPE_USHORT + INT = TYPE_INT + UINT = TYPE_UINT + LONG = TYPE_LONG + ULONG = TYPE_ULONG + LL = TYPE_LL + ULL = TYPE_ULL + FLOAT = TYPE_FLOAT + DOUBLE = TYPE_DOUBLE + PTR = TYPE_PTR + VOID = TYPE_VOID + STRING = TYPE_STRING + STRPTR = TYPE_STRPTR + CHARARR = TYPE_CHARARR + ENUM = TYPE_ENUM + VARARGS = TYPE_VARARGS + end + + ## + # Namespace for holding platform-specific C constants. + + module Platform + def self.windows? + Rubinius.windows? + end + + def self.mac? + Rubinius.darwin? + end + + def self.unix? + ! windows? + end + end + + ## + # Represents a C struct as ruby class. + + class Struct + class InlineArray + include Enumerable + + def initialize(type, ptr) + @pointer = ptr + @type = type.element_type + @size = type.size + end + + private :initialize + + attr_reader :size + + def [](idx) + if idx >= @size + raise ArgumentError, "index out of range (#{idx} >= #{@size})" + end + + @pointer.get_at_offset(idx * @type, @type) + end + + def []=(idx, val) + if idx >= @size + raise ArgumentError, "index out of range (#{idx} >= #{@size})" + end + + @pointer.set_at_offset(idx * @type, @type, val) + end + + def each + @size.times do |ele| + yield @pointer.get_at_offset(ele * @type, @type) + end + end + + def to_a + ary = [] + each { |x| ary << x } + ary + end + + def to_ptr + @pointer + end + end + + class InlineCharArray < InlineArray + def to_s + str = @pointer.read_string(@size) + + # NULL clamp this, to be the same as the ffi gem. + if idx = str.index("\0") + return str[0, idx] + end + + return str + end + + alias_method :to_str, :to_s + alias_method :inspect, :to_s + end + + def self.find_nested_parent + return nil if self.name.nil? + + path = self.name.split("::") + path.pop # remove ourself + + mod = Object + + begin + path.each { |c| mod = mod.const_get(c) } + rescue NameError + # bail. + return nil + end + + mod.respond_to?(:find_type) ? mod : nil + end + + attr_reader :pointer + + def self.layout(*spec) + return @layout if spec.size == 0 + + # Pick up a enclosing FFI::Library + @enclosing_module ||= find_nested_parent + + cspec = Rubinius::LookupTable.new + i = 0 + + @size = 0 + @members = [] + + while i < spec.size + name = spec[i] + @members << name + + f = spec[i + 1] + + type_code = nil + + if f.kind_of? Array + ary_type = f[0] + ary_size = f[1] + + if @enclosing_module + type_code = @enclosing_module.find_type(ary_type) + end + + type_code ||= FFI.find_type(ary_type) + type_size = FFI.type_size(type_code) + + case type_code + when TYPE_CHAR, TYPE_UCHAR + klass = InlineCharArray + else + klass = InlineArray + end + + type = FFI::Type::Array.new(type_code, ary_size, klass) + element_size = type_size * ary_size + elsif f.kind_of?(Class) and (f < FFI::Struct || f < FFI::Union) + type = FFI::Type::StructByValue.new(f) + element_size = type_size = f.size + else + if @enclosing_module + type_code = @enclosing_module.find_type(f) rescue nil + end + + type_code ||= FFI.find_type(f) + + type = type_code + element_size = type_size = FFI.type_size(type_code) + end + + offset = spec[i + 2] + + if offset.kind_of?(Fixnum) + i += 3 + else + if self < FFI::Union + offset = 0 + else + offset = @size + + mod = offset % type_size + unless mod == 0 + # we need to align it. + offset += (type_size - mod) + end + end + + i += 2 + end + + cspec[name] = [offset, type] + ending = offset + element_size + @size = ending if @size < ending + end + + @layout = cspec unless self == FFI::Struct + + return cspec + end + + def self.config(base, *fields) + @size = Rubinius::Config["#{base}.sizeof"] + cspec = Rubinius::LookupTable.new + + fields.each do |field| + offset = Rubinius::Config["#{base}.#{field}.offset"] + element_size = Rubinius::Config["#{base}.#{field}.size"] + type = Rubinius::Config["#{base}.#{field}.type"] + type = type ? type.to_sym : FFI.size_to_type(size) + + type_code = FFI.find_type type + + if :char_array == type + type_code = FFI::Type::Array.new(FFI.find_type(:char), element_size, InlineCharArray) + end + + cspec[field] = [offset, type_code] + ending = offset + element_size + @size = ending if @size < ending + end + + @layout = cspec + + return cspec + end + + def self.size + @size + end + + def size + self.class.size + end + + def self.offset_of(name) + @layout[name].first + end + + def offset_of(name) + @cspec[name].first + end + + def self.offsets + members.map do |member| + [member, @layout[member].first] + end + end + + def offsets + members.map do |member| + [member, @cspec[member].first] + end + end + + def self.members + @members + end + + def members + self.class.members + end + + def initialize(pointer=nil, *spec) + @cspec = self.class.layout(*spec) + + if pointer + @pointer = pointer + else + @pointer = MemoryPointer.new size + end + end + + private :initialize + + def to_ptr + @pointer + end + + def free + @pointer.free + end + + def ==(other) + return false unless other.is_a?(self.class) + @pointer == other.pointer + end + + def initialize_copy(ptr) + @pointer = ptr.pointer.dup + end + + private :initialize + + def []=(field, val) + offset, type = @cspec[field] + raise "Unknown field #{field}" unless offset + + case type + when Fixnum + @pointer.set_at_offset(offset, type, val) + when FFI::Type::Array + if type.implementation == InlineCharArray + (@pointer + offset).write_string StringValue(val), type.size + return val + end + + raise TypeError, "Unable to set inline array" + when Rubinius::NativeFunction + @pointer.set_at_offset(offset, FFI::TYPE_PTR, val) + else + @pointer.set_at_offset(offset, type, val) + end + + return val + end + + def [](field) + offset, type = @cspec[field] + raise "Unknown field #{field}" unless offset + + case type + when FFI::TYPE_CHARARR + (@pointer + offset).read_string + when Fixnum + @pointer.get_at_offset(offset, type) + when FFI::Type::Array + type.implementation.new(type, @pointer + offset) + when FFI::Type::StructByValue + type.implementation.new(@pointer + offset) + when Rubinius::NativeFunction + ptr = @pointer.get_at_offset(offset, FFI::TYPE_PTR) + if ptr + FFI::Function.new(type.return_type, type.argument_types, ptr) + else + nil + end + else + @pointer.get_at_offset(offset, type) + end + end + + def values + members.map { |m| self[m] } + end + + def null? + @pointer == FFI::Pointer::NULL + end + end + + ## + # Represents a C union as ruby class. + + class Union < Struct + end + end +end diff --git a/gxg/core/fiber.rb b/gxg/core/fiber.rb new file mode 100644 index 0000000..cc5306d --- /dev/null +++ b/gxg/core/fiber.rb @@ -0,0 +1,73 @@ +class Fiber + attr_reader :stack_size + attr_reader :thread_name + attr_reader :fiber_id + attr_reader :source + + def self.new(**kw, &block) + if block.nil? + raise ArgumentError, "Fiber.new requires a block" + end + + stack_size = Rubinius::Type.try_convert kw[:stack_size], Fixnum, :to_int + + Rubinius.invoke_primitive :fiber_new, stack_size, block, self + end + + def self.current + Rubinius.primitive :fiber_s_current + raise PrimitiveFailure, "Fiber.current primitive failed" + end + + def self.yield(*args) + Rubinius.primitive :fiber_s_yield + raise PrimitiveFailure, "Fiber.yield primitive failed" + end + + def self.list + Rubinius.primitive :fiber_s_list + raise PrimitiveFailure, "Fiber.list primitive failed" + end + + def self.main + Rubinius.primitive :fiber_s_main + raise PrimitiveFailure, "Fiber.main primitive failed" + end + + def self.count + Rubinius.primitive :fiber_s_count + raise PrimitiveFailure, "Fiber.count primitive failed" + end + + def status + Rubinius.primitive :fiber_status + raise PrimitiveFailure, "Fiber#status primitive failed" + end + + def resume(*args) + Rubinius.primitive :fiber_resume + raise PrimitiveFailure, "Fiber#resume primitive failed" + end + + def transfer(*args) + Rubinius.primitive :fiber_transfer + raise PrimitiveFailure, "Fiber#transfer primitive failed" + end + + def dispose + Rubinius.primitive :fiber_dispose + raise PrimitiveFailure, "Fiber#dispose primitive failed" + end + + def alive? + status != "dead" + end + + def inspect + str = "#<#{self.class}:0x#{object_id.to_s(16)} thread_name=#{@thread_name} fiber_id=#{@fiber_id} status=#{status}" + str << " source=#{@source}" if @source + str << ">" + end + + alias_method :to_s, :inspect +end diff --git a/gxg/core/file.rb b/gxg/core/file.rb new file mode 100644 index 0000000..8d747ce --- /dev/null +++ b/gxg/core/file.rb @@ -0,0 +1,1289 @@ +## +# Platform specific behavior for the File class. + +class File < IO + include Enumerable + + class FileError < Exception; end + class NoFileError < FileError; end + class UnableToStat < FileError; end + class PermissionError < FileError; end + + attr_reader :path + + # The mode_t type is 2 bytes (ushort). Instead of getting whatever + # value happens to be in the least significant 16 bits, just set + # the value to 0 if it is greater than 0xffff. Also, negative values + # don't make any sense here. + def clamp_short(value) + mode = Rubinius::Type.coerce_to value, Integer, :to_int + mode < 0 || mode > 0xffff ? 0 : mode + end + module_function :clamp_short + + def self.absolute_path(obj, dir = nil) + obj = path(obj) + if obj[0] == "~" + File.join Dir.getwd, dir.to_s, obj + else + expand_path(obj, dir) + end + end + + ## + # Returns the last access time for the named file as a Time object). + # + # File.atime("testfile") #=> Wed Apr 09 08:51:48 CDT 2003 + def self.atime(path) + Stat.new(path).atime + end + + ## + # Returns the change time for the named file (the + # time at which directory information about the + # file was changed, not the file itself). + # + # File.ctime("testfile") #=> Wed Apr 09 08:53:13 CDT 2003 + def self.ctime(path) + Stat.new(path).ctime + end + + ## + # Returns the birthtime for the named file + # + # Note this is not supported on all platforms. + # See stat(2) for more information. + def self.birthtime(path) + Stat.new(path).birthtime + end + + ## + # Returns the modification time for the named file as a Time object. + # + # File.mtime("testfile") #=> Tue Apr 08 12:58:04 CDT 2003 + def self.mtime(path) + Stat.new(path).mtime + end + + ## + # Sets the access and modification times of each named + # file to the first two arguments. Returns the number + # of file names in the argument list. + # #=> Integer + def self.utime(a_in, m_in, *paths) + a_in ||= Time.now + a_in_usec = if a_in.is_a?(Time) || a_in.is_a?(Float) || a_in.is_a?(Rational) + Time.at(a_in).usec + else + 0 + end + m_in ||= Time.now + m_in_usec = if m_in.is_a?(Time) || m_in.is_a?(Float) || m_in.is_a?(Rational) + Time.at(m_in).usec + else + 0 + end + + FFI::MemoryPointer.new(POSIX::TimeVal, 2) do |ptr| + atime = POSIX::TimeVal.new ptr + mtime = POSIX::TimeVal.new ptr[1] + atime[:tv_sec] = a_in.to_i + atime[:tv_usec] = a_in_usec + + mtime[:tv_sec] = m_in.to_i + mtime[:tv_usec] = m_in_usec + + paths.each do |path| + + n = POSIX.utimes(Rubinius::Type.coerce_to_path(path), ptr) + Errno.handle unless n == 0 + end + end + end + + ## + # Returns the last component of the filename given + # in file_name, which must be formed using forward + # slashes (``/’’) regardless of the separator used + # on the local file system. If suffix is given and + # present at the end of file_name, it is removed. + # + # File.basename("/home/gumby/work/ruby.rb") #=> "ruby.rb" + # File.basename("/home/gumby/work/ruby.rb", ".rb") #=> "ruby" + def self.basename(path, ext=undefined) + path = Rubinius::Type.coerce_to_path(path) + m = Rubinius::Mirror.reflect path + + slash = "/" + + ext_not_present = undefined.equal?(ext) + + if pos = m.find_string_reverse(slash, path.bytesize) + # special case. If the string ends with a /, ignore it. + if pos == path.bytesize - 1 + + # Find the first non-/ from the right + data = path.data + found = false + pos.downto(0) do |i| + if data[i] != 47 # ?/ + path = path.byteslice(0, i+1) + found = true + break + end + end + + # edge case, it's all /'s, return "/" + return slash unless found + + # Now that we've trimmed the /'s at the end, search again + m = Rubinius::Mirror.reflect path + pos = m.find_string_reverse(slash, path.bytesize) + if ext_not_present and !pos + # No /'s found and ext not present, return path. + return path + end + end + + path = path.byteslice(pos + 1, path.bytesize - pos) if pos + end + + return path if ext_not_present + + m = Rubinius::Mirror.reflect path + + # special case. if ext is ".*", remove any extension + + ext = StringValue(ext) + + if ext == ".*" + if pos = m.find_string_reverse(".", path.bytesize) + return path.byteslice(0, pos) + end + elsif pos = m.find_string_reverse(ext, path.bytesize) + # Check that ext is the last thing in the string + if pos == path.bytesize - ext.size + return path.byteslice(0, pos) + end + end + + return path + end + + ## + # Returns true if the named file is a block device. + def self.blockdev?(path) + st = Stat.stat path + st ? st.blockdev? : false + end + + ## + # Returns true if the named file is a character device. + def self.chardev?(path) + st = Stat.stat path + st ? st.chardev? : false + end + + ## + # Changes permission bits on the named file(s) to + # the bit pattern represented by mode_int. Actual + # effects are operating system dependent (see the + # beginning of this section). On Unix systems, see + # chmod(2) for details. Returns the number of files processed. + # + # File.chmod(0644, "testfile", "out") #=> 2 + def self.chmod(mode, *paths) + mode = clamp_short mode + + paths.each do |path| + n = POSIX.chmod Rubinius::Type.coerce_to_path(path), mode + Errno.handle if n == -1 + end + paths.size + end + + ## + # Equivalent to File::chmod, but does not follow symbolic + # links (so it will change the permissions associated with + # the link, not the file referenced by the link). + # Often not available. + def self.lchmod(mode, *paths) + raise NotImplementedError, "lchmod not implemented on this platform" unless Rubinius::HAVE_LCHMOD + + mode = Rubinius::Type.coerce_to(mode, Integer, :to_int) + + paths.each do |path| + n = POSIX.lchmod Rubinius::Type.coerce_to_path(path), mode + Errno.handle if n == -1 + end + + paths.size + end + + ## + # Changes the owner and group of the + # named file(s) to the given numeric owner + # and group id‘s. Only a process with superuser + # privileges may change the owner of a file. The + # current owner of a file may change the file‘s + # group to any group to which the owner belongs. + # A nil or -1 owner or group id is ignored. + # Returns the number of files processed. + # + # File.chown(nil, 100, "testfile") + def self.chown(owner, group, *paths) + if owner + owner = Rubinius::Type.coerce_to(owner, Integer, :to_int) + else + owner = -1 + end + + if group + group = Rubinius::Type.coerce_to(group, Integer, :to_int) + else + group = -1 + end + + paths.each do |path| + n = POSIX.chown Rubinius::Type.coerce_to_path(path), owner, group + Errno.handle if n == -1 + end + + paths.size + end + + def chmod(mode) + mode = Rubinius::Type.coerce_to(mode, Integer, :to_int) + n = POSIX.fchmod descriptor, clamp_short(mode) + Errno.handle if n == -1 + n + end + + def chown(owner, group) + if owner + owner = Rubinius::Type.coerce_to(owner, Integer, :to_int) + else + owner = -1 + end + + if group + group = Rubinius::Type.coerce_to(group, Integer, :to_int) + else + group = -1 + end + + n = POSIX.fchown descriptor, owner, group + Errno.handle if n == -1 + n + end + + ## + # Equivalent to File::chown, but does not follow + # symbolic links (so it will change the owner + # associated with the link, not the file referenced + # by the link). Often not available. Returns number + # of files in the argument list. + def self.lchown(owner, group, *paths) + raise NotImplementedError, "lchown not implemented on this platform" unless Rubinius::HAVE_LCHOWN + + if owner + owner = Rubinius::Type.coerce_to(owner, Integer, :to_int) + else + owner = -1 + end + + if group + group = Rubinius::Type.coerce_to(group, Integer, :to_int) + else + group = -1 + end + + paths.each do |path| + n = POSIX.lchown Rubinius::Type.coerce_to_path(path), owner, group + Errno.handle if n == -1 + end + + paths.size + end + + ## + # Returns true if the named file is a directory, false otherwise. + # + # File.directory?(".") + def self.directory?(io_or_path) + io = Rubinius::Type.try_convert io_or_path, IO, :to_io + + if io.is_a? IO + Stat.fstat(io.fileno).directory? + else + st = Stat.stat io_or_path + st ? st.directory? : false + end + end + + def self.last_nonslash(path, start=nil) + # Find the first non-/ from the right + data = path.data + idx = nil + start ||= (path.size - 1) + + start.downto(0) do |i| + if data[i] != 47 # ?/ + return i + end + end + + return nil + end + + ## + # Returns all components of the filename given in + # file_name except the last one. The filename must be + # formed using forward slashes (``/’’) regardless of + # the separator used on the local file system. + # + # File.dirname("/home/gumby/work/ruby.rb") #=> "/home/gumby/work" + def self.dirname(path) + path = Rubinius::Type.coerce_to_path(path) + + # edge case + return "." if path.empty? + + slash = "/" + + # pull off any /'s at the end to ignore + chunk_size = last_nonslash(path) + return "/" unless chunk_size + + m = Rubinius::Mirror.reflect path + + if pos = m.find_string_reverse(slash, chunk_size) + return "/" if pos == 0 + + path = path.byteslice(0, pos) + + return "/" if path == "/" + + return path unless path.suffix? slash + + # prune any trailing /'s + idx = last_nonslash(path, pos) + + # edge case, only /'s, return / + return "/" unless idx + + return path.byteslice(0, idx - 1) + end + + return "." + end + + ## + # Returns true if the named file is executable by the + # effective user id of this process. + def self.executable?(path) + st = Stat.stat path + st ? st.executable? : false + end + + ## + # Returns true if the named file is executable by + # the real user id of this process. + def self.executable_real?(path) + st = Stat.stat path + st ? st.executable_real? : false + end + + ## + # Return true if the named file exists. + def self.exist?(path) + st = Stat.stat(path) + st ? true : false + end + + # Pull a constant for Dir local to File so that we don't have to depend + # on the global Dir constant working. This sounds silly, I know, but it's a + # little bit of defensive coding so Rubinius can run things like fakefs better. + PrivateDir = ::Dir + + ## + # Converts a pathname to an absolute pathname. Relative + # paths are referenced from the current working directory + # of the process unless dir_string is given, in which case + # it will be used as the starting point. The given pathname + # may start with a ``~’’, which expands to the process owner‘s + # home directory (the environment variable HOME must be set + # correctly). "~user" expands to the named user‘s home directory. + # + # File.expand_path("~oracle/bin") #=> "/home/oracle/bin" + # File.expand_path("../../bin", "/tmp/x") #=> "/bin" + def self.expand_path(path, dir=nil) + path = Rubinius::Type.coerce_to_path(path) + str = "".force_encoding path.encoding + first = path[0] + if first == ?~ + case path[1] + when ?/ + unless home = ENV["HOME"] + raise ArgumentError, "couldn't find HOME environment variable when expanding '~'" + end + + path = ENV["HOME"] + path.byteslice(1, path.bytesize - 1) + when nil + unless home = ENV["HOME"] + raise ArgumentError, "couldn't find HOME environment variable when expanding '~'" + end + + if home.empty? + raise ArgumentError, "HOME environment variable is empty expanding '~'" + end + + return home.dup + else + m = Rubinius::Mirror.reflect path + unless length = m.find_string("/", 1) + length = path.bytesize + end + + name = path.byteslice 1, length - 1 + unless dir = Rubinius.get_user_home(name) + raise ArgumentError, "user #{name} does not exist" + end + + path = dir + path.byteslice(length, path.bytesize - length) + end + elsif first != ?/ + if dir + dir = expand_path dir + else + dir = PrivateDir.pwd + end + + path = "#{dir}/#{path}" + end + + items = [] + start = 0 + size = path.bytesize + m = Rubinius::Mirror.reflect path + + while index = m.find_string("/", start) or (start < size and index = size) + length = index - start + + if length > 0 + item = path.byteslice start, length + + if item == ".." + items.pop + elsif item != "." + items << item + end + end + + start = index + 1 + end + + if items.empty? + str << "/" + else + items.each { |x| str.append "/#{x}" } + end + + str + end + + ## + # Returns the extension (the portion of file name in + # path after the period). + # + # File.extname("test.rb") #=> ".rb" + # File.extname("a/b/d/test.rb") #=> ".rb" + # File.extname("test") #=> "" + # File.extname(".profile") #=> "" + def self.extname(path) + path = Rubinius::Type.coerce_to_path(path) + path_size = path.bytesize + m = Rubinius::Mirror.reflect path + + dot_idx = m.find_string_reverse(".", path_size) + + # No dots at all + return "" unless dot_idx + + slash_idx = m.find_string_reverse("/", path_size) + + # pretend there is / just to the left of the start of the string + slash_idx ||= -1 + + # no . in the last component of the path + return "" if dot_idx < slash_idx + + # last component starts with a . + return "" if dot_idx == slash_idx + 1 + + # last component ends with a . + return "" if dot_idx == path_size - 1 + + return path.byteslice(dot_idx, path_size - dot_idx) + end + + ## + # Returns true if the named file exists and is a regular file. + def self.file?(path) + st = Stat.stat path + st ? st.file? : false + end + + def self.braces(pattern, flags=0, patterns=[]) + escape = (flags & FNM_NOESCAPE) == 0 + + rbrace = nil + lbrace = nil + + # Do a quick search for a { to start the search better + i = pattern.index("{") + + if i + nest = 0 + + while i < pattern.size + char = pattern[i] + + if char == "{" + lbrace = i if nest == 0 + nest += 1 + end + + if char == "}" + nest -= 1 + end + + if nest == 0 + rbrace = i + break + end + + if char == "\\" and escape + i += 1 + end + + i += 1 + end + end + + # There was a full {} expression detected, expand each part of it + # recursively. + if lbrace and rbrace + pos = lbrace + front = pattern[0...lbrace] + back = pattern[(rbrace + 1)..-1] + + while pos < rbrace + nest = 0 + pos += 1 + last = pos + + while pos < rbrace and not (pattern[pos] == "," and nest == 0) + nest += 1 if pattern[pos] == "{" + nest -= 1 if pattern[pos] == "}" + + if pattern[pos] == "\\" and escape + pos += 1 + break if pos == rbrace + end + + pos += 1 + end + + brace_pattern = "#{front}#{pattern[last...pos]}#{back}" + patterns << brace_pattern + + braces(brace_pattern, flags, patterns) + end + end + patterns + end + + ## + # Returns true if path matches against pattern The pattern + # is not a regular expression; instead it follows rules + # similar to shell filename globbing. It may contain the + # following metacharacters: + # + # *: Matches any file. Can be restricted by other values in the glob. * will match all files; c* will match all files beginning with c; *c will match all files ending with c; and c will match all files that have c in them (including at the beginning or end). Equivalent to / .* /x in regexp. + # **: Matches directories recursively or files expansively. + # ?: Matches any one character. Equivalent to /.{1}/ in regexp. + # [set]: Matches any one character in set. Behaves exactly like character sets in Regexp, including set negation ([^a-z]). + # : Escapes the next metacharacter. + # flags is a bitwise OR of the FNM_xxx parameters. The same glob pattern and flags are used by Dir::glob. + # + # File.fnmatch('cat', 'cat') #=> true : match entire string + # File.fnmatch('cat', 'category') #=> false : only match partial string + # File.fnmatch('c{at,ub}s', 'cats') #=> false : { } isn't supported + # File.fnmatch('c{at,ub}s', 'cats', File::FNM_EXTGLOB) #=> true : { } is supported with FNM_EXTGLOB + # + # File.fnmatch('c?t', 'cat') #=> true : '?' match only 1 character + # File.fnmatch('c??t', 'cat') #=> false : ditto + # File.fnmatch('c*', 'cats') #=> true : '*' match 0 or more characters + # File.fnmatch('c*t', 'c/a/b/t') #=> true : ditto + # File.fnmatch('ca[a-z]', 'cat') #=> true : inclusive bracket expression + # File.fnmatch('ca[^t]', 'cat') #=> false : exclusive bracket expression ('^' or '!') + # + # File.fnmatch('cat', 'CAT') #=> false : case sensitive + # File.fnmatch('cat', 'CAT', File::FNM_CASEFOLD) #=> true : case insensitive + # + # File.fnmatch('?', '/', File::FNM_PATHNAME) #=> false : wildcard doesn't match '/' on FNM_PATHNAME + # File.fnmatch('*', '/', File::FNM_PATHNAME) #=> false : ditto + # File.fnmatch('[/]', '/', File::FNM_PATHNAME) #=> false : ditto + # + # File.fnmatch('\?', '?') #=> true : escaped wildcard becomes ordinary + # File.fnmatch('\a', 'a') #=> true : escaped ordinary remains ordinary + # File.fnmatch('\a', '\a', File::FNM_NOESCAPE) #=> true : FNM_NOESACPE makes '\' ordinary + # File.fnmatch('[\?]', '?') #=> true : can escape inside bracket expression + # + # File.fnmatch('*', '.profile') #=> false : wildcard doesn't match leading + # File.fnmatch('*', '.profile', File::FNM_DOTMATCH) #=> true period by default. + # File.fnmatch('.*', '.profile') #=> true + # + # rbfiles = '**' '/' '*.rb' # you don't have to do like this. just write in single string. + # File.fnmatch(rbfiles, 'main.rb') #=> false + # File.fnmatch(rbfiles, './main.rb') #=> false + # File.fnmatch(rbfiles, 'lib/song.rb') #=> true + # File.fnmatch('**.rb', 'main.rb') #=> true + # File.fnmatch('**.rb', './main.rb') #=> false + # File.fnmatch('**.rb', 'lib/song.rb') #=> true + # File.fnmatch('*', 'dave/.profile') #=> true + # + # pattern = '*' '/' '*' + # File.fnmatch(pattern, 'dave/.profile', File::FNM_PATHNAME) #=> false + # File.fnmatch(pattern, 'dave/.profile', File::FNM_PATHNAME | File::FNM_DOTMATCH) #=> true + # + # pattern = '**' '/' 'foo' + # File.fnmatch(pattern, 'a/b/c/foo', File::FNM_PATHNAME) #=> true + # File.fnmatch(pattern, '/a/b/c/foo', File::FNM_PATHNAME) #=> true + # File.fnmatch(pattern, 'c:/a/b/c/foo', File::FNM_PATHNAME) #=> true + # File.fnmatch(pattern, 'a/.b/c/foo', File::FNM_PATHNAME) #=> false + # File.fnmatch(pattern, 'a/.b/c/foo', File::FNM_PATHNAME | File::FNM_DOTMATCH) #=> true + + def self.fnmatch(pattern, path, flags=0) + pattern = StringValue(pattern) + path = Rubinius::Type.coerce_to_path(path) + flags = Rubinius::Type.coerce_to(flags, Fixnum, :to_int) + brace_match = false + + if (flags & FNM_EXTGLOB) != 0 + brace_match = braces(pattern, flags).any? { |p| super(p, path, flags) } + end + brace_match || super(pattern, path, flags) + end + + ## + # Identifies the type of the named file; the return string is + # one of "file", "directory", "characterSpecial", + # "blockSpecial", "fifo", "link", "socket", or "unknown". + # + # File.ftype("testfile") #=> "file" + # File.ftype("/dev/tty") #=> "characterSpecial" + # File.ftype("/tmp/.X11-unix/X0") #=> "socket" + def self.ftype(path) + lstat(path).ftype + end + + ## + # Returns true if the named file exists and the effective + # group id of the calling process is the owner of the file. + # Returns false on Windows. + def self.grpowned?(path) + begin + lstat(path).grpowned? + rescue + false + end + end + + ## + # Returns true if the named files are identical. + # + # open("a", "w") {} + # p File.identical?("a", "a") #=> true + # p File.identical?("a", "./a") #=> true + # File.link("a", "b") + # p File.identical?("a", "b") #=> true + # File.symlink("a", "c") + # p File.identical?("a", "c") #=> true + # open("d", "w") {} + # p File.identical?("a", "d") #=> false + def self.identical?(orig, copy) + orig = Rubinius::Type.coerce_to_path(orig) + st_o = File::Stat.stat(orig) + copy = Rubinius::Type.coerce_to_path(copy) + st_c = File::Stat.stat(copy) + + return false if st_o.nil? || st_c.nil? + + return false unless st_o.dev == st_c.dev + return false unless st_o.ino == st_c.ino + return false unless st_o.ftype == st_c.ftype + return false unless POSIX.access(orig, Constants::R_OK) + return false unless POSIX.access(copy, Constants::R_OK) + + true + end + + ## + # Returns a new string formed by joining the strings using File::SEPARATOR. + # + # File.join("usr", "mail", "gumby") #=> "usr/mail/gumby" + def self.join(*args) + return '' if args.empty? + + sep = SEPARATOR + + # The first one is unrolled out of the loop to remove a condition + # from the loop. It seems needless, but you'd be surprised how much hinges + # on the performance of File.join + # + first = args.shift + case first + when String + first = first.dup + when Array + recursion = Thread.detect_recursion(first) do + first = join(*first) + end + + raise ArgumentError, "recursive array" if recursion + else + # We need to use dup here, since it's possible that + # StringValue gives us a direct object we shouldn't mutate + first = Rubinius::Type.coerce_to_path(first).dup + end + + ret = first + + args.each do |el| + value = nil + + case el + when String + value = el + when Array + recursion = Thread.detect_recursion(el) do + value = join(*el) + end + + raise ArgumentError, "recursive array" if recursion + else + value = Rubinius::Type.coerce_to_path(el) + end + + if value.prefix? sep + ret.gsub!(/#{SEPARATOR}+$/, '') + elsif not ret.suffix? sep + ret << sep + end + + ret << value + end + ret + end + + ## + # Creates a new name for an existing file using a hard link. + # Will not overwrite new_name if it already exists (raising + # a subclass of SystemCallError). Not available on all platforms. + # + # File.link("testfile", ".testfile") #=> 0 + # IO.readlines(".testfile")[0] #=> "This is line one\n" + def self.link(from, to) + n = POSIX.link Rubinius::Type.coerce_to_path(from), Rubinius::Type.coerce_to_path(to) + Errno.handle if n == -1 + n + end + + ## + # Same as File::stat, but does not follow the last symbolic link. + # Instead, reports on the link itself. + # + # File.symlink("testfile", "link2test") #=> 0 + # File.stat("testfile").size #=> 66 + # File.lstat("link2test").size #=> 8 + # File.stat("link2test").size #=> 66 + def self.lstat(path) + Stat.lstat path + end + + def self.path(obj) + return obj.to_path if obj.respond_to? :to_path + + StringValue(obj) + end + + ## + # Returns true if the named file is a pipe. + def self.pipe?(path) + st = Stat.stat path + st ? st.pipe? : false + end + + ## + # Returns true if the named file is readable by the effective + # user id of this process. + def self.readable?(path) + st = Stat.stat path + st ? st.readable? : false + end + + ## + # Returns true if the named file is readable by the real user + # id of this process. + def self.readable_real?(path) + st = Stat.stat path + st ? st.readable_real? : false + end + + ## + # Returns the name of the file referenced by the given link. + # Not available on all platforms. + # + # File.symlink("testfile", "link2test") #=> 0 + # File.readlink("link2test") #=> "testfile" + def self.readlink(path) + FFI::MemoryPointer.new(1024) do |ptr| + n = POSIX.readlink Rubinius::Type.coerce_to_path(path), ptr, 1024 + Errno.handle if n == -1 + + return ptr.read_string(n) + end + end + + def self.realpath(path, basedir = nil) + real = basic_realpath path, basedir + + unless exist? real + raise Errno::ENOENT, real + end + + real + end + + def self.realdirpath(path, basedir = nil) + real = basic_realpath path, basedir + dir = dirname real + + unless directory? dir + raise Errno::ENOENT, real + end + + real + end + + def self.basic_realpath(path, basedir = nil) + path = expand_path(path, basedir || Dir.pwd) + real = '' + symlinks = {} + + while !path.empty? + pos = path.index(SEPARATOR, 1) + + if pos + name = path[0...pos] + path = path[pos..-1] + else + name = path + path = '' + end + + real = join(real, name) + if symlink?(real) + raise Errno::ELOOP if symlinks[real] + symlinks[real] = true + if path.empty? + path = expand_path(readlink(real), dirname(real)) + else + path = expand_path(join(readlink(real), path), dirname(real)) + end + real = '' + end + end + + real + end + + class << self + private :basic_realpath + end + + ## + # Renames the given file to the new name. Raises a SystemCallError + # if the file cannot be renamed. + # + # File.rename("afile", "afile.bak") #=> 0 + def self.rename(from, to) + n = POSIX.rename Rubinius::Type.coerce_to_path(from), Rubinius::Type.coerce_to_path(to) + Errno.handle if n == -1 + n + end + + ## + # Returns the size of file_name. + def self.size(io_or_path) + io = Rubinius::Type.try_convert io_or_path, IO, :to_io + + if io.is_a? IO + Stat.fstat(io.fileno).size + else + stat(io_or_path).size + end + end + + ## + # Returns nil if file_name doesn‘t exist or has zero size, + # the size of the file otherwise. + def self.size?(io_or_path) + s = 0 + + io = Rubinius::Type.try_convert io_or_path, IO, :to_io + + if io.is_a? IO + s = Stat.fstat(io.fileno).size + else + st = Stat.stat io_or_path + s = st.size if st + end + + s > 0 ? s : nil + end + + ## + # Returns true if the named file is a socket. + def self.socket?(path) + st = Stat.stat path + st ? st.socket? : false + end + + ## + # Splits the given string into a directory and a file component and returns them in a two-element array. See also File::dirname and File::basename. + # + # File.split("/home/gumby/.profile") #=> ["/home/gumby", ".profile"] + def self.split(path) + p = Rubinius::Type.coerce_to_path(path) + [dirname(p), basename(p)] + end + + ## + # Returns a File::Stat object for the named file (see File::Stat). + # + # File.stat("testfile").mtime #=> Tue Apr 08 12:58:04 CDT 2003 + def self.stat(path) + Stat.new path + end + + ## + # Creates a symbolic link called new_name for the + # existing file old_name. Raises a NotImplemented + # exception on platforms that do not support symbolic links. + # + # File.symlink("testfile", "link2test") #=> 0 + def self.symlink(from, to) + n = POSIX.symlink Rubinius::Type.coerce_to_path(from), Rubinius::Type.coerce_to_path(to) + Errno.handle if n == -1 + n + end + + ## + # Returns true if the named file is a symbolic link. + def self.symlink?(path) + Stat.lstat(path).symlink? + rescue Errno::ENOENT, Errno::ENOTDIR + false + end + + ## + # Copies a file from to to. If to is a directory, copies from to to/from. + def self.syscopy(from, to) + out = directory?(to) ? to + basename(from) : to + + open(out, 'w') do |f| + f.write read(from).read + end + end + + ## + # Truncates the file file_name to be at most integer + # bytes long. Not available on all platforms. + # + # f = File.new("out", "w") + # f.write("1234567890") #=> 10 + # f.close #=> nil + # File.truncate("out", 5) #=> 0 + # File.size("out") #=> 5 + def self.truncate(path, length) + path = Rubinius::Type.coerce_to_path(path) + + unless exist?(path) + raise Errno::ENOENT, path + end + + length = Rubinius::Type.coerce_to length, Integer, :to_int + + FileDescriptor.truncate(path, length) + end + + ## + # Returns the current umask value for this process. + # If the optional argument is given, set the umask + # to that value and return the previous value. Umask + # values are subtracted from the default permissions, + # so a umask of 0222 would make a file read-only for + # everyone. + # + # File.umask(0006) #=> 18 + # File.umask #=> 6 + def self.umask(mask = nil) + if mask + POSIX.umask clamp_short(mask) + else + old_mask = POSIX.umask(0) + POSIX.umask old_mask + old_mask + end + end + + ## + # Deletes the named files, returning the number of names + # passed as arguments. Raises an exception on any error. + # + # See also Dir::rmdir. + def self.unlink(*paths) + paths.each do |path| + n = POSIX.unlink Rubinius::Type.coerce_to_path(path) + Errno.handle if n == -1 + end + + paths.size + end + + def self.world_readable?(path) + path = Rubinius::Type.coerce_to_path path + return nil unless exist? path + mode = Stat.new(path).mode + if (mode & Stat::S_IROTH) == Stat::S_IROTH + tmp = mode & (Stat::S_IRUGO | Stat::S_IWUGO | Stat::S_IXUGO) + return Rubinius::Type.coerce_to tmp, Fixnum, :to_int + end + nil + end + + def self.world_writable?(path) + path = Rubinius::Type.coerce_to_path path + return nil unless exist? path + mode = Stat.new(path).mode + if (mode & Stat::S_IWOTH) == Stat::S_IWOTH + tmp = mode & (Stat::S_IRUGO | Stat::S_IWUGO | Stat::S_IXUGO) + return Rubinius::Type.coerce_to tmp, Fixnum, :to_int + end + end + + ## + # Returns true if the named file is writable by the effective + # user id of this process. + def self.writable?(path) + st = Stat.stat path + st ? st.writable? : false + end + + ## + # Returns true if the named file is writable by the real user + # id of this process. + def self.writable_real?(path) + st = Stat.stat path + st ? st.writable_real? : false + end + + ## + # Returns true if the named file exists and has a zero size. + def self.zero?(path) + st = Stat.stat path + st ? st.zero? : false + end + + ## + # Returns true if the named file exists and the effective + # used id of the calling process is the owner of the file. + # File.owned?(file_name) => true or false + def self.owned?(file_name) + Stat.new(file_name).owned? + rescue Errno::ENOENT + return false + end + + ## + # Returns true if the named file has the setgid bit set. + def self.setgid?(file_name) + Stat.new(file_name).setgid? + rescue Errno::ENOENT + return false + end + + ## + # Returns true if the named file has the setuid bit set. + def self.setuid?(file_name) + Stat.new(file_name).setuid? + rescue Errno::ENOENT + return false + end + + ## + # Returns true if the named file has the sticky bit set. + def self.sticky?(file_name) + Stat.new(file_name).sticky? + rescue Errno::ENOENT + return false + end + + def self.mkfifo(file_name, mode = 0666) + raise NotImplementedError, "mkfifo is not implemented on this platform" unless Rubinius::HAVE_MKFIFO + + file_name = file_name.to_path if file_name.respond_to?(:to_path) + file_name = StringValue(file_name) + + ret = POSIX.mkfifo(file_name, mode) + + if ret == 0 + ret + else + Errno.handle(file_name) + end + end + + class << self + alias_method :delete, :unlink + alias_method :exists?, :exist? + alias_method :fnmatch?, :fnmatch + end + + def initialize(path_or_fd, mode=undefined, perm=undefined, options=undefined) + if path_or_fd.kind_of? Integer + super(path_or_fd, mode, options) + @path = nil + else + path = Rubinius::Type.coerce_to_path path_or_fd + + # TODO: fix normalize_options + case mode + when String, Fixnum + # do nothing + when nil, undefined + mode = "r" + when Hash + options = mode + mode = nil + else + options = Rubinius::Type.coerce_to mode, Hash, :to_hash + mode = nil + end + + if undefined.equal?(options) + options = Rubinius::Type.try_convert(perm, Hash, :to_hash) + perm = undefined if options + end + + nmode, binary, external, internal = IO.normalize_options(mode, options) + nmode ||= "r" + + perm = 0666 if undefined.equal? perm + + fd = IO.sysopen(path, nmode, perm) + Errno.handle path if fd < 0 + + @path = path + + super(fd, mode, options) + end + end + + private :initialize + + ## + # see File.atime + def atime + Stat.new(@path).atime + end + + ## + # see File.ctime + def ctime + Stat.new(@path).ctime + end + + ## + # see File.birthtime + def birthtime + Stat.new(@path).birthtime + end + + ## + # see File.mtime + def mtime + Stat.new(@path).mtime + end + + def reopen(other, mode = 'r+') + rewind unless closed? rescue Errno::ESPIPE + unless other.kind_of? IO + other = Rubinius::Type.coerce_to_path(other) + end + super(other, mode) + end + + def flock(const) + const = Rubinius::Type.coerce_to const, Integer, :to_int + + result = POSIX.flock descriptor, const + + return false if result == -1 + result + end + + def lstat + Stat.lstat @path + end + + def stat + Stat.fstat descriptor + end + + alias_method :to_path, :path + + def truncate(length) + length = Rubinius::Type.coerce_to length, Integer, :to_int + + ensure_open_and_writable + raise Errno::EINVAL, "Can't truncate a file to a negative length" if length < 0 + + flush + reset_buffering + @fd.ftruncate(length) + end + + def inspect + return_string = "#<#{self.class}:0x#{object_id.to_s(16)} path=#{@path}" + return_string << " (closed)" if closed? + return_string << ">" + end + + def size + raise IOError, "closed stream" if closed? + stat.size + end + + ## + # Return the equivalent S-Expression of the file given. + # Raises +SyntaxError+ if there is a syntax issue in the + # file, making it unparsable. + # File.to_sexp("/tmp/test.rb") #=> s(...) + def self.to_sexp(name) + File.read(name).to_sexp(name) + end +end diff --git a/gxg/core/file_test.rb b/gxg/core/file_test.rb new file mode 100644 index 0000000..444eda3 --- /dev/null +++ b/gxg/core/file_test.rb @@ -0,0 +1,119 @@ +module FileTest + def blockdev?(path) + File.blockdev? path + end + module_function :blockdev? + + def chardev?(path) + File.chardev? path + end + module_function :chardev? + + def directory?(path) + File.directory? path + end + module_function :directory? + + def executable?(path) + File.executable? path + end + module_function :executable? + + def executable_real?(path) + File.executable_real? path + end + module_function :executable_real? + + def exist?(path) + File.exist? path + end + module_function :exist? + + alias_method :exists?, :exist? + module_function :exists? + + def file?(path) + File.file? path + end + module_function :file? + + def grpowned?(path) + File.grpowned? path + end + module_function :grpowned? + + def identical?(a, b) + File.identical? a, b + end + module_function :identical? + + def owned?(path) + File.owned? path + end + module_function :owned? + + def pipe?(path) + File.pipe? path + end + module_function :pipe? + + def readable?(path) + File.readable? path + end + module_function :readable? + + def readable_real?(path) + File.readable_real? path + end + module_function :readable_real? + + def setgid?(path) + File.setgid? path + end + module_function :setgid? + + def setuid?(path) + File.setuid? path + end + module_function :setuid? + + def size(path) + File.size path + end + module_function :size + + def size?(path) + File.size? path + end + module_function :size? + + def socket?(path) + File.socket? path + end + module_function :socket? + + def sticky?(path) + File.sticky? path + end + module_function :sticky? + + def symlink?(path) + File.symlink? path + end + module_function :symlink? + + def writable?(path) + File.writable? path + end + module_function :writable? + + def writable_real?(path) + File.writable_real? path + end + module_function :writable_real? + + def zero?(path) + File.zero? path + end + module_function :zero? +end diff --git a/gxg/core/float.rb b/gxg/core/float.rb new file mode 100644 index 0000000..923437c --- /dev/null +++ b/gxg/core/float.rb @@ -0,0 +1,602 @@ +class Float < Numeric + def **(other) + Rubinius.primitive :float_pow + + if other.is_a?(Float) && self < 0 && other != other.round + return Complex.new(self, 0) ** other + end + + b, a = math_coerce other + a ** b + end + + def imaginary + 0 + end + + def numerator + if nan? + NAN + elsif infinite? == 1 + INFINITY + elsif infinite? == -1 + -INFINITY + else + super + end + end + + def denominator + if infinite? || nan? + 1 + else + super + end + end + + def to_r + f, e = Math.frexp self + f = Math.ldexp(f, MANT_DIG).to_i + e -= MANT_DIG + + (f * (RADIX ** e)).to_r + end + + def arg + if nan? + self + elsif signbit? + Math::PI + else + 0 + end + end + alias_method :angle, :arg + alias_method :phase, :arg + + def rationalize(eps=undefined) + if undefined.equal?(eps) + f, n = Math.frexp self + f = Math.ldexp(f, Float::MANT_DIG).to_i + n -= Float::MANT_DIG + + Rational.new(2 * f, 1 << (1 - n)).rationalize(Rational.new(1, 1 << (1 - n))) + else + to_r.rationalize(eps) + end + end + + def round(ndigits=0) + ndigits = Rubinius::Type.coerce_to(ndigits, Integer, :to_int) + + if ndigits == 0 + return Rubinius.invoke_primitive :float_round, self + elsif ndigits < 0 + return truncate.round ndigits + end + + return self if infinite? or nan? + + _, exp = Math.frexp(self) + + if ndigits >= (Float::DIG + 2) - (exp > 0 ? exp / 4 : exp / 3 - 1) + return self + end + + if ndigits < -(exp > 0 ? exp / 3 + 1 : exp / 4) + return 0.0 + end + + f = 10**ndigits + Rubinius.invoke_primitive(:float_round, self * f) / f.to_f + end + def coerce(other) + return [other, self] if other.kind_of? Float + [Float(other), self] + end + + def -@ + Rubinius.primitive :float_neg + raise PrimitiveFailure, "Float#-@ primitive failed" + end + + def abs + FFI::Platform::Math.fabs(self) + end + + alias_method :magnitude, :abs + + def signbit? + Rubinius.primitive :float_signbit_p + raise PrimitiveFailure, "Float#signbit? primitive failed" + end + + def +(other) + Rubinius.asm do + flt = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + n_promote r2, r0, r1 + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + goto done + + flt.set! + n_dadd r0, r0, r1 + r_store_float r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + b, a = math_coerce other + a + b + end + + def -(other) + Rubinius.asm do + flt = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + n_promote r2, r0, r1 + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + goto done + + flt.set! + n_dsub r0, r0, r1 + r_store_float r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + b, a = math_coerce other + a - b + end + + def *(other) + Rubinius.asm do + flt = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + n_promote r2, r0, r1 + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + goto done + + flt.set! + n_dmul r0, r0, r1 + r_store_float r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + b, a = math_coerce other + a * b + end + + def /(other) + Rubinius.asm do + flt = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + n_promote r2, r0, r1 + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + goto done + + flt.set! + n_ddiv r0, r0, r1 + r_store_float r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + redo_coerced :/, other + end + + alias_method :divide, :/ + alias_method :quo, :/ + alias_method :fdiv, :/ + + INFINITY = 1.0 / 0.0 + NAN = 0.0 / 0.0 + + def divmod(other) + Rubinius.primitive :float_divmod + b, a = math_coerce other + a.divmod b + end + + def %(other) + return 0 / 0.to_f if other == 0 + Rubinius.primitive :float_mod + b, a = math_coerce other + a % b + end + + alias_method :modulo, :% + + def <(other) + Rubinius.asm do + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + n_dlt r0, r0, r1 + r_load_bool r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + b, a = math_coerce other, :compare_error + a < b + end + + def <=(other) + Rubinius.asm do + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + n_dle r0, r0, r1 + r_load_bool r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + b, a = math_coerce other, :compare_error + a <= b + end + + def >(other) + Rubinius.asm do + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + n_dgt r0, r0, r1 + r_load_bool r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + b, a = math_coerce other, :compare_error + a > b + end + + def >=(other) + Rubinius.asm do + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + n_dge r0, r0, r1 + r_load_bool r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + b, a = math_coerce other, :compare_error + a >= b + end + + def <=>(other) + Rubinius.primitive :float_compare + b, a = math_coerce other, :compare_error + a <=> b + rescue ArgumentError + nil + end + + def !=(other) + Rubinius.asm do + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + n_dne r0, r0, r1 + r_load_bool r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + b, a = math_coerce(other) + a != b ? true : false + end + + def ==(other) + Rubinius.asm do + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + n_deq r0, r0, r1 + r_load_bool r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + begin + b, a = math_coerce(other) + a == b ? true : false + rescue TypeError + return other == self + end + end + + def eql?(other) + Rubinius.primitive :float_eql + false + end + + def nan? + Rubinius.asm do + r0 = new_register + r1 = new_register + + r_load_self r0 + r_load_float r0, r0 + n_dnan r1, r0 + r_load_bool r0, r1 + r_ret r0 + + # TODO: teach the bytecode compiler better + push_true + end + end + + def infinite? + Rubinius.asm do + no = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + + r_load_self r0 + r_load_float r0, r0 + n_dinf r1, r0 + + r_load_0 r0 + n_ieq r2, r1, r0 + b_if r2, no + + r_store_int r0, r1 + r_ret r0 + + no.set! + r_load_nil r0, 0 + r_ret r0 + + # TODO: teach the bytecode compiler better + push_true + end + end + + def finite? + not (nan? or infinite?) + end + + def to_f + self + end + + def to_i + Rubinius.primitive :float_to_i + raise PrimitiveFailure, "Float#to_i primitive failed" + end + + alias_method :to_int, :to_i + alias_method :truncate, :to_i + + def to_s + to_s_minimal + end + + alias_method :inspect, :to_s + + def to_s_minimal + Rubinius.primitive :float_to_s_minimal + raise PrimitiveFailure, "Float#to_s_minimal primitive failed: output exceeds buffer size" + end + + def to_s_formatted(fmt) + Rubinius.primitive :float_to_s_formatted + raise PrimitiveFailure, "Float#to_s_formatted primitive failed: output exceeds buffer size" + end + private :to_s_formatted + + def dtoa + Rubinius.primitive :float_dtoa + raise PrimitiveFailure, "Fload#dtoa primitive failed" + end + + def to_packed(size) + Rubinius.primitive :float_to_packed + raise PrimitiveFailure, "Float#to_packed primitive failed" + end + + def ceil + int = to_i() + + return int if self == int or self < 0 + return int + 1 + end + + def floor + int = to_i() + + return int if self > 0 or self == int + return int - 1 + end + + def next_float + return NAN if self.nan? + return -MAX if self == -INFINITY + return INFINITY if self == MAX + return Math.ldexp(0.5, MIN_EXP - MANT_DIG + 1) if self.zero? + + frac, exp = Math.frexp self + + if frac == -0.5 + frac *= 2 + exp -= 1 + end + + smallest_frac = EPSILON / 2 + smallest_frac = Math.ldexp(smallest_frac, MIN_EXP - exp) if exp < MIN_EXP + + result_frac = frac + smallest_frac + + return -0.0 if result_frac.zero? && frac < 0 + return 0.0 if result_frac.zero? && frac > 0 + + return Math.ldexp result_frac, exp + end + + def prev_float + return -(-self).next_float + end +end diff --git a/gxg/core/fsevent.rb b/gxg/core/fsevent.rb new file mode 100644 index 0000000..58b101c --- /dev/null +++ b/gxg/core/fsevent.rb @@ -0,0 +1,33 @@ +module Rubinius + class FSEvent + attr_accessor :fileno + attr_accessor :path + + def self.allocate + Rubinius.primitive :fsevent_allocate + raise PrimitiveFailure, "Rubinius::FSEvent.allocate failed" + end + + def watch_file(fd, path) + Rubinius.primitive :fsevent_watch_file + raise PrimitiveFailure, "Rubinius::FSEvent#watch_file failed" + end + + def wait_for_event + Rubinius.primitive :fsevent_wait_for_event + raise PrimitiveFailure, "Rubinius::FSEvent#wait_for_event failed" + end + + def inspect + str = "#<#{self.class}:0x#{self.__id__.to_s(16)}" + + str << " fileno=#{fileno}" if fileno + str << " path=#{path}" if path + str << ">" + + Rubinius::Type.infect str, self + + str + end + end +end diff --git a/gxg/core/gc.rb b/gxg/core/gc.rb new file mode 100644 index 0000000..dae0737 --- /dev/null +++ b/gxg/core/gc.rb @@ -0,0 +1,137 @@ +module GC + def self.start + run(false) + end + + def self.run(force) + Rubinius.primitive :vm_gc_start + raise PrimitiveFailure, "GC.run primitive failed" + end + + # Totally fake. + def self.stress + @stress_level ||= false + end + + # Totally fake. + def self.stress=(flag) + @stress_level = !!flag + end + + # Totally fake. + @enabled = true + + def self.enable + # We don't support disable, so sure! enabled! + ret = !@enabled + @enabled = true + + return ret + end + + # Totally fake. + def self.disable + # Treat this like a request that we don't honor. + ret = !@enabled + @enabled = false + + return ret + end + + def garbage_collect + GC.start + end + + def self.count + if data = stat + data[:"gc.young.count"] + data[:"gc.immix.count"] + else + 0 + end + end + + def self.time + if data = stat + data[:"gc.young.ms"] + + data[:"gc.immix.stop.ms"] + + data[:"gc.large.sweep.us"] * 1_000 + else + 0 + end + end + + def self.stat + if Rubinius::Metrics.enabled? + return Rubinius::Metrics.data.to_hash + end + end + + module Profiler + @enabled = true + @since = 0 + + def self.clear + @since = GC.time + nil + end + + def self.disable + # Treat this like a request that we don't honor. + ret = !@enabled + @enabled = false + ret + end + + def self.enable + # We don't support disable, so sure! enabled! + ret = !@enabled + @enabled = true + ret + end + + def self.enabled? + @enabled + end + + def self.report(out = $stdout) + out.write result + end + + def self.result + unless Rubinius::Metrics.enabled? + return "Rubinius::Metrics is disabled. GC::Profiler results are unavailable" + end + + stats = GC.stat + + out = <<-OUT +Complete process runtime statistics +=================================== + +Collections + Count Total time / concurrent (ms) +Young #{sprintf("% 22d", stats[:'gc.young.count'])} #{sprintf("% 16d ", stats[:'gc.young.ms'])} +Full #{sprintf("% 22d", stats[:'gc.immix.count'])} #{sprintf("% 16d / % 10d", stats[:'gc.immix.stop.ms'], stats[:'gc.immix.concurrent.ms'])} + +Allocation + Objects allocated Bytes allocated +Young #{sprintf("% 22d", stats[:'memory.young.objects'])} #{sprintf("% 22d", stats[:'memory.young.bytes'])} +Promoted#{sprintf("% 22d", stats[:'memory.promoted.objects'])} #{sprintf("% 22d", stats[:'memory.promoted.bytes'])} +Mature #{sprintf("% 22d", stats[:'memory.immix.objects'])} #{sprintf("% 22d", stats[:'memory.immix.bytes'])} + + +Usage + Bytes used +Young #{sprintf("% 22d", stats[:'memory.young.bytes'])} +Mature #{sprintf("% 22d", stats[:'memory.immix.bytes'])} +Large #{sprintf("% 22d", stats[:'memory.large.bytes'])} +Code #{sprintf("% 22d", stats[:'memory.code.bytes'])} +Symbols #{sprintf("% 22d", stats[:'memory.symbols.bytes'])} + OUT + end + + def self.total_time + (GC.time - @since) / 1000.0 + end + end +end diff --git a/gxg/core/global.rb b/gxg/core/global.rb new file mode 100644 index 0000000..6b6cee7 --- /dev/null +++ b/gxg/core/global.rb @@ -0,0 +1,159 @@ +## +# Stores global variables and global variable aliases. + +module Rubinius + class GlobalVariables + def initialize + load_path = CoreDB.load_path + loaded_features = CoreDB.loaded_features + + @internal = LookupTable.new + + @internal[:$;] = nil + @internal[:$/] = "\n" # Input record separator + @internal[:$\] = nil # Output record separator + @internal[:$<] = ARGF + @internal[:$:] = load_path + @internal[:$-I] = load_path + @internal[:$"] = loaded_features + @internal[:$,] = nil # Output field separator + @internal[:$.] = 0 + @internal[:$=] = false # ignore case, whatever that is + @internal[:$0] = nil + @internal[:$CONSOLE] = STDOUT + @internal[:$DEBUG] = false + @internal[:$LOADED_FEATURES] = loaded_features + @internal[:$LOAD_PATH] = load_path + @internal[:$SAFE] = 0 + + @internal[:$stderr] = STDERR + @internal[:$stdin] = STDIN + @internal[:$stdout] = STDOUT + + @alias = LookupTable.new + @hooks = LookupTable.new + end + + private :initialize + + def key?(key) + @internal.key?(key) || @alias.key?(key) + end + + def variables + @internal.keys + @alias.keys + end + + def [](key) + if @internal.key? key + @internal[key] + elsif @hooks.key? key + @hooks[key][0].call(key) + elsif @alias.key? key + self[@alias[key]] + end + end + + def []=(key, data) + if alias_key = @alias[key] + self[alias_key] = data + elsif @hooks.key? key + hook = @hooks[key] + val = hook[1].call(key, data) + if hook[2] + @internal[key] = val + end + val + else + @internal[key] = data + end + end + + # Respects internal and alias, but not hooks. Useful to bypass + # normal read only checks. + def set!(key, data) + if alias_key = @alias[key] + set! alias_key, data + else + @internal[key] = data + end + end + + def add_alias(current_name, alias_name) + current_name = current_name.to_sym + alias_name = alias_name.to_sym + + if @internal.key? alias_name + @internal.delete alias_name + end + + if @hooks.key? alias_name + @hooks.delete alias_name + end + + @alias[alias_name] = current_name + end + + def illegal_set(*args) + raise NameError, "unable to set global" + end + + def set_hook(var, getter=nil, setter=nil, &block) + if block + if getter or setter + raise ArgumentError, "duplicate procs provided with block" + end + + @hooks[var] = [block, method(:illegal_set)] + else + set_internal = false + set_internal = true if getter == :[] + + if !getter + getter = method(:nil_return) + elsif getter.kind_of? Symbol + getter = method(getter) + end + + unless getter.respond_to?(:call) + raise ArgumentError, "getter must respond to call" + end + + if !setter + setter = method(:illegal_set) + elsif setter.kind_of? Symbol + setter = method(setter) + end + + unless setter.respond_to?(:call) + raise ArgumentError, "setter must respond to call" + end + + @hooks[var] = [getter, setter, set_internal] + end + end + + def remove_hook(var) + @hooks.delete(var) + end + + def nil_return + nil + end + + def set_filter(var, block) + @hooks[var] = [method(:nil_return), block, true] + end + + def raise_readonly(key, data) + raise NameError, "#{key} is a read-only global variable" + end + + def read_only(*vars) + mo = method(:raise_readonly) + vars.each do |v| + set_filter v, mo + end + end + end +end diff --git a/gxg/core/hash.rb b/gxg/core/hash.rb new file mode 100644 index 0000000..69b966d --- /dev/null +++ b/gxg/core/hash.rb @@ -0,0 +1,1004 @@ +class Hash + include Enumerable + + Vector = Rubinius::Tuple + + class State + attr_accessor :head + attr_accessor :tail + attr_accessor :size + + def self.from(state) + new_state = new + new_state.compare_by_identity if state and state.compare_by_identity? + new_state + end + + def initialize + @size = 0 + @head = nil + @tail = nil + @compare_by_identity = false + end + + private :initialize + + def compare_by_identity? + @compare_by_identity + end + + def compare_by_identity + @compare_by_identity = true + class << self + def match?(this_key, this_hash, other_key, other_hash) + Rubinius::Type.object_equal other_key, this_key + end + end + end + + def match?(this_key, this_hash, other_key, other_hash) + other_hash == this_hash and + (Rubinius::Type::object_equal(other_key, this_key) or + other_key.eql?(this_key)) + end + end + + class Item + attr_accessor :previous + attr_accessor :next + attr_accessor :key + attr_accessor :key_hash + attr_accessor :value + + def initialize(state, key, key_hash, value) + if key.kind_of?(String) and !key.frozen? and !state.compare_by_identity? + key = key.dup + key.freeze + end + + @key = key + @key_hash = key_hash + @value = value + + if tail = state.tail + @previous = tail + state.tail = tail.next = self + else + state.head = state.tail = self + end + + state.size += 1 + end + + private :initialize + + def empty? + not @key_hash + end + + def lookup(state, key, key_hash) + return self if state.match? @key, @key_hash, key, key_hash + end + + def insert(state, key, key_hash, value) + return unless state.match? @key, @key_hash, key, key_hash + @value = value + self + end + + def delete(state, key, key_hash) + if state.match? @key, @key_hash, key, key_hash + + if @previous + @previous.next = @next + else + state.head = @next + end + + if @next + @next.previous = @previous + else + state.tail = @previous + end + + state.size -= 1 + @key_hash = nil + + self + end + end + + def inspect + "\#<#{self.class}:0x#{object_id.to_s(16)} @key=#{@key} @key_hash=#{@key_hash} @value=#{@value} @previous=\#<#{@previous.class}:0x#{@previous.object_id.to_s(16)}> @next=\#<#{@next.class}:0x#{@next.object_id.to_s(16)}>>" + end + end + + class List + attr_accessor :key_hash + attr_accessor :entries + + def initialize(key_hash) + @key_hash = key_hash + @entries = Vector.new 2 + end + + private :initialize + + def empty? + not @entries + end + + def lookup(state, key, key_hash) + @entries.each { |e| return e if e.lookup(state, key, key_hash) } + nil + end + + def insert(state, key, key_hash, value) + @entries.each { |e| return e if e.insert(state, key, key_hash, value) } + + item = Item.new state, key, key_hash, value + @entries = @entries.insert_at_index @entries.size, item + + item + end + + def add(state, item, key, key_hash, value) + @entries[0] = item + @entries[1] = Item.new state, key, key_hash, value + end + + def delete(state, key, key_hash) + @entries.each_with_index do |e, i| + if e.delete state, key, key_hash + @entries = @entries.delete_at_index i + return e + end + end + nil + end + end + + # An Array Mapped Trie + class Trie + MAX_LEVELS = 10 + + attr_accessor :bmp + attr_accessor :entries + attr_accessor :level + + def self.from_key(state, key, key_hash, value) + trie = new 0 + trie.add_key state, key, key_hash, value + trie + end + + def self.from_item(state, level, item, key, key_hash, value) + if level < MAX_LEVELS + trie = new level + 1 + trie.add_item state, item, key, key_hash, value + trie + else + list = List.new key_hash + list.add state, item, key, key_hash, value + list + end + end + + def self.from_none + trie = new 0 + trie.entries = Vector[] + trie + end + + def initialize(level) + @level = level + @bmp = 0 + @entries = nil + end + + private :initialize + + def empty? + not @entries + end + + def lookup(state, key, key_hash) + return unless index = item_index(key_hash) + @entries[index].lookup state, key, key_hash + end + + def insert(state, key, key_hash, value) + if index = item_index(key_hash) + item = @entries[index] + if item.insert state, key, key_hash, value + return item + else + obj = Trie.from_item state, @level, item, key, key_hash, value + @entries[index] = obj + return obj + end + else + @bmp = set_bitmap key_hash + index = item_index key_hash + item = Item.new state, key, key_hash, value + @entries = @entries.insert_at_index index, item + return item + end + end + + def add_key(state, key, key_hash, value) + @bmp = set_bitmap key_hash + item = Item.new state, key, key_hash, value + @entries = Vector[item] + end + + def add_item(state, item, key, key_hash, value) + item_hash = item.key_hash + bmp = @bmp = set_bitmap(item_hash) + @bmp = set_bitmap key_hash + + if bmp == @bmp + if item_hash == key_hash + list = List.new key_hash + list.add state, item, key, key_hash, value + @entries = Vector[list] + else + obj = Trie.from_item state, @level, item, key, key_hash, value + @entries = Vector[obj] + end + else + @entries = Vector.new 2 + @entries[item_index(item_hash)] = item + @entries[item_index(key_hash)] = Item.new state, key, key_hash, value + end + end + + def delete(state, key, key_hash) + return unless index = item_index(key_hash) + + item = @entries[index] + if de = item.delete(state, key, key_hash) + if item.empty? + @bmp = unset_bitmap key_hash + + @entries = @entries.delete_at_index index + end + + return de + end + end + + def inspect + s = "\#<#{self.class}:0x#{object_id.to_s(16)} @bmp=#{@bmp} @level=#{@level} @entries=" + if @entries.size > 0 + s << "[ " + s << @entries.map { |e| "\#<#{e.class}:0x#{e.object_id.to_s(16)}>" }.join(", ") + s << " ]" + end + s << ">" + end + + def item_index(key_hash) + Rubinius.invoke_primitive :vm_hash_trie_item_index, key_hash, @level, @bmp + end + + def set_bitmap(key_hash) + Rubinius.invoke_primitive :vm_hash_trie_set_bitmap, key_hash, @level, @bmp + end + + def unset_bitmap(key_hash) + Rubinius.invoke_primitive :vm_hash_trie_unset_bitmap, key_hash, @level, @bmp + end + end + + # An external iterator that returns entries in insertion order. While + # somewhat following the API of Enumerator, it is named Iterator because it + # does not provide #each and should not conflict with + # +Enumerator+ in MRI 1.8.7+. Returned by Hash#to_iter. + + class Iterator + def initialize(state) + @state = state + end + + private :initialize + + # Returns the next object or +nil+. + def next(item) + if item + return item if item = item.next + else + return @state.head + end + end + end + + # Hash methods + + def self.[](*args) + total = args.size + if total == 1 + obj = args.first + if hash = Rubinius::Type.check_convert_type(obj, Hash, :to_hash) + new_hash = allocate.replace(hash) + new_hash.default = nil + return new_hash + elsif array = Rubinius::Type.check_convert_type(obj, Array, :to_ary) + hash = allocate + array.each do |a| + a = Rubinius::Type.check_convert_type a, Array, :to_ary + next unless a + + size = a.size + unless size >= 1 and size <= 2 + raise ArgumentError, "invalid number of elements (#{size} for 1..2)" + end + hash[a[0]] = a[1] + end + + return hash + end + end + + return allocate if total == 0 + + if total & 1 == 1 + raise ArgumentError, "Expected an even number, got #{total}" + end + + hash = allocate + i = 0 + + while i < total + hash[args[i]] = args[i+1] + i += 2 + end + + hash + end + + def self.try_convert(obj) + Rubinius::Type.try_convert obj, Hash, :to_hash + end + + def self.new_from_literal(size) + allocate + end + + def initialize(default=undefined, &block) + Rubinius.check_frozen + + if !undefined.equal?(default) and block + raise ArgumentError, "Specify a default value or a block, not both" + end + + if block + @default = nil + @default_proc = block + elsif !undefined.equal?(default) + @default = default + @default_proc = nil + end + + self + end + private :initialize + + def hash + val = size + Thread.detect_outermost_recursion self do + each_item do |item| + Rubinius.privately { val ^= item.key.hash } + Rubinius.privately { val ^= item.value.hash } + end + end + + return val + end + + def initialize_copy(other) + replace other + end + private :initialize_copy + + def [](key) + if @trie + Rubinius.privately { key_hash = key.hash } + if item = @trie.lookup(@state, key, key_hash) + return item.value + end + end + + default key + end + + def []=(key, value) + Rubinius.check_frozen + + Rubinius.privately { key_hash = key.hash } + + if @trie + @trie.insert @state, key, key_hash, value + else + @state = State.new unless @state + @trie = Trie.from_key @state, key, key_hash, value + end + + value + end + + alias_method :store, :[]= + + def <(other) + other = Rubinius::Type.coerce_to(other, Hash, :to_hash) + other > self + end + + def <=(other) + other = Rubinius::Type.coerce_to(other, Hash, :to_hash) + other >= self + end + + def >(other) + other = Rubinius::Type.coerce_to(other, Hash, :to_hash) + + return false if size <= other.size + + self >= other + end + + def >=(other) + other = Rubinius::Type.coerce_to(other, Hash, :to_hash) + + return false if size < other.size + + other.each do |other_key, other_val| + val = fetch(other_key, undefined) + return false if undefined.equal?(val) || val != other_val + end + + true + end + + def assoc(key) + each_item { |e| return e.key, e.value if key == e.key } + nil + end + + def clear + @state = State.from @state + @trie = nil + self + end + + def compare_by_identity + Rubinius.check_frozen + + @state = State.new unless @state + @state.compare_by_identity + self + end + + def compare_by_identity? + return false unless @state + @state.compare_by_identity? + end + + def default(key=undefined) + if @default_proc and !undefined.equal?(key) + @default_proc.call(self, key) + else + @default + end + end + + def default=(value) + @default_proc = nil + @default = value + end + + def default_proc + @default_proc + end + + def default_proc=(prc) + Rubinius.check_frozen + unless prc.nil? + prc = Rubinius::Type.coerce_to prc, Proc, :to_proc + + if prc.lambda? and prc.arity != 2 + raise TypeError, "default proc must have arity 2" + end + end + + @default = nil + @default_proc = prc + end + + def delete(key) + Rubinius.check_frozen + + if @trie + Rubinius.privately { key_hash = key.hash } + if item = @trie.delete(@state, key, key_hash) + @trie = nil if @trie.empty? + return item.value + end + end + + return yield(key) if block_given? + + nil + end + + def delete_if + return to_enum(:delete_if) { size } unless block_given? + + Rubinius.check_frozen + + each_item { |e| delete e.key if yield(e.key, e.value) } + + self + end + + def dig(key, *remaining_keys) + item = self[key] + return item if remaining_keys.empty? || item.nil? + + raise TypeError, "#{item.class} does not have #dig method" unless item.respond_to?(:dig) + + item.dig(*remaining_keys) + end + + def each + return to_enum(:each) { size } unless block_given? + + if @state + item = @state.head + while item + yield [item.key, item.value] + item = item.next + end + end + + self + end + + alias_method :each_pair, :each + + def each_item + return unless @state + + item = @state.head + while item + yield item + item = item.next + end + end + + def each_key + return to_enum(:each_key) { size } unless block_given? + + each_item { |e| yield e.key } + + self + end + + def each_value + return to_enum(:each_value) { size } unless block_given? + + each_item { |e| yield e.value } + + self + end + + def empty? + size == 0 + end + + def eql?(other) + return true if self.equal? other + + unless other.kind_of? Hash + return false unless other.respond_to? :to_hash + return other.eql?(self) + end + + return false unless other.size == size + + Thread.detect_recursion self, other do + other.each_item do |e| + + Rubinius.privately { key_hash = e.key.hash } + return false unless item = @trie.lookup(@state, e.key, key_hash) + + # Order of the comparison matters! We must compare our value with + # the other Hash's value and not the other way around. + return false unless item.value.eql?(e.value) + end + end + + true + end + + def ==(other) + return true if self.equal? other + + unless other.kind_of? Hash + return false unless other.respond_to? :to_hash + return other == self + end + + return false unless other.size == size + + Thread.detect_recursion self, other do + other.each_item do |e| + + Rubinius.privately { key_hash = e.key.hash } + item = @trie.lookup(@state, e.key, key_hash) + + return false unless item + + # Order of the comparison matters! We must compare our value with + # the other Hash's value and not the other way around. + unless Rubinius::Type::object_equal(item.value, e.value) or + item.value == e.value + return false + end + end + end + + true + end + + def fetch(key, default=undefined) + if @trie + Rubinius.privately { key_hash = key.hash } + if item = @trie.lookup(@state, key, key_hash) + return item.value + end + end + + return yield(key) if block_given? + return default unless undefined.equal?(default) + raise KeyError, "key #{key} not found" + end + + def fetch_values(*keys, &block) + keys.map { |key| fetch(key, &block) } + end + + # Searches for an item matching +key+. Returns the item + # if found. Otherwise returns +nil+. Called from the C-API. + def find_item(key) + if @trie + Rubinius.privately { key_hash = key.hash } + if item = @trie.lookup(@state, key, key_hash) + return item + end + end + + nil + end + + def flatten(level=1) + to_a.flatten(level) + end + + def initialize_copy(other) + replace other + end + private :initialize_copy + + def inspect + out = [] + return '{...}' if Thread.detect_recursion self do + each_item do |e| + str = Rubinius::Type.coerce_inspect(e.key) + str << '=>' + str << Rubinius::Type.coerce_inspect(e.value) + out << str + end + end + + ret = "{#{out.join ', '}}" + Rubinius::Type.infect(ret, self) unless empty? + ret + end + + alias_method :to_s, :inspect + + def invert + h = Hash.allocate + each_item { |e| h[e.value] = e.key } + h + end + + def keep_if + return to_enum(:keep_if) { size } unless block_given? + + Rubinius.check_frozen + + each_item { |e| delete e.key unless yield(e.key, e.value) } + + self + end + + def key(value) + each_item { |e| return e.key if e.value == value } + + nil + end + + alias_method :index, :key + + def key?(key) + if @trie + Rubinius.privately { key_hash = key.hash } + if @trie.lookup(@state, key, key_hash) + return true + end + end + + false + end + + alias_method :has_key?, :key? + alias_method :include?, :key? + alias_method :member?, :key? + + def keys + return [] unless @state + + ary = Array.new @state.size + + i = 0 + item = @state.head + + while item + ary[i] = item.key + i += 1 + item = item.next + end + + ary + end + + def merge(other, &block) + dup.merge!(other, &block) + end + + def merge!(other) + Rubinius.check_frozen + + other = Rubinius::Type.coerce_to other, Hash, :to_hash + + return self if other.empty? + + unless @trie + @state = State.new unless @state + @trie = Trie.from_none + end + + if block_given? + other.each_item do |e| + key = e.key + Rubinius.privately { key_hash = key.hash } + + if item = @trie.lookup(@state, key, key_hash) + item.value = yield(key, item.value, e.value) + else + @trie.insert @state, key, key_hash, e.value + end + end + else + other.each_item do |e| + key = e.key + Rubinius.privately { key_hash = key.hash } + @trie.insert @state, key, key_hash, e.value + end + end + + self + end + + alias_method :update, :merge! + + def rassoc(value) + each_item { |e| return e.key, e.value if value == e.value } + nil + end + + def rehash + Rubinius.check_frozen + + return if empty? + + item = @state.head + @state = State.from @state + @trie = Trie.from_none + + while item + Rubinius.privately { key_hash = item.key.hash } + @trie.insert @state, item.key, key_hash, item.value + item = item.next + end + + self + end + + def reject(&block) + return to_enum(:reject) { size } unless block_given? + + hsh = dup.delete_if(&block) + hsh.taint if tainted? + hsh + end + + def reject!(&block) + return to_enum(:reject!) { size } unless block_given? + + Rubinius.check_frozen + + unless empty? + size = @state.size + delete_if(&block) + return self if size != @state.size + end + + nil + end + + def replace(other) + Rubinius.check_frozen + + unless other.kind_of? Hash + other = Rubinius::Type.coerce_to other, Hash, :to_hash + end + return self if self.equal? other + + @state = State.from @state + @state.compare_by_identity if other.compare_by_identity? + @trie = Trie.from_none + + other.each_item do |e| + Rubinius.privately { key_hash = e.key.hash } + @trie.insert @state, e.key, key_hash, e.value + end + + @default = other.default + @default_proc = other.default_proc + + self + end + + def select + return to_enum(:select) { size } unless block_given? + + hsh = Hash.allocate + + each_item do |e| + key = e.key + value = e.value + hsh[key] = value if yield(key, value) + end + + hsh + end + + def select! + return to_enum(:select!) { size } unless block_given? + + Rubinius.check_frozen + + return if empty? + + size = @state.size + each_item { |e| delete e.key unless yield(e.key, e.value) } + return if size == @state.size + + self + end + + def shift + Rubinius.check_frozen + + return default(nil) if empty? + + item = @state.head + Rubinius.privately { key_hash = item.key.hash } + @trie.delete @state, item.key, key_hash + @trie = nil if @trie.empty? + + return item.key, item.value + end + + def size + @state ? @state.size : 0 + end + + alias_method :length, :size + + def to_a + if @state + ary = Array.new size + + i = 0 + item = @state.head + + while item + ary[i] = [item.key, item.value] + i += 1 + item = item.next + end + else + ary = [] + end + + Rubinius::Type.infect ary, self + ary + end + + # Returns an external iterator for the bins. See +Iterator+ + def to_iter + Iterator.new @state + end + + def to_h + if instance_of? Hash + self + else + Hash.allocate.replace(to_hash) + end + end + + def to_hash + self + end + + def to_proc + method(:[]).to_proc + end + + def value?(value) + each_item { |e| return true if value == e.value } + false + end + + alias_method :has_value?, :value? + + def values + return [] unless @state + + ary = Array.new size + + i = 0 + item = @state.head + + while item + ary[i] = item.value + i += 1 + item = item.next + end + + ary + end + + def values_at(*args) + if empty? + args.map { |key| default key } + else + args.map do |key| + Rubinius.privately { key_hash = key.hash } + if item = @trie.lookup(@state, key, key_hash) + item.value + else + default key + end + end + end + end + + # Used internally in Rubinius to get around subclasses redefining #[]= + alias_method :__store__, :[]= +end diff --git a/gxg/core/hook.rb b/gxg/core/hook.rb new file mode 100644 index 0000000..11e98b4 --- /dev/null +++ b/gxg/core/hook.rb @@ -0,0 +1,23 @@ +module Rubinius + class Hook + def initialize + @hooks = [] + end + + private :initialize + + def add(obj) + @hooks << obj + end + + def remove(obj) + @hooks.delete(obj) + end + + def trigger!(*args) + @hooks.each do |obj| + obj.call(*args) + end + end + end +end diff --git a/gxg/core/identity_map.rb b/gxg/core/identity_map.rb new file mode 100644 index 0000000..d42d949 --- /dev/null +++ b/gxg/core/identity_map.rb @@ -0,0 +1,307 @@ +module Rubinius + + # IdentityMap is customized for uniquely storing elements from an Array to + # implement the following Array methods: #&, #|, #-, #uniq, and #uniq! + # + # The algorithm used is double hashing with an overflow array. For each + # array element, the element, its hash value, and its ordinal in the + # sequence of elements added to the map are stored. + # + # Methods are provided to test an element for inclusion in the map and to + # delete an entry from the map. The contents of a map can be returned as an + # array in the order the elements were added to the map. + + class IdentityMap + attr_reader :size + + Row = Table = Rubinius::Tuple + MIN_CAPACITY = 64 + MIN_ROW = 10 + ROW_GROWTH = 9 + + # Converts one or more Enumerable instances to a single IdentityMap + def self.from(*arrays, &block) + im = allocate + Rubinius.privately { im.load(arrays, &block) } + im + end + + def initialize + capacity = MIN_CAPACITY + @table = Table.new capacity + @mask = capacity - 4 + @max = capacity + @size = 0 + end + + private :initialize + + # Adds +item+ to the IdentityMap if it does not already exist. May cause + # a row to be added or enlarged. Returns +self+. + def insert(item, &block) + redistribute if @size > @max + + if block_given? + item_hash = yield(item).hash + else + item_hash = item.hash + end + + index = item_hash & @mask + table = @table + + if num_entries = table[index] + index += 1 + + if num_entries == 1 + return self if match?(table, index, item, item_hash, &block) + + table[index-1] = 2 + table[index] = promote_row table, index, item, item_hash, @size + else + i = 1 + row = table[index] + total = row[0] + + while i < total + return self if match?(row, i, item, item_hash, &block) + i += 3 + end + + if total == row.size + table[index] = enlarge_row row, item, item_hash, @size + else + i = row[0] + set_item row, i, item, item_hash, @size + row[0] = i + 3 + end + end + else + table[index] = 1 + set_item table, index+1, item, item_hash, @size + end + @size += 1 + + self + end + + # Returns +true+ if +item+ is in the IdentityMap, +false+ otherwise. + def include?(item) + item_hash = item.hash + + index = item_hash & @mask + table = @table + if num_entries = table[index] + index += 1 + + if num_entries == 1 + return true if match? table, index, item, item_hash + else + row = table[index] + i = 1 + total = row[0] + while i < total + return true if match? row, i, item, item_hash + i += 3 + end + end + end + + false + end + + # If +item+ is in the IdentityMap, removes it and returns +true+. + # Otherwise, returns +false+. + def delete(item) + item_hash = item.hash + + index = item_hash & @mask + table = @table + + if num_entries = table[index] + index += 1 + if num_entries == 1 + if match? table, index, item, item_hash + table[index] = nil + @size -= 1 + return true + end + else + row = table[index] + i = 1 + total = row[0] + while i < total + if match? row, i, item, item_hash + row[i] = nil + @size -= 1 + return true + end + i += 3 + end + end + end + + false + end + + # Returns an Array containing all items in the IdentityMap in the order + # in which they were added to the IdentityMap. + def to_array + array = Array.new @size + + i = 0 + table = @table + total = table.size + + while i < total + if num_entries = table[i] + if num_entries == 1 + array[table[i+3]] = table[i+2] if table[i+1] + else + row = table[i+1] + k = row[0] + j = 1 + while j < k + array[row[j+2]] = row[j+1] if row[j] + j += 3 + end + end + end + + i += 4 + end + + array + end + + # Private implementation methods + + def resize(total) + capacity = MIN_CAPACITY + while capacity < total + capacity <<= 2 + end + + @table = Table.new capacity + @mask = capacity - 4 + @max = capacity + end + private :resize + + def redistribute + table = @table + resize @size + + i = 0 + total = table.size + + while i < total + if num_entries = table[i] + if num_entries == 1 + if item_hash = table[i+1] + add_item table[i+2], item_hash, table[i+3] + end + else + row = table[i+1] + k = row[0] + j = 1 + while j < k + if item_hash = row[j] + add_item row[j+1], item_hash, row[j+2] + end + j += 3 + end + end + end + + i += 4 + end + end + private :redistribute + + def add_item(item, item_hash, ordinal) + index = item_hash & @mask + table = @table + + if num_entries = table[index] + index += 1 + + if num_entries == 1 + table[index-1] = 2 + table[index] = promote_row table, index, item, item_hash, ordinal + else + row = table[index] + i = row[0] + + if i == row.size + table[index] = enlarge_row row, item, item_hash, ordinal + else + set_item row, i, item, item_hash, ordinal + row[0] = i + 3 + end + end + else + table[index] = 1 + set_item table, index+1, item, item_hash, ordinal + end + end + private :add_item + + # Given an Array of Enumerable instances, computes a bounding set + # to contain them and then adds each item to the IdentityMap. + def load(arrays, &block) + resize(arrays.inject(0) { |sum, array| sum + array.size }) + @size = 0 + + arrays.each do |array| + array.each { |item| insert(item, &block) } + end + end + private :load + + def match?(table, index, item, item_hash) + return false unless table[index] == item_hash + other = table[index+1] + if block_given? + item = yield item + other = yield other + end + Rubinius::Type.object_equal(item, other) or item.eql?(other) + end + private :match? + + def set_item(table, index, item, item_hash, ordinal) + table[index] = item_hash + table[index+1] = item + table[index+2] = ordinal + end + private :set_item + + def promote_row(row, index, item, item_hash, ordinal) + new_row = Row.new MIN_ROW + + new_row[0] = 7 + new_row[1] = row[index] + new_row[2] = row[index+1] + new_row[3] = row[index+2] + new_row[4] = item_hash + new_row[5] = item + new_row[6] = ordinal + + new_row + end + private :promote_row + + def enlarge_row(row, item, item_hash, ordinal) + new_row = Row.new row.size + ROW_GROWTH + new_row.copy_from row, 1, row.size-1, 1 + + index = row[0] + new_row[0] = index + 3 + set_item new_row, index, item, item_hash, ordinal + + new_row + end + private :enlarge_row + end +end diff --git a/gxg/core/immediate.rb b/gxg/core/immediate.rb new file mode 100644 index 0000000..76b561f --- /dev/null +++ b/gxg/core/immediate.rb @@ -0,0 +1,31 @@ +## +# Mixin used to identify classes which have no memory storage. + +module ImmediateValue + def singleton_methods(all=true) + [] + end + + def public_singleton_methods + [] + end + private :public_singleton_methods + + def private_singleton_methods + [] + end + private :private_singleton_methods + + def protected_singleton_methods + [] + end + private :protected_singleton_methods + + def dup + raise TypeError, "can't dup #{self.class.name}" + end + + def clone + raise TypeError, "can't clone #{self.class.name}" + end +end diff --git a/gxg/core/integer.rb b/gxg/core/integer.rb new file mode 100644 index 0000000..9517f41 --- /dev/null +++ b/gxg/core/integer.rb @@ -0,0 +1,1580 @@ +class Integer < Numeric + def self.===(obj) + Rubinius.asm do + int = new_label + done = new_label + + r0 = new_register + + r_load_local r0, 0 + + b_if_int r0, r0, int + goto done + + int.set! + r_load_true r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + super + end + + # unary operators + + def ! + false + end + + def ~ + Rubinius.asm do + eint = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_self r0 + + n_promote r1, r0, r0 + + r_load_2 r2 + n_ieq r3, r1, r2 + b_if r3, eint + + n_iinc r2, r2 + n_ine r3, r1, r2 + b_if r3, done + + r_load_int r0, r0 + n_inot r0, r0 + r_store_int r0, r0 + goto val + + eint.set! + n_enot r0, r0 + + val.set! + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + super + end + + def -@ + Rubinius.asm do + eint = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_self r0 + + n_promote r1, r0, r0 + + r_load_2 r2 + n_ieq r3, r1, r2 + b_if r3, eint + + n_iinc r2, r2 + n_ine r3, r1, r2 + b_if r3, done + + n_ineg_o r0, r0 + goto val + + eint.set! + n_eneg r0, r0 + + val.set! + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + super + end + + def bit_length + Rubinius.asm do + eint = new_label + bits = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_self r0 + + n_promote r1, r0, r0 + + r_load_2 r2 + n_ieq r3, r1, r2 + b_if r3, eint + + n_iinc r2, r2 + n_ine r3, r1, r2 + b_if r3, done + + r_load_int r0, r0 + n_ibits r0, r0 + r_store_int r0, r0 + goto val + + eint.set! + r_load_0 r1 + r_store_int r1, r1 + n_promote r2, r0, r1 + + r_load_2 r3 + n_ine r3, r2, r3 + b_if r3, done + + n_egt r2, r0, r1 + b_if r2, bits + n_enot r0, r0 + + bits.set! + n_ebits r0, r0 + + val.set! + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + super + end + + # binary math operators + + def +(other) + Rubinius.asm do + int = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, int + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + n_eadd r0, r0, r1 + goto val + + flt.set! + n_dadd r0, r0, r1 + r_store_float r0, r0 + goto val + + int.set! + n_iadd_o r0, r0, r1 + + val.set! + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + redo_coerced :+, other + end + + def -(other) + Rubinius.asm do + int = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, int + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + n_esub r0, r0, r1 + goto val + + flt.set! + n_dsub r0, r0, r1 + r_store_float r0, r0 + goto val + + int.set! + n_isub_o r0, r0, r1 + + val.set! + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + redo_coerced :-, other + end + + def *(other) + Rubinius.asm do + int = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, int + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + n_emul r0, r0, r1 + goto val + + flt.set! + n_dmul r0, r0, r1 + r_store_float r0, r0 + goto val + + int.set! + n_imul_o r0, r0, r1 + + val.set! + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + redo_coerced :*, other + end + + def /(other) + Rubinius.asm do + int = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, int + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + n_ediv r0, r0, r1 + goto val + + flt.set! + n_ddiv r0, r0, r1 + r_store_float r0, r0 + goto val + + int.set! + n_idiv_o r0, r0, r1 + + val.set! + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + redo_coerced :/, other + end + + alias_method :divide, :/ + + def div(o) + if o.is_a?(Float) && o == 0.0 + raise ZeroDivisionError, "division by zero" + end + + (self / o).floor + end + + def fdiv(other) + to_f / other + end + + def %(other) + Rubinius.asm do + int = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, int + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + n_emod r0, r0, r1 + goto val + + flt.set! + n_dmod r0, r0, r1 + r_store_float r0, r0 + goto val + + int.set! + n_imod_o r0, r0, r1 + + val.set! + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + redo_coerced :%, other + end + + alias_method :modulo, :% + + def divmod(other) + Rubinius.asm do + int = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, int + + n_promote r2, r0, r1 + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + n_edivmod r2, r0, r1 + goto val + + flt.set! + n_ddivmod r2, r0, r1 + goto val + + int.set! + n_idivmod r2, r0, r1 + + val.set! + r_store_stack r0 + r_store_stack r1 + make_array 2 + ret + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + redo_coerced :divmod, other + end + + def **(o) + Rubinius.asm do + int = new_label + eint = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, int + + n_promote r2, r0, r1 + + r_load_2 r3 + n_ieq r3, r3, r2 + b_if r3, eint + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + goto done + + int.set! + r_load_0 r2 + n_ilt r3, r1, r2 + b_if r3, done + + n_ipow_o r0, r0, r1 + goto val + + eint.set! + r_load_0 r2 + r_store_int r2, r2 + n_promote r3, r0, r2 + + n_elt r3, r1, r2 + b_if r3, done + + n_epow r0, r0, r1 + + val.set! + r_ret r0 + + flt.set! + # TODO: add n_dpow + r_store_float r0, r0 + r_store_float r1, r1 + r_store_stack r0 + r_store_stack r1 + send :**, 1 + ret + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + if o.is_a?(Float) && self < 0 && o != o.round + return Complex.new(self, 0) ** o + elsif o.is_a?(Integer) && o < 0 + return Rational.new(self, 1) ** o + end + + redo_coerced :**, o + end + + # comparison operators + + def !=(other) + Rubinius.asm do + cmp = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, cmp + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + n_ene r0, r0, r1 + goto val + + flt.set! + n_dne r0, r0, r1 + goto val + + cmp.set! + n_ine r0, r0, r1 + + val.set! + r_load_bool r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + other != self ? true : false + end + + def eql?(other) + other.is_a?(Integer) && self == other + end + + def ==(other) + Rubinius.asm do + cmp = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, cmp + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + n_eeq r0, r0, r1 + goto val + + flt.set! + n_deq r0, r0, r1 + goto val + + cmp.set! + n_ieq r0, r0, r1 + + val.set! + r_load_bool r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + other == self ? true : false + end + + alias_method :===, :== + + def <(other) + Rubinius.asm do + cmp = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, cmp + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + n_elt r0, r0, r1 + goto val + + flt.set! + n_dlt r0, r0, r1 + goto val + + cmp.set! + n_ilt r0, r0, r1 + + val.set! + r_load_bool r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + b, a = math_coerce other, :compare_error + a < b ? true : false + end + + def <=(other) + Rubinius.asm do + cmp = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, cmp + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + n_ele r0, r0, r1 + goto val + + flt.set! + n_dle r0, r0, r1 + goto val + + cmp.set! + n_ile r0, r0, r1 + + val.set! + r_load_bool r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + b, a = math_coerce other, :compare_error + a <= b ? true : false + end + + def >(other) + Rubinius.asm do + cmp = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, cmp + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + n_egt r0, r0, r1 + goto val + + flt.set! + n_dgt r0, r0, r1 + goto val + + cmp.set! + n_igt r0, r0, r1 + + val.set! + r_load_bool r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + b, a = math_coerce other, :compare_error + a > b ? true : false + end + + def >=(other) + Rubinius.asm do + cmp = new_label + flt = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, cmp + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + r_load_1 r3 + n_ieq r3, r3, r2 + b_if r3, flt + + n_ege r0, r0, r1 + goto val + + flt.set! + n_dge r0, r0, r1 + goto val + + cmp.set! + n_ige r0, r0, r1 + + val.set! + r_load_bool r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + b, a = math_coerce other, :compare_error + a >= b ? true : false + end + + def <=>(other) + Rubinius.asm do + int = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, int + + n_promote r2, r0, r1 + + r_load_2 r3 + n_ine r3, r2, r3 + b_if r3, done + + n_ecmp r0, r0, r1 + goto val + + int.set! + n_icmp r0, r0, r1 + + val.set! + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + if other.kind_of? Float + return nil if other.nan? + return -1 if other.infinite? == 1 + return 1 if other.infinite? == -1 + end + + begin + b, a = math_coerce other, :compare_error + return a <=> b + rescue ArgumentError, TypeError + return nil + end + end + + # bitwise binary operators + + def &(other) + Rubinius.asm do + int = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, int + + n_promote r2, r0, r1 + + r_load_1 r3 + n_iadd r3, r3, r3 + n_ine r2, r2, r3 + b_if r2, done + + n_eand r0, r0, r1 + r_ret r0 + + int.set! + r_load_int r0, r0 + r_load_int r1, r1 + + n_iand r0, r0, r1 + + r_store_int r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + other = Rubinius::Type.coerce_to_bitwise_operand other + + other & self + end + + def |(other) + Rubinius.asm do + int = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, int + + n_promote r2, r0, r1 + + r_load_1 r3 + n_iadd r3, r3, r3 + n_ine r2, r2, r3 + b_if r2, done + + n_eor r0, r0, r1 + r_ret r0 + + int.set! + r_load_int r0, r0 + r_load_int r1, r1 + + n_ior r0, r0, r1 + + r_store_int r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + other = Rubinius::Type.coerce_to_bitwise_operand other + + other | self + end + + def ^(other) + Rubinius.asm do + int = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, int + + n_promote r2, r0, r1 + + r_load_1 r3 + n_iadd r3, r3, r3 + n_ine r2, r2, r3 + b_if r2, done + + n_exor r0, r0, r1 + r_ret r0 + + int.set! + r_load_int r0, r0 + r_load_int r1, r1 + + n_ixor r0, r0, r1 + + r_store_int r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + other = Rubinius::Type.coerce_to_bitwise_operand other + + other ^ self + end + + def <<(other) + Rubinius.asm do + shl = new_label + neg = new_label + eint = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, neg + goto eint + + neg.set! + r_load_int r2, r1 + r_load_0 r3 + + n_ige r3, r2, r3 + b_if r3, shl + + n_ineg r1, r2 + r_store_int r1, r1 + n_ishr_o r0, r0, r1 + r_ret r0 + + shl.set! + n_ishl_o r0, r0, r1 + r_ret r0 + + eint.set! + n_promote r2, r0, r0 + r_load_2 r3 + n_ine r2, r2, r3 + b_if r2, done + + n_promote r2, r1, r1 + n_iinc r3, r3 + n_ine r2, r2, r3 + b_if r2, done + + r_load_int r1, r1 + n_eshl r0, r0, r1 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + other = Rubinius::Type.coerce_to other, Integer, :to_int + + self >> -other if other < 0 + + unless other.kind_of? Fixnum + raise RangeError, "argument is out of range for a Fixnum" + end + + self << other + end + + def >>(other) + Rubinius.asm do + shr = new_label + neg = new_label + eint = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, neg + goto eint + + neg.set! + r_load_int r2, r1 + r_load_0 r3 + + n_ige r3, r2, r3 + b_if r3, shr + + n_ineg r1, r2 + r_store_int r1, r1 + n_ishl_o r0, r0, r1 + r_ret r0 + + shr.set! + n_ishr_o r0, r0, r1 + r_ret r0 + + eint.set! + n_promote r2, r0, r0 + r_load_2 r3 + n_ine r2, r2, r3 + b_if r2, done + + n_promote r2, r1, r1 + n_iinc r3, r3 + n_ine r2, r2, r3 + b_if r2, done + + r_load_int r1, r1 + n_eshr r0, r0, r1 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + other = Rubinius::Type.coerce_to other, Integer, :to_int + + self << -other if other < 0 + + unless other.kind_of? Fixnum + raise RangeError, "argument is out of range for a Fixnum" + end + + self >> other + end + + def to_i + self + end + + alias_method :to_int, :to_i + alias_method :truncate, :to_i + alias_method :ceil, :to_i + alias_method :floor, :to_i + + def chr(enc=undefined) + if self < 0 || (self & 0xffff_ffff) != self + raise RangeError, "#{self} is outside of the valid character range" + end + + if undefined.equal? enc + if 0xff < self + enc = Encoding.default_internal + if enc.nil? + raise RangeError, "#{self} is outside of the valid character range" + end + elsif self < 0x80 + enc = Encoding::US_ASCII + else + enc = Encoding::ASCII_8BIT + end + else + enc = Rubinius::Type.coerce_to_encoding enc + end + + String.from_codepoint self, enc + end + + def round(ndigits=undefined) + return self if undefined.equal? ndigits + + if ndigits.kind_of? Numeric + if ndigits > Fixnum::MAX or ndigits <= Fixnum::MIN + raise RangeError, "precision is outside of the range of Fixnum" + end + end + + ndigits = Rubinius::Type.coerce_to ndigits, Integer, :to_int + + if ndigits > 0 + to_f + elsif ndigits == 0 + self + else + ndigits = -ndigits + + # We want to return 0 if 10 ** ndigits / 2 > self.abs, or, taking + # log_256 of both sides, if log_256(10 ** ndigits / 2) > self.size. + # We have log_256(10) > 0.415241 and log_256(2) = 0.125, so: + return 0 if 0.415241 * ndigits - 0.125 > size + + f = 10 ** ndigits + + if kind_of? Fixnum and f.kind_of? Fixnum + x = self < 0 ? -self : self + x = (x + f / 2) / f * f + x = -x if self < 0 + return x + end + + return 0 if f.kind_of? Float + + h = f / 2 + r = self % f + n = self - r + + unless self < 0 ? r <= h : r < h + n += f + end + + n + end + end + + def gcd(other) + raise TypeError, "Expected Integer but got #{other.class}" unless other.kind_of?(Integer) + min = self.abs + max = other.abs + while min > 0 + tmp = min + min = max % min + max = tmp + end + max + end + + def rationalize(eps = nil) + Rational(self, 1) + end + + def numerator + self + end + + def denominator + 1 + end + + def to_r + Rational(self, 1) + end + + def lcm(other) + raise TypeError, "Expected Integer but got #{other.class}" unless other.kind_of?(Integer) + if self.zero? or other.zero? + 0 + else + (self.div(self.gcd(other)) * other).abs + end + end + + def gcdlcm(other) + gcd = self.gcd(other) + if self.zero? or other.zero? + [gcd, 0] + else + [gcd, (self.div(gcd) * other).abs] + end + end + + def [](index) + index = Rubinius::Type.coerce_to(index, Integer, :to_int) + return 0 if index.is_a?(Bignum) + index < 0 ? 0 : (self >> index) & 1 + end + + def next + self + 1 + end + + alias_method :succ, :next + + def integer? + true + end + + def even? + self & 1 == 0 + end + + def odd? + self & 1 == 1 + end + + def ord + self + end + + def pred + self - 1 + end + + def size + Rubinius.asm do + eint = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_self r0 + + n_promote r1, r0, r0 + + r_load_2 r2 + n_ieq r3, r1, r2 + b_if r3, eint + + n_iinc r2, r2 + n_ine r3, r1, r2 + b_if r3, done + + n_isize r0, r0 + goto val + + eint.set! + n_esize r0, r0 + + val.set! + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + super + end + + def times + return to_enum(:times) { self } unless block_given? + + i = 0 + while i < self + yield i + i += 1 + end + self + end + + def upto(val) + return to_enum(:upto, val) { self <= val ? val - self + 1 : 0 } unless block_given? + + i = self + while i <= val + yield i + i += 1 + end + self + end + + def downto(val) + return to_enum(:downto, val) { self >= val ? self - val + 1 : 0 } unless block_given? + + i = self + while i >= val + yield i + i -= 1 + end + self + end + + # conversions + + def coerce(other) + Rubinius.asm do + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_m_binops r0, r1 + + n_promote r2, r0, r1 + + r_load_0 r3 + n_ieq r3, r3, r2 + b_if r3, done + + r_load_1 r3 + n_ine r3, r3, r2 + b_if r3, val + + r_store_float r0, r0 + r_store_float r1, r1 + + val.set! + r_store_stack r1 + r_store_stack r0 + make_array 2 + ret + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + super other + end + + def imaginary + 0 + end + + def to_f + Rubinius.asm do + eint = new_label + done = new_label + + r0 = new_register + r1 = new_register + r2 = new_register + r3 = new_register + + r_load_self r0 + + n_promote r1, r0, r0 + + r_load_2 r2 + n_ieq r3, r1, r2 + b_if r3, eint + + n_iinc r2, r2 + n_ine r3, r1, r2 + b_if r3, done + + n_iflt r0, r0 + r_store_float r0, r0 + r_ret r0 + + eint.set! + n_eflt r0, r0 + r_store_float r0, r0 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + super + end + + def to_s(base=10) + Rubinius.asm do + int = new_label + eint = new_label + base = new_label + val = new_label + done = new_label + + r0 = new_register + r1 = new_register + + r_load_m_binops r0, r1 + + b_if_int r0, r1, int + + b_if_eint r0, r0, base + goto done + + base.set! + b_if_int r1, r1, eint + goto done + + eint.set! + n_estr r0, r0, r1 + goto val + + int.set! + n_istr r0, r0, r1 + + val.set! + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + super base + end + + # We do not alias this to #to_s in case someone overrides #to_s. + def inspect + Rubinius.asm do + int = new_label + eint = new_label + done = new_label + + r0 = new_register + r1 = new_register + + r_load_literal r1, 10 + + r_load_self r0 + + b_if_int r0, r0, int + b_if_eint r0, r0, eint + goto done + + int.set! + n_istr r0, r0, r1 + r_ret r0 + + eint.set! + n_estr r0, r0, r1 + r_ret r0 + + done.set! + + # TODO: teach the bytecode compiler better + push_true + end + + super + end + + # predicates + + def zero? + self == 0 + end + +end diff --git a/gxg/core/io.rb b/gxg/core/io.rb new file mode 100644 index 0000000..1662c40 --- /dev/null +++ b/gxg/core/io.rb @@ -0,0 +1,3609 @@ +class IO + include Enumerable + + def self.fnmatch(pattern, path, flags) + Rubinius.primitive :io_fnmatch + raise PrimitiveFailure, "IO#fnmatch primitive failed" + end + + def socket_recv(bytes, flags, type) + Rubinius.primitive :io_socket_read + raise PrimitiveFailure, "io_socket_read failed" + end + + module TransferIO + def send_io(io) + Rubinius.primitive :io_send_io + raise PrimitiveFailure, "IO#send_io failed" + end + + def recv_fd + Rubinius.primitive :io_recv_fd + raise PrimitiveFailure, "IO#recv_fd failed" + end + end + + module WaitReadable; end + module WaitWritable; end + + class EAGAINWaitReadable < Errno::EAGAIN + include ::IO::WaitReadable + end + + class EAGAINWaitWritable < Errno::EAGAIN + include ::IO::WaitWritable + end + + class EWOULDBLOCKWaitReadable < Errno::EAGAIN + include WaitReadable + end + + class EWOULDBLOCKWaitWritable < Errno::EAGAIN + include WaitWritable + end + + class EINPROGRESSWaitReadable < Errno::EINPROGRESS + include WaitReadable + end + + class EINPROGRESSWaitWritable < Errno::EINPROGRESS + include WaitWritable + end + + class FileDescriptor + class RIOStream + def self.close(io, allow_exception) + Rubinius.primitive :rio_close + raise PrimitiveFailure, "IO::FileDescriptor::RIOStream.close primitive failed" + end + end + + attr_reader :offset + + def self.choose_type(fd, io) + stat = File::Stat.fstat(fd) + + case stat.ftype + when "file" + BufferedFileDescriptor.new(fd, stat, io) + when "fifo", "characterSpecial" + FIFOFileDescriptor.new(fd, stat, io) + when "socket" + SocketFileDescriptor.new(fd, stat, io) + when "directory" + DirectoryFileDescriptor.new(fd, stat, io) + when "blockSpecial" + raise "cannot make block special" + when "link" + raise "cannot make link" + else + BufferedFileDescriptor.new(fd, stat, io) + end + end + + def self.open_with_mode(path, mode, perm) + fd = open_with_cloexec(path, mode, perm) + + if fd < 0 + Errno.handle("failed to open file") + end + + return fd + end + + def self.open_with_cloexec(path, mode, perm) + if O_CLOEXEC + fd = FFI::Platform::POSIX.open(path, mode | O_CLOEXEC, perm) + update_max_fd(fd) + else + fd = FFI::Platform::POSIX.open(path, mode, perm) + new_open_fd(fd) + end + + return fd + end + + def self.new_open_fd(new_fd) + if new_fd > 2 + flags = FFI::Platform::POSIX.fcntl(new_fd, F_GETFD, 0) + Errno.handle("fcntl(2) failed") if FFI.call_failed?(flags) + flags = FFI::Platform::POSIX.fcntl(new_fd, F_SETFD, flags | FD_CLOEXEC) + Errno.handle("fcntl(2) failed") if FFI.call_failed?(flags) + end + + update_max_fd(new_fd) + end + + def self.pagesize + @pagesize ||= FFI::Platform::POSIX.getpagesize + end + + def self.truncate(name, offset) + raise RangeError, "bignum too big to convert into `long'" if offset.kind_of?(Bignum) + + status = FFI::Platform::POSIX.truncate(name, offset) + Errno.handle("truncate(2) failed") if FFI.call_failed?(status) + return status + end + + def self.update_max_fd(new_fd) + @@max_descriptors.get_and_set(new_fd) + end + + def self.max_fd + @@max_descriptors.get + end + + def self.get_flags(fd) + if IO::F_GETFL + if FFI.call_failed?(flags = FFI::Platform::POSIX.fcntl(fd, IO::F_GETFL, 0)) + Errno.handle("fcntl(2) failed") + end + else + flags = 0 + end + flags + end + + def self.clear_flag(flag, fd) + flags = get_flags(fd) + if (flags & flag) == 0 + flags &= ~flag + if FFI.call_failed?(flags = FFI::Platform::POSIX.fcntl(fd, IO::F_SETFL, flags)) + Errno.handle("fcntl(2) failed") + end + end + end + + def self.set_flag(flag, fd) + flags = get_flags(fd) + if (flags & flag) == 0 + flags |= flag + if FFI.call_failed?(flags = FFI::Platform::POSIX.fcntl(fd, IO::F_SETFL, flags)) + Errno.handle("fcntl(2) failed") + end + end + end + + def self.get_empty_8bit_buffer + "".force_encoding(Encoding::ASCII_8BIT) + end + + def initialize(fd, stat, io) + @descriptor, @stat, @io = fd, stat, io + acc_mode = FileDescriptor.get_flags(@descriptor) + + if acc_mode < 0 + # Assume it's closed. + if Errno.eql?(Errno::EBADF::Errno) + @descriptor = -1 + end + + @mode = nil + else + @mode = acc_mode + end + + @sync = true + @autoclose = false + reset_positioning(@stat) + + # Don't bother to add finalization for stdio + if @descriptor >= 3 + # Sometimes a FileDescriptor class is replaced (see IO#reopen) so we need to be + # careful we don't finalize that descriptor. Probably need a way to cancel + # the finalization when we are transferring an FD from one instance to another. + ObjectSpace.define_finalizer(self) + end + end + + private :initialize + + def autoclose=(bool) + @autoclose = bool + end + + def descriptor + @descriptor + end + + def descriptor=(value) + @descriptor = value + end + + def mode + @mode + end + + def mode=(value) + @mode = value + end + + def sync + @sync + end + + def sync=(value) + @sync = value + end + + CLONG_OVERFLOW = 1 << 64 + + def lseek(offset, whence=SEEK_SET) + ensure_open + + # FIXME: check +amount+ to make sure it isn't too large + raise RangeError if offset > CLONG_OVERFLOW + + position = FFI::Platform::POSIX.lseek(descriptor, offset, whence) + + if FFI.call_failed?(position) + Errno.handle("seek failed: fd: #{descriptor}, offset: #{offset}, from: #{whence}") + end + + @offset = position + determine_eof + + return position + end + + def read(length, output_string=nil) + length ||= FileDescriptor.pagesize + + while true + ensure_open + + storage = FFI::MemoryPointer.new(length) + raise IOError, "read(2) failed to malloc a buffer for read length #{length}" if storage.null? + bytes_read = read_into_storage(length, storage) + + if bytes_read == 0 + @eof = true if length > 0 + return nil + else + break + end + end + + if output_string + output_string.replace(storage.read_string(bytes_read)) + else + output_string = storage.read_string(bytes_read).force_encoding(Encoding::ASCII_8BIT) + end + + @offset += bytes_read + determine_eof + + return output_string + end + + def read_into_storage(count, storage) + while true + bytes_read = FFI::Platform::POSIX.read(descriptor, storage, count) + + if FFI.call_failed?(bytes_read) + errno = Errno.errno + + if errno == Errno::EAGAIN::Errno || errno == Errno::EINTR::Errno + ensure_open + next + else + Errno.handle "read(2) failed" + end + else + break + end + end + + # before returning verify file hasn't been closed in another thread + ensure_open + + return bytes_read + end + private :read_into_storage + + def write(str) + buf_size = str.bytesize + left = buf_size + + buffer = FFI::MemoryPointer.new(left) + buffer.write_string(str) + error = false + + while left > 0 + bytes_written = FFI::Platform::POSIX.write(@descriptor, buffer, left) + + if FFI.call_failed?(bytes_written) + errno = Errno.errno + if errno == Errno::EINTR::Errno || errno == Errno::EAGAIN::Errno + # do a #select and wait for descriptor to become writable + if blocking? + Select.wait_for_writable(@descriptor) + next + else + break + end + end + + error = true + break + end + + break if error + + left -= bytes_written + buffer += bytes_written + @offset += bytes_written + + if @offset > @total_size + @total_size = @offset + @eof = false # only a failed read can set EOF! + end + end + + Errno.handle("write failed") if error + + return(buf_size - left) + end + + def close + ensure_open + fd = @descriptor + + if fd != -1 + # return early if this handle was promoted to a stream by a C-ext + return if RIOStream.close(@io, true) + ret_code = FFI::Platform::POSIX.close(fd) + + if FFI.call_failed?(ret_code) + Errno.handle("close failed") + elsif ret_code == 0 + # no op + else + raise IOError, "::close(): Unknown error on fd #{fd}" + end + end + + @descriptor = -1 + + return nil + end + + def determine_eof + if @offset >= @total_size + @eof = true + @total_size += (@total_size - @offset) + @total_size = @offset if @offset > @total_size + else + @eof = false + end + end + private :determine_eof + + def eof? + @eof + end + + # This is NOT the same as close(). + def shutdown(how) + ensure_open + fd = descriptor + + if how != IO::SHUT_RD && how != IO::SHUT_WR && how != IO::SHUT_RDWR + raise ArgumentError, "::shutdown(): Invalid `how` #{how} for fd #{fd}" + end + + ret_code = FFI::Platform::POSIX.shutdown(fd, how) + + if FFI.call_failed?(ret_code) + Errno.handle("shutdown(2) failed") + elsif ret_code == 0 + if how == IO::SHUT_RDWR + close + self.descriptor = -2 + end + else + Errno.handle("::shutdown(): Unknown error on fd #{fd}") + end + + return how + end + + def ensure_open(fd=nil) + fd ||= descriptor + + if fd.nil? + raise IOError, "uninitialized stream" + elsif fd == -1 + raise IOError, "closed stream" + elsif fd == -2 + raise IOError, "shutdown stream" + end + return nil + end + + def force_read_only + @mode = (@mode & ~IO::O_ACCMODE ) | O_RDONLY + end + + def force_write_only + @mode = (@mode & ~IO::O_ACCMODE) | O_WRONLY + end + + def read_only? + (@mode & O_ACCMODE) == O_RDONLY + end + + def write_only? + (@mode & O_ACCMODE) == O_WRONLY + end + + def read_write? + (@mode & O_ACCMODE) == O_RDWR + end + + def reopen(other_fd) + current_fd = @descriptor + + if FFI.call_failed?(FFI::Platform::POSIX.dup2(other_fd, current_fd)) + Errno.handle("reopen") + end + + set_mode + reset_positioning + + return true + end + + def reopen_path(path, mode) + current_fd = @descriptor + other_fd = FileDescriptor.open_with_cloexec(path, mode, 0666) + + Errno.handle("could not reopen path \"#{path}\"") if other_fd < 0 + + if FFI.call_failed?(FFI::Platform::POSIX.dup2(other_fd, current_fd)) + if Errno.eql?(Errno::EBADF::Errno) + # means current_fd is closed, so set ourselves to use the new fd and continue + @descriptor = other_fd + else + FFI::Platform::POSIX.close(other_fd) if other_fd > 0 + Errno.handle("could not reopen path \"#{path}\"") + end + else + FFI::Platform::POSIX.close(other_fd) + end + + set_mode + reset_positioning + + return true + end + + def reset_positioning(stat=nil) + # Discover final size of file so we can set EOF properly + stat = File::Stat.fstat(@descriptor) unless stat + @total_size = stat.size + + # We may have reopened a file descriptor that went from "file" to a different + # ftype which doesn't allow seeking, so catch it here. + if stat.ftype == "file" + seek_positioning + else + @offset = 0 + end + + determine_eof + end + + def seek_positioning + @offset = lseek(0, SEEK_CUR) # find current position if we are reopening! + end + private :seek_positioning + + def set_mode + @mode = FileDescriptor.get_flags(@descriptor) + end + + def blocking? + (FileDescriptor.get_flags(@descriptor) & O_NONBLOCK) == 0 + end + + def clear_nonblock + flags = FileDescriptor.get_flags(@descriptor) + FileDescriptor.clear_flag(O_NONBLOCK, @descriptor) + end + + def set_nonblock + flags = FileDescriptor.get_flags(@descriptor) + FileDescriptor.set_flag(O_NONBLOCK, @descriptor) + end + + def ftruncate(offset) + ensure_open + + # FIXME: fail if +offset+ is too large, see C++ code + + status = FFI::Platform::POSIX.ftruncate(descriptor, offset) + Errno.handle("ftruncate(2) failed") if FFI.call_failed?(status) + return status + end + + ## + # Returns true if ios is associated with a terminal device (tty), false otherwise. + # + # File.new("testfile").isatty #=> false + # File.new("/dev/tty").isatty #=> true + def tty? + ensure_open + FFI::Platform::POSIX.isatty(@descriptor) == 1 + end + + def ttyname + # Need to protect this call with a lock since the OS copies this string information + # to an internal static object and returns a pointer to that object. Future calls + # to #ttyname modify that same object. Therefore, we need to protect this from + # race conditions. + Rubinius.lock(self) + name = FFI::Platform::POSIX.ttyname(descriptor) + if name + name + else + Errno.handle("ttyname(3) failed") + nil + end + ensure + Rubinius.unlock(self) + end + + def inspect + stat = File::Stat.fstat(@descriptor) + "fd [#{descriptor}], mode [#{@mode}], total_size [#{@total_size}], offset [#{@offset}], eof [#{@eof}], stat.size [#{stat.size}], written? [#{@written}]" + end + + def cancel_finalizer + ObjectSpace.undefine_finalizer(self) + end + + def __finalize__ + return if @descriptor.nil? || @descriptor == -1 + + fd = @descriptor + + # Should flush a write buffer here if one exists. Current + # implementation does not buffer writes internally, so this + # is a no-op. + + # Do not close stdin/out/err (0, 1, and 2) + if @descriptor > 3 + @descriptor = -1 + + # Take care of any IO cleanup for the C API here. An IO may + # have been promoted to a low-level RIO struct using #fdopen, + # so we MUST use #fclose to clost it. + return if RIOStream.close(@io, false) + + # Ignore any return code... don't care if it fails + FFI::Platform::POSIX.close(fd) if @autoclose + end + end + end # class FileDescriptor + + class BufferedFileDescriptor < FileDescriptor + + def buffer_reset + @unget_buffer.clear + end + private :buffer_reset + + def read(length, output_string=nil) + length ||= FileDescriptor.pagesize + + # Preferentially read from the buffer and then from the underlying + # FileDescriptor. + # FIXME: make this logic clearer. + if length > @unget_buffer.size + @offset += @unget_buffer.size + length -= @unget_buffer.size + + str = @unget_buffer.inject(self.class.get_empty_8bit_buffer) { |sum, val| val.chr + sum } + str2 = super(length, output_string) + + if str.size == 0 && str2.nil? + determine_eof + return nil + elsif str2 + str += str2 + end + buffer_reset + elsif length == @unget_buffer.size + @offset += length + length -= @unget_buffer.size + + str = @unget_buffer.inject(self.class.get_empty_8bit_buffer) { |sum, val| val.chr + sum } + buffer_reset + else + @offset += @unget_buffer.size + str = self.class.get_empty_8bit_buffer + + length.times do + str << @unget_buffer.pop + end + end + + if output_string + output_string.replace(str) + else + output_string = str.force_encoding(Encoding::ASCII_8BIT) + end + + determine_eof + return output_string + end + + def read_only_buffer(length) + unless @unget_buffer.empty? + if length >= @unget_buffer.size + @offset += @unget_buffer.size + length -= @unget_buffer.size + + str = @unget_buffer.inject(self.class.get_empty_8bit_buffer) { |sum, val| val.chr + sum } + buffer_reset + [str, length] + else + @offset += @unget_buffer.size + str = self.class.get_empty_8bit_buffer + + length.times do + str << @unget_buffer.pop + end + [str, 0] + end + else + [nil, length] + end + end + + def seek(bytes, whence) + # @offset may not match actual file pointer if there were calls to #unget. + if whence == SEEK_CUR + # adjust the number of bytes to seek based on how far ahead we are with the buffer + bytes -= @unget_buffer.size + end + + buffer_reset + lseek(bytes, whence) + end + + def sysread(byte_count) + raise_if_buffering + read(byte_count) + end + + def sysseek(bytes, whence) + raise_if_buffering + lseek(bytes, whence) + end + + def eof? + super && @unget_buffer.empty? + end + + def flush + #@unget_buffer.clear + end + + def raise_if_buffering + raise IOError unless @unget_buffer.empty? + end + + def reset_positioning(*args) + @unget_buffer = [] + super + end + + def write_nonblock(str) + buffer_reset + set_nonblock + + buf_size = str.bytesize + left = buf_size + + if left > 0 + buffer = FFI::MemoryPointer.new(left) + buffer.write_string(str) + + if FFI.call_failed?(bytes_written = FFI::Platform::POSIX.write(@descriptor, buffer, left)) + clear_nonblock + Errno.raise_waitwritable("write_nonblock") + end + + left -= bytes_written + buffer += bytes_written + @offset += bytes_written + end + + return(buf_size - left) + end + + def unget(byte) + @offset -= 1 + @unget_buffer << byte + end + end # class BufferedFileDescriptor + + class FIFOFileDescriptor < BufferedFileDescriptor + + def self.connect_pipe_fds + fds = FFI::MemoryPointer.new(:int, 2) + + Errno.handle("creating pipe failed") if FFI.call_failed?(FFI::Platform::POSIX.pipe(fds)) + fd0, fd1 = fds.read_array_of_int(2) + + FileDescriptor.new_open_fd(fd0) + FileDescriptor.new_open_fd(fd1) + + return [fd0, fd1] + end + + def initialize(fd, stat, io, mode=nil) + super(fd, stat, self) + @mode = mode if mode + @eof = false # force to false + end + + private :initialize + + def determine_eof + if @offset >= @total_size + @eof = true + + # No seeking allowed on a pipe, so its size is always its offset + @total_size = @offset + end + end + + def eof? + # The only way to confirm we are EOF with a pipe is to try to read from + # it. If we fail, then we are EOF. If we succeed, then unget the byte + # so that EOF is false (i.e. more to read). + str = read(1) + unget(str) if str + super + end + + def seek_positioning + # no seeking allowed for pipes + @offset = 0 + end + + def sysseek(offset, whence=SEEK_SET) + # lseek does not work with pipes. + raise Errno::ESPIPE + end + end # class FIFOFileDescriptor + + class DirectoryFileDescriptor < BufferedFileDescriptor + end # class DirectoryFileDescriptor + + class SocketFileDescriptor < FIFOFileDescriptor + def initialize(fd, stat, io) + super + + @mode &= O_ACCMODE + @sync = true + end + + private :initialize + + def force_read_write + @mode &= ~(O_RDONLY | O_WRONLY) + @mode |= O_RDWR + end + end # class SocketFileDescriptor + + # Encapsulates all of the logic necessary for handling #select. + class Select + #eval(Rubinius::Config['rbx.platform.timeval.class']) + + class FDSet + def self.new + Rubinius.primitive :fdset_allocate + raise PrimitiveFailure, "FDSet.allocate failed" + end + + def zero + Rubinius.primitive :fdset_zero + raise PrimitiveFailure, "FDSet.zero failed" + end + + def set(descriptor) + Rubinius.primitive :fdset_set + raise PrimitiveFailure, "FDSet.set failed" + end + + def set?(descriptor) + Rubinius.primitive :fdset_is_set + raise PrimitiveFailure, "FDSet.set? failed" + end + + def to_set + Rubinius.primitive :fdset_to_set + raise PrimitiveFailure, "FDSet.to_set failed" + end + end + + def self.fd_set_from_array(array) + highest = -1 + fd_set = FDSet.new + fd_set.zero + + array.each do |io| + io = io[1] if io.is_a?(Array) + descriptor = io.descriptor + + if descriptor >= FD_SETSIZE + raise IOError + end + + if descriptor >= 0 + highest = descriptor > highest ? descriptor : highest + fd_set.set(descriptor) + end + end + + return [fd_set, highest] + end + + def self.collect_set_fds(array, fd_set) + return [] unless fd_set + array.map do |io| + key, io = if io.is_a?(Array) + [io[0], io[1]] + else + [io, io] + end + + if fd_set.set?(io.descriptor) || io.descriptor < 0 + key + else + nil + end + end.compact + end + + def self.timer_add(time1, time2, result) + result[:tv_sec] = time1[:tv_sec] + time2[:tv_sec] + result[:tv_usec] = time1[:tv_usec] + time2[:tv_usec] + + if result[:tv_usec] >= 1_000_000 + result[:tv_sec] += 1 + result[:tv_usec] -= 1_000_000 + end + end + + def self.timer_sub(time1, time2, result) + result[:tv_sec] = time1[:tv_sec] - time2[:tv_sec] + result[:tv_usec] = time1[:tv_usec] - time2[:tv_usec] + + if result[:tv_usec] < 0 + result[:tv_sec] -= 1 + result[:tv_usec] += 1_000_000 + end + end + + def self.make_timeval_timeout(timeout) + if timeout + limit = Rubinius::FFI::Platform::POSIX::TimeVal.new + future = Rubinius::FFI::Platform::POSIX::TimeVal.new + + limit[:tv_sec] = (timeout / 1_000_000.0).to_i + limit[:tv_usec] = (timeout % 1_000_000.0).to_i + + # Get current time to be used if select is interrupted and we have to recalculate the sleep time + if FFI.call_failed?(FFI::Platform::POSIX.gettimeofday(future, nil)) + Errno.handle("gettimeofday(2) failed") + end + + timer_add(future, limit, future) + end + + [limit, future] + end + + def self.reset_timeval_timeout(time_limit, future) + now = Rubinius::FFI::Platform::POSIX::TimeVal.new + + if FFI.call_failed?(FFI::Platform::POSIX.gettimeofday(now, nil)) + Errno.handle("gettimeofday(2) failed") + end + + timer_sub(future, now, time_limit) + end + + def self.readable_events(read_fd) + fd_set = FDSet.new + fd_set.zero + fd_set.set(read_fd) + + # sets fields to zero by default + timer = Rubinius::FFI::Platform::POSIX::TimeVal.new + + FFI::Platform::POSIX.select(read_fd + 1, fd_set.to_set, nil, nil, timer) + end + + def self.wait_for_writable(fd) + fd_set = FDSet.new + fd_set.zero + fd_set.set(fd) + + FFI::Platform::POSIX.select(fd + 1, nil, fd_set.to_set, nil, nil) + end + + def self.select(readables, writables, errorables, timeout) + read_set, highest_read_fd = readables.nil? ? [nil, nil] : fd_set_from_array(readables) + write_set, highest_write_fd = writables.nil? ? [nil, nil] : fd_set_from_array(writables) + error_set, highest_err_fd = errorables.nil? ? [nil, nil] : fd_set_from_array(errorables) + max_fd = [highest_read_fd, highest_write_fd, highest_err_fd].compact.max || -1 + + time_limit, future = make_timeval_timeout(timeout) + + events = 0 + loop do + events = FFI::Platform::POSIX.select(max_fd + 1, + (read_set ? read_set.to_set : nil), + (write_set ? write_set.to_set : nil), + (error_set ? error_set.to_set : nil), + time_limit) + + if FFI.call_failed?(events) + + if Errno::EAGAIN::Errno == Errno.errno || Errno::EINTR::Errno == Errno.errno + # return nil if async_interruption? + time_limit = reset_timeval_timeout(time_limit, future) + continue + end + + Errno.handle("select(2) failed") + end + + break + end + + return nil if events.zero? + + output_fds = [] + output_fds << collect_set_fds(readables, read_set) + output_fds << collect_set_fds(writables, write_set) + output_fds << collect_set_fds(errorables, error_set) + return output_fds + end + + def self.validate_and_convert_argument(objects) + if objects + raise TypeError, "Argument must be an Array" unless objects.respond_to?(:to_ary) + objects = + objects.to_ary.map do |obj| + if obj.kind_of? IO + raise IOError, "closed stream" if obj.closed? + obj + else + raise TypeError unless obj.respond_to?(:to_io) + io = obj.to_io + raise TypeError unless io + raise IOError, "closed stream" if io.closed? + [obj, io] + end + end + end + + objects + end + end # class Select + + attr_accessor :external + attr_accessor :internal + # intended to only be used by IO.setup to associate a new FileDescriptor object with instance of IO + attr_accessor :fd + + def self.binread(file, length=nil, offset=0) + raise ArgumentError, "Negative length #{length} given" if !length.nil? && length < 0 + + File.open(file, "r", :encoding => "ascii-8bit:-") do |f| + f.seek(offset) + f.read(length) + end + end + + def self.binwrite(file, string, *args) + offset, opts = args + opts ||= {} + if offset.is_a?(Hash) + offset, opts = nil, offset + end + + mode, binary, external, internal, autoclose, encoding_options = IO.normalize_options(nil, opts) + unless mode + mode = File::CREAT | File::RDWR | File::BINARY + mode |= File::TRUNC unless offset + end + File.open(file, mode, encoding_options.merge(:encoding => (external || "ASCII-8BIT"))) do |f| + f.seek(offset || 0) + f.write(string) + end + end + + class StreamCopier + def initialize(from, to, length, offset) + @length = length + @offset = offset + + @from_io, @from = to_io(from, "rb") + @to_io, @to = to_io(to, "wb") + + @method = read_method @from + end + + private :initialize + + def to_io(obj, mode) + if obj.kind_of? IO + flag = true + io = obj + else + flag = false + + if obj.kind_of? String + io = File.open obj, mode + elsif obj.respond_to? :to_path + path = Rubinius::Type.coerce_to obj, String, :to_path + io = File.open path, mode + else + io = obj + end + end + + return flag, io + end + + def read_method(obj) + if obj.respond_to? :readpartial + :readpartial + else + :read + end + end + + def run + @from.ensure_open_and_readable if @from.kind_of? IO + @to.ensure_open_and_writable if @to.kind_of? IO + + if @offset + if @from_io && !@from.pipe? + saved_pos = @from.pos + else + saved_pos = 0 + end + + @from.seek @offset, IO::SEEK_CUR + end + + size = @length ? @length : 16384 + bytes = 0 + + begin + while data = @from.__send__(@method, size, "") + @to.write data + bytes += data.bytesize + + break if @length && bytes >= @length + end + rescue EOFError + # done reading + end + + @to.flush if @to.kind_of? IO + return bytes + ensure + if @from_io + @from.pos = saved_pos if @offset + else + @from.close if @from.kind_of? IO + end + + @to.close if @to.kind_of? IO unless @to_io + end + end # class StreamCopier + + def self.copy_stream(from, to, max_length=nil, offset=nil) + StreamCopier.new(from, to, max_length, offset).run + end + + def self.foreach(name, separator=undefined, limit=undefined, options=undefined) + return to_enum(:foreach, name, separator, limit, options) unless block_given? + + name = Rubinius::Type.coerce_to_path name + + case separator + when Fixnum + options = limit + limit = separator + separator = $/ + when undefined + separator = $/ + when Hash + options = separator + separator = $/ + when nil + # do nothing + else + separator = StringValue(separator) + end + + case limit + when Fixnum, nil + # do nothing + when undefined + limit = nil + when Hash + if undefined.equal? options + options = limit + limit = nil + else + raise TypeError, "can't convert Hash into Integer" + end + else + value = limit + limit = Rubinius::Type.try_convert limit, Fixnum, :to_int + + unless limit + options = Rubinius::Type.coerce_to value, Hash, :to_hash + end + end + + case options + when Hash + # do nothing + when undefined, nil + options = { } + else + options = Rubinius::Type.coerce_to options, Hash, :to_hash + end + + if name[0] == ?| + io = IO.popen(name[1..-1], "r") + return nil unless io + else + options[:mode] = "r" unless options.key? :mode + io = File.open(name, options) + end + + begin + while line = io.gets(separator, limit) + yield line + end + ensure + $_ = nil + io.close + end + + return nil + end + + def self.readlines(name, separator=undefined, limit=undefined, options=undefined) + lines = [] + saved_line = $_ + foreach(name, separator, limit, options) { |l| lines << l } + + $_ = saved_line + lines + end + + def self.read_encode(io, str, encoding_options) + internal = io.internal_encoding + external = io.external_encoding || Encoding.default_external + + if external.equal? Encoding::ASCII_8BIT + str.force_encoding external + elsif internal and external + ec = Encoding::Converter.new(external, internal, (encoding_options || {})) + ec.convert str + else + str.force_encoding external + end + end + + def self.write(file, string, *args) + if args.size > 2 + raise ArgumentError, "wrong number of arguments (#{args.size + 2} for 2..3)" + end + + offset, opts = args + opts ||= {} + if offset.is_a?(Hash) + offset, opts = nil, offset + end + + mode, binary, external, internal, autoclose, encoding_options = IO.normalize_options(nil, opts) + unless mode + mode = File::CREAT | File::WRONLY + mode |= File::TRUNC unless offset + end + + open_args = opts[:open_args] || [mode, :encoding => (external || "ASCII-8BIT")] + open_args[1].merge!(encoding_options) if open_args[1].kind_of?(Hash) + File.open(file, *open_args) do |f| + f.seek(offset) if offset + f.write(string) + end + end + + def self.for_fd(fd, mode=undefined, options=undefined) + new fd, mode, options + end + + def self.read(name, length_or_options=undefined, offset=0, options=nil) + offset = 0 if offset.nil? + name = Rubinius::Type.coerce_to_path name + mode = "r" + + if undefined.equal? length_or_options + length = undefined + elsif Rubinius::Type.object_kind_of? length_or_options, Hash + length = undefined + offset = 0 + options = length_or_options + elsif length_or_options + offset = Rubinius::Type.coerce_to(offset || 0, Fixnum, :to_int) + raise Errno::EINVAL, "offset must not be negative" if offset < 0 + + length = Rubinius::Type.coerce_to(length_or_options, Fixnum, :to_int) + raise ArgumentError, "length must not be negative" if length < 0 + else + length = undefined + end + + if options + mode = options.delete(:mode) || "r" + end + + # Detect pipe mode + if name[0] == ?| + io = IO.popen(name[1..-1], "r") + return nil unless io # child process + else + io = File.new(name, mode, options) + end + + str = nil + begin + io.seek(offset) unless offset == 0 + if undefined.equal?(length) + str = io.read + else + str = io.read length + end + ensure + io.close + end + + return str + end + + def self.try_convert(obj) + Rubinius::Type.try_convert obj, IO, :to_io + end + + def self.normalize_options(mode, options) + mode = nil if undefined.equal?(mode) + autoclose = true + encoding_options = {} + + if undefined.equal?(options) + options = Rubinius::Type.try_convert(mode, Hash, :to_hash) + mode = nil if options + elsif !options.nil? + options = Rubinius::Type.try_convert(options, Hash, :to_hash) + raise ArgumentError, "wrong number of arguments (3 for 1..2)" unless options + end + + if mode + mode = (Rubinius::Type.try_convert(mode, Integer, :to_int) or + Rubinius::Type.coerce_to(mode, String, :to_str)) + end + + if options + if optmode = options[:mode] + optmode = (Rubinius::Type.try_convert(optmode, Integer, :to_int) or + Rubinius::Type.coerce_to(optmode, String, :to_str)) + end + + if mode + raise ArgumentError, "mode specified twice" if optmode + else + mode = optmode + end + + autoclose = !!options[:autoclose] if options.key?(:autoclose) + end + + if mode.kind_of?(String) + mode, external, internal = mode.split(":") + raise ArgumentError, "invalid access mode" unless mode + + binary = true if mode[1] === ?b + binary = false if mode[1] === ?t + elsif mode + binary = true if (mode & BINARY) != 0 + end + + if options + if options[:textmode] and options[:binmode] + raise ArgumentError, "both textmode and binmode specified" + end + + if binary.nil? + binary = options[:binmode] + elsif options.key?(:textmode) or options.key?(:binmode) + raise ArgumentError, "text/binary mode specified twice" + end + + if !external and !internal + external = options[:external_encoding] + internal = options[:internal_encoding] + elsif options[:external_encoding] or options[:internal_encoding] or options[:encoding] + raise ArgumentError, "encoding specified twice" + end + + if !external and !internal + encoding = options[:encoding] + + if encoding.kind_of? Encoding + external = encoding + elsif !encoding.nil? + encoding = StringValue(encoding) + external, internal = encoding.split(':') + end + end + + [ + :invalid, :undef, :replace, :newline, :universal_newline, :crlf_newline, + :cr_newline, :xml + ].each do |options_key| + encoding_options[options_key] = options[options_key] if options.has_key?(options_key) + end + end + + [mode, binary, external, internal, autoclose, encoding_options] + end + + def self.open(*args) + io = new(*args) + + return io unless block_given? + + begin + yield io + ensure + begin + io.close unless io.closed? + rescue StandardError + # nothing, just swallow them. + end + end + end + + def self.parse_mode(mode) + return mode if Rubinius::Type.object_kind_of? mode, Integer + + mode = StringValue(mode) + + ret = 0 + + case mode[0] + when ?r + ret |= RDONLY + when ?w + ret |= WRONLY | CREAT | TRUNC + when ?a + ret |= WRONLY | CREAT | APPEND + else + raise ArgumentError, "invalid mode -- #{mode}" + end + + return ret if mode.length == 1 + + case mode[1] + when ?+ + ret &= ~(RDONLY | WRONLY) + ret |= RDWR + when ?b + ret |= BINARY + when ?t + ret &= ~BINARY + when ?: + warn("encoding options not supported in 1.8") + return ret + else + raise ArgumentError, "invalid mode -- #{mode}" + end + + return ret if mode.length == 2 + + case mode[2] + when ?+ + ret &= ~(RDONLY | WRONLY) + ret |= RDWR + when ?b + ret |= BINARY + when ?t + ret &= ~BINARY + when ?: + warn("encoding options not supported in 1.8") + return ret + else + raise ArgumentError, "invalid mode -- #{mode}" + end + + ret + end + + def self.pipe(external=nil, internal=nil, options=nil) + # The use of #allocate is to make sure we create an IO obj. Would be so much + # cleaner to just do a PipeIO class as a subclass, but that would not be + # backward compatible. + fd0, fd1 = FIFOFileDescriptor.connect_pipe_fds + lhs = allocate + lhs.send(:new_pipe, fd0, external, internal, options, FileDescriptor::O_RDONLY, true) + rhs = allocate + rhs.send(:new_pipe, fd1, nil, nil, nil, FileDescriptor::O_WRONLY) + + if block_given? + begin + yield lhs, rhs + ensure + lhs.close unless lhs.closed? + rhs.close unless rhs.closed? + end + else + [lhs, rhs] + end + end + + def self.popen(*args) + if env = Rubinius::Type.try_convert(args.first, Hash, :to_hash) + args.shift + end + + if io_options = Rubinius::Type.try_convert(args.last, Hash, :to_hash) + args.pop + end + + if args.size > 2 + raise ArgumentError, "#{__method__}: given #{args.size}, expected 1..2" + end + + cmd, mode = args + mode ||= "r" + + if cmd.kind_of? Array + if sub_env = Rubinius::Type.try_convert(cmd.first, Hash, :to_hash) + env = sub_env unless env + cmd.shift + end + + if exec_options = Rubinius::Type.try_convert(cmd.last, Hash, :to_hash) + cmd.pop + end + end + + mode, binary, external, internal, autoclose, encoding_options = + IO.normalize_options(mode, io_options || {}) + mode_int = parse_mode mode + + readable = false + writable = false + + if mode_int & IO::RDWR != 0 + readable = true + writable = true + elsif mode_int & IO::WRONLY != 0 + writable = true + else # IO::RDONLY + readable = true + end + + pa_read, ch_write = pipe if readable + ch_read, pa_write = pipe if writable + + # We only need the Bidirectional pipe if we're reading and writing. + # If we're only doing one, we can just return the IO object for + # the proper half. + if readable and writable + # Transmogrify pa_read into a BidirectionalPipe object, + # and then tell it abou it's pid and pa_write + + Rubinius::Unsafe.set_class pa_read, IO::BidirectionalPipe + + pipe = pa_read + pipe.set_pipe_info(pa_write) + elsif readable + pipe = pa_read + elsif writable + pipe = pa_write + else + raise ArgumentError, "IO is neither readable nor writable" + end + + pipe.binmode if binary + pipe.set_encoding(external || Encoding.default_external, internal) + + if cmd == "-" + pid = Rubinius::Mirror::Process.fork + + if !pid + # Child + begin + if readable + pa_read.close + STDOUT.reopen ch_write + end + + if writable + pa_write.close + STDIN.reopen ch_read + end + + if block_given? + yield nil + exit! 0 + else + return nil + end + rescue => e + exit! 0 + end + end + else + options = {} + options[:in] = ch_read.fileno if ch_read + options[:out] = ch_write.fileno if ch_write + + if io_options + io_options.delete_if do |key, _| + [:mode, :external_encoding, :internal_encoding, + :encoding, :textmode, :binmode, :autoclose + ].include? key + end + + options.merge! io_options + end + + if exec_options + options.merge! exec_options + end + + pid = Rubinius::Mirror::Process.spawn(env || {}, *cmd, options) + end + + pipe.pid = pid + + ch_write.close if readable + ch_read.close if writable + + return pipe unless block_given? + + begin + yield pipe + ensure + pipe.close unless pipe.closed? + end + end + + # + # +select+ examines the IO object Arrays that are passed in + # as +readables+, +writables+, and +errorables+ to see if any + # of their descriptors are ready for reading, are ready for + # writing, or have an exceptions pending respectively. An IO + # may appear in more than one of the sets. Any of the three + # sets may be +nil+ if you are not interested in those events. + # + # If +timeout+ is not nil, it specifies the number of seconds + # to wait for events (maximum.) The number may be fractional, + # conceptually up to a microsecond resolution. + # + # A +timeout+ of 0 indicates that each descriptor should be + # checked once only, effectively polling the sets. + # + # Leaving the +timeout+ to +nil+ causes +select+ to block + # infinitely until an event transpires. + # + # If the timeout expires without events, +nil+ is returned. + # Otherwise, an [readable, writable, errors] Array of Arrays + # is returned, only, with the IO objects that have events. + # + # @compatibility MRI 1.8 and 1.9 require the +readables+ Array, + # Rubinius does not. + # + def self.select(readables=nil, writables=nil, errorables=nil, timeout=nil) + if timeout + unless Rubinius::Type.object_kind_of? timeout, Numeric + raise TypeError, "Timeout must be numeric" + end + + raise ArgumentError, 'timeout must be positive' if timeout < 0 + + # Microseconds, rounded down + timeout = Integer(timeout * 1_000_000) + end + + readables = IO::Select.validate_and_convert_argument(readables) + writables = IO::Select.validate_and_convert_argument(writables) + errorables = IO::Select.validate_and_convert_argument(errorables) + + IO::Select.select(readables, writables, errorables, timeout) + end + + ## + # Opens the given path, returning the underlying file descriptor as a Fixnum. + # IO.sysopen("testfile") #=> 3 + def self.sysopen(path, mode = nil, perm = nil) + path = Rubinius::Type.coerce_to_path path + mode = parse_mode(mode || "r") + perm ||= 0666 + + FileDescriptor.open_with_mode path, mode, perm + end + + # + # Internally associate +io+ with the given descriptor. + # + # The +mode+ will be checked and set as the current mode if + # the underlying descriptor allows it. + # + # The +sync+ attribute will also be set. + # + def self.setup(io, fd, mode=nil, sync=false) + cur_mode = FileDescriptor.get_flags(fd) + Errno.handle if cur_mode < 0 + + cur_mode &= ACCMODE + + if mode + mode = parse_mode(mode) + mode &= ACCMODE + + if (cur_mode == RDONLY or cur_mode == WRONLY) and mode != cur_mode + raise Errno::EINVAL, "Invalid new mode for existing descriptor #{fd}" + end + end + + # Check the given +io+ for a valid fd instance first. If it has one, cancel + # the existing finalizer since we are about to allocate a new fd instance. + if fd_obj = io.instance_variable_get(:@fd) + fd_obj.cancel_finalizer + end + + fd_obj = FileDescriptor.choose_type(fd, io) + io.instance_variable_set(:@fd, fd_obj) + raise "FD could not be allocated for fd [#{fd}]" unless fd_obj + raise "No descriptor set for fd [#{fd}]" unless fd_obj.descriptor + + io.mode = mode || cur_mode + io.sync = !!sync + io.lineno = 0 if io.lineno.nil? + + # FIXME - re-enable this somehow. Right now this breaks kernel/delta/io.rb when it + # redefines STDIN/STDOUT/STDERR from the IO.open call. The new IO code has already + # loaded so we can no longer access the object that STDIN/STDOUT/STDERR points to + # via Ruby code, so the following code blows up. + # if STDOUT.respond_to?(:fileno) and not STDOUT.closed? + # io.sync ||= STDOUT.fileno == fd + # end + # + # if STDERR.respond_to?(:fileno) and not STDERR.closed? + # io.sync ||= STDERR.fileno == fd + # end + end + + # + # Create a new IO associated with the given fd. + # + def initialize(fd, mode=undefined, options=undefined) + if block_given? + warn 'IO::new() does not take block; use IO::open() instead' + end + + mode, binary, external, internal, @autoclose, @encoding_options = + IO.normalize_options(mode, options) + + fd = Rubinius::Type.coerce_to fd, Integer, :to_int + autoclose = @autoclose + IO.setup self, fd, mode + + binmode if binary + set_encoding external, internal + + if @external && !external + @external = nil + end + + if @internal + if Encoding.default_external == Encoding.default_internal or + (@external || Encoding.default_external) == Encoding::ASCII_8BIT + @internal = nil + end + elsif !mode_read_only? + if Encoding.default_external != Encoding.default_internal + @internal = Encoding.default_internal + end + end + + unless @external + if @binmode + @external = Encoding::ASCII_8BIT + elsif @internal or Encoding.default_internal + @external = Encoding.default_external + end + end + + @pipe = false # FIXME + end + + private :initialize + + ## + # Obtains a new duplicate descriptor for the current one. + def initialize_copy(original_io) # :nodoc: + # Make a complete copy of the +original_io+ object including + # the mode, binmode, path, position, lineno, and a new FD. + dest_io = self + + fd = FFI::Platform::POSIX.dup(original_io.descriptor) + + # The system makes a shallow copy of all ivars, so this copy has + # the same @fd as the original. That shallow copy is really only + # relevant for primitive values (Fixnum, String, etc) and not + # our own objects. Instantiate a new @fd. + @fd = FileDescriptor.choose_type(fd, dest_io) + dest_io.mode = original_io.mode + dest_io.sync = original_io.sync + dest_io.binmode if original_io.binmode? + dest_io.autoclose = original_io.autoclose + + dest_io + end + private :initialize_copy + + def new_pipe(fd, external, internal, options, mode, do_encoding=false) + @fd = FIFOFileDescriptor.new(fd, nil, self, mode) + @lineno = 0 + @pipe = true + + # Why do we only set encoding for the "left hand side" pipe? Why not both? + if do_encoding + set_encoding((external || Encoding.default_external), (internal || Encoding.default_internal), options) + end + end + private :new_pipe + + def super_inspect + " \n#{@fd.inspect}" + end + + # alias_method :prim_write, :write + # alias_method :prim_close, :close + # alias_method :prim_read, :read + + def descriptor + @fd.descriptor if @fd + end + + def descriptor=(value) + @fd.descriptor = value if @fd + end + + def mode + @fd.mode if @fd + end + + def mode=(value) + @fd.mode = value if @fd + end + + def sync + @fd.sync if @fd + end + + def sync=(value) + @fd.sync = value if @fd + end + + def advise(advice, offset = 0, len = 0) + raise IOError, "stream is closed" if closed? + raise TypeError, "advice must be a Symbol" unless advice.kind_of?(Symbol) + + if offset.kind_of?(Bignum) || len.kind_of?(Bignum) + raise RangeError, "bignum too big to convert into `long'" + end + + unless [:normal, :sequential, :random, :noreuse, :dontneed, :willneed].include? advice + raise NotImplementedError, advice.inspect + end + + advice = case advice + when :normal; POSIX_FADV_NORMAL + when :sequential; POSIX_FADV_SEQUENTIAL + when :random; POSIX_FADV_RANDOM + when :willneed; POSIX_FADV_WILLNEED + when :dontneed; POSIX_FADV_DONTNEED + when :noreuse; POSIX_FADV_NOREUSE + end + + offset = Rubinius::Type.coerce_to offset, Integer, :to_int + len = Rubinius::Type.coerce_to len, Integer, :to_int + + begin + if FFI.call_failed?(FFI::Platform::POSIX.posix_fadvise(descriptor, offset, len, advice)) + Errno.handle("posix_fadvise(2) failed") + end + rescue NotImplementedError + # MRI thinks platforms that don't support #advise should silently fail. + # See https://bugs.ruby-lang.org/issues/11806 + nil + end + + nil + end + + def autoclose + @autoclose + end + + def autoclose? + @autoclose + end + + def autoclose=(bool) + @fd.autoclose = bool + @autoclose = !!bool + end + + def binmode + ensure_open + + @binmode = true + @external = Encoding::BINARY + @internal = nil + + # HACK what to do? + self + end + + def binmode? + !@binmode.nil? + end + + def close_on_exec=(value) + if value + fcntl(F_SETFD, fcntl(F_GETFD) | FD_CLOEXEC) + else + fcntl(F_SETFD, fcntl(F_GETFD) & ~FD_CLOEXEC) + end + nil + end + + def close_on_exec? + (fcntl(F_GETFD) & FD_CLOEXEC) != 0 + end + + def <<(obj) + write(obj.to_s) + return self + end + + ## + # Closes the read end of a duplex I/O stream (i.e., one + # that contains both a read and a write stream, such as + # a pipe). Will raise an IOError if the stream is not duplexed. + # + # f = IO.popen("/bin/sh","r+") + # f.close_read + # f.readlines + # produces: + # + # prog.rb:3:in `readlines': not opened for reading (IOError) + # from prog.rb:3 + def close_read + return if invalid_descriptor? + + if mode_write_only? || mode_read_write? + raise IOError, 'closing non-duplex IO for reading' + end + + @fd.close unless closed? + end + + ## + # Closes the write end of a duplex I/O stream (i.e., one + # that contains both a read and a write stream, such as + # a pipe). Will raise an IOError if the stream is not duplexed. + # + # f = IO.popen("/bin/sh","r+") + # f.close_write + # f.print "nowhere" + # produces: + # + # prog.rb:3:in `write': not opened for writing (IOError) + # from prog.rb:3:in `print' + # from prog.rb:3 + def close_write + return if invalid_descriptor? + + if mode_read_only? || mode_read_write? + raise IOError, 'closing non-duplex IO for writing' + end + + @fd.close unless closed? + end + + ## + # Returns true if ios is completely closed (for duplex + # streams, both reader and writer), false otherwise. + # + # f = File.new("testfile") + # f.close #=> nil + # f.closed? #=> true + # f = IO.popen("/bin/sh","r+") + # f.close_write #=> nil + # f.closed? #=> false + # f.close_read #=> nil + # f.closed? #=> true + def closed? + invalid_descriptor? + end + + def dup + ensure_open + super # calls #initialize_copy + end + + # Argument matrix for IO#gets and IO#each: + # + # separator / limit | nil | >= 0 | < 0 + # ===================+=====+======+===== + # String (nonempty) | A | B | C + # +-----+------+----- + # "" | D | E | F + # +-----+------+----- + # nil | G | H | I + # + + class EachReader + READ_SIZE = 512 # bytes + + def initialize(io, separator, limit, encoding_options) + @io = io + @separator = separator ? separator.force_encoding("ASCII-8BIT") : separator + @limit = limit + @encoding_options = encoding_options + @skip = nil + end + + private :initialize + + def each(&block) + if @separator + if @separator.empty? + @separator = "\n\n" + @skip = "\n" + end + + if @limit + read_to_separator_with_limit(&block) + else + read_to_separator(&block) + end + else + if @limit + read_to_limit(&block) + else + read_all(&block) + end + end + end + + def read_and_yield_count_chars(str, buffer, byte_count, &block) + str << buffer.slice!(0, byte_count) + + if @limit + # Always read to char boundary because the +limit+ may have cut a multi-byte + # character in the middle. Returning such a string would have an invalid encoding. + if buffer.size < PEEK_AHEAD_LIMIT + # Use nonblocking read since existing +buffer+ might already have a valid encoding + # and we don't want to block forever if no more data is coming + result = @io.get_empty_8bit_buffer + + # save and restore encodings around the read operation so that we ensure ASCII_8BIT + internal, external = @io.internal, @io.external + @io.external = Encoding::ASCII_8BIT + (@io.read_nonblock(PEEK_AHEAD_LIMIT, result, exception: false) || @io.get_empty_8bit_buffer) + @io.internal, @io.external = internal, external + + buffer << result + end + str, bytes_read = read_to_char_boundary(@io, str, buffer) + else + # We are confident that our +str+ ends on a char boundary + str = IO.read_encode(@io, str, @encoding_options) + end + + str.taint + $. = @io.increment_lineno + skip_contiguous_chars(buffer) + + # Unused bytes/chars should be saved for the next read. Since the block that we yield to + # may +return+ we don't want to drop the bytes that are stored in +buffer+. To save, + # unget them so the next read will fetch them again. This might be expensive and could + # potentially use a little tuning. Maybe use an +unread(bytes)+ method which just moves + # a pointer around. Think about this for the mmap stuff. + @io.ungetc(buffer) + buffer.clear + + yield str + end + + def read_and_yield_entire_string(str, &block) + str = IO.read_encode(@io, str, @encoding_options) + str.taint + $. = @io.increment_lineno + yield str + end + + # method A, D + def read_to_separator(&block) + str = @io.get_empty_8bit_buffer + buffer = @io.get_empty_8bit_buffer + separator_size = @separator.bytesize + + begin + if buffer.size == 0 + buffer = @io.read(READ_SIZE) + end + + break unless buffer.size > 0 + + if count = buffer.index(@separator) + # #index returns a 0-based location but we want a length (so +1) and it should include + # the pattern/separator which may be >1. therefore, add the separator size. + count += separator_size + + read_and_yield_count_chars(str, buffer, count, &block) + str = @io.get_empty_8bit_buffer + else + str << buffer + buffer.clear + end + end until buffer.size == 0 && @io.eof? + + str << buffer + + unless str.empty? + read_and_yield_entire_string(str, &block) + end + end + + # method B, E + + def read_to_separator_with_limit(&block) + str = @io.get_empty_8bit_buffer + buffer = @io.get_empty_8bit_buffer + separator_size = @separator.bytesize + + #TODO: implement ignoring encoding with negative limit + wanted = limit = @limit.abs + + begin + if buffer.size == 0 + buffer = @io.read(READ_SIZE) + end + + break unless buffer && buffer.size > 0 + + if count = buffer.index(@separator) + # #index returns a 0-based location but we want a length (so +1) and it should include + # the pattern/separator which may be >1. therefore, add the separator size. + count += separator_size + count = count < wanted ? count : wanted + read_and_yield_count_chars(str, buffer, count, &block) + + str = @io.get_empty_8bit_buffer + else + if wanted < buffer.size + read_and_yield_count_chars(str, buffer, wanted, &block) + str = @io.get_empty_8bit_buffer + else + str << buffer + wanted -= buffer.size + buffer.clear + end + end + end until buffer.size == 0 && @io.eof? + + unless str.empty? + read_and_yield_entire_string(str, &block) + end + end + + # Method G + def read_all(&block) + str = @io.get_empty_8bit_buffer + + begin + str << @io.read + end until @io.eof? + + unless str.empty? + read_and_yield_entire_string(str, &block) + end + end + + # Method H + # Only ever called if separator or $/ is forced to nil + def read_to_limit(&block) + str = @io.get_empty_8bit_buffer + wanted = limit = @limit.abs + + begin + str << @io.read(wanted) + read_and_yield_count_chars(str, @io.get_empty_8bit_buffer, str.bytesize, &block) + str = @io.get_empty_8bit_buffer + end until @io.eof? + + unless str.empty? + read_and_yield_entire_string(str, &block) + end + end + + # Utility methods + + def try_to_force_encoding(io, str) + str.force_encoding(io.external_encoding || Encoding.default_external) + + IO.read_encode(io, str, @encoding_options) + end + + PEEK_AHEAD_LIMIT = 16 + + def read_to_char_boundary(io, str, buffer) + str.force_encoding(io.external_encoding || Encoding.default_external) + return [IO.read_encode(io, str, @encoding_options), 0] if str.valid_encoding? + + peek_ahead = 0 + while buffer.size > 0 and peek_ahead < PEEK_AHEAD_LIMIT + str.force_encoding Encoding::ASCII_8BIT + substring = buffer.slice!(0, 1) + str << substring + peek_ahead += 1 + + str.force_encoding(io.external_encoding || Encoding.default_external) + if str.valid_encoding? + return [IO.read_encode(io, str, @encoding_options), peek_ahead] + end + end + + [IO.read_encode(io, str, @encoding_options), peek_ahead] + end + + # Advances the buffer index past any number of contiguous + # characters == +skip+ and throws away that data. For + # example, if +skip+ is ?\n and the buffer contents are + # "\n\n\nAbc...", the buffer will discard all chars + # up to 'A'. + def skip_contiguous_chars(buffer) + return 0 unless @skip + + skip_count = 0 + skip_count += 1 while buffer[skip_count] == @skip + if skip_count > 0 + slice = buffer.slice!(0, skip_count) + slice.bytesize + else + 0 + end + end + end + + def increment_lineno + @lineno += 1 + end + + ## + # Return a string describing this IO object. + def inspect + if @fd + if @fd.descriptor != -1 + "#<#{self.class}:fd #{@fd.descriptor}>" + else + "#<#{self.class}:(closed)>" + end + else + "#<#{self.class}:fd nil>" + end + end + + def lines(*args, &block) + if block_given? + each_line(*args, &block) + else + to_enum :each_line, *args + end + end + + def each(sep_or_limit=$/, limit=nil, &block) + return to_enum(:each, sep_or_limit, limit) unless block_given? + + ensure_open_and_readable + + if limit + limit = Rubinius::Type.coerce_to limit, Integer, :to_int + sep = sep_or_limit ? StringValue(sep_or_limit) : nil + else + case sep_or_limit + when String + sep = sep_or_limit + when nil + sep = nil + else + unless sep = Rubinius::Type.check_convert_type(sep_or_limit, String, :to_str) + sep = $/ + limit = Rubinius::Type.coerce_to sep_or_limit, Integer, :to_int + end + end + end + + raise ArgumentError, "limit of 0 is invalid" if limit && limit.zero? + return if eof? + + EachReader.new(self, sep, limit, @encoding_options).each(&block) + + self + end + + def each_line(sep_or_limit=$/, limit=nil, &block) + if limit + limit = Rubinius::Type.coerce_to limit, Integer, :to_int + sep = sep_or_limit ? StringValue(sep_or_limit) : nil + raise ArgumentError, "invalid limit: 0 for each_line" if limit.zero? + else + case sep_or_limit + when String + sep = sep_or_limit + when nil + sep = nil + else + unless sep = Rubinius::Type.check_convert_type(sep_or_limit, String, :to_str) + sep = $/ + limit = Rubinius::Type.coerce_to sep_or_limit, Integer, :to_int + raise ArgumentError, "invalid limit: 0 for each_line" if limit.zero? + sep_or_limit = sep + end + end + end + + each(sep_or_limit, limit, &block) + end + + def each_byte + return to_enum(:each_byte) unless block_given? + + yield getbyte until eof? + + self + end + + alias_method :bytes, :each_byte + + def each_char + return to_enum :each_char unless block_given? + ensure_open_and_readable + + while char = getc + yield char + end + + self + end + + alias_method :chars, :each_char + + def each_codepoint + return to_enum :each_codepoint unless block_given? + ensure_open_and_readable + + while char = getc + yield char.ord + end + + self + end + + alias_method :codepoints, :each_codepoint + + + ## + # Set the pipe so it is at the end of the file + def eof! + @eof = true + end + + ## + # Returns true if ios is at end of file that means + # there are no more data to read. The stream must be + # opened for reading or an IOError will be raised. + # + # f = File.new("testfile") + # dummy = f.readlines + # f.eof #=> true + # If ios is a stream such as pipe or socket, IO#eof? + # blocks until the other end sends some data or closes it. + # + # r, w = IO.pipe + # Thread.new { sleep 1; w.close } + # r.eof? #=> true after 1 second blocking + # + # r, w = IO.pipe + # Thread.new { sleep 1; w.puts "a" } + # r.eof? #=> false after 1 second blocking + # + # r, w = IO.pipe + # r.eof? # blocks forever + # + # Note that IO#eof? reads data to a input buffer. + # So IO#sysread doesn't work with IO#eof?. + def eof? + ensure_open_and_readable + @fd.eof? + end + + alias_method :eof, :eof? + + def ensure_open_and_readable + ensure_open + raise IOError, "not opened for reading" if mode_write_only? + end + + def ensure_open_and_writable + ensure_open + raise IOError, "not opened for writing" if mode_read_only? + end + + def external_encoding + return @external if @external + return Encoding.default_external if mode_read_only? + end + + ## + # Provides a mechanism for issuing low-level commands to + # control or query file-oriented I/O streams. Arguments + # and results are platform dependent. If arg is a number, + # its value is passed directly. If it is a string, it is + # interpreted as a binary sequence of bytes (Array#pack + # might be a useful way to build this string). On Unix + # platforms, see fcntl(2) for details. Not implemented on all platforms. + def fcntl(command, arg=0) + ensure_open + + if !arg + arg = 0 + elsif arg == true + arg = 1 + elsif arg.kind_of? String + raise NotImplementedError, "cannot handle String" + else + arg = Rubinius::Type.coerce_to arg, Fixnum, :to_int + end + + command = Rubinius::Type.coerce_to command, Fixnum, :to_int + FFI::Platform::POSIX.fcntl descriptor, command, arg + end + + def internal_encoding + @internal + end + + ## + # Provides a mechanism for issuing low-level commands to + # control or query file-oriented I/O streams. Arguments + # and results are platform dependent. If arg is a number, + # its value is passed directly. If it is a string, it is + # interpreted as a binary sequence of bytes (Array#pack + # might be a useful way to build this string). On Unix + # platforms, see fcntl(2) for details. Not implemented on all platforms. + def ioctl(command, arg=0) + ensure_open + + if !arg + real_arg = 0 + elsif arg == true + real_arg = 1 + elsif arg.kind_of? String + # This could be faster. + buffer_size = arg.bytesize + # On BSD and Linux, we could read the buffer size out of the ioctl value. + # Most Linux ioctl codes predate the convention, so a fallback like this + # is still necessary. + buffer_size = 4096 if buffer_size < 4096 + buffer = FFI::MemoryPointer.new buffer_size + buffer.write_string arg, arg.bytesize + real_arg = buffer.address + else + real_arg = Rubinius::Type.coerce_to arg, Fixnum, :to_int + end + + command = Rubinius::Type.coerce_to command, Fixnum, :to_int + ret = FFI::Platform::POSIX.ioctl descriptor, command, real_arg + Errno.handle if ret < 0 + if arg.kind_of?(String) + arg.replace buffer.read_string(buffer_size) + buffer.free + end + ret + end + + ## + # Returns an integer representing the numeric file descriptor for ios. + # + # $stdin.fileno #=> 0 + # $stdout.fileno #=> 1 + def fileno + ensure_open + return @fd.descriptor + end + + alias_method :to_i, :fileno + + ## + # Flushes any buffered data within ios to the underlying + # operating system (note that this is Ruby internal + # buffering only; the OS may buffer the data as well). + # + # $stdout.print "no newline" + # $stdout.flush + # produces: + # + # no newline + def flush + ensure_open + #@fd.reset_positioning + return self + end + + def force_read_only + @fd.force_read_only + end + + def force_write_only + @fd.force_write_only + end + + ## + # Immediately writes all buffered data in ios to disk. Returns + # nil if the underlying operating system does not support fsync(2). + # Note that fsync differs from using IO#sync=. The latter ensures + # that data is flushed from Ruby's buffers, but does not guarantee + # that the underlying operating system actually writes it to disk. + def fsync + flush + + err = FFI::Platform::POSIX.fsync descriptor + + Errno.handle 'fsync(2)' if err < 0 + + return err + end + + def getbyte + ensure_open + byte = read(1) + return(byte ? byte.ord : nil) + end + + ## + # Gets the next 8-bit byte (0..255) from ios. + # Returns nil if called at end of file. + # + # f = File.new("testfile") + # f.getc #=> 84 + # f.getc #=> 104 + def getc + ensure_open + return if eof? + + char = get_empty_8bit_buffer + begin + char.force_encoding Encoding::ASCII_8BIT + char << read(1) + + char.force_encoding(self.external_encoding || Encoding.default_external) + if char.chr_at(0) + return IO.read_encode(self, char, @encoding_options) + end + end until eof? + + return nil + end + + def gets(sep_or_limit=$/, limit=nil) + each sep_or_limit, limit do |line| + $_ = line if line + return line + end + + return nil + end + + ## + # Returns the current line number in ios. The + # stream must be opened for reading. lineno + # counts the number of times gets is called, + # rather than the number of newlines encountered. + # The two values will differ if gets is called with + # a separator other than newline. See also the $. variable. + # + # f = File.new("testfile") + # f.lineno #=> 0 + # f.gets #=> "This is line one\n" + # f.lineno #=> 1 + # f.gets #=> "This is line two\n" + # f.lineno #=> 2 + def lineno + ensure_open + + return @lineno + end + + ## + # Manually sets the current line number to the + # given value. $. is updated only on the next read. + # + # f = File.new("testfile") + # f.gets #=> "This is line one\n" + # $. #=> 1 + # f.lineno = 1000 + # f.lineno #=> 1000 + # $. # lineno of last read #=> 1 + # f.gets #=> "This is line two\n" + # $. # lineno of last read #=> 1001 + def lineno=(line_number) + ensure_open + + raise TypeError if line_number.nil? + + @lineno = Integer(line_number) + end + + def mode_read_only? + @fd.read_only? + end + private :mode_read_only? + + def mode_read_write? + @fd.read_write? + end + private :mode_read_write? + + def mode_write_only? + @fd.write_only? + end + private :mode_write_only? + + ## + # FIXME + # Returns the process ID of a child process + # associated with ios. This will be set by IO::popen. + # + # pipe = IO.popen("-") + # if pipe + # $stderr.puts "In parent, child pid is #{pipe.pid}" + # else + # $stderr.puts "In child, pid is #{$$}" + # end + # produces: + # + # In child, pid is 26209 + # In parent, child pid is 26209 + def pid + raise IOError, 'closed stream' if closed? + @pid + end + + attr_writer :pid + + def pipe=(v) + @pipe = !!v + end + + def pipe? + @pipe + end + + ## + # + def pos + ensure_open + @fd.offset + end + + alias_method :tell, :pos + + ## + # Seeks to the given position (in bytes) in ios. + # + # f = File.new("testfile") + # f.pos = 17 + # f.gets #=> "This is line two\n" + def pos=(offset) + seek offset, SEEK_SET + end + + ## + # Writes each given argument.to_s to the stream or $_ (the result of last + # IO#gets) if called without arguments. Appends $\.to_s to output. Returns + # nil. + def print(*args) + if args.empty? + write $_.to_s + else + args.each { |o| write o.to_s } + end + + write $\.to_s + nil + end + + ## + # Formats and writes to ios, converting parameters under + # control of the format string. See Kernel#sprintf for details. + def printf(fmt, *args) + fmt = StringValue(fmt) + write ::Rubinius::Sprinter.get(fmt).call(*args) + end + + ## + # If obj is Numeric, write the character whose code is obj, + # otherwise write the first character of the string + # representation of obj to ios. + # + # $stdout.putc "A" + # $stdout.putc 65 + # produces: + # + # AA + def putc(obj) + if Rubinius::Type.object_kind_of? obj, String + write obj.substring(0, 1) + else + byte = Rubinius::Type.coerce_to(obj, Integer, :to_int) & 0xff + write byte.chr + end + + return obj + end + + ## + # Writes the given objects to ios as with IO#print. + # Writes a record separator (typically a newline) + # after any that do not already end with a newline + # sequence. If called with an array argument, writes + # each element on a new line. If called without arguments, + # outputs a single record separator. + # + # $stdout.puts("this", "is", "a", "test") + # produces: + # + # this + # is + # a + # test + def puts(*args) + if args.empty? + write DEFAULT_RECORD_SEPARATOR + else + args.each do |arg| + if arg.equal? nil + str = "" + elsif arg.kind_of? String + str = arg + elsif Thread.guarding? arg + str = "[...]" + else + Thread.recursion_guard arg do + begin + arg.to_ary.each { |a| puts a } + rescue NoMethodError + unless (str = arg.to_s).kind_of? String + str = "#<#{arg.class}:0x#{arg.object_id.to_s(16)}>" + end + end + end + end + + if str + write str + write DEFAULT_RECORD_SEPARATOR unless str.suffix?(DEFAULT_RECORD_SEPARATOR) + end + end + end + + nil + end + + def read(length=nil, buffer=nil) + ensure_open_and_readable + buffer = StringValue(buffer) if buffer + + unless length + str = IO.read_encode(self, read_all, @encoding_options) + return str unless buffer + + return buffer.replace(str) + end + + str = "" + emulate_blocking_read do + read_nonblock(length, str) + end + + if str.empty? && length > 0 + str = nil + end + + if str + if buffer + buffer.replace str.force_encoding(buffer.encoding) + else + str.force_encoding Encoding::ASCII_8BIT + end + else + buffer.clear if buffer + nil + end + end + + ## + # Reads all input until +#eof?+ is true. Returns the input read. + # If the buffer is already exhausted, returns +""+. + def read_all + str = "" + begin + buffer = "" + emulate_blocking_read do + read_nonblock(FileDescriptor.pagesize, buffer) + end + str << buffer + end until eof? + + str + end + + private :read_all + + def read_if_available(bytes) + return "" if bytes.zero? + buffer, bytes = @fd.read_only_buffer(bytes) + + events = IO::Select.readable_events(descriptor) + if events == 0 && !buffer + Errno.raise_waitreadable("no data ready") + return "" + elsif events < 0 && !buffer + Errno.handle("read(2) failed") + return "" + elsif events == 0 && buffer + # we were able to read from the buffer but no more data is waiting + return buffer + end + + # if we get here then we have data to read from the descriptor + str = "" + bytes_read = @fd.read(bytes, str) + + if bytes_read.nil? + # there's a chance the read could fail even when we have data read from the buffer + # to return to caller + return (nil || buffer) + else + # combine what we read from the buffer with what we read from the descriptor + buffer = buffer.to_s + str + return buffer + end + end + # defined in bootstrap, used here. + private :read_if_available + + ## + # Reads at most maxlen bytes from ios using read(2) system + # call after O_NONBLOCK is set for the underlying file descriptor. + # + # If the optional outbuf argument is present, it must reference + # a String, which will receive the data. + # + # read_nonblock just calls read(2). It causes all errors read(2) + # causes: EAGAIN, EINTR, etc. The caller should care such errors. + # + # read_nonblock causes EOFError on EOF. + # + # If the read buffer is not empty, read_nonblock reads from the + # buffer like readpartial. In this case, read(2) is not called. + def read_nonblock(size, buffer=nil, exception: true) + raise ArgumentError, "illegal read size" if size < 0 + ensure_open + + buffer = StringValue buffer if buffer + + unless @fd.blocking? + @fd.set_nonblock + nonblock_reset = true + end + + begin + str = read_if_available(size) + rescue EAGAINWaitReadable => exc + raise exc if exception + + return :wait_readable + end + + if str + if buffer + buffer.replace(str) + IO.read_encode(self, buffer, @encoding_options) + else + IO.read_encode(self, str, @encoding_options) + end + else + raise EOFError, "stream closed" if exception + end + ensure + @fd.clear_nonblock if nonblock_reset + end + + ## + # Reads a character as with IO#getc, but raises an EOFError on end of file. + def readchar + char = getc + raise EOFError, 'end of file reached' unless char + char + end + + def readbyte + byte = getbyte + raise EOFError, "end of file reached" unless byte + #raise EOFError, "end of file" unless bytes # bytes/each_byte is deprecated, FIXME - is this line necessary? + byte + end + + ## + # Reads a line as with IO#gets, but raises an EOFError on end of file. + def readline(sep=$/) + out = gets(sep) + raise EOFError, "end of file" unless out + return out + end + + ## + # Reads all of the lines in ios, and returns them in an array. + # Lines are separated by the optional sep_string. If sep_string + # is nil, the rest of the stream is returned as a single record. + # The stream must be opened for reading or an IOError will be raised. + # + # f = File.new("testfile") + # f.readlines[0] #=> "This is line one\n" + def readlines(sep=$/) + sep = StringValue sep if sep + + old_line = $_ + ary = Array.new + while line = gets(sep) + ary << line + end + $_ = old_line + + ary + end + + ## + # Reads at most maxlen bytes from the I/O stream. It blocks + # only if ios has no data immediately available. It doesn‘t + # block if some data available. If the optional outbuf argument + # is present, it must reference a String, which will receive the + # data. It raises EOFError on end of file. + # + # readpartial is designed for streams such as pipe, socket, tty, + # etc. It blocks only when no data immediately available. This + # means that it blocks only when following all conditions hold. + # + # the buffer in the IO object is empty. + # the content of the stream is empty. + # the stream is not reached to EOF. + # When readpartial blocks, it waits data or EOF on the stream. + # If some data is reached, readpartial returns with the data. + # If EOF is reached, readpartial raises EOFError. + # + # When readpartial doesn‘t blocks, it returns or raises immediately. + # If the buffer is not empty, it returns the data in the buffer. + # Otherwise if the stream has some content, it returns the data in + # the stream. Otherwise if the stream is reached to EOF, it raises EOFError. + # + # r, w = IO.pipe # buffer pipe content + # w << "abc" # "" "abc". + # r.readpartial(4096) #=> "abc" "" "" + # r.readpartial(4096) # blocks because buffer and pipe is empty. + # + # r, w = IO.pipe # buffer pipe content + # w << "abc" # "" "abc" + # w.close # "" "abc" EOF + # r.readpartial(4096) #=> "abc" "" EOF + # r.readpartial(4096) # raises EOFError + # + # r, w = IO.pipe # buffer pipe content + # w << "abc\ndef\n" # "" "abc\ndef\n" + # r.gets #=> "abc\n" "def\n" "" + # w << "ghi\n" # "def\n" "ghi\n" + # r.readpartial(4096) #=> "def\n" "" "ghi\n" + # r.readpartial(4096) #=> "ghi\n" "" "" + # Note that readpartial behaves similar to sysread. The differences are: + # + # If the buffer is not empty, read from the buffer instead + # of "sysread for buffered IO (IOError)". + # It doesn‘t cause Errno::EAGAIN and Errno::EINTR. When readpartial + # meets EAGAIN and EINTR by read system call, readpartial retry the system call. + # The later means that readpartial is nonblocking-flag insensitive. It + # blocks on the situation IO#sysread causes Errno::EAGAIN as if the fd is blocking mode. + def readpartial(size, buffer=nil) + raise ArgumentError, 'negative string size' unless size >= 0 + ensure_open + + if buffer + buffer = StringValue(buffer) + + buffer.shorten! buffer.bytesize + + return buffer if size == 0 + + data = nil + begin + data = read_nonblock(size) + rescue IO::WaitReadable + IO.select([self]) + retry + rescue IO::WaitWritable + IO.select(nil, [self]) + retry + end + + buffer.replace(data) + + return buffer + else + return "" if size == 0 + + data = nil + begin + data = read_nonblock(size) + rescue IO::WaitReadable + IO.select([self]) + retry + rescue IO::WaitWritable + IO.select(nil, [self]) + retry + end + + return data + end + end + + ## + # Reassociates ios with the I/O stream given in other_IO or to + # a new stream opened on path. This may dynamically change the + # actual class of this stream. + # + # f1 = File.new("testfile") + # f2 = File.new("testfile") + # f2.readlines[0] #=> "This is line one\n" + # f2.reopen(f1) #=> # + # f2.readlines[0] #=> "This is line one\n" + def reopen(other, mode=undefined) + if other.respond_to?(:to_io) + flush + + if other.kind_of? IO + io = other + else + io = other.to_io + unless io.kind_of? IO + raise TypeError, "#to_io must return an instance of IO" + end + end + + # Note: this is the whole reason that FileDescriptor#ensure_open takes an argument. + # + ensure_open(io.descriptor) + # io.reset_buffering + + @fd.reopen(io.descriptor) + + # When reopening we may be going from a Pipe to a File or vice versa. Let the + # system figure out the proper FD class. + @fd.cancel_finalizer # cancel soon-to-be-overwritten instance's finalizer + @fd = FileDescriptor.choose_type(descriptor, self) + Rubinius::Unsafe.set_class self, io.class + if io.respond_to?(:path) + @path = io.path + end + + seek(other.pos, SEEK_SET) rescue Errno::ESPIPE + else + flush unless closed? + + # If a mode isn't passed in, use the mode that the IO is already in. + if undefined.equal? mode + mode = @fd.mode + # If this IO was already opened for writing, we should + # create the target file if it doesn't already exist. + if mode_read_write? || mode_write_only? + mode |= CREAT + end + else + mode = IO.parse_mode(mode) + end + + reopen_path Rubinius::Type.coerce_to_path(other), mode + + unless closed? + seek(0, SEEK_SET) rescue Errno::ESPIPE + end + end + + self + end + + def reopen_path(path, mode) + status = @fd.reopen_path(path, mode) + @fd.cancel_finalizer + @fd = FileDescriptor.choose_type(descriptor, self) + return status + end + + ## + # Internal method used to reset the state of the buffer, including the + # physical position in the stream. + def reset_buffering + # ##@ibuffer.unseek! self + end + + ## + # Positions ios to the beginning of input, resetting lineno to zero. + # + # f = File.new("testfile") + # f.readline #=> "This is line one\n" + # f.rewind #=> 0 + # f.lineno #=> 0 + # f.readline #=> "This is line one\n" + def rewind + seek 0 + @lineno = 0 + return 0 + end + + ## + # Seeks to a given offset +amount+ in the stream according to the value of whence: + # + # IO::SEEK_CUR | Seeks to _amount_ plus current position + # --------------+---------------------------------------------------- + # IO::SEEK_END | Seeks to _amount_ plus end of stream (you probably + # | want a negative value for _amount_) + # --------------+---------------------------------------------------- + # IO::SEEK_SET | Seeks to the absolute location given by _amount_ + # Example: + # + # f = File.new("testfile") + # f.seek(-13, IO::SEEK_END) #=> 0 + # f.readline #=> "And so on...\n" + def seek(amount, whence=SEEK_SET) + flush + + @eof = false + + @fd.seek Integer(amount), whence + + return 0 + end + + def set_encoding(external, internal=nil, options=undefined) + case external + when Encoding + @external = external + when String + @external = nil + when nil + if mode_read_only? || @external + @external = nil + else + @external = Encoding.default_external + end + else + @external = nil + external = StringValue(external) + end + + if @external.nil? and not external.nil? + if index = external.index(":") + internal = external[index+1..-1] + external = external[0, index] + end + + if external[3] == ?| + if encoding = strip_bom + external = encoding + else + external = external[4..-1] + end + end + + @external = Encoding.find external + end + + unless undefined.equal? options + # TODO: set the encoding options on the IO instance + if options and not options.kind_of? Hash + @encoding_options = Rubinius::Type.coerce_to options, Hash, :to_hash + end + end + + case internal + when Encoding + @internal = nil if @external == internal + when String + # do nothing + when nil + internal = Encoding.default_internal + else + internal = StringValue(internal) + end + + if internal.kind_of? String + return self if internal == "-" + internal = Encoding.find internal + end + + @internal = internal unless internal && @external == internal + + self + end + + def read_bom_byte + read_ios, _, _ = IO.select [self], nil, nil, 0.1 + return getbyte if read_ios + end + + def strip_bom + return unless File::Stat.fstat(descriptor).file? + + case b1 = getbyte + when 0x00 + b2 = getbyte + if b2 == 0x00 + b3 = getbyte + if b3 == 0xFE + b4 = getbyte + if b4 == 0xFF + return "UTF-32BE" + end + ungetbyte b4 + end + ungetbyte b3 + end + ungetbyte b2 + + when 0xFF + b2 = getbyte + if b2 == 0xFE + b3 = getbyte + if b3 == 0x00 + b4 = getbyte + if b4 == 0x00 + return "UTF-32LE" + end + ungetbyte b4 + else + ungetbyte b3 + return "UTF-16LE" + end + ungetbyte b3 + end + ungetbyte b2 + + when 0xFE + b2 = getbyte + if b2 == 0xFF + return "UTF-16BE" + end + ungetbyte b2 + + when 0xEF + b2 = getbyte + if b2 == 0xBB + b3 = getbyte + if b3 == 0xBF + return "UTF-8" + end + ungetbyte b3 + end + ungetbyt b2 # FIXME: syntax error waiting to happen! + end + + ungetbyte b1 + nil + end + + ## + # Returns status information for ios as an object of type File::Stat. + # + # f = File.new("testfile") + # s = f.stat + # "%o" % s.mode #=> "100644" + # s.blksize #=> 4096 + # s.atime #=> Wed Apr 09 08:53:54 CDT 2003 + def stat + ensure_open + + File::Stat.fstat descriptor + end + + ## + # Returns the current "sync mode" of ios. When sync mode is true, + # all output is immediately flushed to the underlying operating + # system and is not buffered by Ruby internally. See also IO#fsync. + # + # f = File.new("testfile") + # f.sync #=> false + def sync + ensure_open + @sync == true + end + + ## + # Sets the "sync mode" to true or false. When sync mode is true, + # all output is immediately flushed to the underlying operating + # system and is not buffered internally. Returns the new state. + # See also IO#fsync. + def sync=(v) + ensure_open + @sync = !!v + end + + ## + # Reads integer bytes from ios using a low-level read and returns + # them as a string. Do not mix with other methods that read from + # ios or you may get unpredictable results. Raises SystemCallError + # on error and EOFError at end of file. + # + # f = File.new("testfile") + # f.sysread(16) #=> "This is line one" + # + def sysread(number_of_bytes, buffer=undefined) + str = @fd.sysread number_of_bytes + raise EOFError if str.nil? + + unless undefined.equal? buffer + StringValue(buffer).replace str + end + + str + end + + ## + # Seeks to a given offset in the stream according to the value + # of whence (see IO#seek for values of whence). Returns the new offset into the file. + # + # f = File.new("testfile") + # f.sysseek(-13, IO::SEEK_END) #=> 53 + # f.sysread(10) #=> "And so on." + def sysseek(amount, whence=SEEK_SET) + ensure_open + amount = Integer(amount) + + @fd.sysseek amount, whence + end + + def to_io + self + end + + def ftruncate(offset) + @fd.ftruncate offset + end + + def truncate(name, offset) + @fd.truncate name, offset + end + + ## + # Returns true if ios is associated with a terminal device (tty), false otherwise. + # + # File.new("testfile").isatty #=> false + # File.new("/dev/tty").isatty #=> true + def tty? + @fd.tty? + end + + alias_method :isatty, :tty? + + def ttyname + @fd.ttyname + end + + def syswrite(data) + data = String data + return 0 if data.bytesize == 0 + + ensure_open_and_writable + # @ibuffer.unseek!(self) unless @sync + + @fd.write(data) + end + + def ungetbyte(obj) + ensure_open + + case obj + when String + str = obj + when Integer + @fd.unget(obj & 0xff) + return + when nil + return + else + str = StringValue(obj) + end + + str.bytes.reverse_each { |byte| @fd.unget byte } + + nil + end + + def ungetc(obj) + ensure_open + + case obj + when String + str = obj + when Integer + @fd.unget(obj) + return + when nil + return + else + str = StringValue(obj) + end + + str.bytes.reverse_each { |byte| @fd.unget byte } + + nil + end + + def write(data) + data = String data + return 0 if data.bytesize == 0 + + ensure_open_and_writable + + if !binmode? && external_encoding && + external_encoding != data.encoding && + external_encoding != Encoding::ASCII_8BIT + unless data.ascii_only? && external_encoding.ascii_compatible? + data.encode!(external_encoding) + end + end + data.encode!(@encoding_options) unless (@encoding_options || {}).empty? + + # if @sync + @fd.write(data) + # else + # @ibuffer.unseek! self + # bytes_to_write = data.bytesize + # + # while bytes_to_write > 0 + # bytes_to_write -= @ibuffer.unshift(data, data.bytesize - bytes_to_write) + # @ibuffer.empty_to self if @ibuffer.full? or sync + # end + # end + + data.bytesize + end + + def write_nonblock(data, exception: true) + ensure_open_and_writable + + data = String data + return 0 if data.bytesize == 0 + + @fd.write_nonblock(data) + rescue EAGAINWaitWritable => exc + raise exc if exception + + return :wait_writable + end + + def close + return if closed? + begin + flush + ensure + @fd.close + end + + if @pid and @pid != 0 + begin + Process.wait @pid + rescue Errno::ECHILD + # If the child already exited + end + @pid = nil + end + + return nil + end + + def ensure_open(fd=nil) + raise IOError, "uninitialized stream" unless @fd + @fd.ensure_open(fd) + end + + def ensure_open_and_readable + ensure_open + raise IOError, "not opened for reading" if @fd.write_only? + end + + def ensure_open_and_writable + ensure_open + raise IOError, "not opened for writing" if @fd.read_only? + end + + def get_empty_8bit_buffer + FileDescriptor.get_empty_8bit_buffer + end + + def invalid_descriptor? + descriptor == -1 || descriptor.nil? + end + private :invalid_descriptor? + + def emulate_blocking_read + # Simple wrapper intended to wrap a call to #read_nonblock. + # Loops forever while waiting for data to become available + # just like a blocking read, but avoids blocking on the + # low-level read(2) call. Allows us to catch situations + # where another thread has closed the FD. + begin + yield + rescue IO::EAGAINWaitReadable + sleep 0.10 + retry + rescue EOFError + end + end + private :emulate_blocking_read +end + +## +# Implements the pipe returned by IO::pipe. + +class IO::BidirectionalPipe < IO + + def set_pipe_info(write) + @write = write + @sync = true + end + + ## + # Closes ios and flushes any pending writes to the + # operating system. The stream is unavailable for + # any further data operations; an IOError is raised + # if such an attempt is made. I/O streams are + # automatically closed when they are claimed by + # the garbage collector. + # + # If ios is opened by IO.popen, close sets $?. + def close + @write.close unless @write.closed? + + super unless closed? + + nil + end + + def closed? + super and @write.closed? + end + + def close_read + super + end + + def close_write + return if @write.closed? + + @write.close + end + + # Expand these out rather than using some metaprogramming because it's a fixed + # set and it's faster to have them as normal methods because then InlineCaches + # work right. + # + def <<(obj) + @write << obj + end + + def print(*args) + @write.print(*args) + end + + def printf(fmt, *args) + @write.printf(fmt, *args) + end + + def putc(obj) + @write.putc(obj) + end + + def puts(*args) + @write.puts(*args) + end + + def syswrite(data) + @write.syswrite(data) + end + + def write(data) + @write.write(data) + end + + def write_nonblock(data) + @write.write_nonblock(data) + end + +end + +module Rubinius + class IOUtility + # Redefine STDIN, STDOUT & STDERR to use the new IO class. It reopened and redefined + # all methods used in the bootstrap step. Secondly, update the $std* globals to point + # to the new objects. + + def self.redefine_io(fd, mode) + # Note that we use IO.open instead of IO.reopen. The reason is that we reopened the + # IO class in common/io.rb and overwrote a lot of the methods that were defined in + # bootstrap/io.rb. So if we try to use IO.reopen on the original IO object, it won't + # be able to reach those original methods anymore. So, we just pass in the file + # descriptor integer directly and wrap it up in a new object. The original object + # will probably get garbage collected but we don't set a finalizer for FDs 0-2 which + # correspond to STDIN, STDOUT and STDERR so we don't need to worry that they'll get + # closed out from under us. + # Hopefully we can find a cleaner way to do this in the future, but for now it's a + # bit ugly. + new_io = IO.open(fd) + new_io.sync = true + + if mode == :read_only + new_io.force_read_only + elsif mode == :write_only + new_io.force_write_only + end + + return new_io + end + end +end diff --git a/gxg/core/iseq.rb b/gxg/core/iseq.rb new file mode 100644 index 0000000..90076b1 --- /dev/null +++ b/gxg/core/iseq.rb @@ -0,0 +1,36 @@ +module Rubinius + class InstructionSequence + def self.allocate + Rubinius.primitive :instruction_sequence_allocate + raise PrimitiveFailure, "Rubinius::InstructionSequence.allocate primitive failed" + end + + def initialize(size) + if size.kind_of? Tuple + @opcodes = size + else + @opcodes = Tuple.new(size) + end + end + + private :initialize + + attr_reader :opcodes + + def ==(other) + other.kind_of?(InstructionSequence) and @opcodes == other.opcodes + end + + def []=(idx, val) + @opcodes[idx] = val + end + + def [](idx) + @opcodes[idx] + end + + def size + @opcodes.size + end + end +end diff --git a/gxg/core/jit.rb b/gxg/core/jit.rb new file mode 100644 index 0000000..2915ff3 --- /dev/null +++ b/gxg/core/jit.rb @@ -0,0 +1,32 @@ +module Rubinius + module JIT + class << self + attr_accessor :properties + + def compile(object, compiled_code, block_environment=nil) + Rubinius.invoke_primitive :jit_compile, self, object, compiled_code, block_environment + end + + def enabled? + Rubinius.primitive :jit_enabled_p + raise PrimitiveFailure, "Rubinius::JIT.enabled? primitive failed" + end + + # TODO: Fix configuration + def compile_threshold + Rubinius.primitive :jit_compile_threshold + raise PrimitiveFailure, "Rubinius::JIT.compile_threshold primitive failed" + end + + def sync=(flag) + Rubinius.primitive :jit_sync_set + raise PrimitiveFailure, "Rubinius::JIT.sync primitive failed" + end + + def sync + Rubinius.primitive :jit_sync_get + raise PrimitiveFailure, "Rubinius::JIT.sync primitive failed" + end + end + end +end diff --git a/gxg/core/kernel.rb b/gxg/core/kernel.rb new file mode 100644 index 0000000..4c05dc1 --- /dev/null +++ b/gxg/core/kernel.rb @@ -0,0 +1,1142 @@ +module Kernel + def equal?(other) + Rubinius.primitive :object_equal + raise PrimitiveFailure, "Kernel#equal? primitive failed" + end + + alias_method :eql?, :equal? + alias_method :==, :equal? + alias_method :===, :equal? + + def !~(other) + res = self =~ other ? false : true + Regexp.last_match = Regexp.last_match + res + end + + def become(other) + Rubinius.primitive :object_become + raise PrimitiveFailure, "Object#become primitive failed" + end + + def singleton_class + Rubinius::Type.object_singleton_class self + end + + def extend(mod) + Rubinius::Type.object_singleton_class(self).include(mod) + self + end + + def freeze + Rubinius.primitive :object_freeze + raise PrimitiveFailure, "Kernel#freeze primitive failed" + end + + def frozen? + Rubinius.primitive :object_frozen_p + raise PrimitiveFailure, "Kernel#frozen? primitive failed" + end + + def hash + Rubinius.primitive :object_hash + raise PrimitiveFailure, "Kernel#hash primitive failed" + end + + def inspect + "#<#{self.class.name}" + end + + def respond_to_prim?(meth, visibility) + Rubinius.primitive :object_respond_to + meth = Rubinius::Type.coerce_to_symbol meth + respond_to_prim?(meth, visibility) + end + + private :respond_to_prim? + + def taint + Rubinius.primitive :object_taint + raise PrimitiveFailure, "Kernel#taint primitive failed" + end + + def tainted? + Rubinius.primitive :object_tainted_p + raise PrimitiveFailure, "Kernel#tainted? primitive failed" + end + + def untaint + Rubinius.primitive :object_untaint + raise PrimitiveFailure, "Kernel#untaint primitive failed" + end + + alias_method :untrust, :taint + alias_method :trust, :untaint + alias_method :untrusted?, :tainted? + + # NOTE: The bootstrap method used to add method definitions to the class + # method_table still returns a CompiledCode instance, so this chaining + # works. + # + # TODO: Fix this chaining by introducing a proper facility to operate on + # methods. + def respond_to?(meth, include_private=false) + respond_to_prim?(meth, include_private) + end + + custom_call_site :respond_to? + + def respond_to_missing?(meth, include_private) + false + end + private :respond_to_missing? + + def yield_gdb(obj) + Rubinius.primitive :yield_gdb + raise PrimitiveFailure, "Kernel#yield_gdb primitive failed" + end + private :yield_gdb + + def Array(obj) + ary = Rubinius::Type.check_convert_type obj, Array, :to_ary + + return ary if ary + + if array = Rubinius::Type.check_convert_type(obj, Array, :to_a) + array + else + [obj] + end + end + module_function :Array + + def Complex(*args) + Rubinius.privately do + Complex.convert(*args) + end + end + module_function :Complex + + def Float(obj) + case obj + when String + Rubinius::Type.coerce_string_to_float obj, true + else + Rubinius::Type.coerce_object_to_float obj + end + end + module_function :Float + + ## + # MRI uses a macro named NUM2DBL which has essentially the same semantics as + # Float(), with the difference that it raises a TypeError and not a + # ArgumentError. It is only used in a few places (in MRI and Rubinius). + #-- + # If we can, we should probably get rid of this. + + def FloatValue(obj) + exception = TypeError.new 'no implicit conversion to float' + + case obj + when String + raise exception + else + begin + Rubinius::Type.coerce_object_to_float obj + rescue + raise exception + end + end + end + private :FloatValue + + def Hash(obj) + return {} if obj.nil? || obj == [] + + if hash = Rubinius::Type.check_convert_type(obj, Hash, :to_hash) + return hash + end + + raise TypeError, "can't convert #{obj.class} into Hash" + end + module_function :Hash + + def Integer(obj, base=nil) + if obj.kind_of? String + if obj.empty? + raise ArgumentError, "invalid value for Integer: (empty string)" + else + base ||= 0 + return obj.to_inum(base, true) + end + end + + if base + raise ArgumentError, "base is only valid for String values" + end + + case obj + when Integer + obj + when Float + if obj.nan? or obj.infinite? + raise FloatDomainError, "unable to coerce #{obj} to Integer" + else + obj.to_int + end + when NilClass + raise TypeError, "can't convert nil into Integer" + else + # Can't use coerce_to or try_convert because I think there is an + # MRI bug here where it will return the value without checking + # the return type. + if obj.respond_to? :to_int + if val = obj.to_int + return val + end + end + + Rubinius::Type.coerce_to obj, Integer, :to_i + end + end + module_function :Integer + + def Rational(a, b = 1) + Rubinius.privately do + Rational.convert a, b + end + end + module_function :Rational + + def String(obj) + return obj if obj.kind_of? String + + unless obj.respond_to?(:to_s) + raise TypeError, "can't convert #{obj.class} into String" + end + + begin + str = obj.to_s + rescue NoMethodError + raise TypeError, "can't convert #{obj.class} into String" + end + + unless str.kind_of? String + raise TypeError, "#to_s did not return a String" + end + + return str + end + module_function :String + + ## + # MRI uses a macro named StringValue which has essentially the same + # semantics as obj.coerce_to(String, :to_str), but rather than using that + # long construction everywhere, we define a private method similar to + # String(). + # + # Another possibility would be to change String() as follows: + # + # String(obj, sym=:to_s) + # + # and use String(obj, :to_str) instead of StringValue(obj) + + def StringValue(obj) + Rubinius::Type.coerce_to obj, String, :to_str + end + module_function :StringValue + + def __method__ + scope = Rubinius::VariableScope.of_sender + + return nil if scope.method.for_module_body? + + name = scope.method.name + + # If the name is still __block__, then it's in a script, so return nil + return nil if name == :__block__ or name == :__script__ + + name + end + module_function :__method__ + + def __callee__ + scope = Rubinius::VariableScope.of_sender + + return nil if scope.method.for_module_body? + + name = scope.name + + # If the name is still __block__, then it's in a script, so return nil + return nil if name == :__block__ or name == :__script__ + + name + end + module_function :__callee__ + + def __dir__ + scope = Rubinius::LexicalScope.of_sender + script = scope.current_script + basepath = script.file_path + fullpath = nil + + return nil unless basepath + + fullpath = if script.data_path + script.data_path + else + Rubinius.privately do + File.basic_realpath(basepath) + end + end + + File.dirname fullpath + end + module_function :__dir__ + + def `(str) #` + str = StringValue(str) unless str.kind_of?(String) + pid, output = Rubinius::Mirror::Process.backtick str + Process.waitpid(pid) + + Rubinius::Type.external_string output + end + module_function :` # ` + + def =~(other) + nil + end + + def <=>(other) + self == other ? 0 : nil + end + + def ===(other) + equal?(other) || self == other + end + + def itself + self + end + + def abort(msg=nil) + Process.abort msg + end + module_function :abort + + def at_exit(prc=nil, &block) + if prc + unless prc.respond_to?(:call) + raise "Argument must respond to #call" + end + else + prc = block + end + + unless prc + raise "must pass a #call'able or block" + end + + Rubinius::AtExit.unshift(prc) + end + module_function :at_exit + + def autoload(name, file) + Object.autoload(name, file) + end + private :autoload + + def autoload?(name) + Object.autoload?(name) + end + private :autoload? + + def block_given? + Rubinius::VariableScope.of_sender.block != nil + end + module_function :block_given? + + alias_method :iterator?, :block_given? + module_function :iterator? + + def caller_inner(start, length) + if start.is_a?(Range) + length = start.size + start = start.begin + end + + # The + 2 is to skip this frame and the caller's. + # + # TODO: plumb length into mri_backtrace to save some work. + full_trace = Rubinius.mri_backtrace(start + 2) + if length + full_trace = full_trace.slice(0, length) + end + full_trace + end + module_function :caller_inner + private :caller_inner + + def caller(start = 1, length = nil) + caller_inner(start, length).map do |tup| + code = tup[0] + line = tup[1] + is_block = tup[2] + name = tup[3] + + "#{code.active_path}:#{line}:in `#{name}'" + end + end + module_function :caller + + ## + # Returns the current call stack as an Array of Thread::Backtrace::Location + # instances. This method is available starting with Ruby 2.0. + # + def caller_locations(start = 1, length = nil) + caller_inner(start, length).map do |tup| + scope = tup[0].scope + abs_path = tup[0].active_path + path = scope ? scope.active_path : abs_path + label = tup[3].to_s + + Thread::Backtrace::Location.new(label, abs_path, path, tup[1]) + end + end + module_function :caller_locations + + def define_singleton_method(*args, &block) + singleton_class.send(:define_method, *args, &block) + end + + def display(port=$>) + port.write self + end + + def exec(*args) + Process.exec(*args) + end + module_function :exec + + def exit(code=0) + Process.exit(code) + end + module_function :exit + + def exit!(code=1) + Process.exit!(code) + end + module_function :exit! + + def fork(&block) + Process.fork(&block) + end + module_function :fork + + def getc + $stdin.getc + end + module_function :getc + + def gets(sep=$/) + ARGF.gets(sep) + end + module_function :gets + + def global_variables + Rubinius::Globals.variables + end + module_function :global_variables + + def initialize_dup(other) + initialize_copy(other) + end + private :initialize_dup + + def initialize_clone(other) + initialize_copy(other) + end + private :initialize_clone + + def initialize_copy(source) + unless instance_of?(Rubinius::Type.object_class(source)) + raise TypeError, "initialize_copy should take same class object" + end + end + private :initialize_copy + + def inspect + prefix = "#<#{self.class}:0x#{self.__id__.to_s(16)}" + + # The protocol here seems odd, but it's to match MRI. + # + # MRI side-calls to the C function that implements Kernel#to_s. If that + # method is overridden, the new Ruby method is never called. So, we inline + # the code for Kernel#to_s here because we simply dispatch to Ruby + # methods. + ivars = Rubinius.invoke_primitive :object_instance_variables, self + + if ivars.empty? + return Rubinius::Type.infect "#{prefix}>", self + end + + # Otherwise, if it's already been inspected, return the ... + return "#{prefix} ...>" if Thread.guarding? self + + # Otherwise, gather the ivars and show them. + parts = [] + + Thread.recursion_guard self do + ivars.each do |var| + parts << "#{var}=#{__instance_variable_get__(var).inspect}" + end + end + + if parts.empty? + str = "#{prefix}>" + else + str = "#{prefix} #{parts.join(' ')}>" + end + + Rubinius::Type.infect(str, self) + + return str + end + + ## + # Returns true if this object is an instance of the given class, otherwise + # false. Raises a TypeError if a non-Class object given. + # + # Module objects can also be given for MRI compatibility but the result is + # always false. + + def instance_of?(cls) + Rubinius.primitive :object_instance_of + + arg_class = Rubinius::Type.object_class(cls) + if arg_class != Class and arg_class != Module + # We can obviously compare against Modules but result is always false + raise TypeError, "instance_of? requires a Class argument" + end + + Rubinius::Type.object_class(self) == cls + end + + def instance_variable_get(sym) + Rubinius.primitive :object_get_ivar + + sym = Rubinius::Type.ivar_validate sym + instance_variable_get sym + end + + alias_method :__instance_variable_get__, :instance_variable_get + + def instance_variable_set(sym, value) + Rubinius.primitive :object_set_ivar + + sym = Rubinius::Type.ivar_validate sym + instance_variable_set sym, value + end + + alias_method :__instance_variable_set__, :instance_variable_set + + def instance_variables + Rubinius.invoke_primitive :object_instance_variables, self + end + + def instance_variable_defined?(name) + Rubinius.primitive :object_ivar_defined + + instance_variable_defined? Rubinius::Type.ivar_validate(name) + end + + # Both of these are for defined? when used inside a proxy obj that + # may undef the regular method. The compiler generates __ calls. + alias_method :__instance_variable_defined_p__, :instance_variable_defined? + alias_method :__respond_to_p__, :respond_to? + + alias_method :is_a?, :kind_of? + + def lambda + env = nil + + Rubinius.asm do + push_block + # assign a pushed block to the above local variable "env" + # Note that "env" is indexed at 0. + set_local 0 + end + + raise ArgumentError, "block required" unless env + + prc = Rubinius::Mirror::Proc.from_block ::Proc, env + + # Make a proc lambda only when passed an actual block (ie, not using the + # "&block" notation), otherwise don't modify it at all. + prc.lambda_style! if env.is_a?(Rubinius::BlockEnvironment) + + return prc + end + module_function :lambda + + def load(name, wrap=false) + Rubinius::CodeLoader.new(name).load(wrap) + # cl = Rubinius::CodeLoader.new(name) + # cl.load(wrap) + + # Rubinius.run_script cl.compiled_code + + # Rubinius::CodeLoader.loaded_hook.trigger!(name) + + # return true + end + module_function :load + + def loop + return to_enum(:loop) { Float::INFINITY } unless block_given? + + begin + while true + yield + end + rescue StopIteration => e + e.result + end + end + module_function :loop + + def method(name) + name = Rubinius::Type.coerce_to_symbol name + code = Rubinius.find_method(self, name) + + if code + Method.new(self, code[1], code[0], name) + elsif respond_to_missing?(name, true) + Method.new(self, self.class, Rubinius::MissingMethod.new(self, name), name) + else + raise NameError, "undefined method `#{name}' for class #{self.class}" + end + end + + def methods(all=true) + methods = singleton_methods(all) + + if all + # We have to special case these because unlike true, false, nil, + # Type.object_singleton_class raises a TypeError. + case self + when Fixnum, Symbol + methods |= Rubinius::Type.object_class(self).instance_methods(true) + else + methods |= Rubinius::Type.object_singleton_class(self).instance_methods(true) + end + end + + return methods if kind_of?(ImmediateValue) + + undefs = [] + Rubinius::Type.object_singleton_class(self).method_table.filter_entries do |entry| + undefs << entry.name.to_s if entry.visibility == :undef + end + + return methods - undefs + end + + def object_id + Rubinius.primitive :object_id + raise PrimitiveFailure, "Kernel#object_id primitive failed" + end + + def open(obj, *rest, &block) + if obj.respond_to?(:to_open) + obj = obj.to_open(*rest) + + if block_given? + return yield(obj) + else + return obj + end + end + + path = Rubinius::Type.coerce_to_path obj + + if path.kind_of? String and path.prefix? '|' + return IO.popen(path[1..-1], *rest, &block) + end + + File.open(path, *rest, &block) + end + module_function :open + + def p(*a) + return nil if a.empty? + a.each { |obj| $stdout.puts obj.inspect } + $stdout.flush + + a.size == 1 ? a.first : a + end + module_function :p + + def print(*args) + args.each do |obj| + $stdout.write obj.to_s + end + nil + end + module_function :print + + def printf(target, *args) + if target.kind_of?(String) + output = $stdout + else + output = target + target = args.shift + end + output.write Rubinius::Sprinter.get(target).call(*args) + nil + end + module_function :printf + + def private_methods(all=true) + private_singleton_methods() | Rubinius::Type.object_class(self).private_instance_methods(all) + end + + def private_singleton_methods + sc = Rubinius::Type.object_singleton_class self + methods = sc.method_table.private_names + + m = sc + + while m = m.direct_superclass + unless Rubinius::Type.object_kind_of?(m, Rubinius::IncludedModule) or + Rubinius::Type.singleton_class_object(m) + break + end + + methods.concat m.method_table.private_names + end + + methods + end + private :private_singleton_methods + + def proc + env = nil + + Rubinius.asm do + push_block + # assign a pushed block to the above local variable "env" + set_local 0 + end + + unless env + # Support for ancient pre-block-pass style: + # def something + # proc + # end + # something { a_block } => Proc instance + env = Rubinius::BlockEnvironment.of_sender + + unless env + raise ArgumentError, "tried to create a Proc object without a block" + end + end + + Proc.new(&env) + end + module_function :proc + + def protected_methods(all=true) + protected_singleton_methods() | Rubinius::Type.object_class(self).protected_instance_methods(all) + end + + def protected_singleton_methods + m = Rubinius::Type.object_singleton_class self + methods = m.method_table.protected_names + + while m = m.direct_superclass + unless Rubinius::Type.object_kind_of?(m, Rubinius::IncludedModule) or + Rubinius::Type.singleton_class_object(m) + break + end + + methods.concat m.method_table.protected_names + end + + methods + end + private :protected_singleton_methods + + def public_method(name) + name = Rubinius::Type.coerce_to_symbol name + code = Rubinius.find_public_method(self, name) + + if code + Method.new(self, code[1], code[0], name) + elsif respond_to_missing?(name, false) + Method.new(self, self.class, Rubinius::MissingMethod.new(self, name), name) + else + raise NameError, "undefined method `#{name}' for #{self.inspect}" + end + end + + def public_methods(all=true) + public_singleton_methods | Rubinius::Type.object_class(self).public_instance_methods(all) + end + + def public_send(message, *args) + Rubinius.primitive :object_public_send + raise PrimitiveFailure, "Kernel#public_send primitive failed" + end + + def public_singleton_methods + m = Rubinius::Type.object_singleton_class self + methods = m.method_table.public_names + + while m = m.direct_superclass + unless Rubinius::Type.object_kind_of?(m, Rubinius::IncludedModule) or + Rubinius::Type.singleton_class_object(m) + break + end + + methods.concat m.method_table.public_names + end + + methods + end + private :public_singleton_methods + + def putc(int) + $stdout.putc(int) + end + module_function :putc + + def puts(*a) + $stdout.puts(*a) + nil + end + module_function :puts + + def rand(limit=0) + if limit == 0 + return Thread.current.randomizer.random_float + end + + if limit.kind_of?(Range) + return Thread.current.randomizer.random(limit) + else + limit = Integer(limit).abs + + if limit == 0 + Thread.current.randomizer.random_float + else + Thread.current.randomizer.random_integer(limit - 1) + end + end + end + module_function :rand + + def readline(sep=$/) + ARGF.readline(sep) + end + module_function :readline + + def readlines(sep=$/) + ARGF.readlines(sep) + end + module_function :readlines + + def remove_instance_variable(sym) + Rubinius.primitive :object_del_ivar + + # If it's already a symbol, then we're here because it doesn't exist. + if sym.kind_of? Symbol + raise NameError, "instance variable '#{sym}' not defined" + end + + # Otherwise because sym isn't a symbol, coerce it and try again. + remove_instance_variable Rubinius::Type.ivar_validate(sym) + end + + def require(name) + Rubinius::CodeLoader.new(name).require + end + module_function :require + + def require_relative(name) + scope = Rubinius::LexicalScope.of_sender + Rubinius::CodeLoader.new(name).require_relative(scope) + end + module_function :require_relative + + def select(*args) + IO.select(*args) + end + module_function :select + + def send(message, *args) + Rubinius.primitive :object_send + raise PrimitiveFailure, "Kernel#send primitive failed" + end + + def set_trace_func(*args) + raise NotImplementedError + end + module_function :set_trace_func + + def sprintf(str, *args) + Rubinius::Sprinter.get(str).call(*args) + end + module_function :sprintf + + alias_method :format, :sprintf + module_function :format + + def sleep(duration=undefined) + unless undefined.equal? duration + if duration.kind_of? Numeric + duration = Rubinius::Type.coerce_to duration, Float, :to_f + else + raise TypeError, 'time interval must be a numeric value' + end + end + + Rubinius.invoke_primitive :thread_suspend, Thread.current, duration + end + module_function :sleep + + def srand(seed=undefined) + if undefined.equal? seed + seed = Thread.current.randomizer.generate_seed + end + + seed = Rubinius::Type.coerce_to seed, Integer, :to_int + Thread.current.randomizer.swap_seed seed + end + module_function :srand + + def tap + yield self + self + end + + def test(cmd, file1, file2=nil) + case cmd + when ?d + File.directory? file1 + when ?e + File.exist? file1 + when ?f + File.file? file1 + when ?l + File.symlink? file1 + when ?r + File.readable? file1 + when ?R + File.readable_real? file1 + when ?w + File.writable? file1 + when ?W + File.writable_real? file1 + when ?A + File.atime file1 + when ?C + File.ctime file1 + when ?M + File.mtime file1 + else + raise NotImplementedError, "command ?#{cmd.chr} not implemented" + end + end + module_function :test + + def to_enum(method=:each, *args, &block) + Enumerator.new(self, method, *args).tap do |enum| + Rubinius.privately { enum.size = block } if block_given? + end + end + alias_method :enum_for, :to_enum + + def trap(sig, prc=nil, &block) + Signal.trap(sig, prc, &block) + end + module_function :trap + + def singleton_method_added(name) + end + private :singleton_method_added + + def singleton_method_removed(name) + end + private :singleton_method_removed + + def singleton_method_undefined(name) + end + private :singleton_method_undefined + + def singleton_methods(all=true) + m = Rubinius::Type.object_singleton_class self + mt = m.method_table + methods = mt.public_names + mt.protected_names + + if all + while m = m.direct_superclass + unless Rubinius::Type.object_kind_of?(m, Rubinius::IncludedModule) or + Rubinius::Type.singleton_class_object(m) + break + end + + mt = m.method_table + methods.concat mt.public_names + methods.concat mt.protected_names + end + end + + methods.uniq + end + + def spawn(*args) + Process.spawn(*args) + end + module_function :spawn + + def syscall(*args) + raise NotImplementedError + end + module_function :syscall + + def system(*args) + begin + pid = Process.spawn(*args) + rescue SystemCallError + return nil + end + + Process.waitpid pid + $?.exitstatus == 0 + end + module_function :system + + def trace_var(name, cmd = nil, &block) + if !cmd && !block + raise( + ArgumentError, + 'The 2nd argument should be a Proc/String, alternatively use a block' + ) + end + + # We have to use a custom proc since set_hook passes in both the variable + # name and value. + set = proc do |_, value| + if cmd.is_a?(String) + eval(cmd) + + # In MRI if one passes both a proc in `cmd` and a block the latter will + # be ignored. + elsif cmd.is_a?(Proc) + cmd.call(value) + + elsif block + block.call(value) + end + end + + Rubinius::Globals.set_hook(name, :[], set) + end + module_function :trace_var + + # In MRI one can specify a 2nd argument to remove a specific tracer. + # Rubinius::Globals however only supports one hook per variable, hence the + # 2nd dummy argument. + def untrace_var(name, *args) + Rubinius::Globals.remove_hook(name) + end + module_function :untrace_var + + def warn(*messages) + $stderr.puts(*messages) if !$VERBOSE.nil? && !messages.empty? + nil + end + module_function :warn + + def warning(message) + $stderr.puts message if $VERBOSE + end + module_function :warning + + def catch(obj = Object.new, &block) + raise LocalJumpError unless block_given? + + Rubinius::ThrownValue.register(obj) do + return Rubinius.catch(obj, block) + end + end + module_function :catch + + def throw(obj, value=nil) + unless Rubinius::ThrownValue.available? obj + exc = UncaughtThrowError.new "uncaught throw #{obj.inspect}" + exc.tag = obj + raise exc + end + + Rubinius.throw obj, value + end + module_function :throw + + # Names of local variables at point of call (including evaled) + # + def local_variables + scope = Rubinius::VariableScope.of_sender + scope.local_variables + end + module_function :local_variables + + # Obtain binding here for future evaluation/execution context. + # + def binding + return Binding.setup( + Rubinius::VariableScope.of_sender, + Rubinius::CompiledCode.of_sender, + Rubinius::LexicalScope.of_sender, + Rubinius::VariableScope.of_sender.self, + Rubinius::Location.of_closest_ruby_method + ) + end + module_function :binding + + # Evaluate and execute code given in the String. + # + def eval(string, binding=nil, filename=nil, lineno=nil) + string = StringValue(string) + filename = StringValue(filename) if filename + lineno = Rubinius::Type.coerce_to lineno, Fixnum, :to_i if lineno + lineno = 1 if filename && !lineno + + if binding + binding = Rubinius::Type.coerce_to_binding binding + filename ||= binding.lexical_scope.active_path + else + binding = Binding.setup(Rubinius::VariableScope.of_sender, + Rubinius::CompiledCode.of_sender, + Rubinius::LexicalScope.of_sender, + self) + + filename ||= "(eval)" + end + + lineno ||= binding.line_number + + existing_scope = binding.lexical_scope + binding.lexical_scope = existing_scope.dup + + c = Rubinius::ToolSets::Runtime::Compiler + be = c.construct_block string, binding, filename, lineno + + result = be.call_on_instance(binding.receiver) + binding.lexical_scope = existing_scope + result + end + module_function :eval + private :eval +end diff --git a/gxg/core/lexical_scope.rb b/gxg/core/lexical_scope.rb new file mode 100644 index 0000000..5466679 --- /dev/null +++ b/gxg/core/lexical_scope.rb @@ -0,0 +1,181 @@ +## +# A linked list that details the static, lexical scope the method was created +# in. +# +# TODO: document + +module Rubinius + class LexicalScope + def self.of_sender + Rubinius.primitive :lexical_scope_of_sender + raise PrimitiveFailure, "primitive failed" + end + + def class_variable_get(sym) + Rubinius.primitive :lexical_scope_cvar_get + + raise NameError, "Invalid class variable name '#{sym}'" + end + + def class_variable_set(sym, val) + Rubinius.primitive :lexical_scope_cvar_set + + raise NameError, "Invalid class variable name '#{sym}'" + end + + def class_variable_defined?(sym) + Rubinius.primitive :lexical_scope_cvar_defined + + raise NameError, "Invalid class variable name '#{sym}'" + end + + def class_variable_get_or_set(sym, val) + Rubinius.primitive :lexical_scope_cvar_get_or_set + + raise NameError, "Invalid class variable name '#{sym}'" + end + + # + # @todo Verify the recursion here does not cause problems. --rue + # + def initialize(mod, par = nil) + @module = mod + @parent = par + end + + private :initialize + + # Source code of this scope. + attr_accessor :script + + # The function map for this scope references its top-most parent's map + attr_accessor :functions + + # Module or class this lexical scope enclosed into. + attr_reader :module + + # Constant scope object this scope enclosed into. + attr_reader :parent + + # Module or class representing the 'current class'. MRI manipulates + # this outside of the lexical scope and uses it for undef and method + # definition. + attr_accessor :current_module + + # Set to indicate that no methods may be add to this scope + attr_accessor :disabled_for_methods + + # Lazy initialized hash map used for flip-flops + attr_accessor :flip_flops + + def const_defined?(name, search_parents=true) + scope = self + while scope and scope.module != Object + return true if scope.module.const_defined?(name) + scope = scope.parent + end + + return Object.const_defined?(name) + end + + def inspect + current = " current=#{@current_module}" if @current_module + "#<#{self.class.name}:0x#{self.object_id.to_s(16)} parent=#{@parent.inspect} module=#{@module}#{current}>" + end + + def to_s + inspect + end + + # Indicates if this is a toplevel/default scope + def top_level? + !@parent + end + + # Use the same info as the current LexicalScope, but set current_module to + # +mod+. Chains off the current LexicalScope. + def using_current_as(mod) + if top_level? + # Don't chain up if this is a toplevel, create a new one. + cs = dup + else + cs = LexicalScope.new @module, self + end + + cs.current_module = mod + return cs + end + + def using_disabled_scope + cs = using_current_as(@module) + cs.disabled_for_methods = true + return cs + end + + def for_method_definition + if @disabled_for_methods + raise TypeError, "unable to create methods in this scope" + end + + return @current_module if @current_module + return @module + end + + def alias_method(name, original) + Rubinius.privately do + for_method_definition.alias_method name, original + end + end + + def __undef_method__(name) + Rubinius.privately do + for_method_definition.undef_method name + end + end + + def active_path + script = current_script + if script + if path = script.file_path + return path.dup + end + end + + return "__unknown__.rb" + end + + def absolute_active_path + script = current_script + if script + if path = script.data_path + return path.dup + end + end + + return nil + end + + def data_path + script = current_script + if script and script.main? + if path = script.data_path + return path.dup + end + end + + return nil + end + + def current_script + scope = self + while scope and !scope.script + scope = scope.parent + end + return scope && scope.script + end + + def const_set(name, value) + @module.const_set name, value + end + end +end diff --git a/gxg/core/library.rb b/gxg/core/library.rb new file mode 100644 index 0000000..51c3c54 --- /dev/null +++ b/gxg/core/library.rb @@ -0,0 +1,319 @@ +class Rubinius::NativeFunction + attr_accessor :return_type + attr_accessor :argument_types +end + +module Rubinius + module FFI + def self.generate_function(ptr, name, args, ret) + Rubinius.primitive :nativefunction_generate + raise PrimitiveFailure, "FFI.generate_function primitive failed" + end + + def self.generate_trampoline(obj, name, args, ret) + Rubinius.primitive :nativefunction_generate_tramp + raise PrimitiveFailure, "FFI.generate_function_tramp primitive failed" + end + + module Library + LIBC = Rubinius::LIBC + + # Set which library or libraries +attach_function+ should + # look in. By default it only searches for the function in + # the current process. If you want to specify this as one + # of the locations, add FFI::USE_THIS_PROCESS_AS_LIBRARY. + # The libraries are tried in the order given. + # + def ffi_lib(*names) + @ffi_lib = names.map do |x| + if x == FFI::CURRENT_PROCESS + DynamicLibrary::CURRENT_PROCESS + + # When the element is an array, it's an ordered choice, + # ie, pick the first library that works. + elsif x.kind_of? Array + lib = nil + + x.each do |name| + begin + lib = DynamicLibrary.new(name, @ffi_lib_flags) + break + rescue LoadError + end + end + + # If .new worked, then lib is set and we can use it. + unless lib + raise LoadError, "Unable to find library among: #{x.inspect}" + end + + lib + else + DynamicLibrary.new(x) + end + end + end + + def ffi_libraries + @ffi_lib or [DynamicLibrary::CURRENT_PROCESS] + end + private :ffi_libraries + + # Flags used in {#ffi_lib}. + # + # This map allows you to supply symbols to {#ffi_lib_flags} instead of + # the actual constants. + def flags_map + { + :global => DynamicLibrary::RTLD_GLOBAL, + :local => DynamicLibrary::RTLD_LOCAL, + :lazy => DynamicLibrary::RTLD_LAZY, + :now => DynamicLibrary::RTLD_NOW + } + end + + # Sets library flags for {#ffi_lib}. + # + # @example + # ffi_lib_flags(:lazy, :local) # => 5 + # + # @param [Symbol, …] flags (see {FlagsMap}) + # @return [Fixnum] the new value + def ffi_lib_flags(*flags) + @ffi_lib_flags = flags.inject(0) { |result, f| result | flags_map[f] } + end + + # Attach a C function to this module. The arguments can have two forms: + # + # attach_function c_name, [c_arg1, c_arg2], ret + # attach_function mod_name, c_name, [c_arg1, c_arg2], ret + # + # In the first form, +c_name+ will also be used for the name of the module + # method. In the second form, the module method name is +mod_name+. + # + # In either form, an optional options hash will be accepted as an + # additional argument, although currently all options are ignored. + # + # The +c_name+ and +mod_name+ can be given as Strings or Symbols. + # + # The types of the arguments to the C function, +c_arg1+, +c_arg2+, etc, are + # given as an array even if there is only one. + # + # The final argument, +ret+, is the type of the return value from the C + # function. + def attach_function(name, a2, a3, a4=nil, a5=nil) + if a4 && (a2.kind_of?(String) || a2.kind_of?(Symbol)) + cname = a2.to_s + args = a3 + ret = a4 + else + cname = name.to_s + args = a2 + ret = a3 + end + + mname = name.to_sym + + ffi_libraries.each do |lib| + if ptr = lib.find_symbol(cname) + return pointer_as_function(mname, ptr, args, ret) + end + end + + ffi_function_missing cname, mname, args, ret + end + + # Attach a C variable to this module. The arguments can have two forms: + # + # attach_variable mod_name, type + # attach_variable mod_name, c_name, type + # + # In the first form, +mod_name+ will also be used for the name of the C variable. + # In the second form, the C variable name is +c_name+. + # + # The +mod_name+ and +c_name+ can be given as Strings or Symbols. + # + # The final argument, +type+, is the type of the C variable + def attach_variable(mname, a1, a2=nil) + cname, type = a2 ? [ a1, a2 ] : [ mname.to_s, a1 ] + ptr = nil + + ffi_libraries.each do |lib| + ptr = lib.find_symbol(cname.to_s) + break unless ptr.nil? + end + + raise FFI::NotFoundError, "Unable to find '#{cname}'" if ptr.nil? || ptr.null? + + if type.kind_of?(Class) and type.ancestors.include?(FFI::Struct) + c = type.new(ptr) + + self.module_eval <<-code, __FILE__, __LINE__ + @ffi_gvar_#{mname} = c + def self.#{mname} + @ffi_gvar_#{mname} + end + code + + else + enclosing_module = self + + cs = Class.new(FFI::Struct) do + @enclosing_module = enclosing_module + end + + cs.layout :gvar, type + c = cs.new(ptr) + + self.module_eval <<-code, __FILE__, __LINE__ + @ffi_gvar_#{mname} = c + def self.#{mname} + @ffi_gvar_#{mname}[:gvar] + end + def self.#{mname}=(value) + @ffi_gvar_#{mname}[:gvar] = value + end + code + + end + + return ptr + end + + # Generic error method attached in place of missing foreign functions + # during loading the core library. See core/zed.rb for a version that + # raises immediately if the foreign function is unavaiblable. + def ffi_function_not_implemented(*args) + raise NotImplementedError, "function not implemented on this platform" + end + + # Protocol for attaching foregin functions. If #attach_function fails to + # find a foreign function, this method will be called. Client code can + # provide an override to customize features. + def ffi_function_missing(cname, mname, args, ret) + if func = Rubinius.find_method(self, :ffi_function_not_implemented) + func = func[0].dup + func.name = cname.to_sym + add_function mname, func + end + end + + def add_function(name, func) + # Make it available as a method callable directly.. + sc = Rubinius::Type.object_singleton_class(self) + Rubinius::VM.reset_method_cache sc, name + + sc.method_table.store name, nil, func, nil, 0, :public + + # and expose it as a private method for people who + # want to include this module. + method_table.store name, nil, func, nil, 0, :private + end + private :add_function + + def pointer_as_function(name, ptr, args, ret) + args.map! { |a| find_type a } + + if func = FFI.generate_function(ptr, name.to_sym, args, find_type(ret)) + add_function name, func + return func + end + + raise FFI::NotFoundError, "Unable to attach pointer" + end + + def callback(a1, a2, a3=nil) + if a3 + name = a1 + params = a2 + ret = a3 + else + name = nil + params = a1 + ret = a2 + end + + args = params.map { |x| find_type(x) } + + func, ptr = FFI.generate_trampoline nil, :ffi_tramp, + args, find_type(ret) + + func.argument_types = params + func.return_type = ret + + if name + @ffi_callbacks ||= {} + @ffi_callbacks[name] = func + + typedef func, name + end + + return func + end + + def typedef(old, add) + @typedefs ||= Rubinius::LookupTable.new + @typedefs[add] = find_type(old) + end + + def find_type(name) + @typedefs ||= Rubinius::LookupTable.new + + if name.kind_of? Rubinius::NativeFunction or name.kind_of? FFI::Enum + return name + end + + if type = @typedefs[name] + return type + end + + FFI.find_type(name) + end + + def enum(*args) + @tagged_enums ||= Rubinius::LookupTable.new + @anon_enums ||= Array.new + + tag, values = if args[0].kind_of?(Symbol) && args[1].kind_of?(Array) + [ args[0], args[1] ] + elsif args[0].kind_of?(Array) + [ nil, args[0] ] + else + [ nil, args ] + end + + enum = FFI::Enum.new values, tag + + if tag + typedef(enum, tag) + @tagged_enums[tag] = enum + else + @anon_enums << enum + end + + return enum + end + + def enum_type(tag) + + if enum = @tagged_enums[tag] + enum + else + @anon_enums.detect { |enum| enum.symbols.include?(tag) } + end + + end + + def enum_value(value) + + if enum = @anon_enums.detect { |enum| enum.symbols.include?(value) } + enum + else + tag,enum = @tagged_enums.detect { |tag,enum| enum.symbols.include?(value) } + return enum + end + + end + end + end +end diff --git a/gxg/core/load_order.txt b/gxg/core/load_order.txt new file mode 100644 index 0000000..a7406db --- /dev/null +++ b/gxg/core/load_order.txt @@ -0,0 +1,130 @@ +alpha.rb +alias.rb +argf.rb +array.rb +array_mirror.rb +atomic.rb +autoload.rb +backtrace.rb +basic_object.rb +binding.rb +block_environment.rb +byte_array.rb +call_site.rb +capi.rb +channel.rb +character.rb +class.rb +code_db.rb +code_loader.rb +compact_lookup_table.rb +comparable.rb +compiled_code.rb +complex.rb +complexifier.rb +configuration.rb +console.rb +constant_cache.rb +lexical_scope.rb +constant_table.rb +continuation.rb +ctype.rb +delegated_method.rb +dir.rb +dir_glob.rb +encoding.rb +enumerable.rb +enumerable_helper.rb +enumerator.rb +env.rb +errno.rb +exception.rb +executable.rb +false.rb +ffi.rb +fiber.rb +file.rb +file_test.rb +float.rb +fsevent.rb +gc.rb +global.rb +hash.rb +hook.rb +identity_map.rb +immediate.rb +integer.rb +io.rb +iseq.rb +jit.rb +kernel.rb +library.rb +location.rb +logger.rb +lookup_table.rb +lru_cache.rb +main.rb +marshal.rb +match_data.rb +math.rb +method.rb +method_equality.rb +method_table.rb +metrics.rb +mirror.rb +missing_method.rb +module.rb +mutex.rb +native_method.rb +nil.rb +numeric.rb +numeric_mirror.rb +object_space.rb +options.rb +pack.rb +peg.rb +pointer.rb +pointer_accessors.rb +prediction.rb +proc.rb +proc_mirror.rb +process.rb +process_mirror.rb +random.rb +range.rb +range_mirror.rb +rational.rb +rationalizer.rb +regexp.rb +respond_to_cache.rb +rubinius.rb +runtime.rb +signal.rb +splitter.rb +sprinter.rb +stat.rb +stats.rb +string.rb +string_mirror.rb +struct.rb +symbol.rb +thread.rb +thread_group.rb +thread_group_mirror.rb +thread_mirror.rb +throw_catch.rb +thunk.rb +time.rb +true.rb +tuple.rb +type.rb +undefined.rb +variable_scope.rb +vm.rb +weakref.rb +zed.rb +ruby_constants.rb +build_config.rb +rbconfig.rb +deprecations.rb +loader.rb diff --git a/gxg/core/loader.rb b/gxg/core/loader.rb new file mode 100644 index 0000000..4668e83 --- /dev/null +++ b/gxg/core/loader.rb @@ -0,0 +1,872 @@ +TOPLEVEL_BINDING = binding() + +module Rubinius + class Loader + def initialize + @exit_code = 0 + @process_id = Process.pid + @load_paths = [] + @requires = [] + @evals = [] + @script = nil + @debugging = false + @repl = true + @printed_version = false + @input_loop = false + @input_loop_print = false + @input_loop_split = false + @simple_options = false + @early_option_stop = false + @check_syntax = false + @load_profiler = false + @enable_gems = true + @load_gemfile = false + @deprecations = false + + version = RUBY_VERSION.split(".").first(2).join(".") + @gem_bins = ["#{version}/bin", "bin"].map do |dir| + File.join Rubinius::GEMS_PATH, dir + end + end + + private :initialize + + def self.debugger + @debugger_proc + end + + def self.debugger=(prc) + @debugger_proc = prc + end + + # Finish setting up after loading the core library. + def preamble + set_program_name "rbx" + @stage = "running Loader preamble" + + # Set the default visibility for the top level binding + TOPLEVEL_BINDING.variables.method_visibility = :private + + $VERBOSE = false + end + + # Set up $LOAD_PATH. + def system_load_path + @stage = "setting up system load path" + + @lib_bin = "#{Rubinius::CODEDB_PATH}/source/bin" + + $LOAD_PATH.unshift(Rubinius::ARCH_PATH, + Rubinius::SITE_PATH, + "#{Rubinius::SITE_PATH}/#{RUBY_VERSION}", + Rubinius::VENDOR_PATH, + "#{Rubinius::CODEDB_PATH}/source") + + if libs = ENV['RUBYLIB'] + Rubinius::Logger.system.write "RUBYLIB: #{libs}" + + $LOAD_PATH.unshift(*libs.split(File::PATH_SEPARATOR).compact) + end + end + + # Load customization code: + # /etc/rbxrc + # $HOME/.rbxrc + # $RBX_PRELOAD + def preload + @stage = "preloading rbxrc code" + + ['/etc/rbxrc',"#{ENV['HOME']}/.rbxrc",ENV['RBX_PRELOAD']].each do |file| + begin + load file if file and File.exist?(file) + rescue LoadError + nil + end + end + end + + # Register signal handlers. + def signals + @stage = "registering signal handlers" + + # Set up a handler for SIGINT that raises Interrupt on the main thread + Signal.trap("INT") do |sig| + raise Interrupt, "Thread has been interrupted" + end + + ["HUP", "QUIT", "TERM", "ALRM", "USR1", "USR2"].each do |signal| + Signal.trap(signal) do |sig| + raise SignalException, sig + end + end + end + + def show_syntax_error(e) + STDERR.puts "A syntax error has occurred:" + STDERR.puts " #{e.message}" + STDERR.puts "\nCode:\n#{e.code}" + if e.column + STDERR.puts((" " * (e.column - 1)) + "^") + end + end + + def show_syntax_errors(syns) + syns.each do |e| + STDERR.puts "#{e.file}:#{e.line}: #{e.reason}" + end + end + + # Checks if a subcommand with basename +base+ exists. Returns the full + # path to the subcommand if it does; otherwise, returns nil. + def find_subcommand(base) + command = File.join @lib_bin, "#{base}.rb" + return command if File.exist? command + return nil + end + + # Checks if a gem wrapper named +base+ exists. Returns the full path to + # the gem wrapper if it does; otherwise, returns nil. + def find_gem_wrapper(base) + @gem_bins.each do |dir| + wrapper = File.join dir, base + return wrapper if File.exist? wrapper + end + return nil + end + + # Detects if the Rubinius executable was aliased to a subcommand or a + # rubygems executable. If so, changes ARGV as if Rubinius were invoked + # to run the subcommand or rubygems executable. + def detect_alias + cmd = ARG0.split("/").last + + # ignore the common ones straight away + return if cmd == "rbx" or cmd == "ruby" + + # Check if we are aliased to a Rubinius subcommand. + if subcommand = find_subcommand(cmd) + ARGV.unshift subcommand + return + end + + # Check if we are aliased to a rubygems executable. + if gem_wrapper = find_gem_wrapper(cmd) + ARGV.unshift "-S", gem_wrapper + end + end + + def handle_simple_options(argv) + argv.delete_if do |x| + if x[0] == ?- + if equal = x.index("=") + name = x.substring(1, equal-1) + + equal += 1 + val = x.substring(equal, x.size - equal) + else + name = x.substring(1, x.size - 1) + val = true + end + + name.gsub! "-", "_" + + Rubinius::Globals[:"$#{name}"] = val + + true + else + false + end + end + end + + def define_global_methods + Kernel.module_eval do + def gsub(pattern, rep=nil, &block) + target = $_ + raise TypeError, "$_ must be a String, but is #{target.inspect}" unless target.kind_of? String + $_ = target.gsub(pattern, rep, &block) + end + module_function :gsub + + def sub(pattern, rep=nil, &block) + target = $_ + raise TypeError, "$_ must be a String, but is #{target.inspect}" unless target.kind_of? String + $_ = target.sub(pattern, rep, &block) + end + module_function :sub + + def chomp(string=$/) + raise TypeError, "$_ must be a String" unless $_.kind_of? String + $_ = $_.chomp(string) + end + module_function :chomp + + def chop + raise TypeError, "$_ must be a String" unless $_.kind_of? String + $_ = $_.chop + end + module_function :chop + end + end + + # Process all command line arguments. + def options(argv=ARGV) + @stage = "processing command line arguments" + + options = Options.new "Usage: rbx [subcommand] [options] [--] [script] [arguments]", 25 + + options.left_align + options.on_extra do |x| + raise Options::ParseError, "Unrecognized option: #{x}" if x[0] == ?- + if @script.nil? and @evals.empty? + @script = x + else + argv.unshift x + end + options.stop_parsing + end + + options.doc "Script is any valid Ruby source code file" + + options.doc "\nRuby options" + options.on "-", "Read and evaluate code from STDIN" do + @repl = false + set_program_name "-" + stdin = STDIN.dup + script = STDIN.read + STDIN.reopen(stdin) + execute_script script + end + + options.on "--", "Stop processing command line arguments" do + @early_option_stop = true + options.stop_parsing + end + + options.on "-a", "Used with -n and -p, splits $_ into $F" do + @input_loop_split = true + Rubinius::Globals.set!(:$-a, true) + end + + options.on "-c", "Only check the syntax" do + @repl = false + @check_syntax = true + end + + options.on "-C", "DIR", "Change directory to DIR before running scripts" do |dir| + @directory = dir + end + + options.on "-d", "Enable debugging output and set $DEBUG to true" do + $VERBOSE = true + $DEBUG = true + end + + options.on "--disable-gems", "Do not automatically load rubygems on startup" do + @enable_gems = false + end + + options.on "-e", "CODE", "Compile and execute CODE" do |code| + @repl = false + set_program_name "(eval)" + @evals << code + end + + options.on '-F', "PATTERN", "Set $; to PATTERN" do |pattern| + Rubinius::Globals.set!(:$;, Regexp.new(pattern)) + end + + options.on '-G', '--gemfile', 'Respect a Gemfile in the current path' do + @load_gemfile = true + end + + options.on "-h", "--help", "Display this help" do + @repl = false + puts options + done + end + + options.on "-i", "[EXT]", "Edit ARGV files in place, making backup with EXT" do |ext| + # in place edit mode + ext ||= '' + $-i = ext + end + + options.on "-I", "DIR1[:DIR2]", "Add directories to $LOAD_PATH" do |dir| + @load_paths << dir + end + + options.on "-K", "Ignored $KCODE option for compatibility" do + msg = "The -K option is deprecated. Rubinius internal encoding is always UTF-8" + STDERR.puts msg + Rubinius::Logger.system.warn msg + end + + options.on "-U", "Set Encoding.default_internal to UTF-8" do + msg = "The -U option is deprecated. Rubinius internal encoding is always UTF-8" + STDERR.puts msg + Rubinius::Logger.system.warn msg + end + + options.on "-E", "ENC", "Set external:internal character encoding to ENC" do |enc| + external, internal = enc.split(":") + Encoding.default_external = external if external and !external.empty? + + if internal and !internal.empty? + msg = "The -E option setting internal encoding is deprecated. Rubinius internal encoding is always UTF-8" + STDERR.puts msg + Rubinius::Logger.system.warn msg + end + end + + options.on "--main", "PATH", "Load PATH directly from CodeDB" do |path| + code = Rubinius::CoreDB.load_feature path, "", false, false + if code + code.create_script false + Rubinius.run_script code + exit 0 + else + puts "file not found: #{path}" + exit 1 + end + end + + options.on "-n", "Wrap running code in 'while(gets()) ...'" do + @input_loop = true + define_global_methods + end + + options.on "-p", "Same as -n, but also print $_" do + @input_loop = true + @input_loop_print = true + Rubinius::Globals.set!(:$-p, true) + define_global_methods + end + + options.on "-r", "LIBRARY", "Require library before execution" do |file| + @requires << file + end + + options.on("-s", "Process options after the script into globals") do + @simple_options = true + end + + options.on("-S", "SCRIPT", + "Run SCRIPT using PATH environment variable to find it") do |script| + options.stop_parsing + @repl = false + + # Load a gemfile now if we need to so that -S can see binstubs + # internal to the Gemfile + gemfile + + # First, check if any existing gem wrappers match. + unless file = find_gem_wrapper(script) + # Then, check if any Rubinius subcommands match. + unless file = find_subcommand(script) + # Finally, check if any file on PATH matches. + search = ENV['PATH'].split(File::PATH_SEPARATOR) + search.each do |d| + path = File.join d, script + if File.exist? path + file = path + break + end + end + end + end + + set_program_name script if file + + # if missing, let it die a natural death + @script = file ? file : script + end + + options.on "-v", "Display the version and set $VERBOSE to true" do + @repl = false + $VERBOSE = true + + unless @printed_version + puts Rubinius.version + @printed_version = true + end + end + + options.on "-w", "Enable warnings" do + $VERBOSE = true + end + + options.on("-W", "[level]", + "Set warning level: 0=silence, 1=medium, 2=verbose (default)") do |l| + case l + when "0" + $VERBOSE = nil + when "1" + $VERBOSE = false + when nil + $VERBOSE = true + else + # MRI -h says -W2 sets $VERBOSE to true, but behaviorally + # any value >= 2 sets $VERBOSE to true. + $VERBOSE = true + end + end + + options.on "--show-nil-location", "Show where nil value originated when NoMethodError on nil is raised" do + $rbx_show_nil_location = true + end + + options.on "--deprecated", "Display any deprecations" do + @deprecations = true + end + + options.on "--version", "Display the version" do + @repl = false + puts Rubinius.version + end + + options.doc <<-DOC +\nRubinius subcommands + + Rubinius provides subcommands that implement various facilities. Each + subcommand provides its own help. For example: + + rbx compile -h + + Available subcommands: + + compile Run the bytecode compiler + gem Run RubyGems 'gem' command + irb Run IRB + erb Run ERb + DOC + + options.doc <<-DOC +VM Options + -X[=] + This option is recognized by the VM before any ruby code is loaded. + It is used to set VM configuration options. + + Use -Xhelp to see the list of options the VM recognizes. + All variables, even ones that the VM doesn't understand, are available + in Rubinius::Config. + + Options are parsed from these sources, in this order: + + 1. The file .rbxrc in the current working directory. + 2. The RBXOPT environment variable. + 3. The command line options. + + This order permits environment and command line options to override + "application" configuration. Likewise, command line options can override + environment configuration. + + A number of Rubinius features are driven by setting these variables. + DOC + + options.parse ARGV + + handle_rubyopt(options) + + if @early_option_stop and @simple_options + handle_simple_options(argv) + end + + if Rubinius::Config['help'] + STDOUT.puts "Rubinius configuration variables:" + STDOUT.puts " These variables are normally set via -X and read via Rubinius::Config[var]." + STDOUT.puts + + require 'rubinius/configuration' + Rubinius::ConfigurationVariables.instance.show_help(STDOUT) + exit 0 + end + + exit 0 if Rubinius::Config["config.print"] + + if str = Rubinius::Config['tool.require'] + begin + require str + rescue LoadError + STDERR.puts "Unable to require file for tool: '#{str}'" + end + end + + if Rubinius::Config['profile'] || Rubinius::Config['jit.profile'] + @load_profiler = true + end + + if @check_syntax + check_syntax + end + end + + # Sets $0 ($PROGRAM_NAME) without changing the process title + def set_program_name(name) + Rubinius::Globals.set! :$0, name + end + private :set_program_name + + def handle_rubyopt(options) + if env_opts = ENV['RUBYOPT'] + Rubinius::Logger.system.write "RUBYOPT: #{env_opts}" + + options.start_parsing + env_opts = env_opts.strip.split(/\s+/) + + until env_opts.empty? + entry = env_opts.shift + + unless entry[0] == ?- + entry = "-#{entry}" + end + + opt, arg, rest = options.split entry, 2 + + options.process env_opts, entry, opt, arg + end + end + + end + + # Update the load paths with any -I arguments. + def load_paths + @stage = "setting load paths" + + @load_paths.each do |path| + path.split(":").reverse_each do |path| + # We used to run expand_path on path first, but MRI + # doesn't and it breaks some code if we do. + $LOAD_PATH.unshift(path) + end + end + end + + def deprecations + @stage = "showing deprecations" + + return unless @deprecations + + if Rubinius::DEPRECATIONS.size > 0 + Rubinius::DEPRECATIONS.each do |desc, alt| + STDERR.puts <<-EOM +Deprecation: #{desc} +Alternative: #{alt} + + EOM + end + + exit 1 + else + STDERR.puts "Rubinius #{Rubinius::VERSION} has no deprecation notices" + exit 0 + end + end + + def load_compiler + @stage = "loading the compiler" + + begin + CodeLoader.load_compiler + rescue LoadError => e + message = "Unable to load the bytecode compiler." + Rubinius::Logger.system.error message + Rubinius::Logger.system.error( + "Please run 'rake' or 'rake install' to rebuild the compiler.") + Rubinius::Logger.log_exception message, e + exit 1 + end + end + + def debugger + @stage = "running the debugger" + + if Rubinius::Config['debug'] + if custom = Loader.debugger + custom.call + else + require 'rubinius/debugger' + Rubinius::Debugger.start + end + end + end + + def rubygems + @stage = "loading Rubygems" + + CodeLoader.load_rubygems if @enable_gems + end + + def gemfile + @stage = "loading Gemfile" + + if @load_gemfile + CodeLoader.load_rubygems + require 'bundler/setup' + @load_gemfile = false + end + end + + def profiler + return unless @load_profiler + @stage = "loading profiler" + + require 'profile' + end + + # Require any -r arguments + def requires + @stage = "requiring command line files" + + @requires.each { |file| require file } + end + + # Evaluate any -e arguments + def evals + return if @evals.empty? + + @repl = false + @stage = "evaluating command line code" + + Dir.chdir(@directory) if @directory + + if @input_loop + while gets + $F = $_.split if @input_loop_split + eval @evals.join("\n"), TOPLEVEL_BINDING, "-e", 1 + print $_ if @input_loop_print + end + else + eval @evals.join("\n"), TOPLEVEL_BINDING, "-e", 1 + end + end + + # Run the script passed on the command line + def script + return unless @script and @evals.empty? + + @repl = false + + handle_simple_options(ARGV) if @simple_options + + @stage = "running #{@script}" + Dir.chdir @directory if @directory + + if File.exist? @script + if IO.read(@script, 6) == "!RBIX\n" + raise LoadError, "'#{@script}' is not a Ruby source file" + end + else + if @script.suffix?(".rb") + raise LoadError, "unable to find '#{@script}'" + else + command = File.join @lib_bin, "#{@script}.rb" + unless File.exist? command + raise LoadError, "unable to find Rubinius command '#{@script}'" + else + @script = command + end + end + end + + set_program_name @script + CodeLoader.new(@script).load_script + end + + #Check Ruby syntax of source + def check_syntax + parser_class = Rubinius::ToolSets::Runtime::Melbourne + + if @script + if File.exist?(@script) + parser = parser_class.new @script, 1, [] + parser.parse_file + else + puts "rbx: Unable to find file -- #{@script} (LoadError)" + exit 1 + end + elsif not @evals.empty? + parser = parser_class.new('-e', 1, []) + parser.parse_string(@evals.join("\n")) + else + parser = parser_class.new('-', 1, []) + parser.parse_string(STDIN.read) + end + + puts "Syntax OK" + exit 0 + rescue SyntaxError => e + show_syntax_errors(parser.syntax_errors) + exit 1 + end + + # Run IRB unless we were passed -e, -S arguments or a script to run. + def repl + return unless @repl + + @stage = "running IRB" + + # Check if a different REPL is set. + repl = ENV["RBX_REPL"] || "irb" + + if Terminal + load File.join(Rubinius::GEMS_PATH, "bin", repl) + else + set_program_name "(eval)" + execute_script "p #{STDIN.read}" + end + end + + def execute_script(script) + eval script, TOPLEVEL_BINDING + end + + def run_at_exits + until AtExit.empty? + begin + AtExit.shift.call + rescue SystemExit => e + @exit_code = e.status + # We recurse down so that future at_exits + # see the current exception + run_at_exits + end + end + end + + def flush_stdio + STDOUT.flush unless STDOUT.closed? + STDERR.flush unless STDERR.closed? + end + + def exit_with_exception(e) + @exit_code = 1 + Rubinius::Logger.log_exception "An exception occurred #{@stage}", e + end + + # Cleanup and at_exit processing. + def epilogue + @stage = "running at_exit handlers" + + begin + Signal.run_handler Signal::Names["EXIT"] + rescue SystemExit => e + @exit_code = e.status + end + + run_at_exits + + flush_stdio + + rescue Object => e + exit_with_exception e + end + + # Exit. + def done + # check that this is a valid exit rather than failing to process + # unwinding properly. + # + # TODO: this code is pretty gross, nice object inspectors, please. + thread_state = Rubinius.thread_state + reason = thread_state[0] + unless reason == :none + STDERR.puts "\nERROR: the VM is exiting improperly #{@stage}" + STDERR.puts "intended operation: #{reason.inspect}" + STDERR.puts "associated value: #{thread_state[1].inspect}" + + destination = thread_state[2] + + if destination + method = destination.method + + STDERR.puts "destination scope:" + STDERR.puts " method: #{method.name} at #{method.file}:#{method.first_line}" + STDERR.puts " module: #{destination.module.name}" + STDERR.puts " block: #{destination.block}" if destination.block + else + STDERR.puts "destination scope: unknown" + end + + if reason == :catch_throw + STDERR.puts "throw destination: #{thread_state[4].inspect}" + end + + if exception = thread_state[3] + begin + exception.render + rescue Exception => e + STDERR.puts "Unable to render backtrace: #{e.message} (#{e.class})" + STDERR.puts "Raw backtrace data:" + + exception.locations.each do |loc| + p [loc.file, loc.line] + end + end + end + + @exit_code = 1 + end + + Process.exit! @exit_code + end + + def handle_exception(e) + case e + when SystemExit + @exit_code = e.status + when SyntaxError + @exit_code = 1 + + STDERR.puts e.awesome_backtrace.show + STDERR.puts + + show_syntax_error(e) + when Interrupt + exit_with_exception e + when SignalException + Signal.trap(e.signo, "SIG_DFL") + Process.kill e.signo, Process.pid + when nil + # what? + else + exit_with_exception e + end + rescue Object => e + exit_with_exception e + ensure + epilogue + end + + # Orchestrate everything. + def main + preamble + system_load_path + signals + load_compiler + preload + detect_alias + options + load_paths + deprecations + rubygems + gemfile + debugger + profiler + requires + evals + script + repl + + rescue Object => e + handle_exception e + else + # We do this, run epilogue both in the rescue blocks and also here, + # so that at_exit{} hooks can read $!. + epilogue + ensure + done + end + end +end + diff --git a/gxg/core/location.rb b/gxg/core/location.rb new file mode 100644 index 0000000..ec103b1 --- /dev/null +++ b/gxg/core/location.rb @@ -0,0 +1,167 @@ +module Rubinius + class Location + + attr_reader :method + attr_reader :name + + attr_reader :ip + attr_reader :variables + attr_reader :lexical_scope + + def self.of_closest_ruby_method + Rubinius.primitive :location_of_closest_ruby_method + raise PrimitiveFailure, "Location.of_closest_ruby_method primitive failed" + end + + def is_block + @flags & 1 == 1 + end + + def is_jit + @flags & 2 == 2 + end + + def ip_on_current? + @flags & 4 == 4 + end + + def inlined? + @name.nil? + end + + def describe_receiver + # We can't call @receiver.class because it might be overriden (or in + # the case of Mocha, mocked out) and easily causes stack overflows + klass = Rubinius::Type.object_class @receiver + + if @method_module.equal?(Kernel) + "Kernel." + elsif Rubinius::Type.object_kind_of?(@method_module, Class) and + ao = Rubinius::Type.singleton_class_object(@method_module) + "#{ao}." + elsif @method_module and @method_module != klass + "#{@method_module}(#{klass})#" + else + "#{klass}#" + end + end + + def describe + if is_block + "{ } in #{describe_receiver}#{describe_method}" + else + "#{describe_receiver}#{describe_method}" + end + end + + def inspect + "#" + end + + def describe_method + if is_block or @name == @method.name + @name.to_s + elsif !@name # inlined methods have no name + @method.name.to_s + else + "#{@name} (#{@method.name})" + end + end + + # Current line being executed by the VM. + def line + return 0 unless @method + + ip = @ip + ip -= 1 unless ip_on_current? + + return @method.first_line unless ip > 0 + + @method.line_from_ip(ip) + end + + def file(relative_to=nil) + path = @method.active_path + + if relative_to + # Be sure we can bail out if something doesn't work and still + # show something. + begin + full = File.expand_path path + if full.prefix? relative_to + return full[relative_to.size+1..-1] + else + return path + end + rescue StandardError + return path + end + end + + path + end + + ## + # Place in the source that this method was created at. + def position(relative_to=nil) + l = line() + + if l == 0 + if @ip < 0 + file(relative_to) + else + "#{file(relative_to)}+#{@ip}" + end + else + "#{file(relative_to)}:#{l}" + end + end + + # See Rubinius::Backtrace.backtrace + class Missing + def describe + "*** Missing backtrace! Did the VM not create one? ***" + end + + def describe_receiver + "missing_backtrace_unknown_receiver" + end + + def describe_method + "missing_backtrace_unknown_location" + end + + def method + "missing_backtrace_unknown_location" + end + + def line + "" + end + + def position(dir=nil) + "" + end + + def ip + 0 + end + + def name + "missing_backtrace_unknown_name" + end + + def is_block + false + end + + def is_jit + false + end + + def inlined? + false + end + end + end +end diff --git a/gxg/core/logger.rb b/gxg/core/logger.rb new file mode 100644 index 0000000..e662602 --- /dev/null +++ b/gxg/core/logger.rb @@ -0,0 +1,99 @@ +module Rubinius + class Logger + attr_accessor :name + attr_accessor :format + + def self.allocate + Rubinius.primitive :logger_allocate + raise PrimitiveFailure, "Rubinius::Logger.allocate failed" + end + + def self.system + @system_logger ||= new "system" + end + + def self.log_exception(message, e) + log = system + + exception = e + exceptions = [] + + while exception + exceptions.unshift exception + exception = exception.cause + end + + color = STDERR.tty? + + while exception = exceptions.shift + if custom_trace = exception.custom_backtrace + msg = "User-defined backtrace:" + log.error msg + STDERR.puts msg + + custom_trace.reverse_each do |line| + log.error line + STDERR.puts line + end + + msg = "Backtrace: " + log.error msg + STDERR.puts "\n", msg, "\n" + end + + exception.backtrace.reverse_each { |line| log.error line } + STDERR.puts exception.awesome_backtrace.show("\n", color) + + msg = "#{exception.message} (#{exception.class})" + log.error msg + STDERR.puts "\n", msg + + unless exceptions.empty? + msg = "Causing:" + log.error msg + STDERR.puts "\n", msg, "\n" + end + end + + log.error message + STDERR.puts "\n", message + end + + def initialize(name) + @name = name + @format = "#{name}: %s" + end + + private :initialize + + def write(message) + Rubinius.primitive :logger_write + raise PrimitiveFailure, "Rubinius::Logger#write primitive failed" + end + + def fatal(message) + Rubinius.primitive :logger_fatal + raise PrimitiveFailure, "Rubinius::Logger#fatal primitive failed" + end + + def error(message) + Rubinius.primitive :logger_error + raise PrimitiveFailure, "Rubinius::Logger#error primitive failed" + end + + def warn(message) + Rubinius.primitive :logger_warn + raise PrimitiveFailure, "Rubinius::Logger#warn primitive failed" + end + + def info(message) + Rubinius.primitive :logger_info + raise PrimitiveFailure, "Rubinius::Logger#info primitive failed" + end + + def debug(message) + Rubinius.primitive :logger_debug + raise PrimitiveFailure, "Rubinius::Logger#debug primitive failed" + end + end +end diff --git a/gxg/core/lookup_table.rb b/gxg/core/lookup_table.rb new file mode 100644 index 0000000..f888a48 --- /dev/null +++ b/gxg/core/lookup_table.rb @@ -0,0 +1,134 @@ +## +# A LookupTable is similar to a Hash in that keys are used to set and +# reference values. However, unlike Hash, whether a key matches an +# entry in LookupTable is determined by using the == comparison operator +# in C code. In effect, two keys are equal if they are the same pointer. +# +# NOTE: the value used to determine the bin is the "pointer" value of +# key >> 2. This makes it possible to calculate the bin from Ruby with +# the same result as in C. For example: +# +# l = LookupTable.new +# loc = :a.hash & (l.bins - 1) # => 12 +# class LookupTable +# def show +# @values +# end +# end +# +# l[:a] = 1 +# l.show # => #:a, @value=>1, +# @next=>nil>, nil, nil, nil> +# +# where ... is ten "nil, " entries. +# +# LookupTable is intended to be used with Symbol or Fixnum keys because +# it is a strict identity map. Usage of Strings as keys will likely result +# in strange behavior. +# LookupTable is NOT intended to be used generally like Hash. + +module Rubinius + class LookupTable + include Enumerable + + class Bucket + attr_reader :key + attr_reader :value + attr_reader :next + end + + attr_reader :values + attr_reader :bins + + attr_reader_specific :entries, :size + alias_method :length, :size + + def self.allocate + Rubinius.primitive :lookuptable_allocate + raise PrimitiveFailure, "LookupTable.allocate primitive failed" + end + + def initialize(hash=nil) + return unless hash + hash.each do |k, v| + self[k] = v + end + end + + private :initialize + + def duplicate + Rubinius.primitive :lookuptable_duplicate + raise PrimitiveFailure, "LookupTable#duplicate primitive failed" + end + + def dup + copy = duplicate + Rubinius.privately do + copy.initialize_copy self + end + copy + end + + alias_method :clone, :dup + + def fetch(key, return_on_failure) + Rubinius.primitive :lookuptable_fetch + raise PrimitiveFailure, "LookupTable#fetch primitive failed" + end + + def key?(key) + Rubinius.primitive :lookuptable_has_key + raise PrimitiveFailure, "LookupTable#key? primitive failed" + end + + alias_method :has_key?, :key? + alias_method :include?, :key? + alias_method :member?, :key? + + def delete(key) + Rubinius.primitive :lookuptable_delete + raise PrimitiveFailure, "LookupTable#delete primitive failed" + end + + def keys + Rubinius.primitive :lookuptable_keys + raise PrimitiveFailure, "LookupTable#keys primitive failed" + end + + def values + Rubinius.primitive :lookuptable_values + raise PrimitiveFailure, "LookupTable#values primitive failed" + end + + def each + max = @bins + i = 0 + vals = @values + + while i < max + entry = vals.at(i) + + while entry + yield entry.key, entry.value + entry = entry.next + end + i += 1 + end + self + end + + def empty? + @entries == 0 + end + + def to_s + "#<#{self.class}:0x#{object_id.to_s(16)} #{entries} entries>" + end + + def inspect + ents = collect { |k, v| " #{k.inspect}=>#{v.inspect}" }.join(",") + "#<#{self.class}#{ents}>" + end + end +end diff --git a/gxg/core/lru_cache.rb b/gxg/core/lru_cache.rb new file mode 100644 index 0000000..64f7c02 --- /dev/null +++ b/gxg/core/lru_cache.rb @@ -0,0 +1,147 @@ +module Rubinius + class LRUCache + class Entry + attr_reader :hits + attr_reader :key + attr_accessor :value + attr_accessor :next_entry + attr_accessor :prev_entry + + def initialize(key, value) + @key = key + @value = value + @hits = 0 + @next_entry = nil + @prev_entry = nil + end + + private :initialize + + def insert_after(entry) + nxt = entry.next_entry + + @prev_entry = entry + @next_entry = nxt + + entry.next_entry = self + nxt.prev_entry = self if nxt + end + + def insert_before(entry) + prev = entry.prev_entry + + @prev_entry = prev + @next_entry = entry + + entry.prev_entry = self + prev.next_entry = self if prev + end + + def detach! + @next_entry.prev_entry = @prev_entry if @next_entry + @prev_entry.next_entry = @next_entry if @prev_entry + + @next_entry = nil + @prev_entry = nil + end + + def become_first! + @prev_entry = nil + end + + def inc! + @hits += 1 + end + end + + def initialize(total) + @cache = {} + @total = total + @current = 0 + + @head = Entry.new(nil, nil) + @tail = Entry.new(nil, nil) + + @tail.insert_after(@head) + + @misses = 0 + end + + private :initialize + + attr_reader :current + attr_reader :misses + + def clear! + Rubinius.synchronize(self) do + @cache = {} + @current = 0 + + @head = Entry.new(nil, nil, -1) + @tail = Entry.new(nil, nil, -2) + + @tail.insert_after(@head) + end + end + + def explain + entry = @head.next_entry + while entry != @tail + str, layout = entry.key + puts "hits: #{entry.hits}" + puts "layout: #{layout.inspect}" + puts "" + puts str + puts "" + + entry = entry.next_entry + end + end + + def retrieve(key) + Rubinius.synchronize(self) do + if entry = @cache[key] + entry.inc! + + entry.detach! + entry.insert_before @tail + + return entry.value + end + + @misses += 1 + + nil + end + end + + def set(key, value) + Rubinius.synchronize(self) do + if entry = @cache[key] + entry.value = value + + entry.detach! + entry.insert_before @tail + + return value + end + + if @current == @total + entry = @head.next_entry + + entry.detach! + + @cache.delete entry.key + else + @current += 1 + end + + entry = Entry.new(key, value) + + entry.insert_before @tail + + @cache[key] = entry + end + end + end +end diff --git a/gxg/core/main.rb b/gxg/core/main.rb new file mode 100644 index 0000000..6b06e0c --- /dev/null +++ b/gxg/core/main.rb @@ -0,0 +1,35 @@ +class << MAIN + def include(*mods) + Rubinius.privately do + Object.include(*mods) + end + end + + def public(*methods) + Rubinius.privately do + Object.public(*methods) + end + + Object + end + + def private(*methods) + Rubinius.privately do + Object.private(*methods) + end + + Object + end + + def define_method(*args, &block) + Rubinius.privately do + Object.define_method(*args, &block) + end + end + + def to_s + "main" + end + + alias_method :inspect, :to_s +end diff --git a/gxg/core/marshal.rb b/gxg/core/marshal.rb new file mode 100644 index 0000000..a0c8759 --- /dev/null +++ b/gxg/core/marshal.rb @@ -0,0 +1,1227 @@ +class BasicObject + def __marshal__(ms, strip_ivars = false) + out = ms.serialize_extended_object self + out << "o" + cls = Rubinius::Type.object_class self + name = Rubinius::Type.module_inspect cls + out << ms.serialize(name.to_sym) + out << ms.serialize_instance_variables_suffix(self, true, strip_ivars) + end +end + +class Class + def __marshal__(ms) + if Rubinius::Type.singleton_class_object(self) + raise TypeError, "singleton class can't be dumped" + elsif name.nil? || name.empty? + raise TypeError, "can't dump anonymous module #{self}" + end + + "c#{ms.serialize_integer(name.length)}#{name}" + end +end + +class Module + def __marshal__(ms) + raise TypeError, "can't dump anonymous module #{self}" if name.nil? || name.empty? + "m#{ms.serialize_integer(name.length)}#{name}" + end +end + +class Float + def __marshal__(ms) + if nan? + str = "nan" + elsif zero? + str = (1.0 / self) < 0 ? '-0' : '0' + elsif infinite? + str = self < 0 ? "-inf" : "inf" + else + s, decimal, sign, digits = dtoa + + if decimal < -3 or decimal > digits + str = s.insert(1, ".") << "e#{decimal - 1}" + elsif decimal > 0 + str = s[0, decimal] + digits -= decimal + str << ".#{s[decimal, digits]}" if digits > 0 + else + str = "0." + str << "0" * -decimal if decimal != 0 + str << s[0, digits] + end + end + + sl = str.length + if sign == 1 + ss = "-" + sl += 1 + end + + Rubinius::Type.binary_string("f#{ms.serialize_integer(sl)}#{ss}#{str}") + end +end + +class Exception + def __marshal__(ms) + out = ms.serialize_extended_object self + out << "o" + cls = Rubinius::Type.object_class self + name = Rubinius::Type.module_inspect cls + out << ms.serialize(name.to_sym) + out << ms.serialize_fixnum(2) + + out << ms.serialize(:mesg) + out << ms.serialize(@reason_message) + out << ms.serialize(:bt) + out << ms.serialize(backtrace) + + out + end +end + +class Time + def __custom_marshal__(ms) + out = Rubinius::Type.binary_string("") + + # Order matters. + extra_values = {} + extra_values[:offset] = gmt_offset unless gmt? + extra_values[:zone] = zone + + if nsec > 0 + # MRI serializes nanoseconds as a Rational using an + # obscure and implementation-dependent method. + # To keep compatibility we can just put nanoseconds + # in the numerator and set the denominator to 1. + extra_values[:nano_num] = nsec + extra_values[:nano_den] = 1 + end + + ivars = ms.serializable_instance_variables(self, false) + out << Rubinius::Type.binary_string("I") + out << Rubinius::Type.binary_string("u#{ms.serialize(self.class.name.to_sym)}") + + str = _dump + out << ms.serialize_integer(str.length) + str + + count = ivars.size + extra_values.size + out << ms.serialize_integer(count) + + ivars.each do |ivar| + sym = ivar.to_sym + val = __instance_variable_get__(sym) + out << ms.serialize(sym) + out << ms.serialize(val) + end + + extra_values.each_pair do |key, value| + out << ms.serialize(key) + out << ms.serialize(value) + end + + out + end +end + +module Marshal + class State + def serialize_encoding?(obj) + enc = Rubinius::Type.object_encoding(obj) + enc && enc != Encoding::BINARY + end + + def serialize_encoding(obj) + case enc = Rubinius::Type.object_encoding(obj) + when Encoding::US_ASCII + :E.__marshal__(self) + false.__marshal__(self) + when Encoding::UTF_8 + :E.__marshal__(self) + true.__marshal__(self) + else + :encoding.__marshal__(self) + serialize_string(enc.name) + end + end + + def set_object_encoding(obj, enc) + case obj + when String + obj.force_encoding enc + when Regexp + obj.source.force_encoding enc + when Symbol + # TODO + end + end + + def set_instance_variables(obj) + construct_integer.times do + ivar = get_symbol + value = construct + + case ivar + when :E + if value + set_object_encoding obj, Encoding::UTF_8 + else + set_object_encoding obj, Encoding::US_ASCII + end + next + when :encoding + if enc = Encoding.find(value) + set_object_encoding obj, enc + next + end + end + + obj.__instance_variable_set__ prepare_ivar(ivar), value + end + end + + def construct_string + obj = get_byte_sequence + Rubinius::Unsafe.set_class(obj, get_user_class) if @user_class + + set_object_encoding(obj, Encoding::ASCII_8BIT) + + store_unique_object obj + end + end +end + +class Range + def __marshal__(ms) + super(ms, true) + end +end + +class NilClass + def __marshal__(ms) + Rubinius::Type.binary_string("0") + end +end + +class TrueClass + def __marshal__(ms) + Rubinius::Type.binary_string("T") + end +end + +class FalseClass + def __marshal__(ms) + Rubinius::Type.binary_string("F") + end +end + +class Symbol + def __marshal__(ms) + if idx = ms.find_symlink(self) + Rubinius::Type.binary_string(";#{ms.serialize_integer(idx)}") + else + ms.add_symlink self + ms.serialize_symbol(self) + end + end +end + +class String + def __marshal__(ms) + out = ms.serialize_instance_variables_prefix(self) + out << ms.serialize_extended_object(self) + out << ms.serialize_user_class(self, String) + out << ms.serialize_string(self) + out << ms.serialize_instance_variables_suffix(self) + out + end +end + +class Fixnum + def __marshal__(ms) + ms.serialize_integer(self, "i") + end +end + +class Bignum + def __marshal__(ms) + ms.serialize_bignum(self) + end +end + +class Regexp + def __marshal__(ms) + str = self.source + out = ms.serialize_instance_variables_prefix(self) + out << ms.serialize_extended_object(self) + out << ms.serialize_user_class(self, Regexp) + out << "/" + out << ms.serialize_integer(str.length) + str + out << (options & Regexp::OPTION_MASK).chr + out << ms.serialize_instance_variables_suffix(self) + + out + end +end + +class Struct + def __marshal__(ms) + exclude = _attrs.map { |a| "@#{a}".to_sym } + + out = ms.serialize_instance_variables_prefix(self, exclude) + out << ms.serialize_extended_object(self) + + out << "S" + + out << ms.serialize(self.class.name.to_sym) + out << ms.serialize_integer(self.length) + + self.each_pair do |name, value| + out << ms.serialize(name) + out << ms.serialize(value) + end + + out << ms.serialize_instance_variables_suffix(self, false, false, exclude) + + out + end +end + +class Array + def __marshal__(ms) + out = ms.serialize_instance_variables_prefix(self) + out << ms.serialize_extended_object(self) + out << ms.serialize_user_class(self, Array) + out << "[" + out << ms.serialize_integer(self.length) + unless empty? + each do |element| + out << ms.serialize(element) + end + end + out << ms.serialize_instance_variables_suffix(self) + + out + end +end + +class Hash + def __marshal__(ms) + raise TypeError, "can't dump hash with default proc" if default_proc + + excluded_ivars = %w[ + @head @tail @size @previous @next @key @key_hash @value + @key_hash @entries @bmp @entries @level @state @trie + @compare_by_identity @default @default_proc + ].map { |a| a.to_sym } + + out = ms.serialize_instance_variables_prefix(self, excluded_ivars) + out << ms.serialize_extended_object(self) + out << ms.serialize_user_class(self, Hash) + out << (self.default ? "}" : "{") + out << ms.serialize_integer(length) + unless empty? + each_pair do |key, val| + out << ms.serialize(key) + out << ms.serialize(val) + end + end + out << (self.default ? ms.serialize(self.default) : '') + out << ms.serialize_instance_variables_suffix(self, false, false, + excluded_ivars) + + out + end +end + +class Time + def self.__construct__(ms, data, ivar_index, has_ivar) + obj = _load(data) + ms.store_unique_object obj + + if ivar_index and has_ivar[ivar_index] + ms.set_instance_variables obj + has_ivar[ivar_index] = false + end + + nano_num = obj.instance_variable_get(:@nano_num) + nano_den = obj.instance_variable_get(:@nano_den) + if nano_num && nano_den + obj.send(:nsec=, Rational(nano_num, nano_den).to_i) + end + + obj + end +end + +module Unmarshalable + def __marshal__(ms) + raise TypeError, "marshaling is undefined for class #{self.class}" + end +end + +class Method + include Unmarshalable +end + +class Proc + include Unmarshalable +end + +class IO + include Unmarshalable +end + +class MatchData + include Unmarshalable +end + +module Marshal + + MAJOR_VERSION = 4 + MINOR_VERSION = 8 + + VERSION_STRING = "\x04\x08" + + # Here only for reference + TYPE_NIL = ?0 + TYPE_TRUE = ?T + TYPE_FALSE = ?F + TYPE_FIXNUM = ?i + + TYPE_EXTENDED = ?e + TYPE_UCLASS = ?C + TYPE_OBJECT = ?o + TYPE_DATA = ?d # no specs + TYPE_USERDEF = ?u + TYPE_USRMARSHAL = ?U + TYPE_FLOAT = ?f + TYPE_BIGNUM = ?l + TYPE_STRING = ?" + TYPE_REGEXP = ?/ + TYPE_ARRAY = ?[ + TYPE_HASH = ?{ + TYPE_HASH_DEF = ?} + TYPE_STRUCT = ?S + TYPE_MODULE_OLD = ?M # no specs + TYPE_CLASS = ?c + TYPE_MODULE = ?m + + TYPE_SYMBOL = ?: + TYPE_SYMLINK = ?; + + TYPE_IVAR = ?I + TYPE_LINK = ?@ + + class State + + def initialize(stream, depth, proc) + # shared + @links = Rubinius::LookupTable.new + @symlinks = Rubinius::LookupTable.new + @symbols = [] + @objects = [] + + # dumping + @depth = depth + + # loading + if stream + @stream = stream + else + @stream = nil + end + + if stream + @consumed = 2 + else + @consumed = 0 + end + + @modules = nil + @has_ivar = [] + @proc = proc + @call = true + end + + private :initialize + + def const_lookup(name, type = nil) + mod = Object + + parts = String(name).split '::' + parts.each do |part| + unless Rubinius::Type.const_exists?(mod, part) + raise ArgumentError, "undefined class/module #{name}" + end + + mod = Rubinius::Type.const_get(mod, part, false) + end + + if type and not mod.instance_of? type + raise ArgumentError, "#{name} does not refer to a #{type}" + end + + mod + end + + def add_non_immediate_object(obj) + return if Rubinius::Type.object_kind_of? obj, ImmediateValue + add_object(obj) + end + + def add_object(obj) + sz = @objects.size + @objects[sz] = obj + @links[obj.__id__] = sz + end + + def add_symlink(obj) + sz = @symlinks.size + @symbols[sz] = obj + @symlinks[obj.__id__] = sz + end + + def call(obj) + @proc.call obj if @proc and @call + end + + def construct(ivar_index = nil, call_proc = true) + type = consume_byte() + obj = case type + when 48 # ?0 + nil + when 84 # ?T + true + when 70 # ?F + false + when 99 # ?c + construct_class + when 109 # ?m + construct_module + when 77 # ?M + construct_old_module + when 105 # ?i + construct_integer + when 108 # ?l + construct_bignum + when 102 # ?f + construct_float + when 58 # ?: + construct_symbol + when 34 # ?" + construct_string + when 47 # ?/ + construct_regexp + when 91 # ?[ + construct_array + when 123 # ?{ + construct_hash + when 125 # ?} + construct_hash_def + when 83 # ?S + construct_struct + when 111 # ?o + construct_object + when 117 # ?u + construct_user_defined ivar_index + when 85 # ?U + construct_user_marshal + when 100 # ?d + construct_data + when 64 # ?@ + num = construct_integer + + begin + obj = @objects.fetch(num) + return obj + rescue IndexError + raise ArgumentError, "dump format error (unlinked)" + end + + when 59 # ?; + num = construct_integer + sym = @symbols[num] + + raise ArgumentError, "bad symbol" unless sym + + return sym + when 101 # ?e + @modules ||= [] + + name = get_symbol + @modules << const_lookup(name, Module) + + obj = construct nil, false + + extend_object obj + + obj + when 67 # ?C + name = get_symbol + @user_class = name + + construct nil, false + + when 73 # ?I + ivar_index = @has_ivar.length + @has_ivar.push true + + obj = construct ivar_index, false + + set_instance_variables obj if @has_ivar.pop + + obj + else + raise ArgumentError, "load error, unknown type #{type}" + end + + call obj if @proc and call_proc + + Rubinius::Type.infect obj, @stream unless obj.frozen? + + obj + end + + def construct_class + obj = const_lookup(get_byte_sequence.to_sym, Class) + store_unique_object obj + obj + end + + def construct_module + obj = const_lookup(get_byte_sequence.to_sym, Module) + store_unique_object obj + obj + end + + def construct_old_module + obj = const_lookup(get_byte_sequence.to_sym) + store_unique_object obj + obj + end + + def construct_array + obj = [] + store_unique_object obj + + if @user_class + cls = get_user_class() + if cls < Array + Rubinius::Unsafe.set_class obj, cls + else + # This is what MRI does, it's weird. + return cls.allocate + end + end + + construct_integer.times do |i| + obj.__append__ construct + end + + obj + end + + def construct_bignum + sign = consume_byte() == 45 ? -1 : 1 # ?- + size = construct_integer * 2 + + result = 0 + + data = consume size + (0...size).each do |exp| + result += (data.getbyte(exp) * 2**(exp*8)) + end + + obj = result * sign + + add_object obj + obj + end + + def construct_data + name = get_symbol + klass = const_lookup name, Class + store_unique_object klass + + obj = klass.allocate + + # TODO ensure obj is a wrapped C pointer (T_DATA in MRI-land) + + store_unique_object obj + + unless Rubinius::Type.object_respond_to? obj, :_load_data + raise TypeError, + "class #{name} needs to have instance method `_load_data'" + end + + obj._load_data construct + + obj + end + + def construct_float + s = get_byte_sequence + + if s == "nan" + obj = 0.0 / 0.0 + elsif s == "inf" + obj = 1.0 / 0.0 + elsif s == "-inf" + obj = 1.0 / -0.0 + else + obj = s.to_f + end + + store_unique_object obj + + obj + end + + def construct_hash + obj = @user_class ? get_user_class.allocate : {} + store_unique_object obj + + construct_integer.times do + original_modules = @modules + @modules = nil + key = construct + val = construct + @modules = original_modules + + # Use __store__ (an alias for []=) to get around subclass overrides + obj.__store__ key, val + end + + obj + end + + def construct_hash_def + obj = @user_class ? get_user_class.allocate : {} + store_unique_object obj + + construct_integer.times do + key = construct + val = construct + obj[key] = val + end + + obj.default = construct + + obj + end + + def construct_integer + c = consume_byte() + + # The format appears to be a simple integer compression format + # + # The 0-123 cases are easy, and use one byte + # We've read c as unsigned char in a way, but we need to honor + # the sign bit. We do that by simply comparing with the +128 values + return 0 if c == 0 + return c - 5 if 4 < c and c < 128 + + # negative, but checked known it's instead in 2's complement + return c - 251 if 252 > c and c > 127 + + # otherwise c (now in the 1 to 4 range) indicates how many + # bytes to read to construct the value. + # + # Because we're operating on a small number of possible values, + # it's cleaner to just unroll the calculate of each + + case c + when 1 + consume_byte + when 2 + consume_byte | (consume_byte << 8) + when 3 + consume_byte | (consume_byte << 8) | (consume_byte << 16) + when 4 + consume_byte | (consume_byte << 8) | (consume_byte << 16) | + (consume_byte << 24) + + when 255 # -1 + consume_byte - 256 + when 254 # -2 + (consume_byte | (consume_byte << 8)) - 65536 + when 253 # -3 + (consume_byte | + (consume_byte << 8) | + (consume_byte << 16)) - 16777216 # 2 ** 24 + when 252 # -4 + (consume_byte | + (consume_byte << 8) | + (consume_byte << 16) | + (consume_byte << 24)) - 4294967296 + else + raise "Invalid integer size: #{c}" + end + end + + def construct_object + name = get_symbol + klass = const_lookup name, Class + obj = klass.allocate + + raise TypeError, 'dump format error' unless Object === obj + + store_unique_object obj + if Rubinius::Type.object_kind_of? obj, Exception + set_exception_variables obj + else + set_instance_variables obj + end + + obj + end + + def construct_regexp + s = get_byte_sequence + if @user_class + obj = get_user_class.new s, consume_byte + else + obj = Regexp.new s, consume_byte + end + + store_unique_object obj + end + + def construct_struct + symbols = [] + values = [] + + name = get_symbol + store_unique_object name + + klass = const_lookup name, Class + members = klass.members + + obj = klass.allocate + store_unique_object obj + + construct_integer.times do |i| + slot = get_symbol + unless members[i].intern == slot + raise TypeError, "struct %s is not compatible (%p for %p)" % + [klass, slot, members[i]] + end + + obj.instance_variable_set "@#{slot}", construct + end + + obj + end + + def construct_symbol + obj = get_byte_sequence.to_sym + store_unique_object obj + + obj + end + + def construct_user_defined(ivar_index) + name = get_symbol + klass = const_lookup name, Class + + data = get_byte_sequence + + if Rubinius::Type.object_respond_to? klass, :__construct__ + return klass.__construct__(self, data, ivar_index, @has_ivar) + end + + if ivar_index and @has_ivar[ivar_index] + set_instance_variables data + @has_ivar[ivar_index] = false + end + + obj = nil + Rubinius.privately do + obj = klass._load data + end + + add_object obj + + obj + end + + def construct_user_marshal + name = get_symbol + store_unique_object name + + klass = const_lookup name, Class + obj = klass.allocate + + extend_object obj if @modules + + unless Rubinius::Type.object_respond_to_marshal_load? obj + raise TypeError, "instance of #{klass} needs to have method `marshal_load'" + end + + store_unique_object obj + + data = construct + Rubinius.privately do + obj.marshal_load data + end + + obj + end + + def extend_object(obj) + obj.__extend__(@modules.pop) until @modules.empty? + end + + def find_link(obj) + @links[obj.__id__] + end + + def find_symlink(obj) + @symlinks[obj.__id__] + end + + def get_byte_sequence + size = construct_integer + consume size + end + + def get_user_class + cls = const_lookup @user_class, Class + @user_class = nil + cls + end + + def get_symbol + type = consume_byte() + + case type + when 58 # TYPE_SYMBOL + @call = false + obj = construct_symbol + @call = true + obj + when 59 # TYPE_SYMLINK + num = construct_integer + @symbols[num] + else + raise ArgumentError, "expected TYPE_SYMBOL or TYPE_SYMLINK, got #{type.inspect}" + end + end + + def prepare_ivar(ivar) + ivar.to_s =~ /\A@/ ? ivar : "@#{ivar}".to_sym + end + + def serialize(obj) + raise ArgumentError, "exceed depth limit" if @depth == 0 + + # How much depth we have left. + @depth -= 1; + + if link = find_link(obj) + str = Rubinius::Type.binary_string("@#{serialize_integer(link)}") + else + add_non_immediate_object obj + + # ORDER MATTERS. + if Rubinius::Type.object_respond_to_marshal_dump? obj + str = serialize_user_marshal obj + elsif Rubinius::Type.object_respond_to__dump? obj + str = serialize_user_defined obj + else + str = obj.__marshal__ self + end + end + + @depth += 1 + + Rubinius::Type.infect(str, obj) + end + + def serialize_extended_object(obj) + str = '' + if mods = Rubinius.extended_modules(obj) + mods.each do |mod| + str << "e#{serialize(mod.name.to_sym)}" + end + end + Rubinius::Type.binary_string(str) + end + + def serializable_instance_variables(obj, exclude_ivars) + ivars = Rubinius.invoke_primitive :object_instance_variables, obj + ivars -= exclude_ivars if exclude_ivars + ivars + end + + def serialize_instance_variables_prefix(obj, exclude_ivars = false) + ivars = serializable_instance_variables(obj, exclude_ivars) + Rubinius::Type.binary_string(!ivars.empty? || serialize_encoding?(obj) ? "I" : "") + end + + def serialize_instance_variables_suffix(obj, force=false, + strip_ivars=false, + exclude_ivars=false) + ivars = serializable_instance_variables(obj, exclude_ivars) + + unless force or !ivars.empty? or serialize_encoding?(obj) + return Rubinius::Type.binary_string("") + end + + count = ivars.size + + if serialize_encoding?(obj) + str = serialize_integer(count + 1) + str << serialize_encoding(obj) + else + str = serialize_integer(count) + end + + ivars.each do |ivar| + sym = ivar.to_sym + val = obj.__instance_variable_get__(sym) + if strip_ivars + str << serialize(ivar.to_s[1..-1].to_sym) + else + str << serialize(sym) + end + str << serialize(val) + end + + Rubinius::Type.binary_string(str) + end + + def serialize_integer(n, prefix = nil) + if (!Rubinius::L64 && n.is_a?(Fixnum)) || ((n >> 31) == 0 or (n >> 31) == -1) + Rubinius::Type.binary_string(prefix.to_s + serialize_fixnum(n)) + else + serialize_bignum(n) + end + end + + def serialize_fixnum(n) + if n == 0 + s = n.chr + elsif n > 0 and n < 123 + s = (n + 5).chr + elsif n < 0 and n > -124 + s = (256 + (n - 5)).chr + else + s = "\0" + cnt = 0 + 4.times do + s << (n & 0xff).chr + n >>= 8 + cnt += 1 + break if n == 0 or n == -1 + end + s[0] = (n < 0 ? 256 - cnt : cnt).chr + end + Rubinius::Type.binary_string(s) + end + + def serialize_bignum(n) + str = (n < 0 ? 'l-' : 'l+') + cnt = 0 + num = n.abs + + while num != 0 + str << (num & 0xff).chr + num >>= 8 + cnt += 1 + end + + if cnt % 2 == 1 + str << "\0" + cnt += 1 + end + + Rubinius::Type.binary_string(str[0..1] + serialize_fixnum(cnt / 2) + str[2..-1]) + end + + def serialize_symbol(obj) + str = obj.to_s + mf = "I" unless str.ascii_only? + if mf + if Rubinius::Type.object_encoding(obj).equal? Encoding::BINARY + me = serialize_integer(0) + elsif serialize_encoding?(obj) + me = serialize_integer(1) + serialize_encoding(obj.encoding) + end + end + mi = serialize_integer(str.bytesize) + s = Rubinius::Type.binary_string str + Rubinius::Type.binary_string("#{mf}:#{mi}#{s}#{me}") + end + + def serialize_string(str) + output = Rubinius::Type.binary_string("\"#{serialize_integer(str.bytesize)}") + output + Rubinius::Type.binary_string(str.dup) + end + + def serialize_user_class(obj, cls) + if obj.class != cls + Rubinius::Type.binary_string("C#{serialize(obj.class.name.to_sym)}") + else + Rubinius::Type.binary_string('') + end + end + + def serialize_user_defined(obj) + if Rubinius::Type.object_respond_to? obj, :__custom_marshal__ + return obj.__custom_marshal__(self) + end + + str = nil + Rubinius.privately do + str = obj._dump @depth + end + + unless Rubinius::Type.object_kind_of? str, String + raise TypeError, "_dump() must return string" + end + + out = serialize_instance_variables_prefix(str) + out << Rubinius::Type.binary_string("u#{serialize(obj.class.name.to_sym)}") + out << serialize_integer(str.length) + str + out << serialize_instance_variables_suffix(str) + + out + end + + def serialize_user_marshal(obj) + val = nil + Rubinius.privately do + val = obj.marshal_dump + end + + add_non_immediate_object val + + cls = Rubinius::Type.object_class obj + name = Rubinius::Type.module_inspect cls + Rubinius::Type.binary_string("U#{serialize(name.to_sym)}#{val.__marshal__(self)}") + end + + def store_unique_object(obj) + if Symbol === obj + add_symlink obj + else + add_non_immediate_object obj + end + obj + end + + def set_exception_variables(obj) + construct_integer.times do + ivar = get_symbol + value = construct + case ivar + when :bt + obj.__instance_variable_set__ :@custom_backtrace, value + when :mesg + obj.__instance_variable_set__ :@reason_message, value + end + end + end + + end + + class IOState < State + def consume(bytes) + @stream.read(bytes) + end + + def consume_byte + b = @stream.getbyte + raise EOFError unless b + b + end + end + + class StringState < State + def initialize(stream, depth, prc) + super stream, depth, prc + + if @stream + @byte_array = stream.data + end + end + + private :initialize + + def consume(bytes) + raise ArgumentError, "marshal data too short" if @consumed > @stream.bytesize + data = @stream.byteslice @consumed, bytes + @consumed += bytes + data + end + + def consume_byte + raise ArgumentError, "marshal data too short" if @consumed >= @stream.bytesize + data = @byte_array.get_byte @consumed + @consumed += 1 + return data + end + end + + def self.dump(obj, an_io=nil, limit=nil) + unless limit + if Rubinius::Type.object_kind_of? an_io, Fixnum + limit = an_io + an_io = nil + else + limit = -1 + end + end + + depth = Rubinius::Type.coerce_to limit, Fixnum, :to_int + ms = State.new nil, depth, nil + + if an_io + if !Rubinius::Type.object_respond_to? an_io, :write + raise TypeError, "output must respond to write" + end + if Rubinius::Type.object_respond_to? an_io, :binmode + an_io.binmode + end + end + + str = Rubinius::Type.binary_string(VERSION_STRING) + ms.serialize(obj) + + if an_io + an_io.write(str) + return an_io + end + + return str + end + + def self.load(obj, prc = nil) + if Rubinius::Type.object_respond_to? obj, :to_str + data = obj.to_s + + major = data.getbyte 0 + minor = data.getbyte 1 + + ms = StringState.new data, nil, prc + + elsif Rubinius::Type.object_respond_to? obj, :read and + Rubinius::Type.object_respond_to? obj, :getc + ms = IOState.new obj, nil, prc + + major = ms.consume_byte + minor = ms.consume_byte + else + raise TypeError, "instance of IO needed" + end + + if major != MAJOR_VERSION or minor > MINOR_VERSION + raise TypeError, "incompatible marshal file format (can't be read)\n\tformat version #{MAJOR_VERSION}.#{MINOR_VERSION} required; #{major.inspect}.#{minor.inspect} given" + end + + ms.construct + end + + class << self + alias_method :restore, :load + end + +end diff --git a/gxg/core/match_data.rb b/gxg/core/match_data.rb new file mode 100644 index 0000000..9ac4513 --- /dev/null +++ b/gxg/core/match_data.rb @@ -0,0 +1,208 @@ +class MatchData + def begin(idx) + if idx == 0 + start = @full.at(0) + else + start = @region.at(idx - 1).at(0) + return nil if start == -1 + end + m = Rubinius::Mirror.reflect @source + m.byte_to_character_index start + end + + def end(idx) + if idx == 0 + fin = @full.at(1) + else + fin = @region.at(idx - 1).at(1) + return nil if fin == -1 + end + m = Rubinius::Mirror.reflect @source + m.byte_to_character_index fin + end + + def offset(idx) + out = [] + out << self.begin(idx) + out << self.end(idx) + return out + end + + def [](idx, len = nil) + return to_a[idx, len] if len + + case idx + when Fixnum + if idx <= 0 + return matched_area() if idx == 0 + return to_a[idx] + elsif idx <= @region.size + tup = @region[idx - 1] + + x = tup.at(0) + return nil if x == -1 + + y = tup.at(1) + return @source.byteslice(x, y-x) + end + when String + if @regexp.name_table + return self[idx.to_sym] + end + raise IndexError, "Unknown named group '#{idx}'" + when Symbol + if @regexp.name_table + if nums = @regexp.name_table[idx] + nums.reverse_each do |num| + val = self[num] + return val if val + end + return nil + end + end + raise IndexError, "Unknown named group '#{idx}'" + end + + return to_a[idx] + end + + attr_reader :regexp + + def ==(other) + other.kind_of?(MatchData) && + string == other.string && + regexp == other.regexp && + captures == other.captures + end + alias_method :eql?, :== + + def string + @source.dup.freeze + end + + def source + @source + end + + def full + @full + end + + def length + @region.fields + 1 + end + + def captures + out = Array.new(@region.fields) + + idx = 0 + @region.each do |tup| + x = tup.at(0) + + if x == -1 + val = nil + else + y = tup.at(1) + val = @source.byteslice(x, y-x) + end + + out[idx] = val + idx += 1 + end + + return out + end + + def names + @regexp.names + end + + def pre_match + return @source.byteslice(0, 0) if @full.at(0) == 0 + nd = @full.at(0) - 1 + @source.byteslice(0, nd+1) + end + + def pre_match_from(idx) + return @source.byteslice(0, 0) if @full.at(0) == 0 + nd = @full.at(0) - 1 + @source.byteslice(idx, nd-idx+1) + end + + def collapsing? + @full[0] == @full[1] + end + + def post_match + nd = @source.bytesize - 1 + st = @full.at(1) + @source.byteslice(st, nd-st+1) + end + + def inspect + capts = captures + if capts.empty? + "#" + else + idx = 0 + capts.map! { |capture| "#{idx += 1}:#{capture.inspect}" } + "#" + end + end + + def select + unless block_given? + raise LocalJumpError, "no block given" + end + + out = [] + ma = matched_area() + out << ma if yield ma + + each_capture do |str| + if yield(str) + out << str + end + end + return out + end + + alias_method :size, :length + + def to_a + ary = captures() + ary.unshift matched_area() + return ary + end + + def values_at(*indexes) + indexes.map { |i| self[i] }.flatten(1) + end + + def matched_area + x = @full.at(0) + y = @full.at(1) + @source.byteslice(x, y-x) + end + + alias_method :to_s, :matched_area + private :matched_area + + def get_capture(num) + x, y = @region[num] + return nil if !y or x == -1 + + return @source.byteslice(x, y-x) + end + + private :get_capture + + def each_capture + @region.each do |tup| + x, y = *tup + yield @source.byteslice(x, y-x) + end + end + + private :each_capture +end diff --git a/gxg/core/math.rb b/gxg/core/math.rb new file mode 100644 index 0000000..56e9697 --- /dev/null +++ b/gxg/core/math.rb @@ -0,0 +1,250 @@ +module Math + def atan2(y, x) + y = Rubinius::Type.coerce_to_float y + x = Rubinius::Type.coerce_to_float x + FFI::Platform::Math.atan2 y, x + end + module_function :atan2 + + def cos(x) + FFI::Platform::Math.cos Rubinius::Type.coerce_to_float(x) + end + module_function :cos + + def sin(x) + FFI::Platform::Math.sin Rubinius::Type.coerce_to_float(x) + end + module_function :sin + + def tan(x) + FFI::Platform::Math.tan Rubinius::Type.coerce_to_float(x) + end + module_function :tan + + def atan(x) + FFI::Platform::Math.atan Rubinius::Type.coerce_to_float(x) + end + module_function :atan + + def cosh(x) + FFI::Platform::Math.cosh Rubinius::Type.coerce_to_float(x) + end + module_function :cosh + + def sinh(x) + FFI::Platform::Math.sinh Rubinius::Type.coerce_to_float(x) + end + module_function :sinh + + def tanh(x) + FFI::Platform::Math.tanh Rubinius::Type.coerce_to_float(x) + end + module_function :tanh + + def asinh(x) + FFI::Platform::Math.asinh Rubinius::Type.coerce_to_float(x) + end + module_function :asinh + + def atanh(x) + x = Rubinius::Type.coerce_to_float(x) + raise DomainError, 'atanh' unless x.abs <= 1.0 + + FFI::Platform::POSIX.errno = 0 + + ret = FFI::Platform::Math.atanh x + begin + Errno.handle + + # Linux sets errno to ERANGE, but it should be EDOM + rescue Errno::ERANGE + raise Errno::EDOM + end + + ret + end + module_function :atanh + + def exp(x) + FFI::Platform::Math.exp Rubinius::Type.coerce_to_float(x) + end + module_function :exp + + def frexp(x) + x = Rubinius::Type.coerce_to_float(x) + FFI::MemoryPointer.new :int do |exp| + result = FFI::Platform::Math.frexp x, exp + [result, exp.read_int] + end + end + module_function :frexp + + def ldexp(x, n) + if n.kind_of? Float and n.nan? + raise RangeError, "NaN cannot be converted to an Integer" + end + + n = Rubinius::Type.coerce_to(n, Integer, :to_int) + + FFI::Platform::Math.ldexp Rubinius::Type.coerce_to_float(x), n + end + module_function :ldexp + + # Rubinius-specific, used in Marshal + def modf(x) + FFI::MemoryPointer.new :double do |integral| + fractional = FFI::Platform::Math.modf x, integral + [fractional, integral.read_double] + end + end + module_function :modf + + def hypot(x, y) + x = Rubinius::Type.coerce_to_float x + y = Rubinius::Type.coerce_to_float y + FFI::Platform::Math.hypot x, y + end + module_function :hypot + + def erf(x) + FFI::Platform::Math.erf Rubinius::Type.coerce_to_float(x) + end + module_function :erf + + def erfc(x) + FFI::Platform::Math.erfc Rubinius::Type.coerce_to_float(x) + end + module_function :erfc + + def asin(x) + return Float::NAN if x.kind_of? Float and x.nan? + + x = Rubinius::Type.coerce_to_float(x) + raise DomainError, 'asin' unless x.abs <= 1.0 + FFI::Platform::Math.asin x + end + module_function :asin + + def acos(x) + return Float::NAN if x.kind_of? Float and x.nan? + + x = Rubinius::Type.coerce_to_float(x) + raise DomainError, 'acos' unless x.abs <= 1.0 + + FFI::Platform::POSIX.errno = 0 + + ret = FFI::Platform::Math.acos x + Errno.handle + ret + end + module_function :acos + + def acosh(x) + return Float::NAN if x.kind_of? Float and x.nan? + + x = Rubinius::Type.coerce_to_float(x) + raise DomainError, 'acosh' unless x >= 1.0 + FFI::Platform::Math.acosh x + end + module_function :acosh + + def cbrt(x) + x = Rubinius::Type.coerce_to_float x + FFI::Platform::Math.cbrt x + end + module_function :cbrt + + def gamma(x) + x = Rubinius::Type.coerce_to_float x + + # if x is negative zero, return -infinity + return -Float::INFINITY if (1 / x) == -Float::INFINITY + return Float::INFINITY if x == 0.0 + return Float::NAN if x.nan? + + if sign = x.infinite? + raise DomainError, "gamma" if sign == -1 + + return Float::INFINITY + end + + FFI::MemoryPointer.new :double do |integral| + fractional = FFI::Platform::Math.modf x, integral + int = integral.read_double.to_i + + if fractional == 0.0 + raise DomainError, "gamma" if int < 0 + return FactorialTable[int - 1] if int <= FactorialTable.size + end + end + + FFI::Platform::Math.tgamma x + end + module_function :gamma + + def lgamma(x) + x = Rubinius::Type.coerce_to_float x + + if sign = x.infinite? + raise DomainError, "lgamma" if sign == -1 + + return [Float::INFINITY, 1] + end + + FFI::MemoryPointer.new :int do |sign| + sign.write_int 1 + result = FFI::Platform::Math.lgamma_r x, sign + [result, sign.read_int] + end + end + module_function :lgamma + + def log(x, base=undefined) + return Float::NAN if x.kind_of? Float and x.nan? + + x = Rubinius::Type.coerce_to_float x + raise DomainError, 'log' unless x >= 0.0 + return -Float::INFINITY if x == 0.0 + y = FFI::Platform::Math.log x + unless undefined.equal? base + base = Rubinius::Type.coerce_to_float base + y /= log(base) + end + y + end + module_function :log + + def log2(x) + return Float::NAN if x.kind_of? Float and x.nan? + + if x.kind_of? Bignum and x >= 0 and Float::MAX_EXP <= (bits = x.bit_length) + bits -= Float::MANT_DIG + x >>= bits + else + bits = 0 + end + + x = Rubinius::Type.coerce_to_float(x) + raise DomainError, 'log2' unless x >= 0.0 + FFI::Platform::Math.log2(x) + bits + end + module_function :log2 + + def log10(x) + return Float::NAN if x.kind_of? Float and x.nan? + + x = Rubinius::Type.coerce_to_float(x) + raise DomainError, 'log10' unless x >= 0.0 + FFI::Platform::Math.log10 x + end + module_function :log10 + + def sqrt(x) + return Float::NAN if x.kind_of? Float and x.nan? + + x = Rubinius::Type.coerce_to_float(x) + raise DomainError, 'sqrt' unless x >= 0.0 + FFI::Platform::Math.sqrt x + end + module_function :sqrt +end diff --git a/gxg/core/method.rb b/gxg/core/method.rb new file mode 100644 index 0000000..704943e --- /dev/null +++ b/gxg/core/method.rb @@ -0,0 +1,362 @@ +## +# Method objects are essentially detached, freely passed-around methods. The +# Method is a copy of the method on the object at the time of extraction, so +# if the method itself is changed, overridden, aliased or removed from the +# object, the Method object still contains the old functionality. In addition, +# the call itself is not in any way stored so it will reflect the state of the +# object at the time of calling. +# +# Methods are normally bound to a particular object but it is possible to use +# Method#unbind to create an UnboundMethod object for the purpose of +# re-binding to a different object. + +class Method + + ## + # Takes and stores the receiver object, the method's bytecodes and the + # Module that the method is defined in. + + def initialize(receiver, defined_in, executable, name) + @receiver = receiver + @defined_in = defined_in + @executable = executable + @name = name + end + + private :initialize + + attr_reader :receiver + attr_reader :defined_in + attr_reader :executable + + def name + @name + end + + ## + # Method objects are equal if they have the same body and are bound to the + # same object. + + def ==(other) + other.class == Method && + @receiver.equal?(other.receiver) && + (@executable == other.executable || + Rubinius::MethodEquality.method_equal_to_delegated_method_receiver?(self, other)) + end + + alias_method :eql?, :== + + ## + # Indication of how many arguments this method takes. It is defined so that + # a non-negative Integer means the method takes that fixed amount of + # arguments (up to 1024 currently.) A negative Integer is used to indicate a + # variable argument count. The number is ((-n) - 1), where n is the number + # of required args. Blocks are not counted. + # + # def foo(); end # arity => 0 + # def foo(a, b); end # arity => 2 + # def foo(a, &b); end # arity => 1 + # def foo(a, b = nil); end # arity => ((-1) -1) => -2 + # def foo(*a); end # arity => ((-0) -1) => -1 + # def foo(a, b, *c, &d); end # arity => ((-2) -1) => -3 + + def arity + @executable.arity + end + + ## + # Execute the method. This works exactly like calling a method with the same + # code on the receiver object. Arguments and a block can be supplied + # optionally. + + def call(*args, &block) + @executable.invoke(@name, @defined_in, @receiver, args, block) + end + + alias_method :[], :call + + ## + # String representation of this Method includes the method name, the Module + # it is defined in and the Module that it was extracted from. + + def inspect + file, line = source_location() + + if @executable.respond_to? :eval_source + if src = @executable.eval_source + src_line = src.split("\n")[line - 1] + return "#<#{self.class}: #{@receiver.class}##{@name} (defined in #{@defined_in} from \"#{src_line}\")>" + end + end + + if file + "#<#{self.class}: #{@receiver.class}##{@name} (defined in #{@defined_in} at #{file}:#{line})>" + else + "#<#{self.class}: #{@receiver.class}##{@name} (defined in #{@defined_in})>" + end + end + + alias_method :to_s, :inspect + + ## + # Location gives the file and line number of the start of this method's + # definition. + + def source_location + if @executable.respond_to? :file + [@executable.file.to_s, @executable.defined_line] + elsif @executable.respond_to? :source_location + @executable.source_location + else + nil + end + end + + def source + if @executable.respond_to? :eval_source + return @executable.eval_source + end + + return nil + end + + def parameters + if @executable.respond_to? :parameters + @executable.parameters + else + [] + end + end + + def owner + if @defined_in.class == Rubinius::IncludedModule + @defined_in.module + else + @defined_in + end + end + + ## + # Returns a Proc object corresponding to this Method. + + def to_proc + Proc.from_method self + end + + ## + # Calls curry on the method in proc representation + def curry(n = nil) + to_proc.curry(n) + end + + ## + # Detach this Method from the receiver object it is bound to and create an + # UnboundMethod object. Populates the UnboundMethod with the method data as + # well as the Module it is defined in and the Module it was extracted from. + # + # See UnboundMethod for more information. + + def unbind + UnboundMethod.new(@defined_in, @executable, @receiver.class, @name) + end + + def super_method + superclass = @defined_in.direct_superclass + + if superclass + mod, entry = superclass.lookup_method(@name) + + if entry && entry.visibility != :undef + return Method.new(@receiver, superclass, entry.method, @name) + end + end + + return nil + end + + def for_define_method(name, klass, callable_proc = nil) + Rubinius::Type.bindable_method? self.defined_in, klass + + scope = @executable.scope + + if @executable.is_a? Rubinius::DelegatedMethod + code = @executable + else + if callable_proc + code = Rubinius::DelegatedMethod.new(name, :call, callable_proc, false) + else + code = Rubinius::DelegatedMethod.new(name, :call_on_instance, self.unbind, true) + end + end + + [code, scope] + end +end + +## +# UnboundMethods are similar to Method objects except that they are not +# connected to any particular object. They cannot be used standalone for this +# reason, and must be bound to an object first. The object must be kind_of? +# the Module in which this method was originally defined. +# +# UnboundMethods can be created in two ways: first, any existing Method object +# can be sent #unbind to detach it from its current object and return an +# UnboundMethod instead. Secondly, they can be directly created by calling +# Module#instance_method with the desired method's name. +# +# The UnboundMethod is a copy of the method as it existed at the time of +# creation. Any subsequent changes to the original will not affect any +# existing UnboundMethods. + +class UnboundMethod + + ## + # Accepts and stores the Module where the method is defined in as well as + # the CompiledCode itself. Class of the object the method was extracted + # from can be given but will not be stored. This is always used internally + # only. + + def initialize(mod, executable, pulled_from, name) + @defined_in = mod + @executable = executable + @pulled_from = pulled_from + @name = name + end + + private :initialize + + attr_reader :executable + attr_reader :defined_in + + def name + @name + end + + ## + # Convenience method for #binding to the given receiver object and calling + # it with the optionally supplied arguments. + + def call_on_instance(obj, *args, &block) + @executable.invoke(@name, @defined_in, obj, args, block) + end + + ## + # UnboundMethod objects are equal if and only if they refer to the same + # method. One may be an alias for the other or both for a common one. Both + # must have been extracted from the same class or subclass. Two from + # different subclasses will not be considered equal. + + def ==(other) + other.kind_of? UnboundMethod and + @defined_in == other.defined_in and + @executable == other.executable + end + + def hash + @defined_in.hash ^ @executable.hash + end + + ## + # See Method#arity. + + def arity + @executable.arity + end + + ## + # Creates a new Method object by attaching this method to the supplied + # receiver. One of the following must be true: + # - the method was defined in a module + # - the method is being bound to an instance of a subclass of the method's source + # - the method is being bound to an instance of the same class + + def bind(receiver) + from_module = Rubinius::Type.object_kind_of? @defined_in, Module + from_class = Rubinius::Type.object_kind_of? @defined_in, Class + from_module_not_class = from_module && !from_class + + unless Rubinius::Type.object_kind_of?(receiver, @defined_in) or from_module_not_class + if Rubinius::Type.singleton_class_object(@defined_in) + raise TypeError, "illegal attempt to rebind a singleton method to another object" + end + + raise TypeError, "Must be bound to an object of kind #{@defined_in}" + end + + Method.new receiver, @defined_in, @executable, @name + end + + ## + # String representation for UnboundMethod includes the method name, the + # Module it is defined in and the Module that it was extracted from. + + def inspect + file, line = source_location() + + if file + "#<#{self.class}: #{@pulled_from}##{@name} (defined in #{@defined_in} at #{file}:#{line})>" + else + "#<#{self.class}: #{@pulled_from}##{@name} (defined in #{@defined_in})>" + end + end + + alias_method :to_s, :inspect + + def source_location + if @executable.respond_to? :file + [@executable.file.to_s, @executable.defined_line] + elsif @executable.respond_to? :source_location + @executable.source_location + else + nil + end + end + + def parameters + return @executable.respond_to?(:parameters) ? @executable.parameters : [] + end + + def owner + if @defined_in.class == Rubinius::IncludedModule + @defined_in.module + else + @defined_in + end + end + + def super_method + superclass = @defined_in.direct_superclass + + if superclass + mod, entry = superclass.lookup_method(@name) + + if entry && entry.visibility != :undef + return UnboundMethod.new(superclass, entry.method, @defined_in, @name) + end + end + + return nil + end + + def for_define_method(name, klass, callable_proc = nil) + Rubinius::Type.bindable_method? self.defined_in, klass + + case @executable + when Rubinius::AccessVariable + code = @executable + scope = nil + when Rubinius::DelegatedMethod + code = @executable + scope = code.scope + else + if callable_proc + code = Rubinius::DelegatedMethod.new(name, :call, callable_proc, false) + else + code = Rubinius::DelegatedMethod.new(name, :call_on_instance, self, true) + end + scope = @executable.scope + end + + [code, scope] + end +end diff --git a/gxg/core/method_equality.rb b/gxg/core/method_equality.rb new file mode 100644 index 0000000..995edb0 --- /dev/null +++ b/gxg/core/method_equality.rb @@ -0,0 +1,18 @@ +module Rubinius + module MethodEquality + def self.method_equal_to_delegated_method_receiver?(meth, other) + self_receiver = delegated_receiver_for(meth) || meth.unbind + other_receiver = delegated_receiver_for(other) || other.unbind + either_method_is_delegated?(meth, other) && (self_receiver == other_receiver) + end + + def self.either_method_is_delegated?(meth, other) + (meth.executable.kind_of?(Rubinius::CompiledCode) && other.executable.kind_of?(Rubinius::DelegatedMethod)) || + (meth.executable.kind_of?(Rubinius::DelegatedMethod) && other.executable.kind_of?(Rubinius::CompiledCode)) + end + + def self.delegated_receiver_for(meth) + meth.executable.receiver if meth.executable.kind_of?(Rubinius::DelegatedMethod) + end + end +end diff --git a/gxg/core/method_table.rb b/gxg/core/method_table.rb new file mode 100644 index 0000000..7f590aa --- /dev/null +++ b/gxg/core/method_table.rb @@ -0,0 +1,143 @@ +## +# Method table associates symbols (names) with methods (executables). Each +# entry also contains the visibility of that method. +# + +module Rubinius + class MethodTable + class Bucket + attr_reader :name + attr_reader :visibility + attr_reader :method + attr_reader :prediction + attr_reader :next + end + + attr_reader :values + attr_reader :bins + + def size + @entries + end + + alias_method :length, :size + + def self.allocate + Rubinius.primitive :methodtable_allocate + raise PrimitiveFailure, "MethodTable.allocate primitive failed" + end + + def initialize(hash=nil) + return unless hash + hash.each do |k, v| + self[k] = v + end + end + + private :initialize + + def duplicate + Rubinius.primitive :methodtable_duplicate + raise PrimitiveFailure, "MethodTable#duplicate primitive failed" + end + + def dup + copy = duplicate + Rubinius.privately do + copy.initialize_copy self + end + copy + end + + def name?(name) + Rubinius.primitive :methodtable_has_name + raise PrimitiveFailure, "MethodTable#name? primitive failed" + end + + alias_method :has_name?, :name? + alias_method :include?, :name? + alias_method :member?, :name? + + def delete(name) + Rubinius.primitive :methodtable_delete + raise PrimitiveFailure, "MethodTable#delete primitive failed" + end + + def each + raise LocalJumpError, "no block given" unless block_given? + + i = 0 + max = @bins + vals = @values + + while i < max + if entry = vals.at(i) + while entry + yield entry.name, entry.method, entry.visibility + entry = entry.next + end + end + i += 1 + end + self + end + + end +end +## +# Holds Executables for lookup by the VM. +# +# When looking up an Executable, Rubinius starts at the real class (the object +# in the class slot of the receiver) and looks in its MethodTable for the +# method name. If the method is not found the ancestors list will be +# walked looking for the method. +# +# If no method was found, Rubinius restarts at the real class looking for +# "method_missing", walking the ancestors list if it is not found. +# +# If "method_missing" is not found, a VM assertion is triggered. + +module Rubinius + class MethodTable + include Enumerable + + def filter_entries + out = [] + i = 0 + while i < @bins + if entry = @values.at(i) + while entry + if val = yield(entry) + out << val + end + + entry = entry.next + end + end + i += 1 + end + + out + end + + def public_names + filter_entries do |entry| + entry.visibility == :public ? entry.name : nil + end + end + + def private_names + filter_entries do |entry| + entry.visibility == :private ? entry.name : nil + end + end + + def protected_names + filter_entries do |entry| + entry.visibility == :protected ? entry.name : nil + end + end + + alias_method :to_a, :public_names + end +end diff --git a/gxg/core/metrics.rb b/gxg/core/metrics.rb new file mode 100644 index 0000000..bde400c --- /dev/null +++ b/gxg/core/metrics.rb @@ -0,0 +1,32 @@ +module Rubinius + module Metrics + @enabled = false + + def self.enabled? + !!@enabled + end + + def self.data + @data ||= Data.new + end + + class Data + def keys + Map.keys + end + + def values + Values.to_a + end + + def [](key) + Values[Map[key]] + end + + def to_hash + Map.map { |k, i| MetricsHash[k] = Values[i] } + MetricsHash + end + end + end +end diff --git a/gxg/core/mirror.rb b/gxg/core/mirror.rb new file mode 100644 index 0000000..6fe0f9b --- /dev/null +++ b/gxg/core/mirror.rb @@ -0,0 +1,48 @@ +module Rubinius + class Mirror + def self.subject=(klass) + @subject = klass + end + + def self.subject + @subject + end + + def self.reflect(obj) + klass = Rubinius.invoke_primitive :module_mirror, obj + klass.new obj if klass + end + + attr_reader :object + + def initialize(obj) + @object = obj + end + + private :initialize + + class Object < Mirror + self.subject = ::Object + + def nil_code_id + "%08x" % Rubinius.invoke_primitive(:object_nil_code_id, @object) + end + + def nil_ip + Rubinius.invoke_primitive :object_nil_ip, @object + end + end + + def instance_fields + Rubinius.invoke_primitive :object_instance_fields, @object + end + + def instance_variables + Rubinius.invoke_primitive :object_instance_variables, @object + end + + def inspect + "#<#{self.class.name}:0x#{self.object_id.to_s(16)} object=#{@object.inspect}>" + end + end +end diff --git a/gxg/core/missing_method.rb b/gxg/core/missing_method.rb new file mode 100644 index 0000000..744283f --- /dev/null +++ b/gxg/core/missing_method.rb @@ -0,0 +1,37 @@ +## +# MissingMethod is used to implement the Method returned by Kernel#method +# when #respond_to_missing? returns true. + +module Rubinius + class MissingMethod < Executable + attr_reader :receiver + attr_reader :method + + def initialize(receiver, method) + @receiver = receiver + @method = method + end + + private :initialize + + def call(called_object, called_method, *args, &block) + @receiver.__send__(@method, *args, &block) + end + + def arity + -1 + end + + def parameters + [[:rest]] + end + + def ==(other) + other.kind_of?(MissingMethod) && + @receiver == other.receiver && + @method == other.method + end + + alias_method :eql?, :== + end +end diff --git a/gxg/core/module.rb b/gxg/core/module.rb new file mode 100644 index 0000000..b5764ae --- /dev/null +++ b/gxg/core/module.rb @@ -0,0 +1,797 @@ +## +# Some terminology notes: +# +# [Encloser] The Class or Module inside which this one is defined or, in the +# event we are at top-level, Object. +# +# [Direct superclass] Whatever is next in the chain of superclass invocations. +# This may be either an included Module, a Class or nil. +# +# [Superclass] The real semantic superclass and thus only applies to Class +# objects. + +class Module + + attr_reader :constant_table + attr_writer :method_table + + private :included + + def self.nesting + scope = Rubinius::LexicalScope.of_sender + nesting = [] + while scope and scope.module != Object + nesting << scope.module + scope = scope.parent + end + nesting + end + + def initialize(&block) + # The method and constants tables are created and setup + # by Class and Module allocate. + module_eval(&block) if block + end + + private :initialize + + def deprecate_constant(*syms) + # no-op for now + end + + def const_get(name, inherit=true) + Rubinius::Type.const_get self, name, inherit + end + + def const_defined?(name, inherit=true) + c = Rubinius::Type.const_get self, name, inherit, false + return c.equal?(undefined) ? false : true + end + + def attr(*attributes) + vis = Rubinius::VariableScope.of_sender.method_visibility + + if attributes.size == 2 && (attributes.last.is_a?(TrueClass) || attributes.last.is_a?(FalseClass)) + name = attributes.first + Rubinius.add_reader name, self, vis + Rubinius.add_writer name, self, vis if attributes.last + else + attributes.each do |name| + if name.is_a?(Symbol) || (name.respond_to?(:to_str) && name.to_str.is_a?(String) && !name.to_str.empty?) + Rubinius.add_reader(name, self, vis) + else + raise TypeError, "#{name.inspect} is not a symbol" + end + end + end + + return nil + end + + private :attr + + # Install a new Autoload object into the constants table + # See core/autoload.rb + def autoload(name, path) + path = Rubinius::Type.coerce_to_path(path) + + raise ArgumentError, "empty file name" if path.empty? + + name = Rubinius::Type.coerce_to_constant_name name + + Rubinius.check_frozen + + if entry = @constant_table.lookup(name) + if entry.constant.kind_of? Autoload + # If there is already an Autoload here, just change the path to + # autoload! + entry.constant.set_path(path) + else + # Trying to register an autoload for a constant that already exists, + # ignore the request entirely. + end + + return + end + + autoload_constant = Autoload.new(name, self, path) + constant_table.store(name, autoload_constant, :public) + if self == Kernel + Object.singleton_class.constant_table.store(name, autoload_constant, :public) + end + + Rubinius.inc_global_serial + return nil + end + + def constants(all=undefined) + tbl = Rubinius::LookupTable.new + + @constant_table.each do |name, constant, visibility| + tbl[name] = true unless visibility == :private + end + + if all + current = self.direct_superclass + + while current and current != Object + current.constant_table.each do |name, constant, visibility| + tbl[name] = true unless visibility == :private + end + + current = current.direct_superclass + end + end + + # special case: Module.constants returns Object's constants + if self.equal?(Module) && undefined.equal?(all) + Object.constant_table.each do |name, constant, visibility| + tbl[name] = true unless visibility == :private + end + end + + tbl.keys + end + + #-- + # These have to be aliases, not methods that call instance eval, because we + # need to pull in the binding of the person that calls them, not the + # intermediate binding. + #++ + + def module_eval(string=undefined, filename="(eval)", line=1, &prc) + # we have a custom version with the prc, rather than using instance_exec + # so that we can setup the LexicalScope properly. + if prc + unless undefined.equal?(string) + raise ArgumentError, "cannot pass both string and proc" + end + + # Return a copy of the BlockEnvironment with the receiver set to self + env = prc.block + lexical_scope = env.repoint_scope self + return env.call_under(self, lexical_scope, true, self) + elsif undefined.equal?(string) + raise ArgumentError, 'block not supplied' + end + + string = StringValue(string) + filename = StringValue(filename) + + # The constantscope of a module_eval CM is the receiver of module_eval + cs = Rubinius::LexicalScope.new self, Rubinius::LexicalScope.of_sender + + binding = Binding.setup(Rubinius::VariableScope.of_sender, + Rubinius::CompiledCode.of_sender, + cs) + + c = Rubinius::ToolSets::Runtime::Compiler + be = c.construct_block string, binding, filename, line + + be.call_under self, cs, true, self + end + + alias_method :class_eval, :module_eval + + def private_constant(*names) + names = names.map(&:to_sym) + unknown_constants = names - @constant_table.keys + if unknown_constants.size > 0 + raise NameError, "#{unknown_constants.size > 1 ? 'Constants' : 'Constant'} #{unknown_constants.map{|e| "#{name}::#{e}"}.join(', ')} undefined" + end + names.each do |name| + entry = @constant_table.lookup(name) + entry.visibility = :private + end + end + + def public_constant(*names) + names = names.map(&:to_sym) + unknown_constants = names - @constant_table.keys + if unknown_constants.size > 0 + raise NameError, "#{unknown_constants.size > 1 ? 'Constants' : 'Constant'} #{unknown_constants.map{|e| "#{name}::#{e}"}.join(', ')} undefined" + end + names.each do |name| + entry = @constant_table.lookup(name) + entry.visibility = :public + end + end + + ## + # Returns an UnboundMethod corresponding to the given name. The name will be + # searched for in this Module as well as any included Modules or + # superclasses. The UnboundMethod is populated with the method name and the + # Module that the method was located in. + # + # Raises a TypeError if the given name.to_sym fails and a NameError if the + # name cannot be located. + + def instance_method(name) + name = Rubinius::Type.coerce_to_symbol name + + mod = self + while mod + if mod == mod.origin && entry = mod.method_table.lookup(name) + break if entry.visibility == :undef + + if meth = entry.get_method + if meth.kind_of? Rubinius::Alias + meth = meth.original_exec + end + + mod = mod.module if mod.class == Rubinius::IncludedModule + + return UnboundMethod.new(mod, meth, self, name) + end + end + + mod = mod.direct_superclass + end + + raise NameError.new("undefined method `#{name}' for #{self}", name) + end + + def public_instance_method(name) + name = Rubinius::Type.coerce_to_symbol name + + mod = self + while mod + if mod == mod.origin && entry = mod.method_table.lookup(name) + vis = entry.visibility + break if vis == :undef + + if meth = entry.method + if meth.kind_of? Rubinius::Alias + meth = meth.original_exec + end + + mod = mod.module if mod.class == Rubinius::IncludedModule + + if vis == :public + return UnboundMethod.new(mod, meth, self, name) + else + raise NameError.new("method `#{name}' for module `#{self}' is #{vis}", name) + end + end + end + + mod = mod.direct_superclass + end + + raise NameError.new("undefined method `#{name}' for #{self}", name) + end + + def visibility_for_aliased_method(new_name, current_name_visibility) + special_methods = [ + :initialize, + :initialize_copy, + :initialize_clone, + :initialize_dup, + :respond_to_missing? + ] + + if special_methods.include?(new_name) + :private + else + current_name_visibility + end + end + private :visibility_for_aliased_method + + def verify_class_variable_name(name) + if name.kind_of? Symbol + return name if name.is_cvar? + raise NameError, "#{name} is not an allowed class variable name" + end + + name = StringValue(name).to_sym + unless name.is_cvar? + raise NameError, "#{name} is not an allowed class variable name" + end + name + end + private :verify_class_variable_name + + def class_variable_set(name, val) + Rubinius.primitive :module_cvar_set + + class_variable_set verify_class_variable_name(name), val + end + + def class_variable_get(name) + Rubinius.primitive :module_cvar_get + + class_variable_get verify_class_variable_name(name) + end + + def class_variable_defined?(name) + Rubinius.primitive :module_cvar_defined + + class_variable_defined? verify_class_variable_name(name) + end + + def remove_class_variable(name) + Rubinius.primitive :module_cvar_remove + + remove_class_variable verify_class_variable_name(name) + end + + def __class_variables__ + Rubinius.primitive :module_class_variables + + raise PrimitiveFailure, "Module#__class_variables__ primitive failed" + end + alias_method :class_variables, :__class_variables__ + + def lookup_method(sym, check_object_too=true, trim_im=true) + mod = self + + while mod + if mod == mod.origin && entry = mod.method_table.lookup(sym.to_sym) + mod = mod.module if trim_im and mod.kind_of? Rubinius::IncludedModule + return [mod, entry] + end + + mod = mod.direct_superclass + end + + # Optionally also search Object (and everything included in Object); + # this lets a module alias methods on Object or Kernel. + if check_object_too and instance_of?(Module) + return Object.lookup_method(sym, true, trim_im) + end + end + + def ancestors + out = [] + Rubinius::Type.each_ancestor(self) { |mod| out << mod } + return out + end + + def public_method_defined?(sym) + sym = Rubinius::Type.coerce_to_reflection_name(sym) + mod, meth = lookup_method(sym, false) + meth ? meth.public? : false + end + + def private_method_defined?(sym) + sym = Rubinius::Type.coerce_to_reflection_name(sym) + mod, meth = lookup_method(sym, false) + meth ? meth.private? : false + end + + def protected_method_defined?(sym) + sym = Rubinius::Type.coerce_to_reflection_name(sym) + mod, meth = lookup_method(sym, false) + meth ? meth.protected? : false + end + + def method_defined?(sym) + sym = Rubinius::Type.coerce_to_symbol(sym) + mod, meth = lookup_method(sym, false) + meth ? meth.public? || meth.protected? : false + end + + def instance_methods(all=true) + ary = [] + if all + table = Rubinius::LookupTable.new + + mod = self + + while mod + mod.method_table.each do |name, obj, vis| + unless table.key?(name) + table[name] = true + ary << name if vis == :public or vis == :protected + end + end + + mod = mod.direct_superclass + end + else + method_table.each do |name, obj, vis| + ary << name if vis == :public or vis == :protected + end + end + + ary + end + + def public_instance_methods(all=true) + filter_methods(:public, all) + end + + def private_instance_methods(all=true) + filter_methods(:private, all) + end + + def protected_instance_methods(all=true) + filter_methods(:protected, all) + end + + def filter_methods(filter, all) + ary = [] + if all and self.direct_superclass + table = Rubinius::LookupTable.new + mod = self + + while mod + mod.method_table.each do |name, obj, vis| + unless table.key?(name) + table[name] = true + ary << name if vis == filter + end + end + + mod = mod.direct_superclass + end + else + method_table.each do |name, obj, vis| + ary << name if vis == filter + end + end + + ary + end + + private :filter_methods + + def define_method(name, meth = undefined, &prc) + if undefined.equal?(meth) and !block_given? + raise ArgumentError, "tried to create Proc object without a block" + end + + meth = prc if undefined.equal?(meth) + + name = Rubinius::Type.coerce_to_symbol name + + if meth.respond_to?(:for_define_method) + code, scope = meth.for_define_method(name, self.class) + else + raise TypeError, "wrong argument type #{meth.class} (expected Proc/Method)" + end + + Rubinius.add_method name, code, self, scope, 0, :public + + name + end + + private :define_method + + def thunk_method(name, value) + thunk = Rubinius::Thunk.new(value) + name = Rubinius::Type.coerce_to_symbol name + Rubinius.add_method name, thunk, self, nil, 0, :public + + name + end + + def extend_object(obj) + include_into Rubinius::Type.object_singleton_class(obj) + end + private :extend_object + + def include?(mod) + if !mod.kind_of?(Module) or mod.kind_of?(Class) + raise TypeError, "wrong argument type #{mod.class} (expected Module)" + end + + return false if self.equal?(mod) + + Rubinius::Type.each_ancestor(self) { |m| return true if mod.equal?(m) } + + false + end + + def included_modules + out = [] + sup = direct_superclass + + while sup + if sup.class == Rubinius::IncludedModule + out << sup.module + end + + sup = sup.direct_superclass + end + + out + end + + def set_visibility(meth, vis, where=nil) + name = Rubinius::Type.coerce_to_symbol(meth) + + Rubinius::VM.reset_method_cache self, name + + vis = vis.to_sym + + if entry = @method_table.lookup(name) + entry.visibility = vis + elsif lookup_method(name) + @method_table.store name, nil, nil, nil, 0, vis + else + raise NameError.new("Unknown #{where}method '#{name}' to make #{vis.to_s} (#{self})", name) + end + + return name + end + + def set_class_visibility(meth, vis) + Rubinius::Type.object_singleton_class(self).set_visibility meth, vis, "class " + end + private :set_class_visibility + + def module_exec(*args, &prc) + raise LocalJumpError, "Missing block" unless block_given? + + env = prc.block + lexical_scope = env.repoint_scope self + return env.call_under(self, lexical_scope, true, *args) + end + alias_method :class_exec, :module_exec + + ## + # Return the named constant enclosed in this Module. + # + # Included Modules and, for Class objects, superclasses are also searched. + # Modules will in addition look in Object. The name is attempted to convert + # using #to_str. If the constant is not found, #const_missing is called + # with the name. + + def const_missing(name) + unless self == Object + mod_name = "#{Rubinius::Type.module_name self}::" + end + case Rubinius.constant_missing_reason + when :private + msg = "Private constant: #{mod_name}#{name}" + else + msg = "Missing or uninitialized constant: #{mod_name}#{name}" + end + + raise NameError.new(msg, name) + end + + def <(other) + unless other.kind_of? Module + raise TypeError, "compared with non class/module" + end + + # We're not an ancestor of ourself + return false if self.equal? other + + Rubinius::Type.each_ancestor(self) { |mod| return true if mod.equal?(other) } + Rubinius::Type.each_ancestor(other) { |mod| return false if mod.equal?(self) } + + nil + end + + def <=(other) + equal?(other) || (self < other) + end + + def >(other) + unless other.kind_of? Module + raise TypeError, "compared with non class/module" + end + other < self + end + + def >=(other) + equal?(other) || (self > other) + end + + def <=>(other) + return 0 if self.equal? other + return nil unless other.kind_of? Module + + lt = self < other + return nil if lt.nil? + lt ? -1 : 1 + end + + def ===(inst) + Rubinius::Type.object_kind_of?(inst, self) + end + + # Is an autoload trigger defined for the given path? + def autoload?(name) + name = name.to_sym + entry = constant_table.lookup(name) + return unless entry + trigger = entry.constant + return unless trigger && trigger.kind_of?(Autoload) + trigger.path + end + + def remove_const(name) + unless name.kind_of? Symbol + name = StringValue name + + unless Rubinius::CType.isupper name.getbyte(0) + raise NameError, "constant names must begin with a capital letter: #{name}" + end + end + + sym = name.to_sym + unless constant_table.has_name?(sym) + mod_name = Rubinius::Type.module_name self + raise NameError, "Missing or uninitialized constant: #{mod_name}::#{name}" + end + + val = constant_table.delete(sym) + Rubinius.inc_global_serial + + # Silly API compact. Shield Autoload instances + return nil if Rubinius::Type.object_kind_of?(val, Autoload) + val + end + + private :remove_const + + def extended(name) + end + + private :extended + + def method_added(name) + end + + private :method_added + + def method_removed(name) + end + + private :method_removed + + def method_undefined(name) + end + + private :method_undefined + + def initialize_copy(other) + # If the method table is already different, we already + # initialized this module. + unless @method_table == other.method_table + raise TypeError, "already initialized module" + end + + @method_table = other.method_table.dup + + sc_s = Rubinius::Type.object_singleton_class self + sc_o = Rubinius::Type.object_singleton_class other + + sc_s.method_table = sc_o.method_table.dup + sc_s.superclass = sc_o.direct_superclass + + @constant_table = Rubinius::ConstantTable.new + + other.constant_table.each do |name, val| + if val.kind_of? Autoload + val = Autoload.new(val.name, self, val.path) + end + + @constant_table.store(name, val, :public) + end + + if other == other.origin + @origin = self + else + @origin = other.origin.dup + end + + self + end + + private :initialize_copy + + def add_ivars(code) + case code + when Rubinius::CompiledCode + new_ivars = code.literals.select { |l| l.kind_of?(Symbol) and l.is_ivar? } + return if new_ivars.empty? + + if @seen_ivars + new_ivars.each do |x| + unless @seen_ivars.include?(x) + @seen_ivars << x + end + end + else + @seen_ivars = new_ivars + end + when Rubinius::AccessVariable + if @seen_ivars + unless @seen_ivars.include?(code.name) + @seen_ivars << code.name + end + else + @seen_ivars = [code.name] + end + else + raise "Unknown type of method to learn ivars - #{code.class}" + end + end + + def dynamic_method(name, file="(dynamic)", line=1) + g = Rubinius::ToolSets::Runtime::Generator.new + g.name = name.to_sym + g.file = file.to_sym + g.set_line Integer(line) + + yield g + + g.close + + g.use_detected + g.encode + + code = g.package Rubinius::CompiledCode + + scope = Rubinius::LexicalScope.new(self, Rubinius::LexicalScope.new(Object)) + code.scope = scope + + Rubinius.add_method name, code, self, scope, 0, :public + + return code + end + + def freeze + @method_table.freeze + @constant_table.freeze + super + end + + def origin= origin + @origin = origin + end + + def prepended(mod); end + private :prepended + + def prepend(*modules) + modules.reverse_each do |mod| + if !mod.kind_of?(Module) or mod.kind_of?(Class) + raise TypeError, "wrong argument type #{mod.class} (expected Module)" + end + + Rubinius.privately do + mod.prepend_features self + end + + Rubinius.privately do + mod.prepended self + end + end + self + end + + def prepend_features(klass) + unless klass.kind_of? Module + raise TypeError, "invalid argument class #{klass.class}, expected Module" + end + + if kind_of? Class + raise TypeError, "invalid receiver class #{__class__}, expected Module" + end + + if klass == klass.origin + im = Rubinius::IncludedModule.new(klass) + im.superclass = klass.direct_superclass + klass.superclass = im + klass.origin = im + klass.method_table.each do |meth, obj, vis| + Rubinius::VM.reset_method_cache klass, meth + end + end + Rubinius::Type.include_modules_from(self, klass) + Rubinius::Type.infect(klass, self) + self + end + private :prepend_features + + def singleton_class? + !!Rubinius::Type.singleton_class_object(self) + end +end diff --git a/gxg/core/mutex.rb b/gxg/core/mutex.rb new file mode 100644 index 0000000..87b79b5 --- /dev/null +++ b/gxg/core/mutex.rb @@ -0,0 +1,82 @@ +class Mutex + def initialize + @owner = nil + end + + private :initialize + + # Check and only allow it to be marshal'd if there are no waiters. + def marshal_dump + raise "Unable to dump locked mutex" unless @waiters.empty? + 1 + end + + # Implemented because we must since we use marshal_load PLUS we need + # to create AND prime @lock. If we didn't do this, then Marshal + # wouldn't prime the lock anyway. + def marshal_load(bunk) + initialize + end + + def locked? + Rubinius.locked?(self) + end + + def try_lock + # Locking implies a memory barrier, so we don't need to use + # one explicitly. + if Rubinius.try_lock(self) + return false if @owner == Thread.current + @owner = Thread.current + true + else + false + end + end + + def lock + Rubinius.memory_barrier + if @owner == Thread.current + raise ThreadError, "Recursively locking not allowed" + end + + Rubinius.lock self + @owner = Thread.current + Rubinius.memory_barrier + return self + end + + def unlock + Rubinius.memory_barrier + + if @owner != Thread.current + raise ThreadError, "Not owner, #{@owner.inspect} is" + end + + @owner = nil + Rubinius.unlock self + self + end + + def synchronize + lock + begin + yield + ensure + unlock + end + end + + def sleep(duration=undefined) + if duration.kind_of?(Numeric) && duration < 0 + raise ArgumentError, "time interval must be positive" + end + + unlock + begin + Kernel.sleep(duration) + ensure + lock + end + end +end diff --git a/gxg/core/native_method.rb b/gxg/core/native_method.rb new file mode 100644 index 0000000..d2d8fd8 --- /dev/null +++ b/gxg/core/native_method.rb @@ -0,0 +1,86 @@ +## +# A wrapper for a calling a function in a shared library that has been +# attached via rb_define_method(). +# +# The primitive slot for a NativeMethod points to the nmethod_call primitive +# which dispatches to the underlying C function. + +module Rubinius + class NativeMethod < Executable + + attr_reader :file + attr_reader :name + attr_reader :arity + + # + # Load a dynamic library at path +library+, having name +extension_name+. + # + # +extension_name+ is used to calculate what Init_ function to call. + # + def self.load_extension(library, extension_name) + name = "Init_#{extension_name.sub(/\..*$/, '')}" + + begin + lib = FFI::DynamicLibrary.new(library, FFI::DynamicLibrary::RTLD_NOW | FFI::DynamicLibrary::RTLD_GLOBAL) + rescue LoadError => e + raise LoadError::InvalidExtensionError, e.message, e + end + + ver = lib.find_symbol("__X_rubinius_version") + + unless ver + raise LoadError::InvalidExtensionError, "Out-of-date or not compatible. Recompile or reinstall gem (#{library})" + end + + sym = lib.find_symbol(name) + + unless sym + raise LoadError::InvalidExtensionError, "Missing Init_ function (#{name})" + end + + entry_point = load_entry_point(library, name.to_sym, sym) + + entry_point.invoke(:init, Rubinius, self, [], nil) + + true + end + + # + # Load extension and generate a NativeMethod for its entry point. + # + # +ptr+ is an FFI::Pointer to a C function. + # + def self.load_entry_point(library, name, ptr) + Rubinius.primitive :nativemethod_load_extension_entry_point + raise PrimitiveFailure, "Rubinius::NativeMethod3load_entry_point primitive failed: #{library}" + end + + def lines + nil + end + + def literals + nil + end + + def line_from_ip(i) + 0 + end + + def first_line + 0 + end + + def defined_line + 0 + end + + def active_path + @file + end + + def scope + nil + end + end +end diff --git a/gxg/core/nil.rb b/gxg/core/nil.rb new file mode 100644 index 0000000..fbc4a42 --- /dev/null +++ b/gxg/core/nil.rb @@ -0,0 +1,53 @@ +class NilClass + alias_method :eql?, :equal? + alias_method :==, :equal? + alias_method :===, :equal? + + def &(other) + false + end + + def ^(other) + !!other + end + + alias_method :|, :^ + + def to_s + "" + end + + def inspect + "nil" + end + + def to_a + [] + end + + def to_f + 0.0 + end + + def to_i + 0 + end + + def to_r + Rational(0, 1) + end + + def rationalize(eps = nil) + to_r + end + + def to_c + Complex(0) + end + + def to_h + {} + end +end + +NIL = nil diff --git a/gxg/core/numeric.rb b/gxg/core/numeric.rb new file mode 100644 index 0000000..acbce6d --- /dev/null +++ b/gxg/core/numeric.rb @@ -0,0 +1,289 @@ +class Numeric + include Comparable + + # Always raises TypeError, as dup'ing Numerics is not allowed. + def initialize_copy(other) + raise TypeError, "copy of #{self.class} is not allowed" + end + + private :initialize_copy + + def +@ + self + end + + def -@ + 0 - self + end + + def divmod(other) + [div(other), self % other] + end + + def eql?(other) + return false unless other.instance_of? self.class + self == other + end + + def <=>(other) + # It's important this method NOT contain the coercion protocols! + # MRI doesn't and doing so breaks stuff! + + return 0 if self.equal? other + return nil + end + + def step(limit=nil, step=nil, to: nil, by: nil) + unless block_given? + return to_enum(:step, limit, step, to: to, by: by) do + Rubinius::Mirror::Numeric.reflect(self).step_size(limit, step, to, by) + end + end + + m = Rubinius::Mirror::Numeric.reflect(self) + values = m.step_fetch_args(limit, step, to, by) + value = values[0] + limit = values[1] + step = values[2] + asc = values[3] + is_float = values[4] + + if step == 0 + while true + yield value + end + end + + if is_float + n = m.step_float_size(value, limit, step, asc) + + if n > 0 + if step.infinite? + yield value + else + i = 0 + if asc + while i < n + d = i * step + value + d = limit if limit < d + yield d + i += 1 + end + else + while i < n + d = i * step + value + d = limit if limit > d + yield d + i += 1 + end + end + end + end + else + if asc + until value > limit + yield value + value += step + end + else + until value < limit + yield value + value += step + end + end + end + + return self + end + + def truncate + Float(self).truncate + end + + # Delegate #to_int to #to_i in subclasses + def to_int + to_i + end + + def integer? + false + end + + def zero? + self == 0 + end + + def nonzero? + zero? ? nil : self + end + + def round + to_f.round + end + + def abs + self < 0 ? -self : self + end + + def floor + FloatValue(self).floor + end + + def ceil + FloatValue(self).ceil + end + + def remainder(other) + mod = self % other + + if mod != 0 and ((self < 0 and other > 0) or (self > 0 and other < 0)) + mod - other + else + mod + end + end + + #-- + # We deviate from MRI behavior here because we ensure that Fixnum op Bignum + # => Bignum (possibly normalized to Fixnum) + # + # Note these differences on MRI, where a is a Fixnum, b is a Bignum + # + # a.coerce b => [Float, Float] + # b.coerce a => [Bignum, Bignum] + #++ + + def coerce(other) + if other.instance_of? self.class + return [other, self] + end + + [Float(other), Float(self)] + end + + ## + # This method mimics the semantics of MRI's do_coerce function + # in numeric.c. Note these differences between it and #coerce: + # + # 1.2.coerce("2") => [2.0, 1.2] + # 1.2 + "2" => TypeError: String can't be coerced into Float + # + # See also Integer#coerce + + def math_coerce(other, error=:coerce_error) + begin + values = other.coerce(self) + rescue + if error == :coerce_error + raise TypeError, "#{other.class} can't be coerced into #{self.class}" + else + raise ArgumentError, "comparison of #{self.class} with #{other.class} failed" + end + end + + unless Rubinius::Type.object_kind_of?(values, Array) && values.length == 2 + raise TypeError, "coerce must return [x, y]" + end + + return values[1], values[0] + end + private :math_coerce + + def redo_coerced(meth, right) + b, a = math_coerce(right) + a.__send__ meth, b + end + private :redo_coerced + + def redo_compare(meth, right) + b, a = math_coerce(right, :compare_error) + a.__send__ meth, b + end + + def div(other) + raise ZeroDivisionError, "divided by 0" if other == 0 + (self / other).floor + end + + def fdiv(other) + self.to_f / other + end + + def quo(other) + Rubinius.privately do + Rational.convert(self, 1, false) / other + end + end + + def modulo(other) + self - other * self.div(other) + end + alias_method :%, :modulo + + def i + Complex(0, self) + end + + def to_c + Complex(self, 0) + end + + def real + self + end + + def imag + 0 + end + alias_method :imaginary, :imag + + def arg + if self < 0 + Math::PI + else + 0 + end + end + alias_method :angle, :arg + alias_method :phase, :arg + + def polar + return abs, arg + end + + def conjugate + self + end + alias_method :conj, :conjugate + + def rect + [self, 0] + end + alias_method :rectangular, :rect + + def abs2 + self * self + end + + alias_method :magnitude, :abs + + def numerator + to_r.numerator + end + + def denominator + to_r.denominator + end + + def real? + true + end + + def positive? + self > 0 + end + + def negative? + self < 0 + end +end diff --git a/gxg/core/numeric_mirror.rb b/gxg/core/numeric_mirror.rb new file mode 100644 index 0000000..30e0331 --- /dev/null +++ b/gxg/core/numeric_mirror.rb @@ -0,0 +1,74 @@ +module Rubinius + class Mirror + class Numeric < Mirror + self.subject = ::Numeric + + def step_float_size(value, limit, step, asc) + if (asc && value > limit) || (!asc && value < limit) + return 0 + end + + if step.infinite? + 1 + else + err = (value.abs + limit.abs + (limit - value).abs) / step.abs * Float::EPSILON + if err.finite? + err = 0.5 if err > 0.5 + ((limit - value) / step + err).floor + 1 + else + 0 + end + end + end + + def step_size(limit, step, to, by) + values = step_fetch_args(limit, step, to, by) + value = values[0] + limit = values[1] + step = values[2] + asc = values[3] + is_float = values[4] + + if limit == Float::INFINITY + if step == Float::INFINITY + return 1 + else + return Float::INFINITY + end + end + + return Float::INFINITY if step == 0 + + if is_float + # Ported from MRI + + step_float_size(value, limit, step, asc) + + else + if (asc && value > limit) || (!asc && value < limit) + 0 + else + ((value - limit).abs + 1).fdiv(step.abs).ceil + end + end + end + + def step_fetch_args(limit, step, to, by) + raise ArgumentError, "limit is given twice" if limit && to + raise ArgumentError, "step is given twice" if step && by + raise ArgumentError, "step cannot be 0" if step == 0 + + step ||= by || 1 + asc = step > 0 + limit ||= to || (asc ? Float::INFINITY : -Float::INFINITY) + value = @object + if value.kind_of? Float or limit.kind_of? Float or step.kind_of? Float + return FloatValue(value), FloatValue(limit), FloatValue(step), asc, true + else + return value, limit, step, asc, false + end + end + + end + end +end diff --git a/gxg/core/object_space.rb b/gxg/core/object_space.rb new file mode 100644 index 0000000..1ebd185 --- /dev/null +++ b/gxg/core/object_space.rb @@ -0,0 +1,88 @@ +module ObjectSpace + def self.find_object(query, callable) + Rubinius.primitive :vm_find_object + raise PrimitiveFailure, "ObjectSpace#find_object primitive failed" + end + + def self._id2ref(id) + ary = [] + + if find_object([:object_id, Integer(id)], ary) > 0 + return ary.first + end + + raise RangeError, "#{id} is not a valid ID value" + end + + def self.find_references(obj) + ary = [] + find_object([:references, obj], ary) + return ary + end + + # @todo rewrite each_object + + # Tries to handle as much as it can. + def self.each_object(what=nil, &block) + return to_enum :each_object, what unless block_given? + + what ||= Object + + unless what.kind_of? Object + raise TypeError, "class or module required" + end + + case what + when Fixnum, Symbol + return 0 + when TrueClass + yield true + return 1 + when FalseClass + yield false + return 1 + when NilClass + yield nil + return 1 + else + return find_object([:kind_of, what], block) + end + end + + def self.define_finalizer(obj, prc=nil, &block) + prc ||= block + + if obj.kind_of? ImmediateValue or obj.kind_of? Float + raise ArgumentError, "can't define finalizer for #{obj.class}" + end + + if obj.frozen? + raise RuntimeError, "can't modify frozen #{obj.class}" + end + + if obj.equal? prc + # This is allowed. This is the Rubinius specific API that calls + # __finalize__ when the object is finalized. + elsif !prc and obj.respond_to?(:__finalize__) + # Allowed. Same thing as above + prc = obj + elsif !prc or !prc.respond_to?(:call) + raise ArgumentError, "action must respond to call" + end + + unless Rubinius.invoke_primitive(:vm_set_finalizer, obj, prc) + raise RuntimeError, "cannot define a finalizer for a #{obj.class}" + end + + [0, prc] + end + + def self.undefine_finalizer(obj) + Rubinius.invoke_primitive :vm_set_finalizer, obj, nil + return obj + end + + def self.garbage_collect + GC.start + end +end diff --git a/gxg/core/options.rb b/gxg/core/options.rb new file mode 100644 index 0000000..9023290 --- /dev/null +++ b/gxg/core/options.rb @@ -0,0 +1,254 @@ +module Rubinius + + # A simple command line option parser. + class Options + + # A single command line option. + class Option + attr_reader :short + attr_reader :long + attr_reader :arg + attr_reader :description + attr_reader :block + + def initialize(short, long, arg, description, block) + @short = short + @long = long + @arg = arg + @description = description + @block = block + end + + private :initialize + + def arg? + @arg != nil + end + + def optional? + @arg[0] == ?[ and @arg[-1] == ?] + end + + def match?(opt) + opt == @short or opt == @long + end + end + + # Raised if incorrect or incomplete formats are passed to #on. + class OptionError < Exception; end + + # Raised if an unrecognized option is encountered. + class ParseError < Exception; end + + attr_accessor :config + attr_accessor :banner + attr_accessor :width + attr_accessor :options + + def initialize(banner="", width=30, config=nil) + @parse = true + @banner = banner + @config = config + @width = width + @options = [] + @doc = [] + @extra = [] + @align = true + @on_extra = lambda { |x| + raise ParseError, "Unrecognized option: #{x}" if x[0] == ?- + @extra << x + } + + yield self if block_given? + end + + private :initialize + + # Documentation for options is left aligned. For example, + # + # -a ARG Some description + # --big Another bit of info + # -c, --class Yet more info + # + # See +option_align+. + def left_align + @align = nil + end + + # Documentation for options is aligned by option type. For example, + # + # -a ARG Some description + # --big Another bit of info + # -c, --class Yet more info + # + # See +left_align+ + def option_align + @align = true + end + + # Registers an option. Acceptable formats for arguments are: + # + # on "-a", "description" + # on "-a", "--abdc", "description" + # on "-a", "ARG", "description" + # on "--abdc", "ARG", "description" + # on "-a", "--abdc", "ARG", "description" + # on "-a", "[ARG]", "description" + # + # The [ARG] form specifies an optional argument. The argument + # must be attached to the option. In the case of a short option, + # the form "-aARG" must be used. For a long option, the form + # '--opt=ARG' must be used. + # + # If an block is passed, it will be invoked when the option is + # matched. Not passing a block is permitted, but nonsensical. + def on(*args, &block) + raise OptionError, "option and description are required" if args.size < 2 + + description = args.pop + short, long, argument = nil + args.each do |arg| + if arg[0] == ?- + if arg[1] == ?- + long = arg + else + short = arg + end + else + argument = arg + end + end + + add short, long, argument, description, block + end + + # Adds documentation text for an option and adds an +Option+ + # instance to the list of registered options. + def add(short, long, arg, description, block) + pad = @align ? " " : "" + s = short ? short.dup : pad + s << (short ? ", " : pad) if long + doc " #{s}#{long} #{arg}".ljust(@width-1) + " #{description}" + @options << Option.new(short, long, arg, description, block) + end + + # Searches all registered options to find a match for +opt+. Returns + # +nil+ if no registered options match. + def match?(opt) + @options.find { |o| o.match? opt } + end + + # Processes an option. Calles the #on_extra block (or default) for + # unrecognized options. For registered options, possibly fetches an + # argument and invokes the option's block if it is not nil. + def process(argv, entry, opt, arg) + unless option = match?(opt) + @on_extra[entry] + else + if option.arg? + if arg.nil? + unless option.optional? + arg = argv.shift + end + end + + unless arg or option.optional? + raise ParseError, "No argument provided for #{opt}" + end + + option.block[arg] if option.block + else + option.block[] if option.block + end + end + option + end + + # Splits a string at +n+ characters into the +opt+ and the +rest+. + # The +arg+ is set to +nil+ if +rest+ is an empty string. + def split(str, n) + opt = str[0, n] + rest = str[n, str.size] || "" + arg = rest == "" ? nil : rest + return opt, arg, rest + end + + # Parses an array of command line entries, calling blocks for + # registered options. + def parse(argv=ARGV) + argv = Array(argv) + + while @parse and entry = argv.shift + # collect everything that is not an option + if entry[0] != ?- + @on_extra[entry] + next + end + + # this is a long option + if entry[1] == ?- + opt, arg = entry.split "=" + process argv, entry, opt, arg + next + end + + # disambiguate short option group from short option with argument + opt, arg, rest = split entry, 2 + + # process first option + option = process argv, entry, opt, arg + next unless option and not option.arg? + + # process the rest of the options + while rest.size > 0 + opt, arg, rest = split rest, 1 + opt = "-" + opt + option = process argv, opt, opt, arg + break if option.arg? + end + end + + @extra + rescue ParseError => e + puts self + puts e + exit 1 + end + + def start_parsing + @parse = true + end + + def stop_parsing + @parse = false + end + + # Adds a string of documentation text inline in the text generated + # from the options. See #on and #add. + def doc(str) + @doc << str + end + + # Convenience method for providing -v, --version options. + def version(version, &block) + show = block || lambda { puts "#{File.basename $0} #{version}"; exit } + on "-v", "--version", "Show version", &show + end + + # Convenience method for providing -h, --help options. + def help(&block) + help = block || lambda { puts self; exit 1 } + on "-h", "--help", "Show this message", &help + end + + # Stores a block that will be called with unrecognized options + def on_extra(&block) + @on_extra = block + end + + # Returns a string representation of the options and doc strings. + def to_s + @banner + "\n\n" + @doc.join("\n") + "\n" + end + end +end diff --git a/gxg/core/pack.rb b/gxg/core/pack.rb new file mode 100644 index 0000000..5ffa44e --- /dev/null +++ b/gxg/core/pack.rb @@ -0,0 +1,27 @@ +module Rubinius + def self.pack_to_float(obj) + unless obj.kind_of?(Numeric) + raise TypeError, "unable to convert String to Float" + end + Type.coerce_to obj, Float, :to_f + end + + def self.pack_to_int(obj) + Rubinius::Type.coerce_to obj, Integer, :to_int + end + + def self.pack_to_str(obj) + Rubinius::Type.coerce_to obj, String, :to_str + end + + def self.pack_to_str_or_nil(obj) + return "" if obj.nil? + Rubinius::Type.coerce_to obj, String, :to_str + end + + def self.pack_to_s(obj) + str = obj.to_s + str = obj.inspect unless Type.object_kind_of? str, String + str + end +end diff --git a/gxg/core/peg.rb b/gxg/core/peg.rb new file mode 100644 index 0000000..2a054d1 --- /dev/null +++ b/gxg/core/peg.rb @@ -0,0 +1,347 @@ +module Rubinius + module PEG + class InvalidPatternError < Exception; end + + # Class Pattern is the base class for all nodes used to represent the AST + # for a PEG. Following LPEG, the base is a pattern and not a grammar. + class Pattern + def graph + if defined? Rubinius + Rubinius::AST::AsciiGrapher.new(self, Pattern).print + else + inspect + end + + nil + end + + def visit(visitor) + end + + # Pattern operators + + def /(other) + Choice.new self, other + end + + # TODO: remove after core transform for :/ is removed + alias_method :divide, :/ + + def +(other) + Concatenation.new self, other + end + + def -(other) + Difference.new self, other + end + + def *(other) + Product.new self, other + end + + def +@ + If.new self + end + + def -@ + Unless.new self + end + + def =~(string) + end + + def match(string) + end + end + + class UnaryOp < Pattern + attr_accessor :pattern + + def initialize(pattern) + @pattern = PEG.pattern pattern + end + + private :initialize + end + + class BinaryOp < Pattern + attr_accessor :first + attr_accessor :second + + def initialize(first, second) + @first = PEG.pattern first + @second = PEG.pattern second + end + + private :initialize + end + + class Grammar < Pattern + attr_accessor :start + + def initialize(variable=nil) + if variable + @start = variable.name + set_variable variable.name, variable.pattern + end + end + + private :initialize + + def set_variable(name, pattern) + variable = get_variable name + variable.pattern = PEG.pattern pattern + variable + end + + def get_variable(name) + if singleton_class.method_defined? name + variable = send name + else + vname = name.to_sym + variable = Variable.new vname + singleton_class.thunk_method vname, variable + end + + variable + end + + def method_missing(sym, *args) + name = sym.to_s + + if name[-1] == ?= and args.size == 1 + set_variable name[0..-2], args.first + else + get_variable name + end + end + + def visit(visitor) + visitor.grammar self + end + + def =~(string) + end + + def match(string) + end + end + + class Always < Pattern + def visit(visitor) + visitor.always self + end + end + + class AnyRange < Pattern + attr_accessor :range + + def initialize(range) + @range = range + end + + private :initialize + + def visit(visitor) + visitor.any_range self + end + end + + class Any < Pattern + attr_accessor :count + + def initialize(count) + @count = count + end + + private :initialize + + def visit(visitor) + visitor.any self + end + end + + class CharacterRange < Pattern + attr_accessor :range + + def initialize(range) + @range = range + end + + private :initialize + + def visit(visitor) + visitor.character_range self + end + end + + class Character < Pattern + attr_accessor :string + + def initialize(string) + @string = string + end + + private :initialize + + def visit(visitor) + visitor.character self + end + end + + class Choice < BinaryOp + def visit(visitor) + visitor.choice self + end + end + + class Concatenation < BinaryOp + def visit(visitor) + visitor.concatenation self + end + end + + class Difference < BinaryOp + def visit(visitor) + visitor.difference self + end + end + + # Implements &patt in a PEG. The pattern succeeds if + # patt matches. Does not consume any input. + class If < UnaryOp + def visit(visitor) + visitor.if self + end + end + + class Never < Pattern + def visit(visitor) + visitor.never self + end + end + + class Product < Pattern + attr_accessor :pattern + attr_accessor :count + + def initialize(pattern, count) + @pattern = PEG.pattern pattern + @count = Type.coerce_to(count, Integer, :to_int).abs + end + + private :initialize + + def visit(visitor) + visitor.product self + end + end + + class Set < Pattern + attr_accessor :set + + def initialize(set) + @set = set.uniq + end + + private :initialize + + def visit(visitor) + visitor.set self + end + end + + # Implements !patt in a PEG. The pattern succeeds if + # patt does not match. Does not consume any input. + class Unless < UnaryOp + def visit(visitor) + visitor.unless self + end + end + + class Variable < Pattern + attr_accessor :name + attr_accessor :pattern + + def initialize(name, pattern=nil) + @name = name.to_sym + @pattern = PEG.pattern pattern + end + + private :initialize + + def visit(visitor) + visitor.variable self + end + end + + # Accepts a Ruby object representation of simple PEG "atoms" and returns a + # PEG AST node. + def pattern(spec) + case spec + when Array + Set.new spec + when Integer + node = Any.new spec.abs + node = If.new node if spec < 0 + node + when String + Character.new spec + when Pattern + spec + when true + Always.new + when false + Never.new + when nil + nil + else + raise InvalidPatternError, "unknown specification type for Pattern: #{spec.inspect}" + end + end + module_function :pattern + + def grammar(spec=nil, pattern=nil) + case spec + when Symbol + Grammar.new Variable.new(spec, pattern) + when Variable + Grammar.new spec + when Pattern + Grammar.new Variable.new(:S, spec) + when nil + Grammar.new + else + Grammar.new Variable.new(:S, pattern) + end + end + module_function :grammar + + def variable(name, pattern=nil) + PEG::Variable.new name, pattern + end + module_function :variable + + def self.compile(pattern) + end + + def self.peg + g = grammar = grammar(:grammar) + g.grammar = (g.nonterminal + "=" + g.sp + g.pattern) * 1 + g.pattern = g.alternative + (pattern("/") + g.sp + g.alternative) * 0 + g.alternative = ((+g.predicate + g.predicate + g.sp + g.suffix) / + (g.sp + g.suffix)) * 1 + g.predicate = ["!&"] + g.suffix = g.primary + (pattern(["*+?"]) + g.sp) * 0 + g.primary = (pattern("(") + g.sp + g.pattern + ")" + g.sp) / (pattern(1) + g.sp) / + g.literal / g.char_class / (g.nonterminal + -pattern("=")) + g.literal = pattern("'") + (-pattern("'") + 1) * 0 + "'" + g.sp + g.char_class = (pattern("[") + (-pattern("]") + + ((pattern(1) + "-" + 1) / 1)) * 0) + "]" + g.sp + g.nonterminal = (pattern("_") / ["a".."z"] / ["A".."Z"]) * 1 + g.sp + g.sp = pattern([" \t\n"]) * 0 + + grammar + end + end +end diff --git a/gxg/core/pointer.rb b/gxg/core/pointer.rb new file mode 100644 index 0000000..0b9992b --- /dev/null +++ b/gxg/core/pointer.rb @@ -0,0 +1,468 @@ +module Rubinius + module FFI + # We need to define this so we can include it. Since the file is + # auto-generated, we can't pull it in here but we don't want to break the + # alphabetized load order for random cases. + module PointerAccessors; end + + ## + # Pointer is Rubinius's "fat" pointer class. It represents an actual + # pointer, in C language terms, to an address in memory. They're called + # fat pointers because the Pointer object is an wrapper around + # the actual pointer, the Rubinius runtime doesn't have direct access + # to the raw address. + # + # This class is used extensively in FFI usage to interface with various + # parts of the underlying system. It provides a number of operations + # for operating on the memory that is pointed to. These operations effectively + # give Rubinius the cast/read capabilities available in C, but using + # high level methods. + # + # MemoryPointer objects can be put in autorelease mode. In this mode, + # when the GC cleans up a MemoryPointer object, the memory it points + # to is passed to free(3), releasing the memory back to the OS. + # + # NOTE: MemoryPointer exposes direct, unmanaged operations on any + # memory. It therefore MUST be used carefully. Reading or writing to + # invalid address will cause bus errors and segmentation faults. + # + class Pointer + include PointerAccessors + + def initialize(a1, a2=undefined) + if undefined.equal? a2 + self.address = a1 + else + @type = a1 + self.address = a2 + end + end + + private :initialize + + def inspect + # Don't have this print the data at the location. It can crash everything. + addr = address() + + if addr < 0 + sign = "-" + addr = -addr + else + sign = "" + end + + "#<#{self.class.name} address=#{sign}0x#{addr.to_s(16)}>" + end + + # Return the address pointed to as an Integer + def address + Rubinius.primitive :pointer_address + raise PrimitiveFailure, "FFI::Pointer#address primitive failed" + end + + alias_method :to_i, :address + + # Set the address pointed to from an Integer + def address=(address) + Rubinius.primitive :pointer_set_address + raise PrimitiveFailure, "FFI::Pointer#address= primitive failed" + end + + def null? + address == 0x0 + end + + # Add +value+ to the address pointed to and return a new Pointer + def +(value) + Rubinius.primitive :pointer_add + raise PrimitiveFailure, "FFI::Pointer#+ primitive failed" + end + + # Indicates if +self+ and +other+ point to the same address + def ==(other) + return false unless other.kind_of? Pointer + return address == other.address + end + + def network_order(start, size) + Rubinius.primitive :pointer_network_order + raise PrimitiveFailure, "FFI::Pointer#network_order primitive failed" + end + + # Read +len+ bytes from the memory pointed to and return them as + # a String + def read_string_length(len) + Rubinius.primitive :pointer_read_string + raise PrimitiveFailure, "FFI::Pointer#read_string_length primitive failed" + end + + # Read bytes from the memory pointed to until a NULL is seen, return + # the bytes as a String + def read_string_to_null + Rubinius.primitive :pointer_read_string_to_null + raise PrimitiveFailure, "FFI::Pointer#read_string_to_null primitive failed" + end + + # Read bytes as a String from the memory pointed to + def read_string(len=nil) + if len + read_string_length(len) + else + read_string_to_null + end + end + + # FFI compat methods + def get_bytes(offset, length) + (self + offset).read_string_length(length) + end + + # Write String +str+ as bytes into the memory pointed to. Only + # write up to +len+ bytes. + def write_string_length(str, len) + Rubinius.primitive :pointer_write_string + raise PrimitiveFailure, "FFI::Pointer#write_string_length primitive failed" + end + + # Write a String +str+ as bytes to the memory pointed to. + def write_string(str, len=nil) + len = str.bytesize unless len + + write_string_length(str, len); + end + + # Read a sequence of types +type+, length +length+, using method +reader+ + def read_array_of_type(type, reader, length, signed=nil) + # If signed is not nil and is actually a boolean, + # then use that as an argument to the reader, which + # is then assumed to support signed reading. + args = [] + args = [signed] if !signed.nil? + + # Build up the array + ary = [] + size = FFI.type_size(FFI.find_type type) + tmp = self + length.times { + ary << tmp.send(reader, *args) + tmp += size + } + ary + end + + # Write a sequence of types +type+ using method +reader+ from +ary+ + def write_array_of_type(type, writer, ary) + size = FFI.type_size(FFI.find_type type) + tmp = self + ary.each do |i| + tmp.send(writer, i) + tmp += size + end + self + end + + # Read bytes from +offset+ from the memory pointed to as type +type+ + def get_at_offset(offset, type) + Rubinius.primitive :pointer_get_at_offset + raise PrimitiveFailure, "FFI::Pointer#get_at_offset primitive failed" + end + + # Write +val+ as type +type+ to bytes from +offset+ + def set_at_offset(offset, type, val) + Rubinius.primitive :pointer_set_at_offset + raise PrimitiveFailure, "FFI::Pointer#set_at_offset primitive failed" + end + + # Number of bytes taken up by a pointer. + def self.size + Rubinius::WORDSIZE / 8 + end + + # Primitive methods + def primitive_read_char(signed) + Rubinius.primitive :pointer_read_char + raise PrimitiveFailure, "FFI::Pointer#primitive_read_char primitive failed" + end + + def primitive_write_char(obj) + Rubinius.primitive :pointer_write_char + raise PrimitiveFailure, "FFI::Pointer#primitive_write_char primitive failed" + end + + def primitive_read_short(signed) + Rubinius.primitive :pointer_read_short + raise PrimitiveFailure, "FFI::Pointer#primitive_read_short primitive failed" + end + + def primitive_write_short(obj) + Rubinius.primitive :pointer_write_short + raise PrimitiveFailure, "FFI::Pointer#primitive_write_short primitive failed" + end + + def primitive_read_int(signed) + Rubinius.primitive :pointer_read_int + raise PrimitiveFailure, "FFI::Pointer#primitive_read_int primitive failed" + end + + def primitive_write_int(obj) + Rubinius.primitive :pointer_write_int + raise PrimitiveFailure, "FFI::Pointer#primitive_write_int primitive failed" + end + + def primitive_read_long(signed) + Rubinius.primitive :pointer_read_long + raise PrimitiveFailure, "FFI::Pointer#primitive_read_long primitive failed" + end + + def primitive_write_long(obj) + Rubinius.primitive :pointer_write_long + raise PrimitiveFailure, "FFI::Pointer#primitive_write_long primitive failed" + end + + def primitive_read_long_long(signed) + Rubinius.primitive :pointer_read_long_long + raise PrimitiveFailure, "FFI::Pointer#primitive_read_long_long primitive failed" + end + + def primitive_write_long_long(obj) + Rubinius.primitive :pointer_write_long_long + raise PrimitiveFailure, "FFI::Pointer#primitive_write_long_long primitive failed" + end + + def primitive_read_float + Rubinius.primitive :pointer_read_float + raise PrimitiveFailure, "FFI::Pointer#primitive_read_float primitive failed" + end + + def primitive_write_float(obj) + Rubinius.primitive :pointer_write_float + raise PrimitiveFailure, "FFI::Pointer#primitive_write_float primitive failed" + end + + def primitive_read_double + Rubinius.primitive :pointer_read_double + raise PrimitiveFailure, "FFI::Pointer#primitive_read_double primitive failed" + end + + def primitive_write_double(obj) + Rubinius.primitive :pointer_write_double + raise PrimitiveFailure, "FFI::Pointer#primitive_write_double primitive failed" + end + + def primitive_read_pointer + Rubinius.primitive :pointer_read_pointer + raise PrimitiveFailure, "FFI::Pointer#primitive_read_pointer primitive failed" + end + + def primitive_write_pointer(obj) + Rubinius.primitive :pointer_write_pointer + raise PrimitiveFailure, "FFI::Pointer#primitive_write_pointer primitive failed" + end + + ## + # If +val+ is true, this Pointer object will call + # free() on it's address when it is garbage collected. + def autorelease=(val) + Rubinius.primitive :pointer_set_autorelease + raise PrimitiveFailure, "FFI::Pointer#autorelease= primitive failed" + end + + ## + # Returns true if autorelease is enabled, otherwise false. + def autorelease? + Rubinius.primitive :pointer_autorelease_p + raise PrimitiveFailure, "FFI::Pointer#pointer_autorelease_p primitive failed" + end + + NULL = Pointer.new(0x0) + end + + class MemoryPointer < Pointer + + # call-seq: + # MemoryPointer.new(num) => MemoryPointer instance of num bytes + # MemoryPointer.new(sym) => MemoryPointer instance with number + # of bytes need by FFI type sym + # MemoryPointer.new(obj) => MemoryPointer instance with number + # of obj.size bytes + # MemoryPointer.new(sym, count) => MemoryPointer instance with number + # of bytes need by length-count array + # of FFI type sym + # MemoryPointer.new(obj, count) => MemoryPointer instance with number + # of bytes need by length-count array + # of obj.size bytes + # MemoryPointer.new(arg) { |p| ... } + # + # Both forms create a MemoryPointer instance. The number of bytes to + # allocate is either specified directly or by passing an FFI type, which + # specifies the number of bytes needed for that type. + # + # The form without a block returns the MemoryPointer instance. The form + # with a block yields the MemoryPointer instance and frees the memory + # when the block returns. The value returned is the value of the block. + # + def self.new(type, count=nil, clear=true) + if type.kind_of? Fixnum + size = type + elsif type.kind_of? Symbol + type = FFI.find_type type + size = FFI.type_size(type) + else + size = type.size + end + + if count + total = size * count + else + total = size + end + + return NULL if total < 0 + + ptr = malloc total + ptr.total = total + ptr.type_size = size + FFI::Platform::POSIX.memset ptr, 0, total if clear + + if block_given? + begin + value = yield ptr + ensure + ptr.free + end + + return value + else + ptr.autorelease = true + ptr + end + end + + def self.malloc(total) + Rubinius.primitive :pointer_malloc + raise PrimitiveFailure, "FFI::MemoryPointer.malloc primitive failed" + end + + def self.from_string(str) + ptr = new str.bytesize + 1 + ptr.write_string str + "\0" + + ptr + end + + def copy + other = malloc total + other.total = total + other.type_size = type_size + FFI::Platform::POSIX.memcpy other, self, total + + Rubinius.privately do + other.initialize_copy self + end + + other + end + + # Indicates how many bytes the chunk of memory that is pointed to takes up. + attr_accessor :total + + # Indicates how many bytes the type that the pointer is cast as uses. + attr_accessor :type_size + + # Access the MemoryPointer like a C array, accessing the +which+ number + # element in memory. The position of the element is calculate from + # +@type_size+ and +which+. A new MemoryPointer object is returned, which + # points to the address of the element. + # + # Example: + # ptr = MemoryPointer.new(:int, 20) + # new_ptr = ptr[9] + # + # c-equiv: + # int *ptr = (int*)malloc(sizeof(int) * 20); + # int *new_ptr; + # new_ptr = &ptr[9]; + # + def [](which) + raise ArgumentError, "unknown type size" unless @type_size + self + (which * @type_size) + end + + # Release the memory pointed to back to the OS. + def free + Rubinius.primitive :pointer_free + raise PrimitiveFailure, "FFI::MemoryPointer#free primitive failed" + end + end + + class DynamicLibrary + class Symbol < Pointer + def initialize(library, ptr, name) + @library = library + @name = name + self.address = ptr.address + end + + private :initialize + + def inspect + "#" + end + end + end + + class Function < Pointer + def initialize(ret_type, arg_types, val=nil, options=nil, &block) + if block + if val or options + raise ArgumentError, "specify a block or a proc/address, not both" + end + + val = block + end + + args = arg_types.map { |x| FFI.find_type(x) } + ret = FFI.find_type(ret_type) + + if val.kind_of? Pointer + @function = FFI.generate_function(val, :func, args, ret) + self.address = val.address + elsif val.respond_to? :call + @function, ptr = FFI.generate_trampoline(val, :func, args, ret) + self.address = ptr.address + else + raise ArgumentError, "value wasn't a FFI::Pointer and didn't respond to call" + end + + # Hook the created function into the method_table so that #call goes + # straight to it. + sc = Rubinius::Type.object_singleton_class(self) + Rubinius::VM.reset_method_cache sc, :call + sc.method_table.store :call, nil, @function, nil, 0, :public + end + + private :initialize + + attr_reader :function + + # Hook this Function up to be an instance/class method +name+ on +mod+ + def attach(mod, name) + unless mod.kind_of?(Module) + raise TypeError, "mod must be a Module" + end + + name = name.to_sym + + # Make it available as a method callable directly.. + sc = Rubinius::Type.object_singleton_class(mod) + Rubinius::VM.reset_method_cache sc, name + sc.method_table.store name, nil, @function, nil, 0, :public + + # and expose it as a private method for people who + # want to include this module. + Rubinius::VM.reset_method_cache mod, name + mod.method_table.store name, nil, @function, nil, 0, :public + end + end + end +end diff --git a/gxg/core/pointer_accessors.rb b/gxg/core/pointer_accessors.rb new file mode 100644 index 0000000..a329b64 --- /dev/null +++ b/gxg/core/pointer_accessors.rb @@ -0,0 +1,838 @@ +# This file is automatically generated. It should _not_ be +# modified manually. Due to the vast amount of very similar +# methods in this module, it is automatically generated. + +module Rubinius +module FFI + ## + # This module provides accessors for various different types. It + # provides methods for: + # + # * reading data + # * writing data + # * reading an array of some data + # * writing an array of some data + module PointerAccessors + # + # Name: int8 + # Type: char + # Signed: true + # + + def read_int8(signed=true) + primitive_read_char(signed) + end + + def write_int8(obj) + primitive_write_char(obj) + end + + def get_int8(offset) + (self + offset).read_int8 + end + + def put_int8(offset, obj) + (self + offset).write_int8(obj) + end + + def read_array_of_int8(length, signed=true) + read_array_of_type(:char, :read_char, length, signed) + end + + def write_array_of_int8(ary) + write_array_of_type(:char, :write_char, ary) + end + + def get_array_of_int8(offset, length, signed=true) + (self + offset).read_array_of_int8(length, signed) + end + + def put_array_of_int8(offset, ary) + (self + offset).write_array_of_int8(ary) + end + + def read_uint8 + self.read_int8(false) + end + + def write_uint8(obj) + self.write_int8(obj) + end + + def get_uint8(offset) + (self + offset).read_uint8 + end + + def put_uint8(offset, obj) + (self + offset).write_uint8(obj) + end + + def read_array_of_uint8(length) + self.read_array_of_int8(length, false) + end + + def write_array_of_uint8(ary) + self.write_array_of_int8(ary) + end + + def get_array_of_uint8(offset, length) + (self + offset).read_array_of_uint8(length) + end + + def put_array_of_uint8(offset, ary) + (self + offset).write_array_of_uint8(ary) + end + + # + # Name: int16 + # Type: short + # Signed: true + # + + def read_int16(signed=true) + primitive_read_short(signed) + end + + def write_int16(obj) + primitive_write_short(obj) + end + + def get_int16(offset) + (self + offset).read_int16 + end + + def put_int16(offset, obj) + (self + offset).write_int16(obj) + end + + def read_array_of_int16(length, signed=true) + read_array_of_type(:short, :read_short, length, signed) + end + + def write_array_of_int16(ary) + write_array_of_type(:short, :write_short, ary) + end + + def get_array_of_int16(offset, length, signed=true) + (self + offset).read_array_of_int16(length, signed) + end + + def put_array_of_int16(offset, ary) + (self + offset).write_array_of_int16(ary) + end + + def read_uint16 + self.read_int16(false) + end + + def write_uint16(obj) + self.write_int16(obj) + end + + def get_uint16(offset) + (self + offset).read_uint16 + end + + def put_uint16(offset, obj) + (self + offset).write_uint16(obj) + end + + def read_array_of_uint16(length) + self.read_array_of_int16(length, false) + end + + def write_array_of_uint16(ary) + self.write_array_of_int16(ary) + end + + def get_array_of_uint16(offset, length) + (self + offset).read_array_of_uint16(length) + end + + def put_array_of_uint16(offset, ary) + (self + offset).write_array_of_uint16(ary) + end + + # + # Name: int32 + # Type: int + # Signed: true + # + + def read_int32(signed=true) + primitive_read_int(signed) + end + + def write_int32(obj) + primitive_write_int(obj) + end + + def get_int32(offset) + (self + offset).read_int32 + end + + def put_int32(offset, obj) + (self + offset).write_int32(obj) + end + + def read_array_of_int32(length, signed=true) + read_array_of_type(:int, :read_int, length, signed) + end + + def write_array_of_int32(ary) + write_array_of_type(:int, :write_int, ary) + end + + def get_array_of_int32(offset, length, signed=true) + (self + offset).read_array_of_int32(length, signed) + end + + def put_array_of_int32(offset, ary) + (self + offset).write_array_of_int32(ary) + end + + def read_uint32 + self.read_int32(false) + end + + def write_uint32(obj) + self.write_int32(obj) + end + + def get_uint32(offset) + (self + offset).read_uint32 + end + + def put_uint32(offset, obj) + (self + offset).write_uint32(obj) + end + + def read_array_of_uint32(length) + self.read_array_of_int32(length, false) + end + + def write_array_of_uint32(ary) + self.write_array_of_int32(ary) + end + + def get_array_of_uint32(offset, length) + (self + offset).read_array_of_uint32(length) + end + + def put_array_of_uint32(offset, ary) + (self + offset).write_array_of_uint32(ary) + end + + # + # Name: int64 + # Type: long_long + # Signed: true + # + + def read_int64(signed=true) + primitive_read_long_long(signed) + end + + def write_int64(obj) + primitive_write_long_long(obj) + end + + def get_int64(offset) + (self + offset).read_int64 + end + + def put_int64(offset, obj) + (self + offset).write_int64(obj) + end + + def read_array_of_int64(length, signed=true) + read_array_of_type(:long_long, :read_long_long, length, signed) + end + + def write_array_of_int64(ary) + write_array_of_type(:long_long, :write_long_long, ary) + end + + def get_array_of_int64(offset, length, signed=true) + (self + offset).read_array_of_int64(length, signed) + end + + def put_array_of_int64(offset, ary) + (self + offset).write_array_of_int64(ary) + end + + def read_uint64 + self.read_int64(false) + end + + def write_uint64(obj) + self.write_int64(obj) + end + + def get_uint64(offset) + (self + offset).read_uint64 + end + + def put_uint64(offset, obj) + (self + offset).write_uint64(obj) + end + + def read_array_of_uint64(length) + self.read_array_of_int64(length, false) + end + + def write_array_of_uint64(ary) + self.write_array_of_int64(ary) + end + + def get_array_of_uint64(offset, length) + (self + offset).read_array_of_uint64(length) + end + + def put_array_of_uint64(offset, ary) + (self + offset).write_array_of_uint64(ary) + end + + # + # Name: long + # Type: long + # Signed: true + # + + def read_long(signed=true) + primitive_read_long(signed) + end + + def write_long(obj) + primitive_write_long(obj) + end + + def get_long(offset) + (self + offset).read_long + end + + def put_long(offset, obj) + (self + offset).write_long(obj) + end + + def read_array_of_long(length, signed=true) + read_array_of_type(:long, :read_long, length, signed) + end + + def write_array_of_long(ary) + write_array_of_type(:long, :write_long, ary) + end + + def get_array_of_long(offset, length, signed=true) + (self + offset).read_array_of_long(length, signed) + end + + def put_array_of_long(offset, ary) + (self + offset).write_array_of_long(ary) + end + + def read_ulong + self.read_long(false) + end + + def write_ulong(obj) + self.write_long(obj) + end + + def get_ulong(offset) + (self + offset).read_ulong + end + + def put_ulong(offset, obj) + (self + offset).write_ulong(obj) + end + + def read_array_of_ulong(length) + self.read_array_of_long(length, false) + end + + def write_array_of_ulong(ary) + self.write_array_of_long(ary) + end + + def get_array_of_ulong(offset, length) + (self + offset).read_array_of_ulong(length) + end + + def put_array_of_ulong(offset, ary) + (self + offset).write_array_of_ulong(ary) + end + + # + # Name: float32 + # Type: float + # Signed: false + # + + def read_float32 + primitive_read_float + end + + def write_float32(obj) + primitive_write_float(obj) + end + + def get_float32(offset) + (self + offset).read_float32 + end + + def put_float32(offset, obj) + (self + offset).write_float32(obj) + end + + def read_array_of_float32(length) + read_array_of_type(:float, :read_float, length) + end + + def write_array_of_float32(ary) + write_array_of_type(:float, :write_float, ary) + end + + def get_array_of_float32(offset, length) + (self + offset).read_array_of_float32(length) + end + + def put_array_of_float32(offset, ary) + (self + offset).write_array_of_float32(ary) + end + + # + # Name: float64 + # Type: double + # Signed: false + # + + def read_float64 + primitive_read_double + end + + def write_float64(obj) + primitive_write_double(obj) + end + + def get_float64(offset) + (self + offset).read_float64 + end + + def put_float64(offset, obj) + (self + offset).write_float64(obj) + end + + def read_array_of_float64(length) + read_array_of_type(:double, :read_double, length) + end + + def write_array_of_float64(ary) + write_array_of_type(:double, :write_double, ary) + end + + def get_array_of_float64(offset, length) + (self + offset).read_array_of_float64(length) + end + + def put_array_of_float64(offset, ary) + (self + offset).write_array_of_float64(ary) + end + + # + # Name: float + # Type: float + # Signed: false + # + + def read_float + primitive_read_float + end + + def write_float(obj) + primitive_write_float(obj) + end + + def get_float(offset) + (self + offset).read_float + end + + def put_float(offset, obj) + (self + offset).write_float(obj) + end + + def read_array_of_float(length) + read_array_of_type(:float, :read_float, length) + end + + def write_array_of_float(ary) + write_array_of_type(:float, :write_float, ary) + end + + def get_array_of_float(offset, length) + (self + offset).read_array_of_float(length) + end + + def put_array_of_float(offset, ary) + (self + offset).write_array_of_float(ary) + end + + # + # Name: double + # Type: double + # Signed: false + # + + def read_double + primitive_read_double + end + + def write_double(obj) + primitive_write_double(obj) + end + + def get_double(offset) + (self + offset).read_double + end + + def put_double(offset, obj) + (self + offset).write_double(obj) + end + + def read_array_of_double(length) + read_array_of_type(:double, :read_double, length) + end + + def write_array_of_double(ary) + write_array_of_type(:double, :write_double, ary) + end + + def get_array_of_double(offset, length) + (self + offset).read_array_of_double(length) + end + + def put_array_of_double(offset, ary) + (self + offset).write_array_of_double(ary) + end + + # + # Name: char + # Type: char + # Signed: true + # + + def read_char(signed=true) + primitive_read_char(signed) + end + + def write_char(obj) + primitive_write_char(obj) + end + + def get_char(offset) + (self + offset).read_char + end + + def put_char(offset, obj) + (self + offset).write_char(obj) + end + + def read_array_of_char(length, signed=true) + read_array_of_type(:char, :read_char, length, signed) + end + + def write_array_of_char(ary) + write_array_of_type(:char, :write_char, ary) + end + + def get_array_of_char(offset, length, signed=true) + (self + offset).read_array_of_char(length, signed) + end + + def put_array_of_char(offset, ary) + (self + offset).write_array_of_char(ary) + end + + def read_uchar + self.read_char(false) + end + + def write_uchar(obj) + self.write_char(obj) + end + + def get_uchar(offset) + (self + offset).read_uchar + end + + def put_uchar(offset, obj) + (self + offset).write_uchar(obj) + end + + def read_array_of_uchar(length) + self.read_array_of_char(length, false) + end + + def write_array_of_uchar(ary) + self.write_array_of_char(ary) + end + + def get_array_of_uchar(offset, length) + (self + offset).read_array_of_uchar(length) + end + + def put_array_of_uchar(offset, ary) + (self + offset).write_array_of_uchar(ary) + end + + # + # Name: short + # Type: short + # Signed: true + # + + def read_short(signed=true) + primitive_read_short(signed) + end + + def write_short(obj) + primitive_write_short(obj) + end + + def get_short(offset) + (self + offset).read_short + end + + def put_short(offset, obj) + (self + offset).write_short(obj) + end + + def read_array_of_short(length, signed=true) + read_array_of_type(:short, :read_short, length, signed) + end + + def write_array_of_short(ary) + write_array_of_type(:short, :write_short, ary) + end + + def get_array_of_short(offset, length, signed=true) + (self + offset).read_array_of_short(length, signed) + end + + def put_array_of_short(offset, ary) + (self + offset).write_array_of_short(ary) + end + + def read_ushort + self.read_short(false) + end + + def write_ushort(obj) + self.write_short(obj) + end + + def get_ushort(offset) + (self + offset).read_ushort + end + + def put_ushort(offset, obj) + (self + offset).write_ushort(obj) + end + + def read_array_of_ushort(length) + self.read_array_of_short(length, false) + end + + def write_array_of_ushort(ary) + self.write_array_of_short(ary) + end + + def get_array_of_ushort(offset, length) + (self + offset).read_array_of_ushort(length) + end + + def put_array_of_ushort(offset, ary) + (self + offset).write_array_of_ushort(ary) + end + + # + # Name: int + # Type: int + # Signed: true + # + + def read_int(signed=true) + primitive_read_int(signed) + end + + def write_int(obj) + primitive_write_int(obj) + end + + def get_int(offset) + (self + offset).read_int + end + + def put_int(offset, obj) + (self + offset).write_int(obj) + end + + def read_array_of_int(length, signed=true) + read_array_of_type(:int, :read_int, length, signed) + end + + def write_array_of_int(ary) + write_array_of_type(:int, :write_int, ary) + end + + def get_array_of_int(offset, length, signed=true) + (self + offset).read_array_of_int(length, signed) + end + + def put_array_of_int(offset, ary) + (self + offset).write_array_of_int(ary) + end + + def read_uint + self.read_int(false) + end + + def write_uint(obj) + self.write_int(obj) + end + + def get_uint(offset) + (self + offset).read_uint + end + + def put_uint(offset, obj) + (self + offset).write_uint(obj) + end + + def read_array_of_uint(length) + self.read_array_of_int(length, false) + end + + def write_array_of_uint(ary) + self.write_array_of_int(ary) + end + + def get_array_of_uint(offset, length) + (self + offset).read_array_of_uint(length) + end + + def put_array_of_uint(offset, ary) + (self + offset).write_array_of_uint(ary) + end + + # + # Name: long_long + # Type: long_long + # Signed: true + # + + def read_long_long(signed=true) + primitive_read_long_long(signed) + end + + def write_long_long(obj) + primitive_write_long_long(obj) + end + + def get_long_long(offset) + (self + offset).read_long_long + end + + def put_long_long(offset, obj) + (self + offset).write_long_long(obj) + end + + def read_array_of_long_long(length, signed=true) + read_array_of_type(:long_long, :read_long_long, length, signed) + end + + def write_array_of_long_long(ary) + write_array_of_type(:long_long, :write_long_long, ary) + end + + def get_array_of_long_long(offset, length, signed=true) + (self + offset).read_array_of_long_long(length, signed) + end + + def put_array_of_long_long(offset, ary) + (self + offset).write_array_of_long_long(ary) + end + + def read_ulong_long + self.read_long_long(false) + end + + def write_ulong_long(obj) + self.write_long_long(obj) + end + + def get_ulong_long(offset) + (self + offset).read_ulong_long + end + + def put_ulong_long(offset, obj) + (self + offset).write_ulong_long(obj) + end + + def read_array_of_ulong_long(length) + self.read_array_of_long_long(length, false) + end + + def write_array_of_ulong_long(ary) + self.write_array_of_long_long(ary) + end + + def get_array_of_ulong_long(offset, length) + (self + offset).read_array_of_ulong_long(length) + end + + def put_array_of_ulong_long(offset, ary) + (self + offset).write_array_of_ulong_long(ary) + end + + # + # Name: pointer + # Type: pointer + # Signed: false + # + + def read_pointer + primitive_read_pointer + end + + def write_pointer(obj) + primitive_write_pointer(obj) + end + + def get_pointer(offset) + (self + offset).read_pointer + end + + def put_pointer(offset, obj) + (self + offset).write_pointer(obj) + end + + def read_array_of_pointer(length) + read_array_of_type(:pointer, :read_pointer, length) + end + + def write_array_of_pointer(ary) + write_array_of_type(:pointer, :write_pointer, ary) + end + + def get_array_of_pointer(offset, length) + (self + offset).read_array_of_pointer(length) + end + + def put_array_of_pointer(offset, ary) + (self + offset).write_array_of_pointer(ary) + end + + end +end +end diff --git a/gxg/core/prediction.rb b/gxg/core/prediction.rb new file mode 100644 index 0000000..c371383 --- /dev/null +++ b/gxg/core/prediction.rb @@ -0,0 +1,14 @@ +module Rubinius + class Prediction + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} valid=#{valid?}>" + end + + alias_method :to_s, :inspect + + def valid? + Rubinius.primitive :prediction_valid_p + raise PrimitiveFailure, "Prediction#valid? primitive failed" + end + end +end diff --git a/gxg/core/proc.rb b/gxg/core/proc.rb new file mode 100644 index 0000000..15304bc --- /dev/null +++ b/gxg/core/proc.rb @@ -0,0 +1,276 @@ +class Proc + def self.allocate + raise TypeError, "allocator undefined for Proc" + end + + def self.__allocate__ + Rubinius.primitive :proc_allocate + raise PrimitiveFailure, "Proc#allocate primitive failed" + end + + def call_prim(*args) + Rubinius.primitive :proc_call + raise PrimitiveFailure, "Proc#call primitive failed" + end + + def call(*args, &block) + if @ruby_method + @ruby_method.call(*args, &block) + else + call_prim(*args, &block) + end + end + + def call_on_object(*args) + Rubinius.primitive :proc_call_on_object + raise PrimitiveFailure, "Proc#call_on_object primitive failed" + end + + def lambda_style! + @lambda = true + end + + def lambda? + !!@lambda + end +end +class Proc + def self.__from_block__(env) + # The compiler must be fixed before this method can be removed. + Rubinius::Mirror::Proc.from_block self, env + end + + def self.new(*args) + env = nil + + Rubinius.asm do + push_block + # assign a pushed block to the above local variable "env" + # Note that "env" is indexed at 1, not 0. "args" is indexed at 0. + set_local 1 + end + + unless env + # Support for ancient pre-block-pass style: + # def something; Proc.new; end + # something { a_block } => Proc instance + env = Rubinius::BlockEnvironment.of_sender + + unless env + raise ArgumentError, "tried to create a Proc object without a block" + end + end + + block = Rubinius::Mirror::Proc.from_block self, env + + if block.class != self + block = block.dup + Rubinius::Unsafe.set_class(block, self) + end + + Rubinius.asm(block, args) do |b, a| + run b + run a + run b + send_with_splat :initialize, 0, true + end + + return block + end + + attr_accessor :block + attr_accessor :bound_method + attr_accessor :ruby_method + + def binding + bind = @block.to_binding + bind.proc_environment = @block + bind + end + + def arity + if @ruby_method + return @ruby_method.arity + elsif @bound_method + arity = @bound_method.arity + return arity < 0 ? -1 : arity + end + + arity = @block.arity + return arity if self.lambda? || + @block.compiled_code.splat || + @block.compiled_code.total_args == arity + + -arity - 1 + end + + alias_method :===, :call + + def curry(curried_arity = nil) + if lambda? && curried_arity + if arity >= 0 && curried_arity != arity + raise ArgumentError, "Wrong number of arguments (%i for %i)" % [ + curried_arity, + arity + ] + end + + if arity < 0 && curried_arity < (-arity - 1) + raise ArgumentError, "Wrong number of arguments (%i for %i)" % [ + curried_arity, + -arity - 1 + ] + end + end + + args = [] + + m = Rubinius::Mirror.reflect self + f = m.curry self, [], arity + + f.singleton_class.send(:define_method, :binding) do + raise ArgumentError, "cannot create binding from f proc" + end + + f.singleton_class.thunk_method :parameters, [[:rest]] + f.singleton_class.thunk_method :source_location, nil + + f + end + + def source_location + if @ruby_method + @ruby_method.source_location + elsif @bound_method + if @bound_method.respond_to?(:source_location) + @bound_method.source_location + else + nil + end + else + @block.source_location + end + end + + def to_s + file, line = source_location + + l = " (lambda)" if lambda? + if file and line + "#<#{self.class}:0x#{self.object_id.to_s(16)}@#{file}:#{line}#{l}>" + else + "#<#{self.class}:0x#{self.object_id.to_s(16)}#{l}>" + end + end + + alias_method :inspect, :to_s + + def self.__from_method__(meth) + obj = __allocate__ + obj.ruby_method = meth + obj.lambda_style! + + return obj + end + + def __yield__(*args, &block) + @ruby_method.call(*args, &block) + end + + def parameters + if @ruby_method + return @ruby_method.parameters + elsif @bound_method + return @bound_method.parameters + end + + code = @block.compiled_code + + params = [] + + return params unless code.respond_to? :local_names + + m = code.required_args - code.post_args + o = m + code.total_args - code.required_args + p = o + code.post_args + p += 1 if code.splat + + required_status = self.lambda? ? :req : :opt + + code.local_names.each_with_index do |name, i| + if i < m + params << [required_status, name] + elsif i < o + params << [:opt, name] + elsif code.splat == i + if name == :* + params << [:rest] + else + params << [:rest, name] + end + elsif i < p + params << [required_status, name] + elsif code.block_index == i + params << [:block, name] + end + end + + params + end + + + def to_proc + self + end + + alias_method :[], :call + alias_method :yield, :call + + def clone + copy = self.class.__allocate__ + Rubinius.invoke_primitive :object_copy_object, copy, self + Rubinius.invoke_primitive :object_copy_singleton_class, copy, self + + Rubinius.privately do + copy.initialize_copy self + end + + copy.freeze if frozen? + copy + end + + def dup + copy = self.class.__allocate__ + Rubinius.invoke_primitive :object_copy_object, copy, self + + Rubinius.privately do + copy.initialize_copy self + end + copy + end + + def for_define_method(name, klass) + if @ruby_method + code, scope = @ruby_method.for_define_method(name, klass, self) + else + be = @block.dup + be.change_name name + + duped_proc = self.dup + duped_proc.lambda_style! + + code = Rubinius::BlockEnvironment::AsMethod.new(be) + scope = duped_proc.block.scope + end + + [code, scope] + end + + def self.from_method(meth) + if meth.kind_of? Method + return __from_method__(meth) + else + raise ArgumentError, "tried to create a Proc object without a Method" + end + end +end diff --git a/gxg/core/proc_mirror.rb b/gxg/core/proc_mirror.rb new file mode 100644 index 0000000..dd8f468 --- /dev/null +++ b/gxg/core/proc_mirror.rb @@ -0,0 +1,37 @@ +module Rubinius + class Mirror + class Proc < Mirror + def self.from_block(klass, env) + begin + return Rubinius.invoke_primitive :proc_from_env, env, klass + rescue Rubinius::Internal => exc + if Type.object_kind_of? env, BlockEnvironment + msg = "unable to create Proc from BlockEnvironment" + raise PrimitiveFailure, msg, exc + end + end + + begin + env.to_proc + rescue Exception + raise ArgumentError, "Unable to convert #{env.inspect} to a Proc" + end + end + + def curry(executable, args, arity) + args.freeze + + name = executable.lambda? ? :lambda : :proc + + Proc.__send__(name) do |*a, &b| + all_args = args + a + if all_args.size < arity + curry executable, all_args, arity + else + executable[*all_args] + end + end + end + end + end +end diff --git a/gxg/core/process.rb b/gxg/core/process.rb new file mode 100644 index 0000000..cc55cc4 --- /dev/null +++ b/gxg/core/process.rb @@ -0,0 +1,794 @@ +module Process + def self.time + Rubinius.primitive :vm_time + raise PrimitiveFailure, "Process.time primitive failed" + end + + def self.cpu_times + Rubinius.primitive :vm_times + raise PrimitiveFailure, "Process.cpu_times primitive failed" + end + + ## + # Sets the process title. Calling this method does not affect the value of + # `$0` as per MRI behaviour. This method returns the title set. + # + # @param [String] title + # @return [Title] + # + def self.setproctitle(title) + val = Rubinius::Type.coerce_to(title, String, :to_str) + + Rubinius.invoke_primitive(:vm_set_process_title, val) + end + + def self.setrlimit(resource, cur_limit, max_limit=undefined) + resource = coerce_rlimit_resource(resource) + cur_limit = Rubinius::Type.coerce_to cur_limit, Integer, :to_int + + unless undefined.equal? max_limit + max_limit = Rubinius::Type.coerce_to max_limit, Integer, :to_int + end + + rlimit = Rlimit.new + rlimit[:rlim_cur] = cur_limit + rlimit[:rlim_max] = undefined.equal?(max_limit) ? cur_limit : max_limit + + ret = FFI::Platform::POSIX.setrlimit(resource, rlimit.pointer) + Errno.handle if ret == -1 + nil + end + + def self.getrlimit(resource) + resource = coerce_rlimit_resource(resource) + + lim_max = [] + rlimit = Rlimit.new + ret = FFI::Platform::POSIX.getrlimit(resource, rlimit.pointer) + Errno.handle if ret == -1 + + [rlimit[:rlim_cur], rlimit[:rlim_max]] + end + + def self.setsid + pgid = FFI::Platform::POSIX.setsid + Errno.handle if pgid == -1 + pgid + end + + def self.fork + pid = Rubinius::Mirror::Process.fork + + if block_given? and pid.nil? + begin + yield nil + status = 0 + rescue SystemExit => e + status = e.status + rescue Exception => e + e.render "An exception occurred in a forked block" + status = 1 + end + + until Rubinius::AtExit.empty? + begin + Rubinius::AtExit.shift.call + rescue SystemExit => e + status = e.status + end + end + + # Do not use Kernel.exit. This raises a SystemExit exception, which + # will run ensure blocks. This is not what MRI does and causes bugs + # in programs. See issue http://github.com/rubinius/rubinius/issues#issue/289 for + # an example + + Kernel.exit! status + end + pid + end + + def self.times + Struct::Tms.new(*cpu_times) + end + + def self.kill(signal, *pids) + raise ArgumentError, "PID argument required" if pids.length == 0 + + use_process_group = false + signal = signal.to_s if signal.kind_of?(Symbol) + + if signal.kind_of?(String) + if signal[0] == ?- + signal = signal[1..-1] + use_process_group = true + end + + if signal[0..2] == "SIG" + signal = signal[3..-1] + end + + signal = Signal::Names[signal] + end + + raise ArgumentError unless signal.kind_of? Fixnum + + if signal < 0 + signal = -signal + use_process_group = true + end + + pids.each do |pid| + pid = Rubinius::Type.coerce_to_pid pid + + pid = -pid if use_process_group + result = FFI::Platform::POSIX.kill(pid, signal) + + Errno.handle if result == -1 + end + + return pids.length + end + + def self.abort(msg=nil) + if msg + msg = StringValue(msg) + $stderr.puts(msg) + end + raise SystemExit.new(1, msg) + end + + def self.getpgid(pid) + pid = Rubinius::Type.coerce_to pid, Integer, :to_int + + ret = FFI::Platform::POSIX.getpgid(pid) + Errno.handle if ret == -1 + ret + end + + def self.setpgid(pid, int) + pid = Rubinius::Type.coerce_to pid, Integer, :to_int + int = Rubinius::Type.coerce_to int, Integer, :to_int + + ret = FFI::Platform::POSIX.setpgid(pid, int) + Errno.handle if ret == -1 + ret + end + + @maxgroups = 32 + class << self + attr_reader :maxgroups + def maxgroups=(m) + @maxgroups = m + end + end + + def self.setpgrp + setpgid(0, 0) + end + def self.getpgrp + ret = FFI::Platform::POSIX.getpgrp + Errno.handle if ret == -1 + ret + end + + def self.pid + ret = FFI::Platform::POSIX.getpid + Errno.handle if ret == -1 + ret + end + + def self.ppid + ret = FFI::Platform::POSIX.getppid + Errno.handle if ret == -1 + ret + end + + def self.uid=(uid) + # the 4 rescue clauses below are needed + # until respond_to? can be used to query the implementation of methods attached via FFI + # atm respond_to returns true if a method is attached but not implemented on the platform + uid = Rubinius::Type.coerce_to uid, Integer, :to_int + begin + ret = FFI::Platform::POSIX.setresuid(uid, -1, -1) + rescue NotImplementedError + begin + ret = FFI::Platform::POSIX.setreuid(uid, -1) + rescue NotImplementedError + begin + ret = FFI::Platform::POSIX.setruid(uid) + rescue NotImplementedError + if Process.euid == uid + ret = FFI::Platform::POSIX.setuid(uid) + else + raise NotImplementedError + end + end + end + end + + Errno.handle if ret == -1 + + uid + end + + def self.gid=(gid) + gid = Rubinius::Type.coerce_to gid, Integer, :to_int + Process::Sys.setgid gid + end + + def self.euid=(uid) + # the 4 rescue clauses below are needed + # until respond_to? can be used to query the implementation of methods attached via FFI + # atm respond_to returns true if a method is attached but not implemented on the platform + uid = Rubinius::Type.coerce_to uid, Integer, :to_int + begin + ret = FFI::Platform::POSIX.setresuid(-1, uid, -1) + rescue NotImplementedError + begin + ret = FFI::Platform::POSIX.setreuid(-1, uid) + rescue NotImplementedError + begin + ret = FFI::Platform::POSIX.seteuid(uid) + rescue NotImplementedError + if Process.uid == uid + ret = FFI::Platform::POSIX.setuid(uid) + else + raise NotImplementedError + end + end + end + end + + Errno.handle if ret == -1 + + uid + end + + def self.egid=(gid) + gid = Rubinius::Type.coerce_to gid, Integer, :to_int + Process::Sys.setegid gid + end + + def self.uid + ret = FFI::Platform::POSIX.getuid + Errno.handle if ret == -1 + ret + end + + def self.gid + ret = FFI::Platform::POSIX.getgid + Errno.handle if ret == -1 + ret + end + + def self.euid + ret = FFI::Platform::POSIX.geteuid + Errno.handle if ret == -1 + ret + end + + def self.egid + ret = FFI::Platform::POSIX.getegid + Errno.handle if ret == -1 + ret + end + + def self.getpriority(kind, id) + kind = Rubinius::Type.coerce_to kind, Integer, :to_int + id = Rubinius::Type.coerce_to id, Integer, :to_int + + FFI::Platform::POSIX.errno = 0 + ret = FFI::Platform::POSIX.getpriority(kind, id) + Errno.handle + ret + end + + def self.setpriority(kind, id, priority) + kind = Rubinius::Type.coerce_to kind, Integer, :to_int + id = Rubinius::Type.coerce_to id, Integer, :to_int + priority = Rubinius::Type.coerce_to priority, Integer, :to_int + + ret = FFI::Platform::POSIX.setpriority(kind, id, priority) + Errno.handle if ret == -1 + ret + end + + def self.groups + g = [] + count = Rubinius::FFI::Platform::POSIX.getgroups(0, nil) + FFI::MemoryPointer.new(:int, count) { |p| + num_groups = FFI::Platform::POSIX.getgroups(count, p) + Errno.handle if num_groups == -1 + g = p.read_array_of_int(num_groups) + } + g + end + + def self.groups=(g) + @maxgroups = g.length if g.length > @maxgroups + FFI::MemoryPointer.new(:int, @maxgroups) { |p| + p.write_array_of_int(g) + Errno.handle if FFI::Platform::POSIX.setgroups(g.length, p) == -1 + } + g + end + + def self.initgroups(username, gid) + username = StringValue(username) + gid = Rubinius::Type.coerce_to gid, Integer, :to_int + + if FFI::Platform::POSIX.initgroups(username, gid) == -1 + Errno.handle + end + + Process.groups + end + + # + # Wait for the given process to exit. + # + # The pid may be the specific pid of some previously forked + # process, or -1 to indicate to watch for *any* child process + # exiting. Other options, such as process groups, may be available + # depending on the system. + # + # With no arguments the default is to block waiting for any + # child processes (pid -1.) + # + # The flag may be Process::WNOHANG, which indicates that + # the child should only be quickly checked. If it has not + # exited yet, nil is returned immediately instead. + # + # The return value is the exited pid or nil if Process::WNOHANG + # was used and the child had not yet exited. + # + # If the pid has exited, the global $? is set to a Process::Status + # object representing the exit status (and possibly other info) of + # the child. + # + # If there exists no such pid (e.g. never forked or already + # waited for), or no children at all, Errno::ECHILD is raised. + # + # TODO: Support other options such as WUNTRACED? --rue + # + def self.wait2(input_pid=-1, flags=nil) + input_pid = Rubinius::Type.coerce_to input_pid, Integer, :to_int + + if flags and (flags & WNOHANG) == WNOHANG + value = Rubinius.invoke_primitive :vm_waitpid, input_pid, true + return if value.nil? + else + value = Rubinius.invoke_primitive :vm_waitpid, input_pid, false + end + + if value == false + raise Errno::ECHILD, "No child process: #{input_pid}" + end + + # vm_waitpid returns a tuple when wait needs to communicate the pid that + # was actually detected as stopped (since wait can wait for all child + # pids, groups, etc) + status, termsig, stopsig, pid = value + + status = Process::Status.new(pid, status, termsig, stopsig) + Rubinius::Mirror::Process.set_status_global status + + [pid, status] + end + + # + # Wait for all child processes. + # + # Blocks until all child processes have exited, and returns + # an Array of [pid, Process::Status] results, one for each + # child. + # + # Be mindful of the effects of creating new processes while + # .waitall has been called (usually in a different thread.) + # The .waitall call does not in any way check that it is only + # waiting for children that existed at the time it was called. + # + def self.waitall + statuses = [] + + begin + while true + statuses << Process.wait2 + end + rescue Errno::ECHILD + end + + statuses + end + + def self.wait(pid=-1, flags=nil) + pid, status = Process.wait2(pid, flags) + return pid + end + + class << self + alias_method :waitpid, :wait + alias_method :waitpid2, :wait2 + end + + def self.daemon(stay_in_dir=false, keep_stdio_open=false) + # Do not run at_exit handlers in the parent + exit!(0) if fork + + Process.setsid + + exit!(0) if fork + + Dir.chdir("/") unless stay_in_dir + + unless keep_stdio_open + io = File.open "/dev/null", File::RDWR, 0 + $stdin.reopen io + $stdout.reopen io + $stderr.reopen io + end + + return 0 + end + + def self.exec(*args) + Rubinius::Mirror::Process.exec(*args) + end + + def self.spawn(*args) + Rubinius::Mirror::Process.spawn(*args) + end + + # TODO: Should an error be raised on ECHILD? --rue + # + # TODO: This operates on the assumption that waiting on + # the event consumes very little resources. If this + # is not the case, the check should be made WNOHANG + # and called periodically. + # + def self.detach(pid) + raise ArgumentError, "Only positive pids may be detached" unless pid > 0 + + thread = Thread.new { Process.wait pid; $? } + thread[:pid] = pid + def thread.pid; self[:pid] end + + thread + end + + def self.coerce_rlimit_resource(resource) + case resource + when Integer + return resource + when Symbol, String + # do nothing + else + unless r = Rubinius::Type.check_convert_type(resource, String, :to_str) + return Rubinius::Type.coerce_to resource, Integer, :to_int + end + + resource = r + end + + constant = "RLIMIT_#{resource}" + unless const_defined? constant + raise ArgumentError, "invalid resource name: #{constant}" + end + const_get constant + end + + class << self + private :coerce_rlimit_resource + end + + #-- + # TODO: Most of the fields aren't implemented yet. + # TODO: Also, these objects should only need to be constructed by + # Process.wait and family. + #++ + + class Status + + attr_reader :termsig + attr_reader :stopsig + + def initialize(pid=nil, status=nil, termsig=nil, stopsig=nil) + @pid = pid + @status = status + @termsig = termsig + @stopsig = stopsig + end + + private :initialize + + def exitstatus + @status + end + + def to_i + @status + end + + def to_s + @status.to_s + end + + def &(num) + @status & num + end + + def ==(other) + other = other.to_i if other.kind_of? Process::Status + @status == other + end + + def >>(num) + @status >> num + end + + def coredump? + false + end + + def exited? + @status != nil + end + + def pid + @pid + end + + def signaled? + @termsig != nil + end + + def stopped? + @stopsig != nil + end + + def success? + if exited? + @status == 0 + else + nil + end + end + end + + module Sys + class << self + def getegid + ret = FFI::Platform::POSIX.getegid + Errno.handle if ret == -1 + ret + end + + def geteuid + ret = FFI::Platform::POSIX.geteuid + Errno.handle if ret == -1 + ret + end + + def getgid + ret = FFI::Platform::POSIX.getgid + Errno.handle if ret == -1 + ret + end + + def getuid + ret = FFI::Platform::POSIX.getuid + Errno.handle if ret == -1 + ret + end + + def issetugid + raise "not implemented" + end + + def setgid(gid) + gid = Rubinius::Type.coerce_to gid, Integer, :to_int + + ret = FFI::Platform::POSIX.setgid gid + Errno.handle if ret == -1 + nil + end + + def setuid(uid) + uid = Rubinius::Type.coerce_to uid, Integer, :to_int + + ret = FFI::Platform::POSIX.setuid uid + Errno.handle if ret == -1 + nil + end + + def setegid(egid) + egid = Rubinius::Type.coerce_to egid, Integer, :to_int + + ret = FFI::Platform::POSIX.setegid egid + Errno.handle if ret == -1 + nil + end + + def seteuid(euid) + euid = Rubinius::Type.coerce_to euid, Integer, :to_int + + ret = FFI::Platform::POSIX.seteuid euid + Errno.handle if ret == -1 + nil + end + + def setrgid(rgid) + setregid(rgid, -1) + end + + def setruid(ruid) + setreuid(ruid, -1) + end + + def setregid(rid, eid) + rid = Rubinius::Type.coerce_to rid, Integer, :to_int + eid = Rubinius::Type.coerce_to eid, Integer, :to_int + + ret = FFI::Platform::POSIX.setregid rid, eid + Errno.handle if ret == -1 + nil + end + + def setreuid(rid, eid) + rid = Rubinius::Type.coerce_to rid, Integer, :to_int + eid = Rubinius::Type.coerce_to eid, Integer, :to_int + + ret = FFI::Platform::POSIX.setreuid rid, eid + Errno.handle if ret == -1 + nil + end + + def setresgid(rid, eid, sid) + rid = Rubinius::Type.coerce_to rid, Integer, :to_int + eid = Rubinius::Type.coerce_to eid, Integer, :to_int + sid = Rubinius::Type.coerce_to sid, Integer, :to_int + + ret = FFI::Platform::POSIX.setresgid rid, eid, sid + Errno.handle if ret == -1 + nil + end + + def setresuid(rid, eid, sid) + rid = Rubinius::Type.coerce_to rid, Integer, :to_int + eid = Rubinius::Type.coerce_to eid, Integer, :to_int + sid = Rubinius::Type.coerce_to sid, Integer, :to_int + + ret = FFI::Platform::POSIX.setresuid rid, eid, sid + Errno.handle if ret == -1 + nil + end + end + end + + module UID + class << self + def change_privilege(uid) + uid = Rubinius::Type.coerce_to uid, Integer, :to_int + + ret = FFI::Platform::POSIX.setreuid(uid, uid) + Errno.handle if ret == -1 + uid + end + + def eid + ret = FFI::Platform::POSIX.geteuid + Errno.handle if ret == -1 + ret + end + + def eid=(uid) + uid = Rubinius::Type.coerce_to uid, Integer, :to_int + + ret = FFI::Platform::POSIX.seteuid(uid) + Errno.handle if ret == -1 + uid + end + alias_method :grant_privilege, :eid= + + def re_exchange + real = FFI::Platform::POSIX.getuid + Errno.handle if real == -1 + eff = FFI::Platform::POSIX.geteuid + Errno.handle if eff == -1 + ret = FFI::Platform::POSIX.setreuid(eff, real) + Errno.handle if ret == -1 + eff + end + + def re_exchangeable? + true + end + + def rid + ret = FFI::Platform::POSIX.getuid + Errno.handle if ret == -1 + ret + end + + def sid_available? + true + end + + def switch + eff = re_exchange + if block_given? + ret = yield + re_exchange + return ret + else + return eff + end + end + + end + end + + module GID + class << self + def change_privilege(gid) + gid = Rubinius::Type.coerce_to gid, Integer, :to_int + + ret = FFI::Platform::POSIX.setregid(gid, gid) + Errno.handle if ret == -1 + gid + end + + def eid + ret = FFI::Platform::POSIX.getegid + Errno.handle if ret == -1 + ret + end + + def eid=(gid) + gid = Rubinius::Type.coerce_to gid, Integer, :to_int + + ret = FFI::Platform::POSIX.setegid(gid) + Errno.handle if ret == -1 + gid + end + alias_method :grant_privilege, :eid= + + def re_exchange + real = FFI::Platform::POSIX.getgid + Errno.handle if real == -1 + eff = FFI::Platform::POSIX.getegid + Errno.handle if eff == -1 + ret = FFI::Platform::POSIX.setregid(eff, real) + Errno.handle if ret == -1 + eff + end + + def re_exchangeable? + true + end + + def rid + ret = FFI::Platform::POSIX.getgid + Errno.handle if ret == -1 + ret + end + + def sid_available? + true + end + + def switch + eff = re_exchange + if block_given? + ret = yield + re_exchange + return ret + else + return eff + end + end + + end + end +end diff --git a/gxg/core/process_mirror.rb b/gxg/core/process_mirror.rb new file mode 100644 index 0000000..0cb7c8a --- /dev/null +++ b/gxg/core/process_mirror.rb @@ -0,0 +1,256 @@ +module Rubinius + class Mirror + module Process + def self.set_status_global(status) + ::Thread.current[:$?] = status + end + + def self.fork + Rubinius.primitive :vm_fork + raise PrimitiveFailure, "Rubinius::Mirror::Process.fork primitive failed" + end + + def self.exec(*args) + exe = Execute.new(*args) + exe.spawn_setup + exe.exec exe.command, exe.argv + end + + def self.spawn(*args) + exe = Execute.new(*args) + + begin + pid = exe.spawn exe.options, exe.command, exe.argv + rescue SystemCallError => error + set_status_global ::Process::Status.new(pid, 127) + raise error + end + + pid + end + + def self.backtick(str) + Rubinius.primitive :vm_backtick + raise PrimitiveFailure, "Rubinius::Mirror::Process.backtick primitive failed" + end + + class Execute + attr_reader :command + attr_reader :argv + attr_reader :options + + # Turns the various varargs incantations supported by Process.spawn into a + # [env, prog, argv, redirects, options] tuple. + # + # The following method signature is supported: + # Process.spawn([env], command, ..., [options]) + # + # The env and options hashes are optional. The command may be a variable + # number of strings or an Array full of strings that make up the new process's + # argv. + # + # Assigns @environment, @command, @argv, @redirects, @options. All + # elements are guaranteed to be non-nil. When no env or options are + # given, empty hashes are returned. + def initialize(env_or_cmd, *args) + if options = Rubinius::Type.try_convert(args.last, ::Hash, :to_hash) + args.pop + end + + if env = Rubinius::Type.try_convert(env_or_cmd, ::Hash, :to_hash) + unless command = args.shift + raise ArgumentError, "command argument expected" + end + else + command = env_or_cmd + end + + if args.empty? and + cmd = Rubinius::Type.try_convert(command, ::String, :to_str) + raise Errno::ENOENT if cmd.empty? + + @command = cmd + @argv = [] + else + if cmd = Rubinius::Type.try_convert(command, ::Array, :to_ary) + raise ArgumentError, "wrong first argument" unless cmd.size == 2 + command = StringValue(cmd[0]) + name = StringValue(cmd[1]) + else + name = command = StringValue(command) + end + + argv = [name] + args.each { |arg| argv << StringValue(arg) } + + @command = command + @argv = argv + end + + @options = Rubinius::LookupTable.new if options or env + + if options + options.each do |key, value| + case key + when ::IO, ::Fixnum, :in, :out, :err + from = convert_io_fd key + to = convert_to_fd value, from + redirect @options, from, to + when ::Array + from = convert_io_fd key.first + to = convert_to_fd value, from + key.each { |k| redirect @options, convert_io_fd(k), to } + when :unsetenv_others + if value + array = @options[:env] = [] + ENV.each_key { |k| array << convert_env_key(k) << nil } + end + when :pgroup + if value == true + value = 0 + elsif value + value = Rubinius::Type.coerce_to value, ::Integer, :to_int + raise ArgumentError, "negative process group ID : #{value}" if value < 0 + end + @options[key] = value + when :chdir + @options[key] = Rubinius::Type.coerce_to_path(value) + when :umask + @options[key] = value + when :close_others + @options[key] = value if value + else + raise ArgumentError, "unknown exec option: #{key.inspect}" + end + end + end + + if env + array = (@options[:env] ||= []) + + env.each do |key, value| + array << convert_env_key(key) << convert_env_value(value) + end + end + end + + private :initialize + + def redirect(options, from, to) + case to + when ::Fixnum + map = (options[:redirect_fd] ||= []) + map << from << to + when ::Array + map = (options[:assign_fd] ||= []) + map << from + map.concat to + end + end + + def convert_io_fd(obj) + case obj + when ::Fixnum + obj + when :in + 0 + when :out + 1 + when :err + 2 + when ::IO + obj.fileno + else + raise ArgementError, "wrong exec option: #{obj.inspect}" + end + end + + def convert_to_fd(obj, target) + case obj + when ::Fixnum + obj + when :in + 0 + when :out + 1 + when :err + 2 + when :close + nil + when ::IO + obj.fileno + when ::String + [obj, default_mode(target), 0644] + when ::Array + case obj.size + when 1 + [obj[0], File::RDONLY, 0644] + when 2 + if obj[0] == :child + convert_to_fd obj[1], target + else + [obj[0], convert_file_mode(obj[1]), 0644] + end + when 3 + [obj[0], convert_file_mode(obj[1]), obj[2]] + end + else + raise ArgumentError, "wrong exec redirect: #{obj.inspect}" + end + end + + def default_mode(target) + if target == 1 or target == 2 + OFLAGS["w"] + else + OFLAGS["r"] + end + end + + def convert_file_mode(obj) + case obj + when ::Fixnum + obj + when ::String + OFLAGS[obj] + when nil + OFLAG["r"] + else + Rubinius::Type.coerce_to obj, Integer, :to_int + end + end + + def convert_env_key(key) + key = Rubinius::Type.check_null_safe(StringValue(key)) + + if key.include?("=") + raise ArgumentError, "environment name contains a equal : #{key}" + end + + key + end + + def convert_env_value(value) + return if value.nil? + Rubinius::Type.check_null_safe(StringValue(value)) + end + + def spawn_setup + Rubinius.invoke_primitive :vm_spawn_setup, @options + end + + def spawn(exe, command, args) + Rubinius.primitive :vm_spawn + raise PrimitiveFailure, + "Rubinius::Mirror::Process::Execute#spawn primitive failed" + end + + def exec(command, args) + Rubinius.primitive :vm_exec + raise PrimitiveFailure, + "Rubinius::Mirror::Process::Execute#exec primitive failed" + end + end + end + end +end diff --git a/gxg/core/random.rb b/gxg/core/random.rb new file mode 100644 index 0000000..8ebb293 --- /dev/null +++ b/gxg/core/random.rb @@ -0,0 +1,188 @@ +class Rubinius::Randomizer + def self.allocate + Rubinius.primitive :randomizer_allocate + raise PrimitiveFailure, "Randomizer.allocate primitive failed" + end + + def initialize + self.seed = generate_seed + end + + private :initialize + + attr_reader :seed + def seed=(new_seed) + set_seed new_seed + @seed = new_seed + end + + def set_seed(new_seed) + Rubinius.primitive :randomizer_seed + raise PrimitiveFailure, "Randomizer#seed primitive failed" + end + + def swap_seed(new_seed) + old_seed = self.seed + self.seed = new_seed + old_seed + end + + def random(limit) + if undefined.equal?(limit) + random_float + else + if limit.kind_of?(Range) + if time_value?(limit.min) + random_time_range(limit) + else + random_range(limit) + end + elsif limit.kind_of?(Float) + raise ArgumentError, "invalid argument - #{limit}" if limit <= 0 + random_float * limit + elsif limit.is_a?(Integer) + raise ArgumentError, "invalid argument - #{limit}" if limit <= 0 + random_integer(limit - 1) + elsif limit.respond_to?(:to_f) + raise ArgumentError, "invalid argument - #{limit}" if limit <= 0 + random_float * limit + else + limit_int = Rubinius::Type.coerce_to limit, Integer, :to_int + raise ArgumentError, "invalid argument - #{limit}" if limit_int <= 0 + + random_integer(limit_int - 1) + end + end + end + + # Generate a random Float, in the range 0...1.0 + def random_float + Rubinius.primitive :randomizer_rand_float + raise PrimitiveFailure, "Randomizer#rand_float primitive failed" + end + + # Generate a random Integer, in the range 0...limit + def random_integer(limit) + Rubinius.primitive :randomizer_rand_int + raise PrimitiveFailure, "Randomizer#rand_int primitive failed" + end + + def random_range(limit) + return nil if limit.first.nil? || limit.last.nil? + + min, max = limit.last.coerce(limit.first) + diff = max - min + + return if diff < 0 + return min if diff == 0 + + diff += 1 if max.kind_of?(Integer) && !limit.exclude_end? + random(diff) + min + end + + def generate_seed + Rubinius.primitive :randomizer_gen_seed + raise PrimitiveFailure, "Randomizer#gen_seed primitive failed" + end + + ## + # Returns a random value from a range made out of Time, Date or DateTime + # instances. + # + # @param [Range] range + # @return [Time|Date|DateTime] + # + def random_time_range(range) + min = time_to_float(range.min) + max = time_to_float(range.max) + time = Time.at(random(min..max)) + + if Object.const_defined?(:DateTime) && range.min.is_a?(DateTime) + time = time.to_datetime + elsif Object.const_defined?(:DateTime) && range.min.is_a?(Date) + time = time.to_date + end + + return time + end + + ## + # Casts a Time/Date/DateTime instance to a Float. + # + # @param [Time|Date|DateTime] input + # @return [Float] + # + def time_to_float(input) + return input.respond_to?(:to_time) ? input.to_time.to_f : input.to_f + end + + ## + # Checks if a given value is a Time, Date or DateTime object. + # + # @param [Mixed] input + # @return [TrueClass|FalseClass] + # + def time_value?(input) + return input.is_a?(Time) || (Object.const_defined?(:Date) && input.is_a?(Date)) + end +end + +class Random + def self.new_seed + Thread.current.randomizer.generate_seed + end + + def self.srand(seed=undefined) + if undefined.equal? seed + seed = Thread.current.randomizer.generate_seed + end + + seed = Rubinius::Type.coerce_to seed, Integer, :to_int + Thread.current.randomizer.swap_seed seed + end + + def self.rand(limit=undefined) + Thread.current.randomizer.random(limit) + end + + def initialize(seed=undefined) + @randomizer = Rubinius::Randomizer.new + if !undefined.equal?(seed) + @randomizer.swap_seed seed.to_int + end + end + + private :initialize + + def rand(limit=undefined) + @randomizer.random(limit) + end + + def seed + @randomizer.seed + end + + def state + @randomizer.seed + end + private :state + + def ==(other) + return false unless other.kind_of?(Random) + seed == other.seed + end + + # Returns a random binary string. + # The argument size specified the length of the result string. + def bytes(length) + length = Rubinius::Type.coerce_to length, Integer, :to_int + s = '' + i = 0 + while i < length + s << @randomizer.random_integer(255).chr + i += 1 + end + + s + end +end diff --git a/gxg/core/range.rb b/gxg/core/range.rb new file mode 100644 index 0000000..f87d5d8 --- /dev/null +++ b/gxg/core/range.rb @@ -0,0 +1,318 @@ +class Range + include Enumerable + + def initialize(first, last, exclude_end = false) + raise NameError, "`initialize' called twice" if @begin + + unless first.kind_of?(Fixnum) && last.kind_of?(Fixnum) + begin + raise ArgumentError, "bad value for range" unless first <=> last + rescue + raise ArgumentError, "bad value for range" + end + end + + @begin = first + @end = last + @excl = exclude_end + end + private :initialize + + def ==(other) + return true if equal? other + + other.kind_of?(Range) and + self.first == other.first and + self.last == other.last and + self.exclude_end? == other.exclude_end? + end + + alias_method :eql?, :== + + attr_reader_specific :excl, :exclude_end? + + attr_reader :begin + attr_reader :end + + def bsearch + return to_enum :bsearch unless block_given? + + unless @begin.kind_of? Numeric and @end.kind_of? Numeric + raise TypeError, "bsearch is not available for #{@begin.class}" + end + + min = @begin + max = @end + + max -= 1 if max.kind_of? Integer and @excl + + start = min = Rubinius::Type.coerce_to min, Integer, :to_int + total = max = Rubinius::Type.coerce_to max, Integer, :to_int + + last_true = nil + + seeker = Proc.new do |current| + x = yield current + + return current if x == 0 + + case x + when Numeric + if x > 0 + min = current + 1 + else + max = current + end + when true + last_true = current + max = current + when false, nil + min = current + 1 + else + raise TypeError, "Range#bsearch block must return Numeric or boolean" + end + end + + while min < max + if max < 0 and min < 0 + mid = min + (max - min) / 2 + elsif min < -max + mid = -((-1 - min - max) / 2 + 1) + else + mid = (min + max) / 2 + end + + seeker.call mid + end + + if min == max + seeker.call min + end + + if min < max + return @begin if mid == start + return @begin.kind_of?(Float) ? mid.to_f : mid + end + + if last_true + return @begin if last_true == start + return @begin.kind_of?(Float) ? last_true.to_f : last_true + end + + nil + end + + def each + return to_enum { size } unless block_given? + first, last = @begin, @end + + unless first.respond_to?(:succ) && !first.kind_of?(Time) + raise TypeError, "can't iterate from #{first.class}" + end + + case first + when Fixnum + last -= 1 if @excl + + i = first + while i <= last + yield i + i += 1 + end + when String + first.upto(last, @excl) do |str| + yield str + end + when Symbol + first.to_s.upto(last.to_s, @excl) do |str| + yield str.to_sym + end + else + current = first + if @excl + while (current <=> last) < 0 + yield current + current = current.succ + end + else + while (c = current <=> last) && c <= 0 + yield current + break if c == 0 + current = current.succ + end + end + end + + self + end + + def first(n=undefined) + return @begin if undefined.equal? n + + super + end + + def hash + excl = @excl ? 1 : 0 + hash = excl + hash ^= @begin.hash << 1 + hash ^= @end.hash << 9 + hash ^= excl << 24; + # Are we throwing away too much here for a good hash value distribution? + return hash & Fixnum::MAX + end + + def include?(value) + if @begin.respond_to?(:to_int) || + @end.respond_to?(:to_int) || + @begin.kind_of?(Numeric) || + @end.kind_of?(Numeric) + cover? value + else + super + end + end + + alias_method :member?, :include? + + def ===(value) + include?(value) + end + + def inspect + "#{@begin.inspect}#{@excl ? "..." : ".."}#{@end.inspect}" + end + + def last(n=undefined) + return @end if undefined.equal? n + + to_a.last(n) + end + + def max + return super if block_given? || (@excl && !@end.kind_of?(Numeric)) + return nil if @end < @begin || (@excl && @end == @begin) + return @end unless @excl + + unless @end.kind_of?(Integer) + raise TypeError, "cannot exclude non Integer end value" + end + + unless @begin.kind_of?(Integer) + raise TypeError, "cannot exclude end value with non Integer begin value" + end + + @end - 1 + end + + def min + return super if block_given? + return nil if @end < @begin || (@excl && @end == @begin) + + @begin + end + + def step(step_size=1) # :yields: object + return to_enum(:step, step_size) do + m = Rubinius::Mirror::Range.reflect(self) + m.step_iterations_size(*m.validate_step_size(@begin, @end, step_size)) + end unless block_given? + + m = Rubinius::Mirror::Range.reflect(self) + values = m.validate_step_size(@begin, @end, step_size) + first = values[0] + last = values[1] + step_size = values[2] + + case first + when Float + iterations = m.step_float_iterations_size(first, last, step_size) + + i = 0 + while i < iterations + curr = i * step_size + first + curr = last if last < curr + yield curr + i += 1 + end + when Numeric + curr = first + last -= 1 if @excl + + while curr <= last + yield curr + curr += step_size + end + else + i = 0 + each do |curr| + yield curr if i % step_size == 0 + i += 1 + end + end + + self + end + + def to_s + "#{@begin}#{@excl ? "..." : ".."}#{@end}" + end + + def to_a + return super unless @begin.kind_of? Fixnum and @end.kind_of? Fixnum + + fin = @end + fin += 1 unless @excl + + size = fin - @begin + return [] if size <= 0 + + ary = Array.new(size) + + i = 0 + while i < size + ary[i] = @begin + i + i += 1 + end + + ary + end + + def cover?(value) + # MRI uses <=> to compare, so must we. + + beg_compare = (@begin <=> value) + return false unless beg_compare + + if Comparable.compare_int(beg_compare) <= 0 + end_compare = (value <=> @end) + + if @excl + return true if Comparable.compare_int(end_compare) < 0 + else + return true if Comparable.compare_int(end_compare) <= 0 + end + end + + false + end + + def size + return nil unless @begin.kind_of?(Numeric) + + delta = @end - @begin + return 0 if delta < 0 + + if @begin.kind_of?(Float) || @end.kind_of?(Float) + return delta if delta == Float::INFINITY + + err = (@begin.abs + @end.abs + delta.abs) * Float::EPSILON + err = 0.5 if err > 0.5 + + (@excl ? delta - err : delta + err).floor + 1 + else + delta += 1 unless @excl + delta + end + end +end diff --git a/gxg/core/range_mirror.rb b/gxg/core/range_mirror.rb new file mode 100644 index 0000000..fa53c0d --- /dev/null +++ b/gxg/core/range_mirror.rb @@ -0,0 +1,61 @@ +module Rubinius + class Mirror + class Range < Mirror + self.subject = ::Range + + def excl + Rubinius.invoke_primitive :object_get_ivar, @object, :@excl + end + + def step_float_iterations_size(first, last, step_size) + err = (first.abs + last.abs + (last - first).abs) / step_size.abs * Float::EPSILON + err = 0.5 if err > 0.5 + + if excl + iterations = ((last - first) / step_size - err).floor + iterations += 1 if iterations * step_size + first < last + else + iterations = ((last - first) / step_size + err).floor + 1 + end + + iterations + end + + def step_iterations_size(first, last, step_size) + case first + when Float + step_float_iterations_size(first, last, step_size) + else + @object.size.nil? ? nil : (@object.size.fdiv(step_size)).ceil + end + end + + def validate_step_size(first, last, step_size) + if step_size.kind_of? Float or first.kind_of? Float or last.kind_of? Float + # if any are floats they all must be + begin + step_size = Float(from = step_size) + first = Float(from = first) + last = Float(from = last) + rescue ArgumentError + raise TypeError, "no implicit conversion to float from #{from.class}" + end + else + step_size = Integer(from = step_size) + + unless step_size.kind_of? Integer + raise TypeError, "can't convert #{from.class} to Integer (#{from.class}#to_int gives #{step_size.class})" + end + end + + if step_size <= 0 + raise ArgumentError, "step can't be negative" if step_size < 0 + raise ArgumentError, "step can't be 0" + end + + return first, last, step_size + end + + end + end +end diff --git a/gxg/core/rational.rb b/gxg/core/rational.rb new file mode 100644 index 0000000..d1ea435 --- /dev/null +++ b/gxg/core/rational.rb @@ -0,0 +1,383 @@ +# +# rational.rb - +# $Release Version: 0.5 $ +# $Revision: 1.7 $ +# $Date: 1999/08/24 12:49:28 $ +# by Keiju ISHITSUKA(SHL Japan Inc.) +# + +class Rational < Numeric + attr_reader :numerator + attr_reader :denominator + + def *(other) + case other + when Rational + num = @numerator * other.numerator + den = @denominator * other.denominator + Rational(num, den) + when Integer + Rational(@numerator * other, @denominator) + when Float + to_f * other + else + a, b = other.coerce(self) + a * b + end + end + + def **(other) + if other.kind_of?(Rational) && other.denominator == 1 + other = other.numerator + end + + case other + when Fixnum + if other > 0 + Rational(@numerator ** other, @denominator ** other) + elsif other < 0 + raise ZeroDivisionError, "divided by 0" if self == 0 + Rational(@denominator ** -other, @numerator ** -other) + elsif other == 0 + Rational.new(1, 1) + end + when Bignum + if self == 0 + if other < 0 + raise ZeroDivisionError, "divided by 0" + elsif other > 0 + Rational.new(0, 1) + end + elsif self == 1 + Rational.new(1, 1) + elsif self == -1 + Rational.new(other.even? ? 1 : -1, 1) + else + to_f ** other + end + when Float + to_f ** other + when Rational + if self == 0 && other < 0 + raise ZeroDivisionError, "divided by 0" + end + + to_f ** other + else + a, b = other.coerce(self) + a ** b + end + end + + def +(other) + case other + when Rational + num = @numerator * other.denominator + @denominator * other.numerator + den = @denominator * other.denominator + Rational(num, den) + when Integer + Rational(@numerator + other * @denominator, @denominator) + when Float + to_f + other + else + a, b = other.coerce(self) + a + b + end + end + + def -(other) + case other + when Rational + num = @numerator * other.denominator - @denominator * other.numerator + den = @denominator * other.denominator + Rational(num, den) + when Integer + Rational(@numerator - other * @denominator, @denominator) + when Float + to_f - other + else + a, b = other.coerce(self) + a - b + end + end + + def /(other) + case other + when Rational + num = @numerator * other.denominator + den = @denominator * other.numerator + Rational(num, den) + when Integer + raise ZeroDivisionError, "divided by 0" if other == 0 + Rational(@numerator, @denominator * other) + when Float + to_f / other + else + redo_coerced :/, other + end + end + alias_method :divide, :/ + alias_method :quo, :/ + + def <=>(other) + case other + when Rational + diff = @numerator * other.denominator - @denominator * other.numerator + diff <=> 0 + when Integer + diff = @numerator - @denominator * other + diff <=> 0 + when Float + to_f <=> other + else + if defined?(other.coerce) + a, b = other.coerce(self) + a <=> b + end + end + end + + def ==(other) + case other + when Rational + @numerator == other.numerator && @denominator == other.denominator + when Integer + @numerator == other && @denominator == 1 + when Float + to_f == other + else + other == self + end + end + + def abs + return self if @numerator >= 0 + + Rational.new(-@numerator, @denominator) + end + + def ceil(precision = 0) + if precision == 0 + -(-@numerator / @denominator) + else + with_precision(:ceil, precision) + end + end + + def coerce(other) + case other + when Integer + return Rational.new(other, 1), self + when Float + return other, self.to_f + else + super + end + end + + def floor(precision = 0) + if precision == 0 + @numerator / @denominator + else + with_precision(:floor, precision) + end + end + + def hash + @numerator.hash ^ @denominator.hash + end + + def inspect + "(#{to_s})" + end + + def rationalize(eps = undefined) + return self if undefined.equal?(eps) + + e = eps.abs + a = self - e + b = self + e + + p0 = 0 + p1 = 1 + q0 = 1 + q1 = 0 + + while true + c = a.ceil + + break if c < b + + k = c - 1 + p2 = k * p1 + p0 + q2 = k * q1 + q0 + t = 1 / (b - k) + b = 1 / (a - k) + a = t + + p0 = p1 + q0 = q1 + p1 = p2 + q1 = q2 + end + + # The rational number is guaranteed to be in lowest terms. + Rational.new(c * p1 + p0, c * q1 + q0) + end + + def round(precision = 0) + return with_precision(:round, precision) unless precision == 0 + return 0 if @numerator == 0 + return @numerator if @denominator == 1 + + num = @numerator.abs * 2 + @denominator + den = @denominator * 2 + + approx = num / den + + if @numerator < 0 + -approx + else + approx + end + end + + def to_f + @numerator.to_f / @denominator.to_f + end + + def to_i + truncate + end + + def to_r + self + end + + def to_s + "#{@numerator.to_s}/#{@denominator.to_s}" + end + + def truncate(precision = 0) + if precision == 0 + @numerator < 0 ? ceil : floor + else + with_precision(:truncate, precision) + end + end + + def self.convert(num, den, mathn = true) + if num.nil? || den.nil? + raise TypeError, "cannot convert nil into Rational" + end + + if num.kind_of?(Integer) && den.kind_of?(Integer) + return reduce(num, den, mathn) + end + + case num + when Integer + # nothing + when Float, String, Complex + num = num.to_r + end + + case den + when Integer + # nothing + when Float, String, Complex + den = den.to_r + end + + if den.equal?(1) && !num.kind_of?(Integer) + return Rubinius::Type.coerce_to(num, Rational, :to_r) + elsif num.kind_of?(Numeric) && den.kind_of?(Numeric) && + !(num.kind_of?(Integer) && den.kind_of?(Integer)) + return num / den + end + + reduce(num, den) + end + + def self.reduce(num, den, mathn = true) + case num + when Integer + # nothing + when Numeric + num = num.to_i + else + raise TypeError, "numerator is not an Integer" + end + + case den + when Integer + if den == 0 + raise ZeroDivisionError, "divided by 0" + elsif den < 0 + num = -num + den = -den + end + + if den == 1 + return (mathn && Rubinius.mathn_loaded?) ? num : new(num, den) + end + when Numeric + den = den.to_i + else + raise TypeError, "denominator is not an Integer" + end + + gcd = num.gcd(den) + num = num / gcd + den = den / gcd + + return num if mathn && Rubinius.mathn_loaded? && den == 1 + + new(num, den) + end + + class << self + private :convert + private :reduce + end + + def initialize(num, den) + @numerator = num + @denominator = den + end + private :initialize + + def marshal_dump + ary = [@numerator, @denominator] + + instance_variables.each do |ivar| + ary.instance_variable_set(ivar, instance_variable_get(ivar)) + end + + ary + end + private :marshal_dump + + def marshal_load(ary) + @numerator, @denominator = ary + + ary.instance_variables.each do |ivar| + instance_variable_set(ivar, ary.instance_variable_get(ivar)) + end + + self + end + private :marshal_load + + def with_precision(method, n) + raise TypeError, "not an Integer" unless n.kind_of?(Integer) + + p = 10 ** n + s = self * p + + r = Rational(s.send(method), p) + + n < 1 ? r.to_i : r + end + private :with_precision +end diff --git a/gxg/core/rationalizer.rb b/gxg/core/rationalizer.rb new file mode 100644 index 0000000..5d21d74 --- /dev/null +++ b/gxg/core/rationalizer.rb @@ -0,0 +1,52 @@ +# Float#rationalize and String#to_r require complex algorithms. The Regexp +# required for String#to_r cannot be created at class scope when loading the +# core library because Regexp needs to load after String and making a simpler +# Regexp#initialize for bootstrapping isn't really feasible. So these +# algorithms are located here. +# +class String + class Rationalizer + def initialize(value) + @value = value + end + + private :initialize + + def convert + if m = PATTERN.match(@value) + si = m[1] + nu = m[2] + de = m[3] + re = m.post_match + + ifp, exp = nu.split /[eE]/ + ip, fp = ifp.split /\./ + + value = Rational.new(ip.to_i, 1) + + if fp + ctype = Rubinius::CType + i = count = 0 + size = fp.size + while i < size + count += 1 if ctype.isdigit fp.getbyte(i) + i += 1 + end + + l = 10 ** count + value *= l + value += fp.to_i + value = value.quo(l) + end + + value = -value if si == "-" + value *= 10 ** exp.to_i if exp + value = value.quo(de.to_i) if de + + value + else + Rational(0, 1) + end + end + end +end diff --git a/gxg/core/rbconfig.rb b/gxg/core/rbconfig.rb new file mode 100644 index 0000000..1a592d8 --- /dev/null +++ b/gxg/core/rbconfig.rb @@ -0,0 +1,277 @@ +module RbConfig + unless defined? RUBY_ENGINE and RUBY_ENGINE == "rbx" + raise "This RbConfig is only compatible with Rubinius. " \ + "Your RUBY_ENGINE is #{RUBY_ENGINE or "undefined"}" + end + + CONFIG = {} + + CONFIG["prefix"] = Rubinius::PREFIX_PATH.dup + CONFIG["install_prefix"] = '' + CONFIG["DLEXT"] = Rubinius::LIBSUFFIX[1..-1] + CONFIG["EXEEXT"] = "" + CONFIG["ruby_install_name"] = Rubinius::PROGRAM_NAME.dup + CONFIG["RUBY_INSTALL_NAME"] = Rubinius::PROGRAM_NAME.dup + CONFIG["exec_prefix"] = "$(prefix)" + CONFIG["bindir"] = Rubinius::BIN_PATH.dup + CONFIG["sbindir"] = "$(exec_prefix)/sbin" + CONFIG["libexecdir"] = "$(exec_prefix)/libexec" + CONFIG["datarootdir"] = "$(prefix)/share" + CONFIG["datadir"] = "$(datarootdir)" + CONFIG["sysconfdir"] = "$(prefix)/etc" + CONFIG["sharedstatedir"] = "$(prefix)/com" + CONFIG["localstatedir"] = "$(prefix)/var" + CONFIG["includedir"] = "$(prefix)/include" + CONFIG["oldincludedir"] = "/usr/include" + CONFIG["docdir"] = "$(datarootdir)/doc/$(PACKAGE)" + CONFIG["infodir"] = "$(datarootdir)/info" + CONFIG["htmldir"] = "$(docdir)" + CONFIG["dvidir"] = "$(docdir)" + CONFIG["pdfdir"] = "$(docdir)" + CONFIG["psdir"] = "$(docdir)" + CONFIG["libdir"] = "$(exec_prefix)/lib" + CONFIG["localedir"] = "$(datarootdir)/locale" + CONFIG["mandir"] = "$(datarootdir)/man" + major, minor, teeny = RUBY_VERSION.split(".") + CONFIG["MAJOR"] = major + CONFIG["MINOR"] = minor + CONFIG["TEENY"] = teeny || 0 + CONFIG["ruby_version"] = "$(MAJOR).$(MINOR)" + CONFIG["RUBY_SO_NAME"] = "rubinius-#{Rubinius::VERSION}" + CONFIG["rubyhdrdir"] = "#{Rubinius::HDR_PATH}" + CONFIG["LIBS"] = "" + + sitedir = Rubinius::SITE_PATH.dup + sitelibdir = sitedir + arch = "#{Rubinius::CPU}-#{Rubinius::OS}" + + CONFIG["sitedir"] = sitedir + CONFIG["sitelibdir"] = sitelibdir + CONFIG["arch"] = arch + CONFIG["sitearch"] = arch + CONFIG["rubylibdir"] = sitelibdir + CONFIG["archdir"] = "#{sitelibdir}/#{arch}" + CONFIG["sitearchdir"] = "#{sitelibdir}/#{arch}" + CONFIG["rubyarchhdrdir"] = Rubinius::HDR_PATH.dup + CONFIG["topdir"] = File.dirname(__FILE__) + # some of these only relevant to cross-compiling + cpu = Rubinius::CPU.dup + vendor = Rubinius::VENDOR.dup + os = Rubinius::OS.dup + CONFIG["build"] = "#{cpu}-#{vendor}-#{os}" + CONFIG["build_cpu"] = "#{cpu}" + CONFIG["build_vendor"] = "#{vendor}" + CONFIG["build_os"] = "#{os}" + CONFIG["host"] = "#{cpu}-#{vendor}-#{os}" + CONFIG["host_cpu"] = "#{cpu}" + CONFIG["host_vendor"] = "#{vendor}" + CONFIG["host_os"] = "#{os}" + CONFIG["target"] = "#{cpu}-#{vendor}-#{os}" + CONFIG["target_cpu"] = "#{cpu}" + CONFIG["target_vendor"] = "#{vendor}" + CONFIG["target_os"] = "#{os}" + CONFIG["build_alias"] = "" + CONFIG["host_alias"] = "" + CONFIG["target_alias"] = "" + CONFIG["warnflags"] = "" + # command line utilities + CONFIG["SHELL"] = "/bin/sh" + CONFIG["ECHO_C"] = "" + CONFIG["ECHO_N"] = "-n" + CONFIG["ECHO_T"] = "" + CONFIG["GREP"] = "/usr/bin/grep" + CONFIG["EGREP"] = "/usr/bin/grep -E" + CONFIG["RM"] = "rm -f" + CONFIG["CP"] = "cp" + CONFIG["NROFF"] = "/usr/bin/nroff" + CONFIG["MAKEDIRS"] = "mkdir -p" + # compile tools + CONFIG["CC"] = (ENV["CC"] || Rubinius::BUILD_CONFIG[:cc].sub(/^((gcc|clang)[-.0-9]*)/, "cc")).dup + CONFIG["CXX"] = (ENV["CXX"] || Rubinius::BUILD_CONFIG[:cxx].sub(/^((g|clang)\+\+[-.0-9]*)/, "c++")).dup + CONFIG["CPP"] = "#{(ENV["CPP"] || Rubinius::BUILD_CONFIG[:cc].sub(/^((gcc|clang)[-.0-9]*)/, "cc"))} -E" + CONFIG["YACC"] = "bison -y" + CONFIG["RANLIB"] = "ranlib" + CONFIG["AR"] = "ar" + CONFIG["AS"] = "as" + CONFIG["WINDRES"] = "" + CONFIG["DLLWRAP"] = "" + CONFIG["OBJDUMP"] = "" + CONFIG["LN_S"] = "ln -s" + CONFIG["NM"] = "" + CONFIG["INSTALL"] = "install -c" + CONFIG["INSTALL_PROGRAM"] = "$(INSTALL)" + CONFIG["INSTALL_SCRIPT"] = "$(INSTALL)" + CONFIG["INSTALL_DATA"] = "$(INSTALL) -m 644" + CONFIG["STRIP"] = "strip -A -n" + CONFIG["MANTYPE"] = "doc" + CONFIG["MAKEFILES"] = "Makefile" + CONFIG["NULLCMD"] = ":" + CONFIG["THREAD_MODEL"] = "pthread" + # compile tools flags + # since we hardcode using gcc, and this flag is only + # used by mkmf to compile extensions, be sure PIC is in + # there + CONFIG["CFLAGS"] = "-g" + CONFIG["CXXFLAGS"] = "-g" + CONFIG["LDFLAGS"] = "" + CONFIG["optflags"] = "" + if Rubinius::BUILD_CONFIG[:debug_build] + CONFIG["CFLAGS"] << " -O0 " + CONFIG["CXXFLAGS"] << " -O0 " + CONFIG["optflags"] << " -O0 " + else + CONFIG["CFLAGS"] << " -O2 " + CONFIG["CXXFLAGS"] << " -O2 " + CONFIG["optflags"] << " -O2 " + end + + if sys = Rubinius::BUILD_CONFIG[:system_cflags] + CONFIG["CFLAGS"] << " #{sys}" unless sys.empty? + end + + if user = Rubinius::BUILD_CONFIG[:user_cflags] + CONFIG["CFLAGS"] << " #{user}" unless user.empty? + end + + if sys = Rubinius::BUILD_CONFIG[:system_cxxflags] + CONFIG["CXXFLAGS"] << " #{sys}" unless sys.empty? + end + + if user = Rubinius::BUILD_CONFIG[:user_cxxflags] + CONFIG["CXXFLAGS"] << " #{user}" unless user.empty? + end + + if sys = Rubinius::BUILD_CONFIG[:system_incflags] + unless sys.empty? + CONFIG["CFLAGS"] << " #{sys}" + CONFIG["CXXFLAGS"] << " #{sys}" + end + end + + if user = Rubinius::BUILD_CONFIG[:user_incflags] + unless user.empty? + CONFIG["CFLAGS"] << " #{user}" + CONFIG["CXXFLAGS"] << " #{user}" + end + end + + if sys = Rubinius::BUILD_CONFIG[:system_ldflags] + CONFIG["LDFLAGS"] << " #{sys}" unless sys.empty? + end + + if user = Rubinius::BUILD_CONFIG[:user_ldflags] + CONFIG["LDFLAGS"] << " #{user}" unless user.empty? + end + + CONFIG["CPPFLAGS"] = "" + + if sys = Rubinius::BUILD_CONFIG[:system_cppflags] + CONFIG["CPPFLAGS"] << " #{sys}" unless sys.empty? + end + + if user = Rubinius::BUILD_CONFIG[:user_cppflags] + CONFIG["CPPFLAGS"] << " #{user}" unless user.empty? + end + + CONFIG["OBJEXT"] = "o" + CONFIG["GNU_LD"] = "" + CONFIG["CPPOUTFILE"] = "-o conftest.i" + CONFIG["OUTFLAG"] = "-o " + CONFIG["COUTFLAG"] = "-o " + CONFIG["OUTFLAG"] = "-o " + CONFIG["YFLAGS"] = "" + CONFIG["ASFLAGS"] = "" + CONFIG["DLDFLAGS"] = "" + CONFIG["ARCH_FLAG"] = "" + CONFIG["STATIC"] = "" + CONFIG["CCDLFLAGS"] = " -fPIC" + CONFIG["XCFLAGS"] = "" + CONFIG["XLDFLAGS"] = "" + CONFIG["LIBRUBY_DLDFLAGS"] = "" + CONFIG["rubyw_install_name"] = "" + CONFIG["RUBYW_INSTALL_NAME"] = "" + CONFIG["SOLIBS"] = "" + CONFIG["DLDLIBS"] = "" + CONFIG["ENABLE_SHARED"] = "" + CONFIG["MAINLIBS"] = "" + CONFIG["COMMON_LIBS"] = "" + CONFIG["COMMON_MACROS"] = "" + CONFIG["COMMON_HEADERS"] = "" + CONFIG["EXPORT_PREFIX"] = "" + CONFIG["EXTOUT"] = ".ext" + CONFIG["ARCHFILE"] = "" + CONFIG["RDOCTARGET"] = "" + CONFIG["LIBRUBY_A"] = "" + CONFIG["LIBRUBY_SO"] = "lib$(RUBY_SO_NAME).$(DLEXT)" + CONFIG["LIBRUBY_ALIASES"] = "lib$(RUBY_SO_NAME).$(DLEXT)" + CONFIG["LIBRUBY"] = "$(LIBRUBY_SO)" + CONFIG["LIBRUBYARG"] = "$(LIBRUBYARG_STATIC)" + CONFIG["LIBRUBYARG_STATIC"] = "" + CONFIG["LIBRUBYARG_SHARED"] = "" + CONFIG["configure_args"] = "" + CONFIG["ALLOCA"] = "" + CONFIG["LIBEXT"] = "a" + CONFIG["LINK_SO"] = "" + # On Linux it needs to pass down the -R flags into the linker so it will + # properly link files not located in paths in LD_LIBRARY_PATH. On other + # platforms this is not necessary and it is linked properly even for + # paths outside LD_LIBRARY_PATH. + CONFIG["LIBPATHFLAG"] = Rubinius::BUILD_CONFIG[:rpath] ? " -L%1$-s" : " -L%s" + CONFIG["RPATHFLAG"] = Rubinius::BUILD_CONFIG[:rpath] ? " -Wl,-R%1$-s" : "" + CONFIG["LIBPATHENV"] = "DYLD_LIBRARY_PATH" + CONFIG["TRY_LINK"] = "" + CONFIG["EXTSTATIC"] = "" + CONFIG["setup"] = "Setup" + CONFIG["PATH_SEPARATOR"] = ":" + CONFIG["PACKAGE_NAME"] = "" + CONFIG["PACKAGE_TARNAME"] = "" + CONFIG["PACKAGE_VERSION"] = "" + CONFIG["PACKAGE_STRING"] = "" + CONFIG["PACKAGE_BUGREPORT"] = "" + + ldshared = (ENV["LDSHARED"] || Rubinius::LDSHARED.sub(/^((gcc|clang)[-.0-9]*)/, "cc")).dup + ldsharedxx = (ENV["LDSHAREDXX"] || Rubinius::LDSHAREDXX.sub(/^((g|clang)\+\+[-.0-9]*)/, "c++")).dup + + CONFIG["LDSHARED"] = ldshared + CONFIG["LIBRUBY_LDSHARED"] = ldshared + CONFIG["LDSHAREDXX"] = ldsharedxx + + # absolute path to ruby executable (pulled directly from MRI's mkconfig.rb) + def RbConfig.ruby + File.join( + RbConfig::CONFIG["bindir"], + RbConfig::CONFIG["ruby_install_name"] + RbConfig::CONFIG["EXEEXT"] + ) + end + + # Adapted from MRI's' rbconfig.rb + MAKEFILE_CONFIG = {} + CONFIG.each { |k,v| MAKEFILE_CONFIG[k] = v.kind_of?(String) ? v.dup : v } + + def RbConfig.expand(val, config = CONFIG) + return val unless val.kind_of? String + + val.gsub!(/\$\$|\$\(([^()]+)\)|\$\{([^{}]+)\}/) do |var| + if !(v = $1 || $2) + '$' + elsif key = config[v = v[/\A[^:]+(?=(?::(.*?)=(.*))?\z)/]] + pat, sub = $1, $2 + config[v] = false + RbConfig.expand(key, config) + config[v] = key + key = key.gsub(/#{Regexp.quote(pat)}(?=\s|\z)/n) {sub} if pat + key + else + var + end + end + val + end + + CONFIG.each_value do |val| + RbConfig.expand(val) + end +end + +CROSS_COMPILING = nil unless defined? CROSS_COMPILING +Config = RbConfig diff --git a/gxg/core/regexp.rb b/gxg/core/regexp.rb new file mode 100644 index 0000000..336eb26 --- /dev/null +++ b/gxg/core/regexp.rb @@ -0,0 +1,594 @@ +class Regexp + def self.allocate + Rubinius.primitive :regexp_allocate + raise PrimitiveFailure, "Regexp.allocate primitive failed" + end + + class << self + alias_method :compile, :new + end + + def self.try_convert(obj) + Rubinius::Type.try_convert obj, Regexp, :to_regexp + end + + def self.convert(pattern) + return pattern if pattern.kind_of? Regexp + if pattern.kind_of? Array + return union(*pattern) + else + return Regexp.quote(pattern.to_s) + end + end + + def self.compatible?(*patterns) + encodings = patterns.map{ |r| convert(r).encoding } + last_enc = encodings.pop + encodings.each do |encoding| + raise ArgumentError, "incompatible encodings: #{encoding} and #{last_enc}" unless Encoding.compatible?(last_enc, encoding) + last_enc = encoding + end + end + + def self.union(*patterns) + case patterns.size + when 0 + return %r/(?!)/ + when 1 + pat = patterns.first + case pat + when Array + return union(*pat) + when Regexp + return pat + else + return Regexp.new(Regexp.quote(StringValue(pat))) + end + else + compatible?(*patterns) + enc = convert(patterns.first).encoding + end + + str = "".encode(enc) + sep = "|".encode(enc) + patterns.each_with_index do |pat, idx| + str << sep if idx != 0 + if pat.kind_of? Regexp + str << pat.to_s + else + str << Regexp.quote(StringValue(pat)) + end + end + + Regexp.new(str) + end + + def self.escape(str) + str = str.to_s if str.is_a?(Symbol) + escaped = StringValue(str).transform(ESCAPE_TABLE) + if escaped.ascii_only? + escaped.force_encoding Encoding::US_ASCII + elsif str.valid_encoding? + escaped.force_encoding str.encoding + else + escaped.force_encoding Encoding::ASCII_8BIT + end + end + + class << self + alias_method :quote, :escape + end + + ## + # See Regexp.new. This may be overridden by subclasses. + + def compile(pattern, opts) + Rubinius.primitive :regexp_initialize + raise PrimitiveFailure, "Regexp.compile(#{pattern.inspect}, #{opts}) primitive failed" + end + + private :compile + + def search_region(str, start, finish, forward) # equiv to MRI's re_search + Rubinius.primitive :regexp_search_region + raise PrimitiveFailure, "Regexp#search_region primitive failed" + end + + def match_start(str, offset) # equiv to MRI's re_match + Rubinius.primitive :regexp_match_start + raise PrimitiveFailure, "Regexp#match_start primitive failed" + end + + def search_from(str, offset) # equiv to MRI's rb_reg_search + Rubinius.primitive :regexp_search_from + raise PrimitiveFailure, "Regexp#search_from primitive failed" + end + + def options + Rubinius.primitive :regexp_options + raise PrimitiveFailure, "Regexp#options primitive failed" + end + + def self.last_match(field=nil) + Rubinius.primitive :regexp_last_match + + return last_match(Integer(field)) if field + raise PrimitiveFailure, "Regexp#last_match primitive failed" + end + + def self.last_match=(match) + Rubinius.primitive :regexp_set_last_match + + unless match.kind_of? MatchData + raise TypeError, "Expected MatchData, got #{match.inspect}" + end + + raise PrimitiveFailure, "Regexp#set_last_match primitive failed" + end + + def self.propagate_last_match + Rubinius.primitive :regexp_propagate_last_match + raise PrimitiveFailure, "Regexp#propagate_last_match primitive failed" + end + + def self.set_block_last_match + Rubinius.primitive :regexp_set_block_last_match + raise PrimitiveFailure, "Regexp#set_block_last_match primitive failed" + end + + def fixed_encoding? + Rubinius.primitive :regexp_fixed_encoding_p + raise PrimitiveFailure, "Regexp.fixed_encoding? primitive failed" + end + + def initialize(pattern, opts=nil, lang=nil) + if pattern.kind_of?(Regexp) + opts = pattern.options + pattern = pattern.source + elsif pattern.kind_of?(Fixnum) or pattern.kind_of?(Float) + raise TypeError, "can't convert Fixnum into String" + elsif opts.kind_of?(Fixnum) + opts = opts & (OPTION_MASK | KCODE_MASK) if opts > 0 + elsif opts + opts = IGNORECASE + else + opts = 0 + end + + code = lang[0] if lang + opts |= NOENCODING if code == ?n or code == ?N + + compile pattern, opts + end + + private :initialize + + def initialize_copy(other) + initialize other.source, other.options + end + + private :initialize_copy + + def match(str, pos=0) + unless str + Regexp.last_match = nil + return nil + end + + str = str.to_s if str.is_a?(Symbol) + str = StringValue(str) + + m = Rubinius::Mirror.reflect str + pos = pos < 0 ? pos + str.size : pos + pos = m.character_to_byte_index pos + result = search_region(str, pos, str.bytesize, true) + Regexp.last_match = result + + if result && block_given? + yield result + else + result + end + end + + def match?(str, pos=0) + return false unless str + + str = str.to_s if str.is_a?(Symbol) + str = StringValue(str) + + m = Rubinius::Mirror.reflect str + pos = pos < 0 ? pos + str.size : pos + pos = m.character_to_byte_index pos + + search_region(str, pos, str.bytesize, true) ? true : false + end + + # Returns the index of the first character in the region that + # matched or nil if there was no match. See #match for returning + # the MatchData instead. + def =~(str) + str = str.to_s if str.is_a?(Symbol) + # unless str.nil? because it's nil and only nil, not false. + str = StringValue(str) unless str.nil? + + match = match_from(str, 0) + if match + Regexp.last_match = match + return match.begin(0) + else + Regexp.last_match = nil + return nil + end + end + + def ===(other) + if other.kind_of? Symbol + other = other.to_s + elsif !other.kind_of? String + other = Rubinius::Type.check_convert_type other, String, :to_str + unless other + Regexp.last_match = nil + return false + end + end + + if match = match_from(other, 0) + Regexp.last_match = match + true + else + Regexp.last_match = nil + false + end + end + + def eql?(other) + return false unless other.kind_of?(Regexp) + return false unless source == other.source + (options & ~NOENCODING) == (other.options & ~NOENCODING) + end + + alias_method :==, :eql? + + def hash + str = '/' << source << '/' << option_to_string(options) + str.hash + end + + def inspect + # the regexp matches any / that is after anything except for a \ + escape = source.gsub(%r!(\\.)|/!) { $1 || '\/' } + str = "/#{escape}/#{option_to_string(options)}" + str << 'n' if (options & NOENCODING) > 0 + str + end + + def encoding + source.encoding + end + + + def source + @source.dup + end + + def ~ + line = $_ + + unless line.kind_of?(String) + Regexp.last_match = nil + return nil + end + + res = match(line) + return res ? res.begin(0) : nil + end + + def match_all(str) + start = 0 + arr = [] + while match = self.match_from(str, start) + arr << match + if match.collapsing? + start += 1 + else + start = match.full.at(1) + end + end + arr + end + + def casefold? + (options & IGNORECASE) > 0 ? true : false + end + + def match_from(str, count) + return nil unless str + search_region(str, count, str.bytesize, true) + end + + class SourceParser + class Part + attr_accessor :options + attr_accessor :source + + def initialize(source="") + @source = source + @options = [] + @negated_options = [] + end + + private :initialize + + def <<(str) + @source << str + end + + def empty? + @source.empty? + end + + def flatten + end + + def to_s + # Put in the proper \'s to escape /'s + # This is the same regexp used by #inspect + source.gsub(%r!(\\.)|/!) { $1 || '\/' } + end + + def has_options! + @has_options = true + end + + def has_options? + @has_options + end + end + + class OptionsGroupPart < Part + def to_s + @flatten ? "#{source}" : "(#{options_string}#{source})" + end + + def push_option!(identifier) + @options << identifier + end + + def push_negated_option!(identifier) + @negated_options << identifier + end + + def flatten + @flatten = true + end + + def options_string + string = @options.join + (@negated_options.empty? ? "" : @negated_options.join) + "?#{string}:" + end + private :options_string + end + + class LookAheadGroupPart < Part + def to_s + "(#{source})" + end + end + + def initialize(source, options = 0) + @source = source + @options = options + @parts = [Part.new] + end + + private :initialize + + def string + "(?#{options_string}:#{parts_string})" + end + + def parts_string + if parts.size == 1 && parts.first.has_options? + parts.first.flatten + end + parts.map { |part| part.to_s }.join + end + + def parts + return @parts if @already_parsed + @index = 0 + create_parts + @parts.reject! { |part| part.empty? } + @already_parsed = true + @parts + end + + # TODO: audit specs for this method when specs are running + def create_parts + while @index < @source.size + if @source[@index].chr == '(' + escaped = @index > 0 && @source[@index - 1].chr == '\\' + + idx = @index + 1 + if idx < @source.size and @source[idx].chr == '?' and !escaped + process_group + else + push_current_character! + end + else + push_current_character! + end + end + end + + def process_group + @index += 1 + @parts << group_part_class.new + + if in_group_with_options? + @index += 1 + process_group_options + end + + process_look_ahead if in_lookahead_group? + process_until_group_finished + add_part! + end + + def group_part_class + if in_group_with_options? + OptionsGroupPart + else + LookAheadGroupPart + end + end + + def in_lookahead_group? + @source[@index, 2] == "?=" || @source[@index, 2] == "?!" + end + + def process_look_ahead + push_current_character! + push_current_character! + end + + def in_group_with_options? + return false if @source[@index, 1] != '?' + + @source[@index + 1..-1].each_byte do |b| + c = b.chr + return true if ':' == c + return false unless %w[m i x -].include? c + end + end + + def process_group_options + @parts.last.has_options! + case @source[@index].chr + when ')' + return + when ':' + @index += 1 + return + else + push_option! + process_group_options + end + end + + def process_until_group_finished + push_current_character! until @source[@index].chr == ")" + @index += 1 + end + + def push_current_character! + @parts.last << @source[@index].chr + @index += 1 + end + + def push_option! + @parts.last.push_option!(@source[@index].chr) + @index += 1 + end + + def add_part! + @parts << Part.new + end + + def options_string + chosen_options = [] + possible_options = PossibleOptions + possible_options.each do |flag, identifier| + chosen_options << identifier if @options & flag > 0 + end + + if parts.size == 1 + chosen_options.concat parts.first.options + end + + excluded_options = possible_options.map { |e| e.last }.select do |identifier| + !chosen_options.include?(identifier) + end + + options_to_return = chosen_options + if !excluded_options.empty? + options_to_return << "-" << excluded_options + end + options_to_return.join + end + + end + + def to_s + SourceParser.new(source, options).string + end + + def option_to_string(option) + string = "" + string << 'm' if (option & MULTILINE) > 0 + string << 'i' if (option & IGNORECASE) > 0 + string << 'x' if (option & EXTENDED) > 0 + string + end + + def name_table + @names + end + + # + # call-seq: + # rxp.named_captures => hash + # + # Returns a hash representing information about named captures of rxp. + # + # A key of the hash is a name of the named captures. + # A value of the hash is an array which is list of indexes of corresponding + # named captures. + # + # /(?.)(?.)/.named_captures + # #=> {"foo"=>[1], "bar"=>[2]} + # + # /(?.)(?.)/.named_captures + # #=> {"foo"=>[1, 2]} + # + # If there are no named captures, an empty hash is returned. + # + # /(.)(.)/.named_captures + # #=> {} + # + def named_captures + hash = {} + + if @names + @names.sort_by { |a,b| b.first }.each do |k, v| # LookupTable is unordered + hash[k.to_s] = v + end + end + + return hash + end + + # + # call-seq: + # rxp.names => [name1, name2, ...] + # + # Returns a list of names of captures as an array of strings. + # + # /(?.)(?.)(?.)/.names + # #=> ["foo", "bar", "baz"] + # + # /(?.)(?.)/.names + # #=> ["foo"] + # + # /(.)(.)/.names + # #=> [] + # + def names + if @names + @names.sort_by { |a,b| b.first }.map { |x| x.first.to_s } # LookupTable is unordered + else + [] + end + end +end diff --git a/gxg/core/respond_to_cache.rb b/gxg/core/respond_to_cache.rb new file mode 100644 index 0000000..b4a028f --- /dev/null +++ b/gxg/core/respond_to_cache.rb @@ -0,0 +1,22 @@ +module Rubinius + class RespondToCache < CallSite + + attr_reader :name + attr_reader :executable + attr_reader :receiver_class + attr_reader :message + attr_reader :visibility + attr_reader :responds + attr_reader :fallback_call_site + + def hits + Rubinius.primitive :respond_to_cache_hits + raise PrimitiveFailure, "RespondToCache#hits primitive failed" + end + + def inspect + "#<#{self.class.name}:0x#{self.object_id.to_s(16)} #{location}##{@name}(#{hits}) receiver_class=#{@receiver_class} message=#{@message} visibility=#{@visibility} responds=#{@responds}>" + end + + end +end diff --git a/gxg/core/rubinius.rb b/gxg/core/rubinius.rb new file mode 100644 index 0000000..445fb53 --- /dev/null +++ b/gxg/core/rubinius.rb @@ -0,0 +1,349 @@ +module Rubinius + def self.watch_signal(sig, ignored) + Rubinius.primitive :vm_watch_signal + watch_signal(sig.to_signal, ignored) + end + + def self.signal_thread + Rubinius.primitive :vm_signal_thread + raise PrimitiveFailure, "Rubinius.signal_thread primitive failed" + end + + def self.find_method(obj, name) + Rubinius.primitive :vm_find_method + raise PrimitiveFailure, "Rubinius.find_method primitive failed" + end + + def self.find_public_method(obj, name) + Rubinius.primitive :vm_find_public_method + raise PrimitiveFailure, "Rubinius.find_public_method primitive failed" + end + + def self.extended_modules(obj) + Rubinius.primitive :vm_extended_modules + raise PrimitiveFailure, "Rubinius.extended_modules primitive failed" + end + + def self.global_serial + Rubinius.primitive :vm_global_serial + raise PrimitiveFailure, "Rubinius.vm_global_serial primitive failed" + end + + def self.inc_global_serial + Rubinius.primitive :vm_inc_global_serial + raise PrimitiveFailure, "Rubinius.vm_inc_global_serial primitive failed" + end + + # Deoptimize all methods in the system. + def self.deoptimize_all(disable) + Rubinius.primitive :vm_deoptimize_all + raise PrimitiveFailure, "Rubinius.vm_deoptimize_all primitive failed" + end + + def self.raise_exception(exc) + Rubinius.primitive :vm_raise_exception + raise PrimitiveFailure, "Rubinius.vm_raise_exception primitive failed" + end + + def self.memory_size(obj) + Rubinius.primitive :vm_memory_size + raise PrimitiveFailure, "Rubinius.vm_memory_size primitive failed" + end + + def self.throw(dest, obj) + Rubinius.primitive :vm_throw + raise PrimitiveFailure, "Rubinius.throw primitive failed" + end + + def self.catch(dest, obj) + Rubinius.primitive :vm_catch + raise PrimitiveFailure, "Rubinius.catch primitive failed" + end + + def self.method_missing_reason + Rubinius.primitive :vm_method_missing_reason + :unknown + end + + def self.constant_missing_reason + Rubinius.primitive :vm_constant_missing_reason + :unknown + end + + def self.mri_backtrace(skip) + Rubinius.primitive :vm_mri_backtrace + raise PrimitiveFailure, "Rubinius.mri_backtrace primitive failed" + end + + def self.add_method(name, executable, mod, scope, serial, vis) + vis ||= :public + + # Don't change the visibility for methods added to singleton + # classes + if Type.singleton_class_object(mod) + visibility = vis + elsif vis == :module or privatized_method?(name) + visibility = :private + else + visibility = vis + end + + Rubinius::VM.reset_method_cache mod, name + + if Type.object_kind_of? executable, String + mod.method_table.store name, executable, nil, scope, serial, visibility + else + mod.method_table.store name, nil, executable, scope, serial, visibility + end + + Rubinius.privately do + mod.module_function name if vis == :module + end + + # Have to use Rubinius::Type.object_respond_to? rather than #respond_to? + # because code will redefine #respond_to? itself, which is added + # via #add_method, and then we'll call this new #respond_to?, which + # commonly can't run yet because it requires methods that haven't been + # added yet. (ActionMailer does this) + + if obj = Type.singleton_class_object(mod) + if Type.object_kind_of? obj, Numeric + + # Such a weird protocol. If :singleton_method_added exists, allow this. + # le sigh. + unless obj.respond_to? :singleton_method_added + raise TypeError, "Unable to define singleton methods on Numerics" + end + end + + Rubinius.privately do + obj.singleton_method_added(name) + end + else + case executable + when CompiledCode, AccessVariable + mod.add_ivars(executable) + end + + Rubinius.privately do + mod.method_added(name) + end + end + + @add_method_hook.trigger! mod, name, executable + + return executable + end + + def self.mathn_loaded? + false + end + + module Unsafe + def self.set_class(obj, cls) + Rubinius.primitive :vm_set_class + + if obj.kind_of? ImmediateValue + raise TypeError, "Can not change the class of an immediate" + end + + raise ArgumentError, "Class #{cls} is not compatible with #{obj.inspect}" + end + end + + def self.get_user_home(name) + Rubinius.primitive :vm_get_user_home + raise PrimitiveFailure, "Rubinius.get_user_home primitive failed" + end + + def self.lock(obj) + Rubinius.primitive :vm_object_lock + raise PrimitiveFailure, "Rubinius.lock primitive failed" + end + + def self.try_lock(obj) + Rubinius.primitive :vm_object_trylock + raise PrimitiveFailure, "Rubinius.try_lock primitive failed" + end + + def self.locked?(obj) + Rubinius.primitive :vm_object_locked_p + raise PrimitiveFailure, "Rubinius.locked? primitive failed" + end + + def self.lock_owned?(obj) + Rubinius.primitive :vm_object_lock_owned_p + raise PrimitiveFailure, "Rubinius.lock_owned? primitive failed" + end + + def self.unlock(obj) + Rubinius.primitive :vm_object_unlock + raise PrimitiveFailure, "Rubinius.unlock primitive failed" + end + + def self.synchronize(obj) + lock obj + begin + yield + ensure + unlock obj + end + end + + def self.memory_barrier + Rubinius.primitive :vm_memory_barrier + raise PrimitiveFailure, "Rubinius.memory_barrier primitive failed" + end + + def self.windows? + Rubinius.primitive :vm_windows_p + raise PrimitiveFailure, "Rubinius.windows? primitive failed" + end + + def self.darwin? + Rubinius.primitive :vm_darwin_p + raise PrimitiveFailure, "Rubinius.darwin? primitive failed" + end + + def self.bsd? + Rubinius.primitive :vm_bsd_p + raise PrimitiveFailure, "Rubinius.bsd? primitive failed" + end + + def self.linux? + Rubinius.primitive :vm_linux_p + raise PrimitiveFailure, "Rubinius.linux? primitive failed" + end + + def self.thread_state + Rubinius.primitive :vm_thread_state + raise PrimitiveFailure, "Rubinius.thread_state primitive failed" + end + + # Used to invoke a CompiledCode instance as a script body. Sets up the MAIN + # object as self and bypasses JIT'ing (because why JIT a script you only run + # once). + def self.run_script(compiled_code) + Rubinius.primitive :vm_run_script + raise PrimitiveFailure, "Rubinius.run_script primitive failed" + end + + def self.dtrace_fire(payload) + Rubinius.primitive :vm_dtrace_fire + raise PrimitiveFailure, "Rubinius.dtrace_fire primitive failed" + end + + def self.received_signal(sig) + Signal.run_handler(sig) + end + + def self.version + extra = "" + + if Rubinius::JIT.enabled? # and jit = Rubinius::JIT.properties + extra << "J" + + # if jit.include? :inline_generic + # extra << "I" + # end + end + + extra << "P" if Rubinius::PROFILER + extra << "D" if Rubinius::DEBUG_BUILD + + rev = BUILD_REV[0..7] + llvm = " #{Rubinius::LLVM_VERSION}" if defined?(Rubinius::LLVM_VERSION) + + str = "rubinius #{VERSION} (#{RUBY_VERSION} #{rev} #{RUBY_RELEASE_DATE}#{llvm}" + + unless extra.empty? + str << " #{extra}" + end + + str << ") [#{RUBY_PLATFORM}]" + + return str + end + + def self.unrecognized_defined(file, line) + message = "#{file}:#{line}: defined? passed an unrecognized expression" + raise Exception, message + end + + def self.jump_error(name) + raise LocalJumpError, "invalid context for '#{name}'" + end + + # Support for __END__ and DATA + def self.set_data(name, offset) + # If the name is nil, then it's not for the main script, so we should + # ignore it. + return unless name + + if File.exist? name + file = File.open name, "r" + file.seek Integer(offset), IO::SEEK_SET + Object.const_set :DATA, file + else + # Instead of letting the Errno::ENOENT bubble, give a bit more info + raise "unable to open #{name} to set DATA, source .rb file does not exist." + end + end + + def self.compile_file(name) + ToolSet::Runtime::Compiler.compile name + end + + def self.allocation_site(obj) + obj.instance_variable_get("@__allocation_site__") + end + + ## + # API Status: official + # + # Return the absolute path to the file that contains + # the current method. This works like __FILE__, but returns + # the file that require/load resolved directly to, providing + # an absolute path to the file. + # + # Returns nil if there is no file, such as inside eval. + # + def self.current_file + cs = Rubinius::LexicalScope.of_sender + return cs.absolute_active_path + end + + def self.privatized_method?(name) + [:initialize, + :initialize_copy, :initialize_dup, :initialize_clone, + :respond_to_missing?].include? name + end + + class << self + private :privatized_method? + end + + # TODO: This is presently duplicated from Kernel::lambda because -> () { } + # in MRI dispatches to a hidden function that isn't modifiable by overriding + # Kernel::lambda. + def self.lambda + env = nil + + Rubinius.asm do + push_block + # assign a pushed block to the above local variable "env" + # Note that "env" is indexed at 0. + set_local 0 + end + + raise ArgumentError, "block required" unless env + + prc = Rubinius::Mirror::Proc.from_block ::Proc, env + + # Make a proc lambda only when passed an actual block (ie, not using the + # "&block" notation), otherwise don't modify it at all. + prc.lambda_style! if env.is_a?(Rubinius::BlockEnvironment) + + return prc + end +end diff --git a/gxg/core/ruby_constants.rb b/gxg/core/ruby_constants.rb new file mode 100644 index 0000000..a0ef9d7 --- /dev/null +++ b/gxg/core/ruby_constants.rb @@ -0,0 +1,14 @@ +# Ruby constants +# + +RUBY_ENGINE = "rbx" +RUBY_ENGINE_VERSION = Rubinius::ENGINE_VERSION +RUBY_VERSION = Rubinius::RUBY_VERSION +RUBY_PATCHLEVEL = 0 +RUBY_PLATFORM = "#{Rubinius::CPU}-#{Rubinius::OS}" +RUBY_RELEASE_DATE = Rubinius::RELEASE_DATE.dup +RUBY_COPYRIGHT = "rubinius - Copyright (C) 2006-2011 Evan Phoenix" +RUBY_REVISION = 0 + +# Must be last, it reads the above constants +RUBY_DESCRIPTION = Rubinius.version diff --git a/gxg/core/runtime.rb b/gxg/core/runtime.rb new file mode 100644 index 0000000..d21d54e --- /dev/null +++ b/gxg/core/runtime.rb @@ -0,0 +1,175 @@ +# -*- encoding: us-ascii -*- + +module Rubinius + module Runtime + def self.matches_when(array, receiver) + array.each { |x| return true if x === receiver } + false + end + + def self.unwrap_block_arg(arg) + if arg.size == 1 + elem = arg.at(0) + return elem if elem.kind_of?(Array) + end + + arg + end + + def self.find_constant_for_op_asign_or(name, scope) + name = Rubinius::Type.coerce_to_constant_name name + + current, constant = scope.module, undefined + + while current + if entry = current.constant_table.lookup(name) + constant = entry.constant + constant = constant.call if constant.kind_of?(Autoload) + return constant + end + + current = current.direct_superclass + end + + if instance_of?(Module) + if entry = Object.constant_table.lookup(name) + constant = entry.constant + constant = constant.call if constant.kind_of?(Autoload) + return constant + end + end + + nil + end + + def self.get_flip_flop(scope, index) + scope.flip_flops ||= {} + scope.flip_flops[index] + end + + def self.set_flip_flop(scope, index, value) + scope.flip_flops ||= {} + scope.flip_flops[index] = value + end + + def self.rbx_marshal_constant + name + end + + def self.rbx_marshal_rational(obj) + Rubinius::Tuple[obj.numerator, obj.denominator] + end + + def self.rbx_marshal_complex(obj) + Rubinius::Tuple[obj.real, obj.imaginary] + end + + def self.get_encoding(name) + if defined?(Encoding) + Encoding.find name + else + name + end + end + + def self.pre_exe + yield + end + + def self.splat_hash_value(hash, value) + value = Rubinius::Type.coerce_to value, Hash, :to_hash + value.merge hash + end + + def self.splat_hash_entry(hash, key, value) + hash[key] = value unless hash.key? key + hash + end + + def self.keyword_object?(obj, optional) + hash = Rubinius::Type.try_convert obj, Hash, :to_hash + return unless hash + + hs = hash.size + return [nil, hash] if hs == 0 + + kw = {} + t = Tuple.new hs + i = 0 + + hash.each do |key, value| + if key.instance_of? Symbol + kw[key] = value + else + return unless optional + + t[i] = key + i += 1 + end + end + + if i == 0 + [nil, kw] + elsif i == hs + [obj, kw] + else + nk = {} + j = 0 + + while j < i + k = t[j] + nk[k] = hash[k] + j += 1 + end + + [nk, kw] + end + end + + def self.keywords_missing(hash) + keywords = CompiledCode.of_sender.keywords + + missing = [] + total = keywords.size + i = 0 + + while i < total + break unless keywords[i+1] + + key = keywords[i] + missing << key unless hash.key? key + + i += 2 + end + + msg = "missing keyword#{"s"if missing.size > 1}: #{missing.join(", ")}" + raise ArgumentError, msg + end + + def self.keywords_extra(hash, kwrest) + keywords = CompiledCode.of_sender.keywords + + total = keywords.size + i = 0 + + while i < total + hash.delete keywords[i] + + i += 2 + end + + return hash if kwrest or hash.size == 0 + + msg = "unknown keyword#{"s" if hash.size > 1}: #{hash.keys.join(", ")}" + raise ArgumentError, msg + end + + def self.unmarshal_rational(a, b) + Rational(a, b) + end + + def self.unmarshal_complex(r, i) + Complex(r, i) + end + end +end diff --git a/gxg/core/signal.rb b/gxg/core/signal.rb new file mode 100644 index 0000000..9e53f66 --- /dev/null +++ b/gxg/core/signal.rb @@ -0,0 +1,71 @@ +module Signal + def self.trap(sig, prc=nil, &block) + sig = sig.to_s if sig.kind_of?(Symbol) + + if sig.kind_of?(String) + osig = sig + + if sig.prefix? "SIG" + sig = sig[3..-1] + end + + unless number = Names[sig] + raise ArgumentError, "Unknown signal '#{osig}'" + end + else + number = sig.to_i + end + + # If no command, use the block. + prc ||= block + prc = prc.to_s if prc.kind_of?(Symbol) + + case prc + when "DEFAULT", "SIG_DFL" + had_old = @handlers.key?(number) + old = @handlers.delete(number) + + if number != Names["EXIT"] + Rubinius.watch_signal(-number, prc.nil? || prc == 'IGNORE') + end + + return "DEFAULT" unless had_old + return old ? old : nil + when "IGNORE", "SIG_IGN" + prc = "IGNORE" + when nil + prc = nil + when "EXIT" + prc = proc { exit } + when String + raise ArgumentError, "Unsupported command '#{prc}'" + else + unless prc.respond_to? :call + raise ArgumentError, "Handler must respond to #call (was #{prc.class})" + end + end + + had_old = @handlers.key?(number) + + old = @handlers[number] + @handlers[number] = prc + + if number != Names["EXIT"] + Rubinius.watch_signal(number, prc.nil? || prc == 'IGNORE') + end + + return "DEFAULT" unless had_old + return old ? old : nil + end + + def self.run_handler(sig) + # Ignore nil handlers + if handler = @handlers[sig] + handler.call(sig) + end + end + + def self.list + Names.dup + end +end diff --git a/gxg/core/splitter.rb b/gxg/core/splitter.rb new file mode 100644 index 0000000..3d08c0a --- /dev/null +++ b/gxg/core/splitter.rb @@ -0,0 +1,157 @@ +module Rubinius + class Splitter + def self.split_characters(string, pattern, limit, tail_empty) + if limit + string.chars.take(limit - 1) << string[(limit - 1)..-1] + else + ret = string.chars.to_a + # Use #byteslice because it returns the right class and taints + # automatically. This is just appending a "", which is this + # strange protocol if a negative limit is passed in + ret << string.byteslice(0,0) if tail_empty + ret + end + end + + def self.valid_encoding?(string) + raise ArgumentError, "invalid byte sequence in #{string.encoding.name}" unless string.valid_encoding? + end + + def self.split(string, pattern, limit) + # Odd edge case + return [] if string.empty? + + tail_empty = false + + if undefined.equal?(limit) + limited = false + else + limit = Rubinius::Type.coerce_to limit, Fixnum, :to_int + + if limit > 0 + return [string.dup] if limit == 1 + limited = true + else + if limit < 0 + tail_empty = true + end + limited = false + end + end + + pattern ||= ($; || " ") + + if pattern == ' ' + if limited + lim = limit + elsif tail_empty + lim = -1 + else + lim = 0 + end + + return Rubinius.invoke_primitive :string_awk_split, string, lim + elsif pattern.kind_of?(Regexp) + else + pattern = StringValue(pattern) unless pattern.kind_of?(String) + + valid_encoding?(string) + valid_encoding?(pattern) + + trim_end = !tail_empty || limit == 0 + + unless limited + if pattern.empty? + if trim_end + return string.chars.to_a + end + else + return split_on_string(string, pattern, trim_end) + end + end + + pattern = Regexp.new(Regexp.quote(pattern)) + end + + # Handle // as a special case. + if pattern.source.empty? + return split_characters(string, pattern, limited && limit, tail_empty) + end + + start = 0 + ret = [] + + last_match = nil + last_match_end = 0 + + while match = pattern.match_from(string, start) + break if limited && limit - ret.size <= 1 + + collapsed = match.collapsing? + + unless collapsed && (match.full.at(0) == last_match_end) + ret << match.pre_match_from(last_match_end) + + # length > 1 means there are captures + if match.length > 1 + ret.concat(match.captures.compact) + end + end + + start = match.full.at(1) + if collapsed + start += 1 + end + + last_match = match + last_match_end = last_match.full.at(1) + end + + if last_match + ret << last_match.post_match + elsif ret.empty? + ret << string.dup + end + + # Trim from end + if undefined.equal?(limit) || limit == 0 + while s = ret.at(-1) and s.empty? + ret.pop + end + end + + ret + end + + def self.split_on_string(string, pattern, trim_end) + pos = 0 + + ret = [] + + pat_size = pattern.bytesize + str_size = string.bytesize + m = Rubinius::Mirror.reflect string + + while pos < str_size + nxt = m.find_string(pattern, pos) + break unless nxt + + match_size = nxt - pos + ret << string.byteslice(pos, match_size) + + pos = nxt + pat_size + end + + # No more separators, but we need to grab the last part still. + ret << string.byteslice(pos, str_size - pos) + + if trim_end + while s = ret.at(-1) and s.empty? + ret.pop + end + end + + ret + end + end +end diff --git a/gxg/core/sprinter.rb b/gxg/core/sprinter.rb new file mode 100644 index 0000000..cb5e2bc --- /dev/null +++ b/gxg/core/sprinter.rb @@ -0,0 +1,1134 @@ +module Rubinius + class Sprinter + def self.cache + @cache + end + + def self.get(format) + if ec = @cache + if sprinter = ec.retrieve(format) + return sprinter + end + end + + sprinter = new(format) + + ec.set(format, sprinter) if ec + + sprinter + end + + def initialize(format) + begin + Rubinius::Type.object_singleton_class(self).dynamic_method :call do |g| + Builder.new(self, format, g).build + end + rescue Rubinius::ToolSets::Runtime::CompileError => e + raise RuntimeError, "failed to compile printf format: #{format}", e + end + end + + private :initialize + + def debug_print + printer = Rubinius::ToolSets::Runtime::Compiler::MethodPrinter.new + printer.input(method(:call).executable) + printer.bytecode = true + printer.run + end + + def zero_two_expand_integer(int) + int = Integer(int) unless int.kind_of? Fixnum + + return int.to_s if int < 0 or int >= 10 + + "0#{int}" + end + + def zero_expand_integer(int, width) + int = Integer(int) unless int.kind_of? Fixnum + + str = int.to_s + return str if width <= str.size + diff = width - str.size + + if int < 0 + case diff + when 1 + "-0#{-int}" + when 2 + "-00#{-int}" + when 3 + "-000#{-int}" + else + "-#{'0' * diff}#{-int}" + end + else + case diff + when 1 + "0#{str}" + when 2 + "00#{str}" + when 3 + "000#{str}" + when 4 + "0000#{str}" + else + ("0" * diff) << str + end + end + end + + def zero_expand_leader(int, width) + if int < 0 + zero_expand_integer(int, width) + else + zero_expand_integer(int, width-1) + end + end + + def space_expand_integer(int, width) + int = Integer(int) unless int.kind_of? Fixnum + + str = int.to_s + + width = -width if width < 0 + return str if width <= str.size + diff = width - str.size + + case diff + when 1 + " #{str}" + when 2 + " #{str}" + when 3 + " #{str}" + when 4 + " #{str}" + else + (" " * diff) << str + end + end + + def space_expand_leader(int, width) + if int < 0 + space_expand_integer(int, width) + else + space_expand_integer(int, width-1) + end + end + + def space_expand_integer_left(int, width) + int = Integer(int) unless int.kind_of? Fixnum + + str = int.to_s + return str if width <= str.size + diff = width - str.size + + case diff + when 1 + "#{str} " + when 2 + "#{str} " + when 3 + "#{str} " + when 4 + "#{str} " + else + str + (" " * diff) + end + end + + def space_expand_leader_left(int, width) + if int < 0 + space_expand_integer_left(int, width) + else + space_expand_integer_left(int, width-1) + end + end + + def as_int(int) + if int.kind_of? Integer + return int + else + Integer(int) + end + end + + def compute_space(int) + if int >= 0 + " " + else + "" + end + end + + def compute_plus(int) + if int >= 0 + "+" + else + "" + end + end + + def digit_expand_precision(int, precision) + str = int.to_s + + diff = Integer(precision) - str.size + + if int < 0 + # account for the - sign infront + diff += 1 + + return str if diff <= 0 + + case diff + when 1 + "-0#{-int}" + when 2 + "-00#{-int}" + when 3 + "-000#{-int}" + else + "-#{'0' * diff}#{-int}" + end + else + return str if diff <= 0 + case diff + when 1 + "0#{str}" + when 2 + "00#{str}" + when 3 + "000#{str}" + when 4 + "0000#{str}" + else + ("0" * diff) << str + end + end + end + + def space_expand_left(str, width) + width = Integer(width) + + sz = str.size + + width = -width if width < 0 + return str if sz >= width + + diff = width - sz + + case diff + when 1 + "#{str} " + when 2 + "#{str} " + when 3 + "#{str} " + when 4 + "#{str} " + else + str + (" " * diff) + end + end + + def space_expand(str, width) + width = Integer(width) + return space_expand_left(str, -width) if width < 0 + + sz = str.size + + return str if sz >= width + + diff = width - sz + + case diff + when 1 + " #{str}" + when 2 + " #{str}" + when 3 + " #{str}" + when 4 + " #{str}" + else + (" " * diff) << str + end + end + + class Builder + def initialize(code, format, g) + @code, @format = code, format + + @append_parts = 0 + + @g = g + end + + private :initialize + + def build + self.parse + + @g.required_args = @arg_count + + @g.total_args = @arg_count + 1 + # We won't use it, but we accept a splat; our semantics require + # that we ignore any excess arguments provided. + @g.splat_index = @arg_count + + @g.local_count = @arg_count + 1 + if @index_mode == :absolute + @g.local_names = (0...@arg_count).map { |i| :"#{i + 1}$" } + [:splat] + else + @g.local_names = (0...@arg_count).map { |i| :"arg#{i}" } + [:splat] + end + + @g.string_build @append_parts + @g.ret + end + + def invert + @g.push_int 0 + @g.swap + @g.send :-, 1 + end + + def is_negative + @g.push_int 0 + @g.send :<, 1 + end + + def justify(direction, may_be_negative=true) + if may_be_negative && direction != :ljust + width_done = @g.new_label + + @g.dup + is_negative + if_false do + @g.send direction, 1 + @g.goto width_done + end + + invert + @g.send :ljust, 1 + + width_done.set! + else + @g.send direction, 1 + end + end + + def next_index(specified=nil) + if specified + specified = specified.to_i + raise ArgumentError, "invalid positional index" if specified == 0 + + if @index_mode == :relative + raise ArgumentError, "unnumbered mixed with numbered" + end + + @index_mode = :absolute + @arg_count = specified if specified > @arg_count + specified - 1 + else + if @index_mode == :absolute + raise ArgumentError, "unnumbered mixed with numbered" + end + @index_mode = :relative + (@arg_count += 1) - 1 + end + end + + def append_literal(str) + @g.push_literal str + append_str + end + + def append_str + @append_parts += 1 + end + + def encode_value(str) + str.force_encoding(@format.encoding) + str + end + + def is_zero + @g.push_int 0 + @g.send :equal?, 1 + end + + class Atom + def initialize(b, g, format_code, flags, name = nil) + @b, @g = b, g + @format_code, @flags = format_code, flags + @name = name + + @f_alt = flags.index(?#) + @f_zero = flags.index(?0) + @f_plus = flags.index(?+) + @f_ljust = flags.index(?-) + @f_space = flags.index(?\ ) + + @just_dir = @f_ljust ? :ljust : :rjust + + @prefix = PREFIX[@format_code] if @f_alt + + @full_leader_size = @prefix ? @prefix.size : 0 + @full_leader_size += 1 if @f_plus || @f_space + end + + private :initialize + + def prepend_prefix + @g.push_literal @prefix + @g.string_dup + @g.string_append + end + + def set_value(ref) + unless @name + @field_index = @b.next_index(ref) + end + end + + def set_width(full, ref, static) + @width_static = static && static.to_i + if !@name && full && !static + @width_index = @b.next_index(ref) + end + + @has_width = @width_static || @width_index + end + + def set_precision(full, ref, static) + @prec_static = static && static.to_i + if !@name && full && !static + @prec_index = @b.next_index(ref) + end + + @has_precision = @prec_static || @prec_index + end + + def push_value + if @name + @g.push_local 0 + @g.push_literal @name + @g.send :fetch, 1 + else + @g.push_local @field_index + end + end + + def push_width_value + if @width_static + @g.push_int @width_static + elsif + @g.push_local @width_index + else + raise "No width to push" + end + end + + def push_width(adjust=true) + if @width_static + raise ArgumentError, "width too big" unless @width_static.class == Fixnum + if adjust && @full_leader_size > 0 + @g.push_int(@width_static - @full_leader_size) + else + @g.push_int @width_static + end + + elsif @width_index + @g.push_local @width_index + + @b.force_type :Fixnum, :Integer do + # If we had to do a conversion, we check we ended up + # with a Fixnum + @g.dup + @b.push_Fixnum + @g.swap + @g.kind_of + + @b.if_false do + @b.raise_ArgumentError "width too big" + end + end + + else + raise "push without a width" + + end + end + + def push_precision_value + if @prec_static + @g.push_int @prec_static + else + @g.push_local @prec_index + end + end + + def push_precision + if @prec_static + raise ArgumentError, "precision too big" unless @prec_static.class == Fixnum + @g.push_int @prec_static + + elsif @prec_index + @g.push_local @prec_index + + @b.force_type :Fixnum, :Integer do + # If we had to do a conversion, we check we ended up + # with a Fixnum + @g.dup + @b.push_Fixnum + @g.swap + @g.kind_of + + @b.if_false do + @b.raise_ArgumentError "precision too big" + end + end + + else + raise "push without a precision" + + end + end + + def push_format_string + float_format_code = @format_code + + leader = "%#{@flags}" + if !@width_index && !@prec_index + leader << @width_static.to_s if @width_static + leader << ".#{@prec_static}" if @prec_static + @g.push_literal "#{leader}#{float_format_code}" + else + format_parts = 1 + if @prec_static + @g.push_literal ".#{@prec_static}#{float_format_code}" + else + @g.push_literal(float_format_code) + if @prec_index + push_precision + @g.send :to_s, 0 + format_parts += 1 + + if @width_index + @g.push_literal "." + format_parts += 1 + end + end + end + + if @width_static + leader << @width_static.to_s + elsif @width_index + push_width + @g.send :to_s, 0 + format_parts += 1 + end + leader << "." if @prec_index && !@width_index + + @g.push_literal leader + @g.string_dup + format_parts.times do + @g.string_append + end + end + end + + def positive_sign + if @f_plus + '+' + elsif @f_space + ' ' + else + '' + end + end + + def justify_width(adjust=true) + if @has_width + push_width adjust + @b.justify @just_dir, @width_static.nil? + end + end + + def zero_pad(pad="0") + if @has_precision + push_precision + elsif @has_width && @f_zero + push_width true + else + # exit early if no width has been pushed + return + end + + # let the caller adjust the width if needed + yield if block_given? + + @g.push_literal pad + @g.send :rjust, 2 + end + + attr_reader :width_static + + def leader? + @full_leader_size > 0 + end + end + + class CharAtom < Atom + def bytecode + push_value + # We need to push the original value here + # because we have to possibly make multiple + # attempts to coerce it to either a string + # or an integer. If we don't duplicate we can't + # retry it for another type with the same original + # value. + @g.dup + + @b.try_type :String, :to_str do + # Attempt failed, pop the nil value + # and retry for Integer + @g.pop + @g.dup + @b.force_type :Fixnum, :Integer + @g.send :chr, 0 + end + + @g.dup + @g.send :length, 0 + @g.push_int 1 + @g.send :equal?, 1 + @b.if_false do + @b.raise_ArgumentError "%c requires a character" + end + + justify_width + + # Swap the given value with the coerced value and + # pop off the given value afterwards + @g.swap + @g.pop + @b.append_str + end + end + + class IntegerAtom < Atom + def fast_common_case? + @f_zero && @width_static && !@has_precision && !@f_space && !@f_plus && !@f_ljust + end + + def expand_with_width + if @f_ljust + if @f_space || @f_plus + @g.send :space_expand_leader_left, 2 + else + @g.send :space_expand_integer_left, 2 + end + else + if @f_zero + if @f_space || @f_plus + @g.send :zero_expand_leader, 2 + else + @g.send :zero_expand_integer, 2 + end + else + if @f_space || @f_plus + @g.send :space_expand_leader, 2 + else + @g.send :space_expand_integer, 2 + end + end + end + end + + def bytecode + if fast_common_case? + @g.push_self + + push_value + + if @width_static == 2 + @g.send :zero_two_expand_integer, 1 + else + @g.push_int @width_static + @g.send :zero_expand_integer, 2 + end + + @b.append_str + else + @g.push_self + push_value + @g.send :as_int, 1 + + val_idx = @g.new_stack_local + @g.set_stack_local val_idx + @g.pop + + # generic case + + if @f_space + @g.push_self + @g.push_stack_local val_idx + @g.send :compute_space, 1 + @b.append_str + elsif @f_plus + @g.push_self + @g.push_stack_local val_idx + @g.send :compute_plus, 1 + @b.append_str + end + + if @has_precision + @g.push_self + @g.push_stack_local val_idx + + push_precision_value + + @g.send :digit_expand_precision, 2 + + if @has_width + @g.push_self + @g.swap + push_width_value + + if @f_ljust + @g.send :space_expand_left, 2 + else + @g.send :space_expand, 2 + end + end + + @b.append_str + + elsif @has_width + @g.push_self + @g.push_stack_local val_idx + + push_width_value + + expand_with_width + + @b.append_str + else + @g.push_stack_local val_idx + @g.send :to_s, 0 + + @b.append_str + end + end + end + end + + class ExtIntegerAtom < Atom + def pad_negative_int(padding) + zero_pad(padding) do + # decrease the width by 2 to account for the ".." below + @g.push_int 2 + @g.send :-, 1 + end + + @g.push_literal ".." + @g.string_dup + @g.string_append + end + + def prepend_prefix_bytecode + skip_prefix = @g.new_label + + # For 'x' and 'X', don't prepend the "0x" prefix + # if the value is zero + if @format_code == 'x' || @format_code == 'X' + push_value + @b.is_zero + @g.goto_if_true skip_prefix + end + + prepend_prefix + + skip_prefix.set! + end + + def format_negative_int(radix) + # (num + radix ** num.to_s(radix).size).to_s(radix) + @g.push_int radix + @g.dup_many 2 + @g.send :to_s, 1 + @g.send :size, 0 + @g.send :**, 1 + @g.send :+, 1 + @g.push_int radix + @g.send :to_s, 1 + + (radix - 1).to_s(radix) + end + + def bytecode + radix = RADIX[@format_code] + + push_value + + # Bignum is obviously also perfectly acceptable. But we + # just address the most common case by avoiding the call + # if we've been given a Fixnum. The call is enough + # overhead to bother, but not something to panic about. + @b.force_type :Fixnum, :Integer + + + if @f_plus || @f_space + @g.dup + + # stash away whether it's negative + @b.is_negative + @g.dup + @g.move_down 2 + + @b.if_true do + # but treat it as positive for now + @b.invert + end + + @g.push_int radix + @g.send :to_s, 1 + else + have_formatted = @g.new_label + + @g.dup + @b.is_negative + + @b.if_false do + @g.push_int radix + @g.send :to_s, 1 + @g.goto have_formatted + end + + padding = format_negative_int(radix) + pad_negative_int(padding) + + have_formatted.set! + end + + # 'B' also returns an uppercase string, but there, the + # only alpha character is in the prefix -- and that's + # already uppercase + if @format_code == 'X' + @g.send :upcase, 0 + end + + zero_pad + + if @prefix + prepend_prefix_bytecode + end + + if @f_plus || @f_space + append_sign = @g.new_label + + @g.swap + @b.if_true do + @g.push_literal '-' + + @g.goto append_sign + end + + @g.push_literal positive_sign + + append_sign.set! + @g.string_dup + @g.string_append + end + + + if @has_precision || !@f_zero + justify_width false + end + + @b.append_str + end + end + + class StringAtom < Atom + def string_justify + # by default, f_space changes the padding introduced + # by justify_width, but for string, we want to go + # ahead and actually ignore it not have it count + # against the justification space width. + if @f_space + @full_leader_size -= 1 + end + + justify_width + + if @has_precision + @g.push_int 0 + push_precision + @g.send :[], 2 + end + end + + def bytecode + push_value + @b.force_type :String + + string_justify + + @b.append_str + end + end + + class InspectAtom < StringAtom + def bytecode + push_value + @g.send :inspect, 0 + + string_justify + + @b.append_str + end + end + + class FloatAtom < Atom + def bytecode + push_value + @b.force_type :Float + + format_done = @g.new_label + + @g.dup + @g.send :finite?, 0 + + @b.if_true do + push_format_string + @g.send :to_s_formatted, 1, true + + @g.goto format_done + end + + formatted_non_finite = @g.new_label + + @g.dup + @g.send :nan?, 0 + + @b.if_false do + @b.is_negative + + @b.if_false do + @g.push_literal "#{positive_sign}Inf" + @g.goto formatted_non_finite + end + @g.push_literal '-Inf' + @g.goto formatted_non_finite + end + + @g.pop + @g.push_literal 'NaN' + + formatted_non_finite.set! + justify_width false + + format_done.set! + + @b.append_str + end + end + + class LiteralAtom < Atom + def set_value(ref) + @value = ref + end + + def bytecode + @b.append_literal(@value) + end + end + + def push_Kernel + @g.push_const :Kernel + end + + def push_Fixnum + @g.push_const :Fixnum + end + + def push_String + @g.push_const :String + end + + def push_Hash + @g.push_const :Hash + end + + def raise_ArgumentError(msg) + @g.push_const :ArgumentError + @g.push_literal msg + @g.send :new, 1 + @g.raise_exc + end + + def force_type(klass, method=klass) + @g.dup + @g.push_const klass + @g.swap + @g.kind_of + if_false do + @g.push_self + @g.swap + @g.send method, 1, true + + yield if block_given? + end + end + + def try_type(klass, method) + @g.dup + @g.push_const klass + @g.swap + @g.kind_of + if_false do + @g.push_type + @g.swap + @g.push_const klass + @g.push_literal method + @g.send :check_convert_type, 3 + @g.dup + if_false do + yield if block_given? + end + end + end + + def if_true + l = @g.new_label + @g.goto_if_false l + yield + l.set! + end + + def if_false + l = @g.new_label + @g.goto_if_true l + yield + l.set! + end + + def parse + @arg_count = 0 + @index_mode = nil + + atoms = [] + + # Always push an empty string at first for correct + # encoding protocols + atom = LiteralAtom.new(self, @g, "", "") + atom.set_value(encode_value("")) + + atoms << atom + pos = 0 + while match = RE.match_start(@format, pos) + pos = match.full.at(1) + + _, + plain_string, + whole_format, + name_format, + flags_a, + field_ref_a, + flags_b, + width_full, width_ref, width_static, + prec_full, prec_ref, prec_static, + field_ref_b, + format_code, + literal_char, + name_reference, + invalid_format = *match + + flags = "#{flags_a}#{flags_b}" + + + if plain_string + atom = LiteralAtom.new(self, @g, format_code, flags) + atom.set_value(plain_string) + atoms << atom + elsif literal_char + atom = LiteralAtom.new(self, @g, format_code, flags) + atom.set_value(literal_char) + atoms << atom + elsif invalid_format || (field_ref_a && field_ref_b) + raise ArgumentError, "malformed format string: #{@format.inspect}" + else + field_ref = field_ref_a || field_ref_b + + if name_reference + format_code = "s" + @index_mode = :name + name = name_reference[2...-1].to_sym + elsif name_format + @index_mode = :name + name = name_format[1...-1].to_sym + end + + klass = AtomMap[format_code[0]] + + unless klass + raise ArgumentError, "unknown format type - #{format_code}" + end + + atom = klass.new(self, @g, format_code, flags, name) + atom.set_width width_full, width_ref, width_static + atom.set_precision prec_full, prec_ref, prec_static + atom.set_value field_ref + atoms << atom + end + end + + if @index_mode == :name + exception = @g.new_label + continue = @g.new_label + + @arg_count = 1 + @g.passed_arg @arg_count + @g.goto_if_true exception + push_Hash + @g.push_local 0 + @g.kind_of + @g.goto_if_false exception + @g.goto continue + + exception.set! + + raise_ArgumentError "named format string needs single Hash argument" + + continue.set! + elsif @index_mode != :absolute + no_exception = @g.new_label + + # If we've used relative arguments, and $DEBUG is true, we + # throw an exception if passed more arguments than we need. + + # Check this first; it's much faster, and generally false + @g.passed_arg @arg_count + @g.goto_if_false no_exception + + gva = Rubinius::ToolSets::Runtime::AST::GlobalVariableAccess + gva.new(1, :$DEBUG).bytecode(@g) + @g.goto_if_false no_exception + + raise_ArgumentError "too many arguments for format string" + + no_exception.set! + end + + atoms.each do |atom| + atom.bytecode + end + end + end + end +end diff --git a/gxg/core/stat.rb b/gxg/core/stat.rb new file mode 100644 index 0000000..cc74e8e --- /dev/null +++ b/gxg/core/stat.rb @@ -0,0 +1,298 @@ +module Rubinius + class Stat + include Comparable + + attr_reader :path + + def self.stat(path) + stat = allocate + if Rubinius.privately { stat.setup path } == 0 + stat + else + nil + end + end + + def self.fstat(fd) + stat = allocate + result = Rubinius.privately { stat.fsetup fd } + Errno.handle "file descriptor #{fd}" unless result == 0 + stat + end + + def self.lstat(path) + stat = allocate + result = Rubinius.privately { stat.lsetup path } + Errno.handle path unless result == 0 + stat + end + + def initialize(path) + Errno.handle path unless setup(path) == 0 + end + + private :initialize + + def setup(path) + Rubinius.primitive :stat_stat + path = Rubinius::Type.coerce_to_path(path) + setup(path) + end + private :setup + + def fsetup(fd) + Rubinius.primitive :stat_fstat + fd = Rubinius::Type.coerce_to fd, Integer, :to_int + fsetup(fd) + end + private :fsetup + + def lsetup(path) + Rubinius.primitive :stat_lstat + path = Rubinius::Type.coerce_to_path(path) + lsetup(path) + end + private :lsetup + + def dev + Rubinius.primitive :stat_dev + raise PrimitiveFailure, "Rubinius::Stat#dev primitive failed" + end + + def ino + Rubinius.primitive :stat_ino + raise PrimitiveFailure, "Rubinius::Stat#ino primitive failed" + end + + def mode + Rubinius.primitive :stat_mode + raise PrimitiveFailure, "Rubinius::Stat#mode primitive failed" + end + + def nlink + Rubinius.primitive :stat_nlink + raise PrimitiveFailure, "Rubinius::Stat#nlink primitive failed" + end + + def uid + Rubinius.primitive :stat_uid + raise PrimitiveFailure, "Rubinius::Stat#uid primitive failed" + end + + def gid + Rubinius.primitive :stat_gid + raise PrimitiveFailure, "Rubinius::Stat#gid primitive failed" + end + + def rdev + Rubinius.primitive :stat_rdev + raise PrimitiveFailure, "Rubinius::Stat#rdev primitive failed" + end + + def size + Rubinius.primitive :stat_size + raise PrimitiveFailure, "Rubinius::Stat#size primitive failed" + end + + def blksize + Rubinius.primitive :stat_blksize + raise PrimitiveFailure, "Rubinius::Stat#blksize primitive failed" + end + + def blocks + Rubinius.primitive :stat_blocks + raise PrimitiveFailure, "Rubinius::Stat#blocks primitive failed" + end + + def atime + Rubinius.primitive :stat_atime + raise PrimitiveFailure, "Rubinius::Stat#atime primitive failed" + end + + def mtime + Rubinius.primitive :stat_mtime + raise PrimitiveFailure, "Rubinius::Stat#mtime primitive failed" + end + + def ctime + Rubinius.primitive :stat_ctime + raise PrimitiveFailure, "Rubinius::Stat#ctime primitive failed" + end + + def birthtime + Rubinius.primitive :stat_birthtime + raise NotImplementedError, "birthtime() function is unimplemented on this machine" + end + + def inspect + "#<#{self.class.name} dev=0x#{self.dev.to_s(16)}, ino=#{self.ino}, " \ + "mode=#{sprintf("%07d", self.mode.to_s(8).to_i)}, nlink=#{self.nlink}, " \ + "uid=#{self.uid}, gid=#{self.gid}, rdev=0x#{self.rdev.to_s(16)}, " \ + "size=#{self.size}, blksize=#{self.blksize}, blocks=#{self.blocks}, " \ + "atime=#{self.atime}, mtime=#{self.mtime}, ctime=#{self.ctime}>" + end + + def blockdev? + mode & S_IFMT == S_IFBLK + end + + def chardev? + mode & S_IFMT == S_IFCHR + end + + def dev_major + major = Rubinius::FFI::Platform::POSIX.major dev + major < 0 ? nil : major + end + + def dev_minor + minor = Rubinius::FFI::Platform::POSIX.major dev + minor < 0 ? nil : minor + end + + def directory? + mode & S_IFMT == S_IFDIR + end + + def executable? + return true if superuser? + return mode & S_IXUSR != 0 if owned? + return mode & S_IXGRP != 0 if grpowned? + return mode & S_IXOTH != 0 + end + + def executable_real? + return true if rsuperuser? + return mode & S_IXUSR != 0 if rowned? + return mode & S_IXGRP != 0 if rgrpowned? + return mode & S_IXOTH != 0 + end + + def file? + mode & S_IFMT == S_IFREG + end + + def ftype + if file? + "file" + elsif directory? + "directory" + elsif chardev? + "characterSpecial" + elsif blockdev? + "blockSpecial" + elsif pipe? + "fifo" + elsif socket? + "socket" + elsif symlink? + "link" + else + "unknown" + end + end + + def grpowned? + Process.groups.include?(gid) + end + + def owned? + uid == Rubinius::FFI::Platform::POSIX.geteuid + end + + def pipe? + mode & S_IFMT == S_IFIFO + end + + def rdev_major + major = Rubinius::FFI::Platform::POSIX.major rdev + major < 0 ? nil : major + end + + def rdev_minor + minor = Rubinius::FFI::Platform::POSIX.minor rdev + minor < 0 ? nil : minor + end + + def readable? + return true if superuser? + return mode & S_IRUSR != 0 if owned? + return mode & S_IRGRP != 0 if grpowned? + return mode & S_IROTH != 0 + end + + def readable_real? + return true if rsuperuser? + return mode & S_IRUSR != 0 if rowned? + return mode & S_IRGRP != 0 if rgrpowned? + return mode & S_IROTH != 0 + end + + def setgid? + mode & S_ISGID != 0 + end + + def setuid? + mode & S_ISUID != 0 + end + + def sticky? + mode & S_ISVTX != 0 + end + + def size? + size == 0 ? nil : size + end + + def socket? + mode & S_IFMT == S_IFSOCK + end + + def symlink? + mode & S_IFMT == S_IFLNK + end + + def writable? + return true if superuser? + return mode & S_IWUSR != 0 if owned? + return mode & S_IWGRP != 0 if grpowned? + return mode & S_IWOTH != 0 + end + + def writable_real? + return true if rsuperuser? + return mode & S_IWUSR != 0 if rowned? + return mode & S_IWGRP != 0 if rgrpowned? + return mode & S_IWOTH != 0 + end + + def zero? + size == 0 + end + + def <=>(other) + return nil unless other.is_a?(File::Stat) + self.mtime <=> other.mtime + end + + def rgrpowned? + gid == Rubinius::FFI::Platform::POSIX.getgid + end + private :rgrpowned? + + def rowned? + uid == Rubinius::FFI::Platform::POSIX.getuid + end + private :rowned? + + def rsuperuser? + Rubinius::FFI::Platform::POSIX.getuid == 0 + end + private :rsuperuser? + + def superuser? + Rubinius::FFI::Platform::POSIX.geteuid == 0 + end + private :superuser? + end +end diff --git a/gxg/core/stats.rb b/gxg/core/stats.rb new file mode 100644 index 0000000..852217f --- /dev/null +++ b/gxg/core/stats.rb @@ -0,0 +1,108 @@ +module Rubinius + module Stats + module Units + # Time unit factors, where one second is + # 10^9 nanoseconds (ns) + # 10^6 microseconds (us) + # 10^3 milliseconds (ms) + + SEC_PER_NS = 1.0e-9 + MSEC_PER_NS = 1.0e-6 + USEC_PER_NS = 1.0e-3 + + KBYTES = 1024 + + def usec(ns) + ns * USEC_PER_NS + end + + def msec(ns) + ns * MSEC_PER_NS + end + + def sec(ns) + ns * SEC_PER_NS + end + + def auto_time(time) + return "0" if time == 0 + + elapsed = sec time + return "%.2fs" % elapsed if elapsed > 1.0 + + elapsed = msec time + return "%.2fm" % elapsed if elapsed > 1.0 + + elapsed = usec time + return "%.2fu" % elapsed if elapsed > 1.0 + + "%dn" % time + end + + # Thanks Ruby Quiz #113 + def comma(number, digits=2) + if number.is_a? Float + str = "%.#{digits}f" % number + else + str = number.to_s + end + + str.reverse.scan(/(?:\d*\.)?\d{1,3}-?/).join(',').reverse + end + + def percentage(part, whole, digits=1, percent='%%') + "%.#{digits}f#{percent}" % (part * 100.0 / whole) + end + + def auto_bytes(bytes) + return bytes.to_s if bytes < KBYTES + + bytes /= KBYTES.to_f + return "%.1fK" % bytes if bytes < KBYTES + + "%.1fM" % (bytes / KBYTES) + end + end + + module Printer + def columns(*columns) + @width = columns.inject(0) { |sum, x| sum + x } + + text_column = "%%-%ds" % columns.shift + @value_format = "#{text_column}%#{columns[1]}s\n" + + format = columns.map { |c| "%%%ds" % c }.join + @stats_format = "#{text_column}#{format}\n" + + @header_format = "\n" << @stats_format + end + + def headings(*headings) + @headings = headings + end + + def heading(leader) + printf @header_format, leader, *@headings + separator + end + + def separator(limit=nil) + width = limit || @width + puts "-" * width + end + + def string(metric, string) + printf @value_format, metric, string + end + + def value(metric, value, meth=:comma) + printf @value_format, metric, send(meth, value) + end + + def statistics(metric, data, meth=:comma) + values = [data[:total], data[:max], data[:min], data[:average]] + printf @stats_format, metric, *values.map { |d| send meth, d } + end + end + end +end diff --git a/gxg/core/string.rb b/gxg/core/string.rb new file mode 100644 index 0000000..db92464 --- /dev/null +++ b/gxg/core/string.rb @@ -0,0 +1,2691 @@ +class String + include Comparable + + attr_reader :num_bytes + attr_reader_specific :num_bytes, :bytesize + + attr_writer :encoding + attr_writer :ascii_only + attr_writer :valid_encoding + + attr_accessor :data + + alias_method :__data__, :data + alias_method :__data__=, :data= + + def self.__allocate__ + Rubinius.primitive :string_allocate + raise PrimitiveFailure, "String.allocate primitive failed" + end + + def self.allocate + str = __allocate__ + str.__data__ = Rubinius::ByteArray.allocate_sized(1) + str.num_bytes = 0 + str + end + + def self.from_codepoint(code, enc) + Rubinius.primitive :string_from_codepoint + raise PrimitiveFailure, "String.from_codepoint primitive failed" + end + + def self.pattern(size, str) + Rubinius.primitive :string_pattern + raise PrimitiveFailure, "String.pattern primitive failed" + end + + def ascii_only? + Rubinius.primitive :string_ascii_only_p + raise PrimitiveFailure, "String#ascii_only? primitive failed" + end + + def encoding + Rubinius.primitive :string_encoding + raise PrimitiveFailure, "String#encoding primitive failed" + end + + def ord + Rubinius.primitive :string_codepoint + raise ArgumentError, 'empty string' if empty? + raise ArgumentError, "invalid byte sequence in #{encoding}" + end + + def chr_at(byte) + Rubinius.primitive :string_chr_at + raise ArgumentError, "String#chr_at primitive failed" + end + + def to_f + Rubinius::Type::coerce_to_float self, false, false + end + + alias_method :convert_float, :to_f + + def __crypt__(other_str) + Rubinius.primitive :string_crypt + raise PrimitiveFailure, "String#crypt primitive failed" + end + + def append(str) + Rubinius.primitive :string_append + raise TypeError, "String#append primitive only accepts Strings" + end + + def byte_append(str) + Rubinius.primitive :string_byte_append + raise TypeError, "String#byte_append primitive only accepts Strings" + end + + def byteslice(index_or_range, length=undefined) + Rubinius.primitive :string_byte_substring + + if index_or_range.kind_of? Range + index = Rubinius::Type.coerce_to index_or_range.begin, Fixnum, :to_int + index += @num_bytes if index < 0 + return if index < 0 or index > @num_bytes + + finish = Rubinius::Type.coerce_to index_or_range.end, Fixnum, :to_int + finish += @num_bytes if finish < 0 + + finish += 1 unless index_or_range.exclude_end? + length = finish - index + + return byteslice 0, 0 if length < 0 + else + index = Rubinius::Type.coerce_to index_or_range, Fixnum, :to_int + index += @num_bytes if index < 0 + + if undefined.equal?(length) + return if index == @num_bytes + length = 1 + else + length = Rubinius::Type.coerce_to length, Fixnum, :to_int + return if length < 0 + end + + return if index < 0 or index > @num_bytes + end + + byteslice index, length + end + + def dup + other = Rubinius.invoke_primitive :string_dup, self + Rubinius::Type.object_initialize_dup self, other + other + end + + def clone + other = Rubinius.invoke_primitive :string_dup, self + Rubinius.invoke_primitive :object_copy_singleton_class, other, self + Rubinius::Type.object_initialize_clone self, other + other.freeze if frozen? + other + end + + def substring(start, count) + Rubinius.primitive :string_substring + raise PrimitiveFailure, "String#substring primitive failed" + end + + def ==(other) + Rubinius.primitive :string_equal + raise PrimitiveFailure, "String#== primitive failed" + end + + def secure_compare(other) + Rubinius.primitive :string_secure_compare + + if other.kind_of?(String) + raise PrimitiveFailure, "String#secure_compare primitive failed" + else + secure_compare StringValue(other) + end + end + + def find_character(offset) + Rubinius.primitive :string_find_character + raise PrimitiveFailure, "String#find_character primitive failed" + end + + def size + Rubinius.primitive :string_size + raise PrimitiveFailure, "String#size primitive failed" + end + + alias_method :length, :size + + # This is a work-in-progress. String is entirely coded around the idea of + # bytes, but we have to convert to the idea of characters, even for 1.8 + # mode, where UTF-8, EUC, and SJIS will be represented properly as encoded + # strings rather than some ad hoc internal state. However, the byte + # characteristic of String is still important. More work remains. + def num_bytes=(bytes) + @num_chars = nil + @num_bytes = bytes + end + + # In time, the JIT should be able to handle this as a ruby method. + def transform(tbl) + Rubinius.primitive :string_transform + raise PrimitiveFailure, "String#transform primitive failed" + end + + def reverse! + Rubinius.primitive :string_reverse + raise PrimitiveFailure, "String#reverse primitive failed" + end + + def valid_encoding? + Rubinius.primitive :string_valid_encoding_p + raise PrimitiveFailure, "String#valid_encoding? primitive failed" + end + + ## + # Creates a new string from copying _count_ bytes from the + # _start_ of _bytes_. + def self.from_bytearray(bytes, start, count) + Rubinius.primitive :string_from_bytearray + raise PrimitiveFailure, "String.from_bytearray primitive failed" + end + + def self.try_convert(obj) + Rubinius::Type.try_convert obj, String, :to_str + end + + class << self + def clone + raise TypeError, "Unable to clone/dup String class" + end + + alias_method :dup, :clone + end + + def initialize(arg = undefined, encoding: nil) + replace arg unless undefined.equal?(arg) + self.force_encoding(encoding) if encoding + self + end + + private :initialize + + def %(args) + *args = args + ret = Rubinius::Sprinter.get(self).call(*args) + + Rubinius::Type.infect ret, self + end + + def *(num) + num = Rubinius::Type.coerce_to(num, Integer, :to_int) unless num.kind_of? Integer + + if num.kind_of? Bignum + raise RangeError, "bignum too big to convert into `long' (#{num})" + end + + if num < 0 + raise ArgumentError, "unable to multiple negative times (#{num})" + end + + str = self.class.pattern num * @num_bytes, self + return str + end + + def +(other) + other = StringValue(other) + Rubinius::Type.compatible_encoding self, other + String.new(self) << other + end + + def ==(other) + Rubinius.primitive :string_equal + + # Use #=== rather than #kind_of? because other might redefine kind_of? + unless String === other + if other.respond_to?(:to_str) + return other == self + end + return false + end + + return false unless @num_bytes == other.bytesize + return false unless Encoding.compatible?(self, other) + return @data.compare_bytes(other.__data__, @num_bytes, other.bytesize) == 0 + end + + def =~(pattern) + case pattern + when Regexp + match_data = pattern.search_region(self, 0, @num_bytes, true) + Regexp.last_match = match_data + return match_data.begin(0) if match_data + when String + raise TypeError, "type mismatch: String given" + else + pattern =~ self + end + end + + def [](index, other = undefined) + Rubinius.primitive :string_aref + + unless undefined.equal?(other) + if index.kind_of?(Fixnum) && other.kind_of?(Fixnum) + return substring(index, other) + elsif index.kind_of? Regexp + match, str = subpattern(index, other) + Regexp.last_match = match + return str + else + length = Rubinius::Type.coerce_to(other, Fixnum, :to_int) + start = Rubinius::Type.coerce_to(index, Fixnum, :to_int) + return substring(start, length) + end + end + + case index + when Regexp + match_data = index.search_region(self, 0, @num_bytes, true) + Regexp.last_match = match_data + if match_data + result = match_data.to_s + Rubinius::Type.infect result, index + return result + end + when String + return include?(index) ? index.dup : nil + when Range + start = Rubinius::Type.coerce_to index.first, Fixnum, :to_int + length = Rubinius::Type.coerce_to index.last, Fixnum, :to_int + + start += size if start < 0 + + length += size if length < 0 + length += 1 unless index.exclude_end? + + return "" if start == size + return nil if start < 0 || start > size + + length = size if length > size + length = length - start + length = 0 if length < 0 + + return substring(start, length) + else + index = Rubinius::Type.coerce_to index, Fixnum, :to_int + return self[index] + end + end + alias_method :slice, :[] + + def capitalize + return dup if @num_bytes == 0 + + str = transform(Rubinius::CType::Lowered) + + str.modify! + + # Now do the actual capitalization + ba = str.__data__ + ba[0] = Rubinius::CType.toupper(ba[0]) + + return str + end + + def capitalize! + Rubinius.check_frozen + + cap = capitalize() + return nil if cap == self + + replace(cap) + return self + end + + def casecmp(to) + to = StringValue(to) + order = @num_bytes - to.num_bytes + size = order < 0 ? @num_bytes : to.num_bytes + + ctype = Rubinius::CType + + i = 0 + while i < size + a = @data[i] + b = to.__data__[i] + i += 1 + + a = ctype.toupper!(a) if ctype.islower(a) + b = ctype.toupper!(b) if ctype.islower(b) + r = a - b + + next if r == 0 + return r < 0 ? -1 : 1 + end + + return 0 if order == 0 + return order < 0 ? -1 : 1 + end + + def chomp(separator=$/) + str = dup + str.chomp!(separator) || str + end + + def chop + str = dup + str.chop! || str + end + + def count(*strings) + raise ArgumentError, "wrong number of Arguments" if strings.empty? + + return 0 if @num_bytes == 0 + + table = count_table(*strings).__data__ + + count = bytes = 0 + while bytes < @num_bytes + count += 1 if table[@data[bytes]] == 1 + bytes += find_character(bytes).num_bytes + end + + count + end + + def crypt(other_str) + other_str = StringValue(other_str) + + if other_str.size < 2 + raise ArgumentError, "salt must be at least 2 characters" + end + + hash = __crypt__(other_str) + + Rubinius::Type.infect hash, self + Rubinius::Type.infect hash, other_str + end + + def delete(*strings) + str = dup + str.delete!(*strings) || str + end + + def delete!(*strings) + raise ArgumentError, "wrong number of arguments" if strings.empty? + + table = count_table(*strings).__data__ + + self.modify! + + i = 0 + j = -1 + + while i < @num_bytes + c = @data[i] + unless table[c] == 1 + @data[j+=1] = c + end + i += 1 + end + + if (j += 1) < @num_bytes + self.num_bytes = j + self + else + nil + end + end + + def downcase + return dup if @num_bytes == 0 + transform(Rubinius::CType::Lowered) + end + + def downcase! + Rubinius.check_frozen + + return if @num_bytes == 0 + + str = transform(Rubinius::CType::Lowered) + + return nil if str == self + + replace(str) + + return self + end + + def each_char + return to_enum(:each_char) { size } unless block_given? + + bytes = 0 + while bytes < @num_bytes + char = find_character(bytes) + yield char + bytes += char.num_bytes + end + + self + end + + def each_byte + return to_enum(:each_byte) { bytesize } unless block_given? + i = 0 + while i < @num_bytes do + yield @data.get_byte(i) + i += 1 + end + self + end + + def empty? + @num_bytes == 0 + end + + def end_with?(*suffixes) + suffixes.each do |suffix| + suffix = Rubinius::Type.check_convert_type suffix, String, :to_str + next unless suffix + + return true if self[-suffix.length, suffix.length] == suffix + end + false + end + + def eql?(other) + Rubinius.primitive :string_equal + + return false unless other.kind_of?(String) && other.bytesize == @num_bytes + return false unless Encoding.compatible?(self, other) + return @data.compare_bytes(other.__data__, @num_bytes, other.bytesize) == 0 + end + + # This method is specifically part of 1.9 but we enable it in 1.8 also + # because we need it internally. + def getbyte(index) + index = Rubinius::Type.coerce_to index, Fixnum, :to_int + + index += bytesize if index < 0 + return if index < 0 or index >= bytesize + + @data[index] + end + + def include?(needle) + !!Rubinius::Mirror.reflect(self).find_string(StringValue(needle), 0) + end + + ControlCharacters = [10, 9, 7, 11, 12, 13, 27, 8] + ControlPrintValue = ["\\n", "\\t", "\\a", "\\v", "\\f", "\\r", "\\e", "\\b"] + + def lstrip + str = dup + str.lstrip! || str + end + + def oct + to_inum(-8, false) + end + + # Treats leading characters from self as a string of hexadecimal digits + # (with an optional sign and an optional 0x) and returns the + # corresponding number. Zero is returned on error. + # + # "0x0a".hex #=> 10 + # "-1234".hex #=> -4660 + # "0".hex #=> 0 + # "wombat".hex #=> 0 + def hex + to_inum(16, false) + end + + def reverse + dup.reverse! + end + + def partition(pattern=nil) + return super() if pattern == nil && block_given? + + if pattern.kind_of? Regexp + if m = pattern.match(self) + Regexp.last_match = m + return [m.pre_match, m.to_s, m.post_match] + end + else + pattern = StringValue(pattern) + if i = index(pattern) + post_start = i + pattern.length + post_len = size - post_start + + return [substring(0, i), + pattern.dup, + substring(post_start, post_len)] + end + end + + # Nothing worked out, this is the default. + return [self, "", ""] + end + + def rpartition(pattern) + if pattern.kind_of? Regexp + if m = pattern.search_region(self, 0, size, false) + Regexp.last_match = m + [m.pre_match, m[0], m.post_match] + end + else + pattern = StringValue(pattern) + if i = rindex(pattern) + post_start = i + pattern.length + post_len = size - post_start + + return [substring(0, i), + pattern.dup, + substring(post_start, post_len)] + end + + # Nothing worked out, this is the default. + return ["", "", self] + end + end + + def rstrip + str = dup + str.rstrip! || str + end + + def scan(pattern) + taint = tainted? || pattern.tainted? + pattern = Rubinius::Type.coerce_to_regexp(pattern, true) + index = 0 + + last_match = nil + + if block_given? + ret = self + else + ret = [] + end + + while match = pattern.match_from(self, index) + fin = match.full.at(1) + + if match.collapsing? + if char = find_character(fin) + index = fin + char.bytesize + else + index = fin + 1 + end + else + index = fin + end + + last_match = match + val = (match.length == 1 ? match[0] : match.captures) + val.taint if taint + + if block_given? + Regexp.last_match = match + yield(val) + else + ret << val + end + end + + Regexp.last_match = last_match + return ret + end + + # This method is specifically part of 1.9 but we enable it in 1.8 also + # because we need it internally. + def setbyte(index, byte) + self.modify! + + index = Rubinius::Type.coerce_to index, Fixnum, :to_int + byte = Rubinius::Type.coerce_to byte, Fixnum, :to_int + + index += bytesize if index < 0 + if index < 0 or index >= bytesize + raise IndexError, "byte index #{index} is outside bounds of String" + end + + @ascii_only = @valid_encoding = nil + @data[index] = byte + end + + def split(pattern=nil, limit=undefined) + Rubinius::Splitter.split(self, pattern, limit) + end + + def squeeze(*strings) + str = dup + str.squeeze!(*strings) || str + end + + def squeeze!(*strings) + return if @num_bytes == 0 + + table = count_table(*strings).__data__ + self.modify! + + i = 1 + j = 0 + last = @data[0] + + while i < @num_bytes + c = @data[i] + unless c == last and table[c] == 1 + @data[j+=1] = last = c + end + i += 1 + end + + if (j += 1) < @num_bytes + self.num_bytes = j + self + else + nil + end + end + + def delete_prefix(prefix) + prefix = Rubinius::Type.check_convert_type prefix, String, :to_str + return self[prefix.size..-1] if self.start_with?(prefix) + dup + end + + def delete_prefix!(prefix) + Rubinius.check_frozen + result = delete_prefix(prefix) + return nil if result == self + + replace(result) + self + end + + def start_with?(*prefixes) + prefixes.each do |prefix| + prefix = Rubinius::Type.check_convert_type prefix, String, :to_str + next unless prefix + return true if self[0, prefix.length] == prefix + end + false + end + + def strip + str = dup + str.strip! || str + end + + def strip! + left = lstrip! + right = rstrip! + left.nil? && right.nil? ? nil : self + end + + def succ + dup.succ! + end + + def sum(bits=16) + bits = Rubinius::Type.coerce_to bits, Fixnum, :to_int + i = -1 + sum = 0 + + sum += @data[i] while (i += 1) < @num_bytes + if bits > 0 + sum & ((1 << bits) - 1) + else + sum + end + end + + def swapcase + str = dup + str.swapcase! || str + end + + def swapcase! + self.modify! + return if @num_bytes == 0 + + modified = false + + ctype = Rubinius::CType + + i = 0 + while i < @num_bytes + c = @data[i] + if ctype.islower(c) + @data[i] = ctype.toupper!(c) + modified = true + elsif ctype.isupper(c) + @data[i] = ctype.tolower!(c) + modified = true + end + i += 1 + end + + modified ? self : nil + end + + alias_method :intern, :to_sym + + def to_i(base=10) + base = Rubinius::Type.coerce_to base, Integer, :to_int + + if base < 0 || base == 1 || base > 36 + raise ArgumentError, "illegal radix #{base}" + end + + to_inum(base, false) + end + + def to_s + instance_of?(String) ? self : "".replace(self) + end + alias_method :to_str, :to_s + + def tr(source, replacement) + str = dup + str.tr!(source, replacement) || str + end + + def tr!(source, replacement) + tr_trans(source, replacement, false) + end + + def tr_s(source, replacement) + str = dup + str.tr_s!(source, replacement) || str + end + + def tr_s!(source, replacement) + tr_trans(source, replacement, true) + end + + def unpack(directives) + Rubinius.primitive :string_unpack + + unless directives.kind_of? String + return unpack(StringValue(directives)) + end + + raise ArgumentError, "invalid directives string: #{directives}" + end + + def upcase + str = dup + str.upcase! || str + end + + def upcase! + return if @num_bytes == 0 + self.modify! + + modified = false + + ctype = Rubinius::CType + + i = 0 + while i < @num_bytes + c = @data[i] + if ctype.islower(c) + @data[i] = ctype.toupper!(c) + modified = true + end + i += 1 + end + + modified ? self : nil + end + + def to_sub_replacement(result, match) + index = 0 + while index < @num_bytes + current = index + while current < @num_bytes && @data[current] != 92 # ?\\ + current += 1 + end + result.append(byteslice(index, current - index)) + break if current == @num_bytes + + # found backslash escape, looking next + if current == @num_bytes - 1 + result.append("\\") # backslash at end of string + break + end + index = current + 1 + + cap = @data[index] + + additional = case cap + when 38 # ?& + match[0] + when 96 # ?` + match.pre_match + when 39 # ?' + match.post_match + when 43 # ?+ + match.captures.compact[-1].to_s + when 48..57 # ?0..?9 + match[cap - 48].to_s + when 92 # ?\\ escaped backslash + '\\' + when 107 # \k named capture + if @data[index + 1] == 60 + name = "" + i = index + 2 + while i < @data.size && @data[i] != 62 + name << @data[i] + i += 1 + end + if i >= @data.size + '\\'.append(cap.chr) + index += 1 + next + end + index = i + name.force_encoding result.encoding + match[name] + else + '\\'.append(cap.chr) + end + else # unknown escape + '\\'.append(cap.chr) + end + result.append(additional) + index += 1 + end + end + + def to_inum(base, check) + Rubinius.primitive :string_to_inum + raise ArgumentError, "invalid value for Integer" + end + + def apply_and!(other) + Rubinius.primitive :string_apply_and + raise PrimitiveFailure, "String#apply_and! primitive failed" + end + + def compare_substring(other, start, size) + Rubinius.primitive :string_compare_substring + + if start > @num_bytes || start + @num_bytes < 0 + raise IndexError, "index #{start} out of string" + end + raise PrimitiveFailure, "String#compare_substring primitive failed" + end + + def count_table(*strings) + table = String.pattern 256, 1 + + i = 0 + size = strings.size + while i < size + str = StringValue(strings[i]).dup + if str.bytesize > 1 && str.getbyte(0) == 94 # ?^ + pos = 0 + neg = 1 + str.slice!(0) + else + pos = 1 + neg = 0 + end + + set = String.pattern 256, neg + set_data = set.__data__ + str.tr_expand! nil, true + str_data = str.__data__ + j = -1 + chars = str.bytesize + set_data[str_data[j]] = pos while (j += 1) < chars + + table.apply_and! set + i += 1 + end + table + end + + def tr_expand!(limit, invalid_as_empty) + Rubinius.primitive :string_tr_expand + raise PrimitiveFailure, "String#tr_expand primitive failed" + end + + # Unshares shared strings. + def modify! + Rubinius.check_frozen + + if @shared + @data = @data.dup + @shared = nil + end + + @ascii_only = @valid_encoding = nil + @hash_value = nil # reset the hash value + end + + def subpattern(pattern, capture) + match = pattern.match(self) + + return nil unless match + + if index = Rubinius::Type.check_convert_type(capture, Fixnum, :to_int) + return nil if index >= match.size || -index >= match.size + capture = index + end + + str = match[capture] + Rubinius::Type.infect str, pattern + [match, str] + end + private :subpattern + + def prefix?(other) + size = other.size + return false if size > @num_bytes + other.compare_substring(self, 0, size) == 0 + end + + def suffix?(other) + size = other.size + return false if size > @num_bytes + other.compare_substring(self, -size, size) == 0 + end + + def shorten!(size) + self.modify! + return if @num_bytes == 0 + self.num_bytes -= size + end + + def shared! + @shared = true + end + + def each_codepoint + return to_enum(:each_codepoint) { size } unless block_given? + + each_char { |c| yield c.ord } + self + end + + def b + dup.force_encoding Encoding::ASCII_8BIT + end + + def bytes + if block_given? + each_byte do |byte| + yield byte + end + else + each_byte.to_a + end + end + + def chars + if block_given? + each_char do |char| + yield char + end + else + each_char.to_a + end + end + + def codepoints + if block_given? + each_codepoint do |codepoint| + yield codepoint + end + else + each_codepoint.to_a + end + end + + def encode!(to=undefined, from=undefined, options=undefined) + Rubinius.check_frozen + + case to + when Encoding + to_enc = to + when Hash + options = to + to_enc = Encoding.default_internal + when undefined + to_enc = Encoding.default_internal + return self unless to_enc + else + opts = Rubinius::Type::check_convert_type to, Hash, :to_hash + + if opts + options = opts + to_enc = Encoding.default_internal + else + to_enc = Rubinius::Type.try_convert_to_encoding to + end + end + + case from + when undefined + from_enc = encoding + when Encoding + from_enc = from + when Hash + options = from + from_enc = encoding + else + opts = Rubinius::Type::check_convert_type from, Hash, :to_hash + + if opts + options = opts + from_enc = encoding + else + from_enc = Rubinius::Type.coerce_to_encoding from + end + end + + if undefined.equal? from_enc or undefined.equal? to_enc + raise Encoding::ConverterNotFoundError, "undefined code converter (#{from} to #{to})" + end + + case options + when undefined + options = 0 + when Hash + # do nothing + else + options = Rubinius::Type.coerce_to options, Hash, :to_hash + end + + # TODO: Only UTF-8 is allowed for internal encoding. This is a preliminary + # step to fixing all encoding-related interfaces. + to_enc = Encoding::UTF_8 unless to_enc == Encoding::UTF_8 + + if ascii_only? and from_enc.ascii_compatible? and to_enc and to_enc.ascii_compatible? + force_encoding to_enc + elsif to_enc and from_enc != to_enc + ec = Encoding::Converter.new from_enc, to_enc, options + dest = "" + status = ec.primitive_convert self.dup, dest, nil, nil, ec.options + raise ec.last_error unless status == :finished + replace dest + end + + # TODO: replace this hack with transcoders + if options.kind_of? Hash + case xml = options[:xml] + when :text + gsub!(/[&><]/, '&' => '&', '>' => '>', '<' => '<') + when :attr + gsub!(/[&><"]/, '&' => '&', '>' => '>', '<' => '<', '"' => '"') + insert(0, '"') + insert(-1, '"') + when nil + # nothing + else + raise ArgumentError, "unexpected value for xml option: #{xml.inspect}" + end + + if options[:universal_newline] + gsub!(/\r\n|\r/, "\r\n" => "\n", "\r" => "\n") + end + end + + self + end + + def encode(to=undefined, from=undefined, options=undefined) + dup.encode! to, from, options + end + + def end_with?(*suffixes) + suffixes.each do |original_suffix| + suffix = Rubinius::Type.check_convert_type original_suffix, String, :to_str + unless suffix + raise TypeError, "no implicit conversion of #{original_suffix.class} into String" + end + return true if self[-suffix.length, suffix.length] == suffix + end + false + end + + def force_encoding(enc) + enc = Rubinius::Type.coerce_to_encoding enc + + # TODO: Only UTF-8 encodings are supported internally. + return self unless enc.equal?(Encoding::UTF_8) or enc.equal?(Encoding::BINARY) + + @encoding = enc + + unless @ascii_only && @encoding.ascii_compatible? + @ascii_only = @valid_encoding = @num_chars = nil + end + if bytesize == 0 && @encoding.ascii_compatible? + @ascii_only = true + @valid_encoding = true + @num_chars = 0 + end + + self + end + + def inspect + result_encoding = Encoding.default_internal || Encoding.default_external + unless result_encoding.ascii_compatible? + result_encoding = Encoding::US_ASCII + end + + enc = encoding + ascii = enc.ascii_compatible? + enc_name = enc.name + unicode = enc_name.start_with?("UTF-") && enc_name[4] != ?7 + + if unicode + if enc.equal? Encoding::UTF_16 + a = getbyte 0 + b = getbyte 1 + + if a == 0xfe and b == 0xff + enc = Encoding::UTF_16BE + elsif a == 0xff and b == 0xfe + enc = Encoding::UTF_16LE + else + unicode = false + end + elsif enc.equal? Encoding::UTF_32 + a = getbyte 0 + b = getbyte 1 + c = getbyte 2 + d = getbyte 3 + + if a == 0 and b == 0 and c == 0xfe and d == 0xfe + enc = Encoding::UTF_32BE + elsif a == 0xff and b == 0xfe and c == 0 and d == 0 + enc = Encoding::UTF_32LE + else + unicode = false + end + end + end + + array = [] + + index = 0 + total = bytesize + while index < total + char = chr_at index + + if char + bs = char.bytesize + + if (ascii or unicode) and bs == 1 + escaped = nil + + byte = getbyte(index) + if byte >= 7 and byte <= 92 + case byte + when 7 # \a + escaped = '\a' + when 8 # \b + escaped = '\b' + when 9 # \t + escaped = '\t' + when 10 # \n + escaped = '\n' + when 11 # \v + escaped = '\v' + when 12 # \f + escaped = '\f' + when 13 # \r + escaped = '\r' + when 27 # \e + escaped = '\e' + when 34 # \" + escaped = '\"' + when 35 # # + case getbyte(index += 1) + when 36 # $ + escaped = '\#$' + when 64 # @ + escaped = '\#@' + when 123 # { + escaped = '\#{' + else + index -= 1 + end + when 92 # \\ + escaped = '\\\\' + end + + if escaped + array << escaped + index += 1 + next + end + end + end + + if char.printable? + array << char + else + code = char.ord + escaped = code.to_s(16).upcase + + if unicode + if code < 0x10000 + pad = "0" * (4 - escaped.bytesize) + array << "\\u#{pad}#{escaped}" + else + array << "\\u{#{escaped}}" + end + else + if code < 0x100 + pad = "0" * (2 - escaped.bytesize) + array << "\\x#{pad}#{escaped}" + else + array << "\\x{#{escaped}}" + end + end + end + + index += bs + else + array << "\\x#{getbyte(index).to_s(16)}" + index += 1 + end + end + + size = array.inject(0) { |s, chr| s += chr.bytesize } + result = String.pattern size + 2, ?".ord + m = Rubinius::Mirror.reflect result + + index = 1 + array.each do |chr| + m.copy_from chr, 0, chr.bytesize, index + index += chr.bytesize + end + + Rubinius::Type.infect result, self + result.force_encoding result_encoding + end + + def prepend(other) + self[0, 0] = other + self + end + + def upto(stop, exclusive=false) + return to_enum :upto, stop, exclusive unless block_given? + stop = StringValue(stop) + + if stop.size == 1 && size == 1 + return self if self > stop + after_stop = stop.getbyte(0) + (exclusive ? 0 : 1) + current = getbyte(0) + until current == after_stop + yield current.chr + current += 1 + end + else + unless stop.size < size + after_stop = exclusive ? stop : stop.succ + current = self + + until current == after_stop + yield current + current = StringValue(current.succ) + break if current.size > stop.size || current.size == 0 + end + end + end + self + end + + def sub(pattern, replacement=undefined) + # Because of the behavior of $~, this is duplicated from sub! because + # if we call sub! from sub, the last_match can't be updated properly. + + unless valid_encoding? + raise ArgumentError, "invalid byte sequence in #{encoding}" + end + + ret = byteslice(0, 0) # Empty string and string subclass + + if undefined.equal? replacement + unless block_given? + raise ArgumentError, "method '#{__method__}': given 1, expected 2" + end + use_yield = true + else + unless replacement.kind_of?(String) + hash = Rubinius::Type.check_convert_type(replacement, Hash, :to_hash) + replacement = StringValue(replacement) unless hash + end + use_yield = false + + Rubinius::Type.infect ret, replacement + end + + pattern = Rubinius::Type.coerce_to_regexp(pattern, true) unless pattern.kind_of? Regexp + match = pattern.match_from(self, 0) + + Regexp.last_match = match + + if match + ret.append match.pre_match + + if use_yield || hash + Regexp.last_match = match + + if use_yield + val = yield match.to_s + else + val = hash[match.to_s] + end + val = val.to_s unless val.kind_of?(String) + + Rubinius::Type.infect ret, val + + ret.append val + else + replacement.to_sub_replacement(ret, match) + end + + Rubinius::Type.infect ret, val + ret.append(match.post_match) + else + ret = dup + end + + ret + end + + def sub!(pattern, replacement=undefined) + # Because of the behavior of $~, this is duplicated from sub! because + # if we call sub! from sub, the last_match can't be updated properly. + + unless valid_encoding? + raise ArgumentError, "invalid byte sequence in #{encoding}" + end + + ret = byteslice(0, 0) # Empty string and string subclass + + if undefined.equal? replacement + unless block_given? + raise ArgumentError, "method '#{__method__}': given 1, expected 2" + end + Rubinius.check_frozen + use_yield = true + else + Rubinius.check_frozen + + unless replacement.kind_of?(String) + hash = Rubinius::Type.check_convert_type(replacement, Hash, :to_hash) + replacement = StringValue(replacement) unless hash + end + use_yield = false + + Rubinius::Type.infect ret, replacement + end + + pattern = Rubinius::Type.coerce_to_regexp(pattern, true) unless pattern.kind_of? Regexp + match = pattern.match_from(self, 0) + + Regexp.last_match = match + + if match + ret.append match.pre_match + + if use_yield || hash + Regexp.last_match = match + + if use_yield + val = yield match.to_s + else + val = hash[match.to_s] + end + val = val.to_s unless val.kind_of?(String) + + Rubinius::Type.infect ret, val + + ret.append val + else + replacement.to_sub_replacement(ret, match) + end + + Rubinius::Type.infect ret, val + + ret.append(match.post_match) + else + return nil + end + + replace(ret) + self + end + + def slice!(one, two=undefined) + Rubinius.check_frozen + # This is un-DRY, but it's a simple manual argument splitting. Keeps + # the code fast and clean since the sequence are pretty short. + # + if undefined.equal?(two) + result = slice(one) + + if one.kind_of? Regexp + lm = Regexp.last_match + self[one] = '' if result + Regexp.last_match = lm + else + self[one] = '' if result + end + else + result = slice(one, two) + + if one.kind_of? Regexp + lm = Regexp.last_match + self[one, two] = '' if result + Regexp.last_match = lm + else + self[one, two] = '' if result + end + end + + result + end + + # TODO: make encoding aware. + def succ! + self.modify! + + return self if @num_bytes == 0 + + carry = nil + last_alnum = 0 + start = @num_bytes - 1 + + ctype = Rubinius::CType + + while start >= 0 + s = @data[start] + if ctype.isalnum(s) + carry = 0 + if (48 <= s && s < 57) || + (97 <= s && s < 122) || + (65 <= s && s < 90) + @data[start] += 1 + elsif s == 57 + @data[start] = 48 + carry = 49 + elsif s == 122 + @data[start] = carry = 97 + elsif s == 90 + @data[start] = carry = 65 + end + + break if carry == 0 + last_alnum = start + end + + start -= 1 + end + + if carry.nil? + start = length - 1 + carry = 1 + + while start >= 0 + if @data[start] >= 255 + @data[start] = 0 + else + @data[start] += 1 + break + end + + start -= 1 + end + end + + if start < 0 + m = Rubinius::Mirror.reflect self + m.splice last_alnum, 1, carry.chr + @data[last_alnum].chr + end + + return self + end + + alias_method :next, :succ + alias_method :next!, :succ! + + def to_c + Complexifier.new(self).convert + end + + def to_r + Rationalizer.new(self).convert + end + + def rstrip! + Rubinius.check_frozen + return if @num_bytes == 0 + + stop = @num_bytes - 1 + + ctype = Rubinius::CType + + while stop >= 0 && (@data[stop] == 0 || ctype.isspace(@data[stop])) + stop -= 1 + end + + return if (stop += 1) == @num_bytes + + modify! + self.num_bytes = stop + self + end + + def lstrip! + Rubinius.check_frozen + return if @num_bytes == 0 + + start = 0 + + ctype = Rubinius::CType + + while start < @num_bytes && ctype.isspace(@data[start]) + start += 1 + end + + return if start == 0 + + modify! + self.num_bytes -= start + @data.move_bytes start, @num_bytes, 0 + self + end + + def chop! + Rubinius.check_frozen + + m = Rubinius::Mirror.reflect self + + bytes = m.previous_byte_index @num_bytes + return unless bytes + + chr = chr_at bytes + if chr.ord == 10 and chr.ascii? + if i = m.previous_byte_index(bytes) + chr = chr_at i + + bytes = i if chr.ord == 13 and chr.ascii? + end + end + + self.num_bytes = bytes + + # We do not need to dup the data, so don't use #modify! + @hash_value = nil + + self + end + + def chomp!(sep=undefined) + Rubinius.check_frozen + + if undefined.equal?(sep) + sep = $/ + elsif sep + sep = StringValue(sep) + end + + return if sep.nil? + + m = Rubinius::Mirror.reflect self + + if sep == DEFAULT_RECORD_SEPARATOR + return unless bytes = m.previous_byte_index(@num_bytes) + + chr = chr_at bytes + return unless chr.ascii? + + case chr.ord + when 13 + # do nothing + when 10 + if j = m.previous_byte_index(bytes) + chr = chr_at j + + if chr.ord == 13 and chr.ascii? + bytes = j + end + end + else + return + end + elsif sep.size == 0 + return if @num_bytes == 0 + bytes = @num_bytes + + while i = m.previous_byte_index(bytes) + chr = chr_at i + break unless chr.ord == 10 and chr.ascii? + + bytes = i + + if j = m.previous_byte_index(i) + chr = chr_at j + if chr.ord == 13 and chr.ascii? + bytes = j + end + end + end + + return if bytes == @num_bytes + else + size = sep.size + return if size > @num_bytes + + # TODO: Move #compare_substring to mirror. + return unless sep.compare_substring(self, -size, size) == 0 + bytes = @num_bytes - size + end + + # We do not need to dup the data, so don't use #modify! + @hash_value = nil + self.num_bytes = bytes + + self + end + + def clear + Rubinius.check_frozen + self.num_bytes = 0 + self + end + + def replace(other) + Rubinius.check_frozen + + # If we're replacing with ourselves, then we have nothing to do + return self if Rubinius::Type.object_equal(self, other) + + other = StringValue(other) + + @shared = true + if other.frozen? + @data = other.__data__.dup + else + other.shared! + @data = other.__data__ + end + self.num_bytes = other.num_bytes + @hash_value = nil + force_encoding(other.encoding) + @valid_encoding = other.valid_encoding? + @ascii_only = nil + + Rubinius::Type.infect(self, other) + end + + def initialize_copy(other) + end + + private :initialize_copy + + def <<(other) + Rubinius.check_frozen + + unless other.kind_of? String + if other.kind_of? Integer + if encoding == Encoding::US_ASCII and other >= 128 and other < 256 + force_encoding(Encoding::ASCII_8BIT) + end + + other = other.chr(encoding) + else + other = StringValue(other) + end + end + + Rubinius::Type.infect(self, other) + append(other) + end + alias_method :concat, :<< + + def chr + substring 0, 1 + end + + def each_line(sep=$/) + return to_enum(:each_line, sep) unless block_given? + + # weird edge case. + if sep.nil? + yield self + return self + end + + sep = StringValue(sep) + + pos = 0 + + size = @num_bytes + orig_data = @data + + # If the separator is empty, we're actually in paragraph mode. This + # is used so infrequently, we'll handle it completely separately from + # normal line breaking. + if sep.empty? + sep = "\n\n" + pat_size = 2 + m = Rubinius::Mirror.reflect self + + while pos < size + nxt = m.find_string(sep, pos) + break unless nxt + + while @data[nxt] == 10 and nxt < @num_bytes + nxt += 1 + end + + match_size = nxt - pos + + # string ends with \n's + break if pos == @num_bytes + + str = byteslice pos, match_size + yield str unless str.empty? + + # detect mutation within the block + if !@data.equal?(orig_data) or @num_bytes != size + raise RuntimeError, "string modified while iterating" + end + + pos = nxt + end + + # No more separates, but we need to grab the last part still. + fin = byteslice pos, @num_bytes - pos + yield fin if fin and !fin.empty? + + else + + # This is the normal case. + pat_size = sep.size + unmodified_self = clone + m = Rubinius::Mirror.reflect unmodified_self + + while pos < size + nxt = m.find_string(sep, pos) + break unless nxt + + match_size = nxt - pos + str = unmodified_self.byteslice pos, match_size + pat_size + yield str unless str.empty? + + pos = nxt + pat_size + end + + # No more separates, but we need to grab the last part still. + fin = unmodified_self.byteslice pos, @num_bytes - pos + yield fin unless fin.empty? + end + + self + end + + def lines(sep=$/) + if block_given? + each_line(sep) do |line| + yield line + end + else + each_line(sep).to_a + end + end + + + def gsub(pattern, replacement=undefined) + # Because of the behavior of $~, this is duplicated from gsub! because + # if we call gsub! from gsub, the last_match can't be updated properly. + + unless valid_encoding? + raise ArgumentError, "invalid byte sequence in #{encoding}" + end + + ret = byteslice(0, 0) # Empty string and string subclass + + if undefined.equal? replacement + unless block_given? + return to_enum(:gsub, pattern, replacement) + end + use_yield = true + else + unless replacement.kind_of?(String) + hash = Rubinius::Type.check_convert_type(replacement, Hash, :to_hash) + replacement = StringValue(replacement) unless hash + end + use_yield = false + + Rubinius::Type.infect ret, replacement + end + + pattern = Rubinius::Type.coerce_to_regexp(pattern, true) unless pattern.kind_of? Regexp + match = pattern.search_region(self, 0, @num_bytes, true) + + unless match + Regexp.last_match = nil + end + + orig_len = @num_bytes + orig_data = @data + + last_end = 0 + offset = nil + + last_match = nil + + offset = match.full.at(0) if match + + while match + if str = match.pre_match_from(last_end) + ret.append str + end + + if use_yield || hash + Regexp.last_match = match + + if use_yield + val = yield match.to_s + else + val = hash[match.to_s] + end + val = val.to_s unless val.kind_of?(String) + + Rubinius::Type.infect ret, val + + ret.append val + + if !@data.equal?(orig_data) or @num_bytes != orig_len + raise RuntimeError, "string modified" + end + else + replacement.to_sub_replacement(ret, match) + end + + Rubinius::Type.infect ret, val + + last_end = match.full.at(1) + + if match.collapsing? + if char = find_character(offset) + offset += char.bytesize + else + offset += 1 + end + else + offset = match.full.at(1) + end + + last_match = match + + match = pattern.match_from self, offset + break unless match + + offset = match.full.at(0) + end + + Regexp.last_match = last_match + + str = byteslice(last_end, @num_bytes-last_end+1) + if str + ret.append str + end + + ret + end + + def gsub!(pattern, replacement=undefined) + # Because of the behavior of $~, this is duplicated from gsub! because + # if we call gsub! from gsub, the last_match can't be updated properly. + + unless valid_encoding? + raise ArgumentError, "invalid byte sequence in #{encoding}" + end + + ret = byteslice(0, 0) # Empty string and string subclass + + if undefined.equal? replacement + unless block_given? + return to_enum(:gsub, pattern, replacement) + end + Rubinius.check_frozen + use_yield = true + else + Rubinius.check_frozen + + unless replacement.kind_of?(String) + hash = Rubinius::Type.check_convert_type(replacement, Hash, :to_hash) + replacement = StringValue(replacement) unless hash + end + use_yield = false + + Rubinius::Type.infect ret, replacement + end + + pattern = Rubinius::Type.coerce_to_regexp(pattern, true) unless pattern.kind_of? Regexp + match = pattern.search_region(self, 0, @num_bytes, true) + + unless match + Regexp.last_match = nil + return nil + end + + orig_len = @num_bytes + orig_data = @data + + last_end = 0 + offset = nil + + last_match = nil + + offset = match.full.at(0) + + while match + if str = match.pre_match_from(last_end) + ret.append str + end + + if use_yield || hash + Regexp.last_match = match + + if use_yield + val = yield match.to_s + else + val = hash[match.to_s] + end + val = val.to_s unless val.kind_of?(String) + + Rubinius::Type.infect ret, val + + ret.append val + + if !@data.equal?(orig_data) or @num_bytes != orig_len + raise RuntimeError, "string modified" + end + else + replacement.to_sub_replacement(ret, match) + end + + Rubinius::Type.infect ret, val + + last_end = match.full.at(1) + + if match.collapsing? + if char = find_character(offset) + offset += char.bytesize + else + offset += 1 + end + else + offset = match.full.at(1) + end + + last_match = match + + match = pattern.match_from self, offset + break unless match + + offset = match.full.at(0) + end + + Regexp.last_match = last_match + + str = byteslice(last_end, @num_bytes-last_end+1) + if str + ret.append str + end + + replace(ret) + self + end + + def match(pattern, pos=0) + pattern = Rubinius::Type.coerce_to_regexp(pattern) unless pattern.kind_of? Regexp + + result = if block_given? + pattern.match self, pos do |match| + yield match + end + else + pattern.match self, pos + end + Regexp.propagate_last_match + result + end + + # Removes invalid byte sequences from a String, available since Ruby 2.1. + def scrub(replace = nil) + output = '' + input = dup + + # The default replacement character is the "Unicode replacement" character. + # (U+FFFD). + if !replace and !block_given? + replace = "\xEF\xBF\xBD".force_encoding("UTF-8") + .encode(self.encoding, :undef => :replace, :replace => '?') + end + + if replace + unless replace.is_a?(String) + raise( + TypeError, + "no implicit conversion of #{replace.class} into String" + ) + end + + unless replace.valid_encoding? + raise( + ArgumentError, + "replacement must be a valid byte sequence '#{replace.inspect}'" + ) + end + + replace = replace.force_encoding(Encoding::BINARY) + end + + # MRI appears to just return a copy of self when the input encoding is + # BINARY/ASCII_8BIT. + if input.encoding == Encoding::BINARY + return input + end + + converter = Encoding::Converter.new(input.encoding, Encoding::BINARY) + + while input.length > 0 + result = converter.primitive_convert(input, output, output.length) + + if result == :finished + break + elsif result == :undefined_conversion + output << converter.primitive_errinfo[3] + else + # Blocks can return strings in any encoding so we'll make sure it's the + # same as our buffer for the mean time. + if block_given? + block_output = yield(converter.primitive_errinfo[3]) + + output << block_output.force_encoding(output.encoding) + else + output << replace + end + end + end + + return output.force_encoding(encoding) + end + + def scrub!(replace = nil, &block) + replace(scrub(replace, &block)) + return self + end + + def []=(index, count_or_replacement, replacement=undefined) + if undefined.equal?(replacement) + replacement = count_or_replacement + count = nil + else + count = count_or_replacement + end + + m = Rubinius::Mirror.reflect self + + case index + when Fixnum + index += size if index < 0 + + if index < 0 or index > size + raise IndexError, "index #{index} out of string" + end + + unless bi = m.byte_index(index) + raise IndexError, "unable to find character at: #{index}" + end + + if count + count = Rubinius::Type.coerce_to count, Fixnum, :to_int + + if count < 0 + raise IndexError, "count is negative" + end + + total = index + count + if total >= size + bs = bytesize - bi + else + bs = m.byte_index(total) - bi + end + else + bs = index == size ? 0 : m.byte_index(index + 1) - bi + end + + replacement = StringValue replacement + enc = Rubinius::Type.compatible_encoding self, replacement + + m.splice bi, bs, replacement + when String + unless start = m.byte_index(index) + raise IndexError, "string not matched" + end + + replacement = StringValue replacement + enc = Rubinius::Type.compatible_encoding self, replacement + + m.splice start, index.bytesize, replacement + when Range + start = Rubinius::Type.coerce_to index.first, Fixnum, :to_int + + start += size if start < 0 + + if start < 0 or start > size + raise RangeError, "#{index.first} is out of range" + end + + unless bi = m.byte_index(start) + raise IndexError, "unable to find character at: #{start}" + end + + stop = Rubinius::Type.coerce_to index.last, Fixnum, :to_int + stop += size if stop < 0 + stop -= 1 if index.exclude_end? + + if stop < start + bs = 0 + elsif stop >= size + bs = bytesize - bi + else + bs = m.byte_index(stop + 1) - bi + end + + replacement = StringValue replacement + enc = Rubinius::Type.compatible_encoding self, replacement + + m.splice bi, bs, replacement + when Regexp + if count + count = Rubinius::Type.coerce_to count, Fixnum, :to_int + else + count = 0 + end + + if match = index.match(self) + ms = match.size + else + raise IndexError, "regexp does not match" + end + + count += ms if count < 0 and -count < ms + unless count < ms and count >= 0 + raise IndexError, "index #{count} out of match bounds" + end + + unless match[count] + raise IndexError, "regexp group #{count} not matched" + end + + replacement = StringValue replacement + enc = Rubinius::Type.compatible_encoding self, replacement + + bi = m.byte_index match.begin(count) + bs = m.byte_index(match.end(count)) - bi + + m.splice bi, bs, replacement + else + index = Rubinius::Type.coerce_to index, Fixnum, :to_int + + if count + return self[index, count] = replacement + else + return self[index] = replacement + end + end + + Rubinius::Type.infect self, replacement + force_encoding enc + + return replacement + end + + def center(width, padding=" ") + padding = StringValue(padding) + raise ArgumentError, "zero width padding" if padding.size == 0 + + enc = Rubinius::Type.compatible_encoding self, padding + + width = Rubinius::Type.coerce_to width, Fixnum, :to_int + return dup if width <= size + + width -= size + left = width / 2 + + bs = bytesize + pbs = padding.bytesize + + if pbs > 1 + ps = padding.size + pm = Rubinius::Mirror.reflect padding + + x = left / ps + y = left % ps + + lpbi = pm.byte_index(y) + lbytes = x * pbs + lpbi + + right = left + (width & 0x1) + + x = right / ps + y = right % ps + + rpbi = pm.byte_index(y) + rbytes = x * pbs + rpbi + + pad = self.class.pattern rbytes, padding + str = self.class.pattern lbytes + bs + rbytes, "" + m = Rubinius::Mirror.reflect str + + m.copy_from self, 0, bs, lbytes + m.copy_from pad, 0, lbytes, 0 + m.copy_from pad, 0, rbytes, lbytes + bs + else + str = self.class.pattern width + bs, padding + m = Rubinius::Mirror.reflect str + m.copy_from self, 0, bs, left + end + + Rubinius::Type.infect str, self + Rubinius::Type.infect str, padding + + str.force_encoding enc + end + + def ljust(width, padding=" ") + padding = StringValue(padding) + raise ArgumentError, "zero width padding" if padding.size == 0 + + enc = Rubinius::Type.compatible_encoding self, padding + + width = Rubinius::Type.coerce_to width, Fixnum, :to_int + return dup if width <= size + + width -= size + + bs = bytesize + pbs = padding.bytesize + + if pbs > 1 + ps = padding.size + pm = Rubinius::Mirror.reflect padding + + x = width / ps + y = width % ps + + pbi = pm.byte_index(y) + bytes = x * pbs + pbi + + str = self.class.pattern bytes + bs, self + m = Rubinius::Mirror.reflect str + + i = 0 + bi = bs + + while i < x + m.copy_from padding, 0, pbs, bi + + bi += pbs + i += 1 + end + + m.copy_from padding, 0, pbi, bi + else + str = self.class.pattern width + bs, padding + m = Rubinius::Mirror.reflect str + + m.copy_from self, 0, bs, 0 + end + + Rubinius::Type.infect str, self + Rubinius::Type.infect str, padding + + str.force_encoding enc + end + + def rjust(width, padding=" ") + padding = StringValue(padding) + raise ArgumentError, "zero width padding" if padding.size == 0 + + enc = Rubinius::Type.compatible_encoding self, padding + + width = Rubinius::Type.coerce_to width, Fixnum, :to_int + return dup if width <= size + + width -= size + + bs = bytesize + pbs = padding.bytesize + + if pbs > 1 + ps = padding.size + pm = Rubinius::Mirror.reflect padding + + x = width / ps + y = width % ps + + bytes = x * pbs + pm.byte_index(y) + else + bytes = width + end + + str = self.class.pattern bytes + bs, padding + m = Rubinius::Mirror.reflect str + + m.copy_from self, 0, bs, bytes + + Rubinius::Type.infect str, self + Rubinius::Type.infect str, padding + + str.force_encoding enc + end + + def index(str, start=undefined) + if undefined.equal?(start) + start = 0 + else + start = Rubinius::Type.coerce_to start, Fixnum, :to_int + + start += size if start < 0 + return if start < 0 or start > size + end + + if str.kind_of? Regexp + Rubinius::Type.compatible_encoding self, str + + m = Rubinius::Mirror.reflect self + start = m.character_to_byte_index start + if match = str.match_from(self, start) + Regexp.last_match = match + return match.begin(0) + else + Regexp.last_match = nil + return + end + end + + str = StringValue(str) + return start if str == "" + + Rubinius::Type.compatible_encoding self, str + + return if str.size > size + + m = Rubinius::Mirror.reflect self + m.character_index str, start + end + + def rindex(sub, finish=undefined) + if undefined.equal?(finish) + finish = size + else + finish = Rubinius::Type.coerce_to(finish, Integer, :to_int) + finish += size if finish < 0 + return nil if finish < 0 + finish = size if finish >= size + end + + m = Rubinius::Mirror.reflect self + byte_finish = m.character_to_byte_index finish + + case sub + when Fixnum + if finish == size + return nil if finish == 0 + finish -= 1 + end + + begin + str = sub.chr + rescue RangeError + return nil + end + + if byte_index = m.find_string_reverse(str, byte_finish) + return m.byte_to_character_index byte_index + end + + when Regexp + Rubinius::Type.compatible_encoding self, sub + + match_data = sub.search_region(self, 0, byte_finish, false) + Regexp.last_match = match_data + return match_data.begin(0) if match_data + + else + needle = StringValue(sub) + needle_size = needle.size + + # needle is bigger that haystack + return nil if size < needle_size + + # Boundary case + return finish if needle_size == 0 + + Rubinius::Type.compatible_encoding self, needle + + if byte_index = m.find_string_reverse(needle, byte_finish) + return m.byte_to_character_index byte_index + end + end + + return nil + end + + def start_with?(*prefixes) + prefixes.each do |original_prefix| + prefix = Rubinius::Type.check_convert_type original_prefix, String, :to_str + unless prefix + raise TypeError, "no implicit conversion of #{original_prefix.class} into String" + end + return true if self[0, prefix.length] == prefix + end + false + end + + def insert(index, other) + other = StringValue(other) + + enc = Rubinius::Type.compatible_encoding self, other + index = Rubinius::Type.coerce_to index, Fixnum, :to_int + index = length + 1 + index if index < 0 + + if index > length or index < 0 + raise IndexError, "index #{index} out of string" + end + + osize = other.bytesize + size = @num_bytes + osize + str = self.class.pattern size, "\0" + + self_m = Rubinius::Mirror.reflect self + index = self_m.character_to_byte_index index + + Rubinius.check_frozen + @hash_value = nil + + m = Rubinius::Mirror.reflect str + if index == @num_bytes + m.copy_from self, 0, @num_bytes, 0 + m.copy_from other, 0, osize, @num_bytes + else + m.copy_from self, 0, index, 0 if index > 0 + m.copy_from other, 0, osize, index + m.copy_from self, index, @num_bytes - index, index + osize + end + + self.num_bytes = size + @data = str.__data__ + Rubinius::Type.infect self, other + force_encoding enc + + self + end + + def tr_trans(source, replacement, squeeze) + source = StringValue(source).dup + replacement = StringValue(replacement).dup + + return delete!(source) if replacement.empty? + return if @num_bytes == 0 + + invert = source[0] == ?^ && source.length > 1 + + source.slice!(0) if invert + source.tr_expand! nil, true + replacement.tr_expand! nil, false + + multi_table = {} + + if invert + r = replacement.__data__[replacement.size - 1] + table = Rubinius::Tuple.pattern 256, r + + source.each_char do |chr| + if chr.bytesize > 1 + multi_table[chr] = -1 + else + table[chr.ord] = -1 + end + end + else + repl = replacement.__data__ + rsize = replacement.size + table = Rubinius::Tuple.pattern 256, -1 + + i = 0 + source.each_char do |chr| + repl_char = replacement[i] + + if repl_char && (chr.bytesize > 1 || repl_char.bytesize > 1) + multi_table[chr] = repl_char + else + r = repl[i] if i < rsize + table[chr.ord] = r + end + + i += 1 + end + end + + destination = dup + modified = false + + if squeeze + last = nil + byte_size = 0 + + i = 0 + each_char do |chr| + c = -1 + c = table[chr.ord] if chr.bytesize == 1 + + if c >= 0 + c_char = c.chr + next if last == c_char + byte_size += 1 + destination[i] = c_char + last = c_char + modified = true + elsif c = multi_table[chr] + next if last == c + destination[i] = c + last = c + modified = true + byte_size += c.bytesize + else + destination[i] = chr + byte_size += chr.bytesize + last = nil + end + + i += 1 + end + + destination.num_bytes = byte_size if byte_size < @num_bytes + else + i = 0 + each_char do |chr| + c = -1 + c = table[chr.ord] if chr.bytesize == 1 + + if c >= 0 + c_char = c.chr + destination[i] = c_char + modified = true + elsif c = multi_table[chr] + destination[i] = c + modified = true + end + i += 1 + end + end + + if modified + replace(destination) + else + nil + end + end + + def <=>(other) + if other.kind_of?(String) + result = @data.compare_bytes(other.__data__, @num_bytes, other.bytesize) + + if result == 0 + if Encoding.compatible?(self, other) + 0 + else + Rubinius::Type.encoding_order(encoding, other.encoding) + end + else + result + end + else + if other.respond_to?(:<=>) && !other.respond_to?(:to_str) + return unless tmp = (other <=> self) + elsif other.respond_to?(:to_str) + return unless tmp = (other.to_str <=> self) + else + return + end + return -tmp # We're not supposed to convert to integer here + end + end + + def dump + s = self.class.allocate + str = %{"#{transform(Rubinius::CType::Printed).force_encoding(Encoding::US_ASCII)}"} + str += ".force_encoding(\"#{encoding}\")" unless encoding.ascii_compatible? + s.replace(str) + end + + def -@ + frozen? ? self : dup.freeze + end + + def +@ + frozen? ? dup : self + end +end diff --git a/gxg/core/string_mirror.rb b/gxg/core/string_mirror.rb new file mode 100644 index 0000000..69de5ff --- /dev/null +++ b/gxg/core/string_mirror.rb @@ -0,0 +1,76 @@ +module Rubinius + class Mirror + class String < Mirror + self.subject = ::String + + def character_to_byte_index(idx, start=0) + Rubinius.invoke_primitive :string_character_byte_index, @object, idx, start + end + + def byte_to_character_index(idx, start=0) + Rubinius.invoke_primitive :string_byte_character_index, @object, idx, start + end + + def character_index(str, start) + Rubinius.invoke_primitive :string_character_index, @object, str, start + end + + def byte_index(value, start=0) + Rubinius.invoke_primitive :string_byte_index, @object, value, start + end + + def find_string(pattern, start) + Rubinius.invoke_primitive :string_index, @object, pattern, start + end + + def find_string_reverse(pattern, start) + Rubinius.invoke_primitive :string_rindex, @object, pattern, start + end + + def previous_byte_index(index) + Rubinius.invoke_primitive :string_previous_byte_index, @object, index + end + + def copy_from(other, start, size, dest) + Rubinius.invoke_primitive :string_copy_from, @object, other, start, size, dest + end + + def resize_capacity(count) + Rubinius.invoke_primitive :string_resize_capacity, @object, count + end + + def splice(start, count, replacement) + str = @object + + # TODO: copy_from unshares + str.modify! + + bs = str.bytesize + rbs = replacement.bytesize + + bytes = bs - count + rbs + + s = start + count + b = bs - s + d = start + rbs + + # Always resize if the resulting size is different to prevent "leaking" + # bytes in large ByteArray instances when splicing out chunks. + + if rbs < count + copy_from str, s, b, d + resize_capacity bytes + elsif rbs > count + resize_capacity bytes + copy_from str, s, b, d + end + + copy_from replacement, 0, rbs, start if rbs > 0 + + str.num_bytes = bytes + + self + end + end + end +end diff --git a/gxg/core/struct.rb b/gxg/core/struct.rb new file mode 100644 index 0000000..e46e572 --- /dev/null +++ b/gxg/core/struct.rb @@ -0,0 +1,264 @@ +class Struct + include Enumerable + + class << self + alias_method :subclass_new, :new + end + + def self.new(klass_name, *attrs, &block) + if klass_name + begin + klass_name = StringValue klass_name + rescue TypeError + attrs.unshift klass_name + klass_name = nil + end + end + + attrs = attrs.map do |a| + case a + when Symbol + a + when String + sym = a.to_sym + unless sym.kind_of? Symbol + raise TypeError, "#to_sym didn't return a symbol" + end + sym + else + raise TypeError, "#{a.inspect} is not a symbol" + end + end + + klass = Class.new self do + _specialize attrs + attr_accessor(*attrs) + + def self.new(*args, &block) + return subclass_new(*args, &block) + end + + def self.[](*args) + return new(*args) + end + + const_set :STRUCT_ATTRS, attrs + end + + Struct.const_set klass_name, klass if klass_name + + klass.module_eval(&block) if block + + return klass + end + + # Don't specialize any thing created in the core library. We hook up better + # form of this in core/zed.rb. + def self._specialize(attrs) + end + + def self.make_struct(name, attrs) + new name, *attrs + end + + def _attrs # :nodoc: + return self.class::STRUCT_ATTRS + end + private :_attrs + + def select + return to_enum(:select) { size } unless block_given? + + to_a.select do |v| + yield v + end + end + + def to_h + Hash[each_pair.to_a] + end + + def to_s + return "[...]" if Thread.guarding? self + + Thread.recursion_guard self do + values = [] + + _attrs.each do |var| + val = instance_variable_get :"@#{var}" + values << "#{var}=#{val.inspect}" + end + + name = self.class.name + + if name.nil? || name.empty? + "#" + else + "#" + end + end + end + + alias_method :inspect, :to_s + + def instance_variables + # Hide the ivars used to store the struct fields + super() - _attrs.map { |a| "@#{a}".to_sym } + end + + def initialize(*args) + attrs = _attrs + + unless args.length <= attrs.length + raise ArgumentError, "Expected #{attrs.size}, got #{args.size}" + end + + attrs.each_with_index do |attr, i| + instance_variable_set :"@#{attr}", args[i] + end + end + + private :initialize + + def ==(other) + return false if self.class != other.class + + Thread.detect_recursion self, other do + return self.values == other.values + end + + # Subtle: if we are here, we are recursing and haven't found any difference, so: + true + end + + def [](var) + case var + when Symbol, String + # ok + else + var = Integer(var) + a_len = _attrs.length + if var > a_len - 1 + raise IndexError, "offset #{var} too large for struct(size:#{a_len})" + end + if var < -a_len + raise IndexError, "offset #{var + a_len} too small for struct(size:#{a_len})" + end + var = _attrs[var] + end + + unless _attrs.include? var.to_sym + raise NameError, "no member '#{var}' in struct" + end + + return instance_variable_get(:"@#{var}") + end + + def []=(var, obj) + case var + when Symbol + unless _attrs.include? var + raise NameError, "no member '#{var}' in struct" + end + when String + var = var.to_sym + unless _attrs.include? var + raise NameError, "no member '#{var}' in struct" + end + else + var = Integer(var) + a_len = _attrs.length + if var > a_len - 1 + raise IndexError, "offset #{var} too large for struct(size:#{a_len})" + end + if var < -a_len + raise IndexError, "offset #{var + a_len} too small for struct(size:#{a_len})" + end + + var = _attrs[var] + end + + return instance_variable_set(:"@#{var}", obj) + end + + def dig(key, *remaining_keys) + item = begin + self[key] + rescue NameError + nil + end + return item if remaining_keys.empty? || item.nil? + + raise TypeError, "#{item.class} does not have #dig method" unless item.respond_to?(:dig) + + item.dig(*remaining_keys) + end + + def eql?(other) + return true if equal? other + return false if self.class != other.class + + Thread.detect_recursion self, other do + _attrs.each do |var| + mine = instance_variable_get(:"@#{var}") + theirs = other.instance_variable_get(:"@#{var}") + + return false unless mine.eql? theirs + end + end + + # Subtle: if we are here, then no difference was found, or we are recursing + # In either case, return + true + end + + def each + return to_enum(:each) { size } unless block_given? + values.each do |v| + yield v + end + self + end + + def each_pair + return to_enum(:each_pair) { size } unless block_given? + _attrs.each { |var| yield [var, instance_variable_get(:"@#{var}")] } + self + end + + def hash + hash_val = size + return _attrs.size if Thread.detect_outermost_recursion self do + _attrs.each { |var| hash_val ^= instance_variable_get(:"@#{var}").hash } + end + return hash_val + end + + def length + return _attrs.length + end + + alias_method :size, :length + + def self.length + return self::STRUCT_ATTRS.size + end + + def self.members + self::STRUCT_ATTRS.dup + end + + def members + return self.class.members + end + + def to_a + return _attrs.map { |var| instance_variable_get :"@#{var}" } + end + + alias_method :values, :to_a + + def values_at(*args) + to_a.values_at(*args) + end +end diff --git a/gxg/core/symbol.rb b/gxg/core/symbol.rb new file mode 100644 index 0000000..03ad4b9 --- /dev/null +++ b/gxg/core/symbol.rb @@ -0,0 +1,162 @@ +class Symbol + include Comparable + + def self.===(obj) + Rubinius.primitive :symbol_s_eqq + super + end + + def encoding + Rubinius.primitive :symbol_encoding + raise PrimitiveFailure, "Symbol#encoding primitive failed" + end + + def index + Rubinius.primitive :symbol_index + raise PrimitiveFailure, "Symbol#index primitive failed." + end + + def is_ivar? + Rubinius.primitive :symbol_is_ivar + raise PrimitiveFailure, "Symbol#is_ivar primitive failed." + end + + def is_cvar? + Rubinius.primitive :symbol_is_cvar + raise PrimitiveFailure, "Symbol#is_cvar primitive failed." + end + + def is_constant? + Rubinius.primitive :symbol_is_constant + raise PrimitiveFailure, "Symbol#is_constant primitive failed." + end + + def <=>(other) + return unless other.kind_of? Symbol + + to_s <=> other.to_s + end + + # Use equal? for == (and not Comparable version) + alias_method :==, :equal? + + def capitalize + to_s.capitalize.to_sym + end + + def casecmp(other) + return unless other.kind_of? Symbol + + to_s.casecmp(other.to_s) + end + + def downcase + to_s.downcase.to_sym + end + + ## + # Returns a pretty version of the symbol, fit for viewing + # :symbol.inspect #=> ":symbol" + def inspect + str = to_s + + case str + when /\A(\$|@@?)[a-z_][a-z_\d]*\z/i, # Variable names + /\A[a-z_][a-z_\d]*[=?!]?\z/i, # Method names + /\A\$(-[a-z_\d]|[+~:?<_\/'"$.,`!;\\=*>&@]|\d+)\z/i, # Special global variables + /\A([|^&\/%~`!]|!=|!~|<<|>>|<=>|===?|=~|[<>]=?|[+-]@?|\*\*?|\[\]=?)\z/ # Operators + ":#{str}" + else + ":#{str.inspect}" + end + end + + def empty? + to_s.empty? + end + + alias_method :intern, :to_sym + alias_method :id2name, :to_s + + def length + to_s.length + end + + alias_method :size, :length + + def match(pattern) + str = to_s + + case pattern + when Regexp + match_data = pattern.search_region(str, 0, str.bytesize, true) + Regexp.last_match = match_data + return match_data.full[0] if match_data + when String + raise TypeError, "type mismatch: String given" + else + pattern =~ str + end + end + + alias_method :=~, :match + + def swapcase + to_s.swapcase.to_sym + end + + def upcase + to_s.upcase.to_sym + end + + def succ + to_s.succ.to_sym + end + + alias_method :next, :succ + + def [](index, other = undefined) + if index.kind_of?(Regexp) + unless undefined.equal?(other) + match, str = to_s.send(:subpattern, index, other) + Regexp.last_match = match + return str + end + + str = to_s + match_data = index.search_region(str, 0, str.num_bytes, true) + Regexp.last_match = match_data + if match_data + result = match_data.to_s + Rubinius::Type.infect result, index + return result + end + else + to_s[index, other] + end + end + + alias_method :slice, :[] + + # Returns a Proc object which respond to the given method by sym. + def to_proc + # Put sym in the outer enclosure so that this proc can be instance_eval'd. + # If we used self in the block and the block is passed to instance_eval, then + # self becomes the object instance_eval was called on. So to get around this, + # we leave the symbol in sym and use it in the block. + # + sym = self + Proc.new do |*args, &b| + raise ArgumentError, "no receiver given" if args.empty? + args.shift.__send__(sym, *args, &b) + end + end + + # Use equal? for === + alias_method :===, :equal? + + def self.all_symbols + Rubinius.primitive :symbol_all_symbols + raise PrimitiveFailure, "Symbol.all_symbols primitive failed." + end +end diff --git a/gxg/core/thread.rb b/gxg/core/thread.rb new file mode 100644 index 0000000..0e88538 --- /dev/null +++ b/gxg/core/thread.rb @@ -0,0 +1,454 @@ +#-- +# Be very careful about calling raise in here! Thread has its own +# raise which, if you're calling raise, you probably don't want. Use +# Kernel.raise to call the proper raise. +#++ +class Thread + attr_reader :recursive_objects + attr_reader :pid + attr_reader :exception + attr_reader :stack_size + attr_reader :source + + def self.new(*args, **kw, &block) + stack_size = Rubinius::Type.try_convert kw[:stack_size], Fixnum, :to_int + + thr = Rubinius.invoke_primitive :thread_s_new, args, stack_size, block, self + + Rubinius::VariableScope.of_sender.locked! + + unless thr.send :initialized? + raise ThreadError, "Thread#initialize not called" + end + + return thr + end + + def self.start(*args, **kw, &block) + raise ArgumentError.new("no block passed to Thread.start") unless block + + stack_size = Rubinius::Type.try_convert kw[:stack_size], Fixnum, :to_int + + Rubinius.invoke_primitive :thread_s_start, args, stack_size, block, self + end + + class << self + alias_method :fork, :start + end + + def initialize(*args, &block) + unless block + Kernel.raise ThreadError, "no block passed to Thread#initialize" + end + + if @initialized + Kernel.raise ThreadError, "already initialized thread" + end + + @args = args + @block = block + @initialized = true + + Thread.current.group.add self + end + + private :initialize + + alias_method :__thread_initialize__, :initialize + + def initialized? + @initialized + end + + private :initialized? + + def self.current + Rubinius.primitive :thread_current + Kernel.raise PrimitiveFailure, "Thread.current primitive failed" + end + + def self.allocate + raise TypeError, "allocator undefined for Thread" + end + + def self.pass + Rubinius.primitive :thread_pass + Kernel.raise PrimitiveFailure, "Thread.pass primitive failed" + end + + def self.list + Rubinius.primitive :thread_list + Kernel.raise PrimitiveFailure, "Thread.list primitive failed" + end + + def self.count + Rubinius.primitive :thread_count + Kernel.raise PrimitiveFailure, "Thread.count primitive failed" + end + + def self.stop + sleep + nil + end + + def name + Rubinius.primitive :thread_name + Kernel.raise PrimitiveFailure, "Thread#name primitive failed" + end + + def name=(name) + return unless name + + thread_name = StringValue(name) + unless thread_name.ascii_only? + Kernel.raise ArgumentError, "name must be ASCII only" + end + + Rubinius.invoke_primitive :thread_set_name, self, thread_name + + name + end + + def fiber_list + Rubinius.primitive :thread_fiber_list + Kernel.raise PrimitiveFailure, "Thread.fiber_list primitive failed" + end + + def wakeup + Rubinius.primitive :thread_wakeup + Kernel.raise PrimitiveFailure, "Thread#wakeup primitive failed" + end + + def priority + Rubinius.primitive :thread_get_priority + Kernel.raise ThreadError, "Thread#priority primitive failed" + end + + def priority=(val) + Rubinius.primitive :thread_set_priority + Kernel.raise TypeError, "priority must be a Fixnum" unless val.kind_of? Fixnum + Kernel.raise ThreadError, "Thread#priority= primitive failed" + end + + def __context__ + Rubinius.primitive :thread_context + Kernel.raise PrimitiveFailure, "Thread#__context__ primitive failed" + end + + def mri_backtrace + Rubinius.primitive :thread_mri_backtrace + Kernel.raise PrimitiveFailure, "Thread#mri_backtrace primitive failed" + end + + @abort_on_exception = false + + def self.abort_on_exception + @abort_on_exception + end + + def self.abort_on_exception=(val) + @abort_on_exception = val + end + + # It's also an instance method... + def abort_on_exception=(val) + @abort_on_exception = val + end + + def abort_on_exception + @abort_on_exception || false + end + + def inspect + str = "#<#{self.class}:0x#{object_id.to_s(16)} id=#{@thread_id} pid=#{@pid}" + str << " source=#{@source}" if @source + str << " status=#{status || "dead"}" + str << ">" + end + + alias_method :to_s, :inspect + + def alive? + Rubinius::Mirror.reflect(self).alive? + end + + def stop? + Rubinius::Mirror.reflect(self).stop? + end + + def status + Rubinius::Mirror.reflect(self).status + end + + def join(timeout=undefined) + if undefined.equal? timeout or nil.equal? timeout + timeout = nil + else + timeout = Rubinius::Type.coerce_to_float timeout + end + + value = Rubinius.invoke_primitive :thread_join, self, timeout + + if @exception + Kernel.raise @exception + else + value + end + end + + def group + @group + end + + def raise(exc=undefined, msg=nil, trace=nil) + return self unless alive? + + if exc.respond_to? :exception + exc = exc.exception msg + Kernel.raise TypeError, 'exception class/object expected' unless Exception === exc + exc.set_backtrace trace if trace + elsif undefined.equal? exc + exc = RuntimeError.exception nil + elsif exc.kind_of? String + exc = RuntimeError.exception exc + else + Kernel.raise TypeError, 'exception class/object expected' + end + + if $DEBUG + STDERR.puts "Exception: #{exc.message} (#{exc.class})" + end + + if self == Thread.current + Kernel.raise exc + else + Rubinius.invoke_primitive :thread_raise, self, exc + end + end + + def thread_variable_get(key) + key = Rubinius::Type.coerce_to_symbol key + Rubinius.invoke_primitive :thread_variable_get, self, key + end + + def thread_variable_set(key, value) + key = Rubinius::Type.coerce_to_symbol key + Rubinius.invoke_primitive :thread_variable_set, self, key, value + end + + def thread_variable?(key) + key = Rubinius::Type.coerce_to_symbol key + Rubinius.invoke_primitive :thread_variable_key_p, self, key + end + + def thread_variables + Rubinius.primitive :thread_variables + raise PrimitiveFailure, "Thread#thread_variables primitive failed" + end + + def [](key) + key = Rubinius::Type.coerce_to_symbol key + Rubinius.invoke_primitive :thread_fiber_variable_get, self, key + end + + def []=(key, value) + key = Rubinius::Type.coerce_to_symbol key + Rubinius.invoke_primitive :thread_fiber_variable_set, self, key, value + end + + def key?(key) + key = Rubinius::Type.coerce_to_symbol key + Rubinius.invoke_primitive :thread_fiber_variable_key_p, self, key + end + + def keys + Rubinius.primitive :thread_fiber_variables + raise PrimitiveFailure, "Thread#keys primitive failed" + end + + def self.main + @main_thread + end + + def self.initialize_main_thread(thread) + @main_thread = thread + end + + def self.exit + Thread.current.kill + end + + def self.kill(thread) + thread.kill + end + + alias_method :run, :wakeup + + def kill + Rubinius.invoke_primitive :thread_kill, self + end + + alias_method :exit, :kill + alias_method :terminate, :kill + + def value + join + @value + end + + def self.exclusive + MUTEX_FOR_THREAD_EXCLUSIVE.synchronize { yield } + end + + # Implementation note: ideally, the recursive_objects + # lookup table would be different per method call. + # Currently it doesn't cause problems, but if ever + # a method :foo calls a method :bar which could + # recurse back to :foo, it could require making + # the tables independant. + + def self.recursion_guard(obj) + id = obj.object_id + objects = current.recursive_objects + objects[id] = true + + begin + yield + ensure + objects.delete id + end + end + + def self.guarding?(obj) + current.recursive_objects[obj.object_id] + end + + # detect_recursion will return if there's a recursion + # on obj (or the pair obj+paired_obj). + # If there is one, it returns true. + # Otherwise, it will yield once and return false. + + def self.detect_recursion(obj, paired_obj=undefined) + id = obj.object_id + pair_id = paired_obj.object_id + objects = current.recursive_objects + + case objects[id] + + # Default case, we haven't seen +obj+ yet, so we add it and run the block. + when nil + objects[id] = pair_id + begin + yield + ensure + objects.delete id + end + + # We've seen +obj+ before and it's got multiple paired objects associated + # with it, so check the pair and yield if there is no recursion. + when Rubinius::LookupTable + return true if objects[id][pair_id] + objects[id][pair_id] = true + + begin + yield + ensure + objects[id].delete pair_id + end + + # We've seen +obj+ with one paired object, so check the stored one for + # recursion. + # + # This promotes the value to a LookupTable since there is another new paired + # object. + else + previous = objects[id] + return true if previous == pair_id + + objects[id] = Rubinius::LookupTable.new(previous => true, pair_id => true) + + begin + yield + ensure + objects[id] = previous + end + end + + false + end + + # Similar to detect_recursion, but will short circuit all inner recursion + # levels (using a throw) + + class InnerRecursionDetected < Exception; end + + def self.detect_outermost_recursion(obj, paired_obj=undefined, &block) + rec = current.recursive_objects + + if rec[:__detect_outermost_recursion__] + if detect_recursion(obj, paired_obj, &block) + raise InnerRecursionDetected + end + false + else + begin + rec[:__detect_outermost_recursion__] = true + + begin + detect_recursion(obj, paired_obj, &block) + rescue InnerRecursionDetected + return true + end + + return nil + ensure + rec.delete :__detect_outermost_recursion__ + end + end + end + + def randomizer + @randomizer ||= Rubinius::Randomizer.new + end + + def backtrace + mri_backtrace.map do |tup| + code = tup[0] + line = tup[1] + is_block = tup[2] + name = tup[3] + + "#{code.active_path}:#{line}:in `#{name}'" + end + end + + # This is a class in MRI, although you can't initialize it. + class Backtrace + # Stores location of a single call frame, available since Ruby 2.0. + class Location + attr_reader :label + attr_reader :path + attr_reader :absolute_path + attr_reader :lineno + + def initialize(label, absolute_path, path, lineno) + @label = label + @absolute_path = absolute_path + @path = path + @lineno = lineno + end + + private :initialize + + alias_method :base_label, :label + + def to_s + "#{absolute_path}:#{lineno}:in `#{label}'" + end + + def inspect + to_s + end + end + end +end diff --git a/gxg/core/thread_group.rb b/gxg/core/thread_group.rb new file mode 100644 index 0000000..e343afc --- /dev/null +++ b/gxg/core/thread_group.rb @@ -0,0 +1,39 @@ +class ThreadGroup + def initialize + @threads = [] + @enclosed = false + end + + private :initialize + + Default = ThreadGroup.new + + def add(thread) + if g = thread.group + raise ThreadError, "can't move from the enclosed thread group" if g.enclosed? + + gm = Rubinius::Mirror.reflect g + gm.remove thread + end + + tm = Rubinius::Mirror.reflect thread + tm.group = self + + Rubinius.synchronize(@threads) { @threads << thread } + + self + end + + def list + @threads + end + + def enclose + @enclosed = true + self + end + + def enclosed? + @enclosed + end +end diff --git a/gxg/core/thread_group_mirror.rb b/gxg/core/thread_group_mirror.rb new file mode 100644 index 0000000..ab53c22 --- /dev/null +++ b/gxg/core/thread_group_mirror.rb @@ -0,0 +1,12 @@ +module Rubinius + class Mirror + class ThreadGroup < Mirror + self.subject = ::ThreadGroup + + def remove(thread) + ary = Rubinius.invoke_primitive :object_get_ivar, @object, :@threads + Rubinius.synchronize(ary) { ary.delete thread } + end + end + end +end diff --git a/gxg/core/thread_mirror.rb b/gxg/core/thread_mirror.rb new file mode 100644 index 0000000..cff24ef --- /dev/null +++ b/gxg/core/thread_mirror.rb @@ -0,0 +1,54 @@ +module Rubinius + class Mirror + class Thread < Mirror + StatusRun = 1 + StatusSleep = 2 + StatusDead = 3 + StatusException = 4 + + def group=(group) + Rubinius.invoke_primitive :object_set_ivar, @object, :@group, group + end + + def thread_status + Rubinius.invoke_primitive :thread_status, @object + end + + def alive? + s = thread_status + s == StatusRun or s == StatusSleep + end + + def stop? + thread_status != StatusRun + end + + def status + case thread_status + when StatusRun + "run" + when StatusSleep + "sleep" + when StatusDead + false + when StatusException + nil + end + end + + def finish + Rubinius::Mirror.reflect(@object.group).remove @object + + if exception = @object.exception + if $DEBUG + STDERR.puts "Exception in thread: #{exception.message} (#{exception.class})" + end + + if @object.abort_on_exception or ::Thread.abort_on_exception + ::Thread.main.raise exception + end + end + end + end + end +end diff --git a/gxg/core/throw_catch.rb b/gxg/core/throw_catch.rb new file mode 100644 index 0000000..e116598 --- /dev/null +++ b/gxg/core/throw_catch.rb @@ -0,0 +1,23 @@ +module Rubinius + module ThrownValue + def self.register(obj) + cur = (Thread.current[:__catches__] ||= []) + cur << obj + + begin + yield + ensure + cur.pop + end + end + + def self.available?(obj) + cur = Thread.current[:__catches__] + return false unless cur + cur.each do |c| + return true if Rubinius::Type.object_equal(c, obj) + end + false + end + end +end diff --git a/gxg/core/thunk.rb b/gxg/core/thunk.rb new file mode 100644 index 0000000..3f1f475 --- /dev/null +++ b/gxg/core/thunk.rb @@ -0,0 +1,43 @@ +module Rubinius + class Thunk + def self.new(value) + Rubinius.primitive :thunk_create + raise PrimitiveFailure, "Thunk.new primitive failed" + end + + attr_accessor :value + end + + class CallUnit + def self.constant_value(obj) + Rubinius.primitive :callunit_constant_value + raise PrimitiveFailure, "CallUnit.constant_value primitive failed" + end + + def self.for_method_parts(mod, exec, name) + Rubinius.primitive :callunit_for_method_parts + raise PrimitiveFailure, "CallUnit.for_method_parts primitive failed" + end + + def self.for_method(mo) + for_method_parts mo.defined_in, mo.executable, mo.name.to_sym + end + + def self.test(cond, c_then, c_else) + Rubinius.primitive :callunit_test + raise PrimitiveFailure, "CallUnit.test primitive failed" + end + + def self.test_kind_of(mod, which) + Rubinius.primitive :callunit_kind_of + raise PrimitiveFailure, "CallUnit.test_kind_of primitive failed" + end + end + + class CallUnitAdapter + def self.new(unit) + Rubinius.primitive :callunitadapter_create + raise PrimitiveFailure, "CallUnitAdapter.new primitive failed" + end + end +end diff --git a/gxg/core/time.rb b/gxg/core/time.rb new file mode 100644 index 0000000..07c7f25 --- /dev/null +++ b/gxg/core/time.rb @@ -0,0 +1,520 @@ +class Time + include Comparable + + def self.now + Rubinius.primitive :time_s_now + raise PrimitiveFailure, "Time.now primitive failed" + end + + def self.duplicate(other) + Rubinius.primitive :time_s_dup + raise ArgumentError, "descriptors reference invalid time" + end + + def self.specific(sec, nsec, from_gmt, offset) + Rubinius.primitive :time_s_specific + raise ArgumentError, "descriptors reference invalid time" + end + + def dup + self.class.duplicate(self) + end + + def seconds + Rubinius.primitive :time_seconds + raise PrimitiveFailure, "Time#second primitive failed" + end + + def usec + Rubinius.primitive :time_useconds + raise PrimitiveFailure, "Time#usec primitive failed" + end + + def to_a + Rubinius.primitive :time_decompose + raise PrimitiveFailure, "Time#to_a primitive failed" + end + + def strftime(format) + Rubinius.primitive :time_strftime + raise PrimitiveFailure, "Time#strftime primitive failed" + end + + def self.at(sec, usec=undefined) + if undefined.equal?(usec) + if sec.kind_of?(Time) + return duplicate(sec) + elsif sec.kind_of?(Integer) + return specific(sec, 0, false, nil) + end + end + + if sec.kind_of?(Time) && usec.kind_of?(Integer) + raise TypeError, "can't convert Time into an exact number" + end + + usec = 0 if undefined.equal?(usec) + + s = Rubinius::Type.coerce_to_exact_num(sec) + u = Rubinius::Type.coerce_to_exact_num(usec) + + sec = s.to_i + nsec_frac = s % 1.0 + + sec -= 1 if s < 0 && nsec_frac > 0 + nsec = (nsec_frac * 1_000_000_000 + 0.5).to_i + (u * 1000).to_i + + sec += nsec / 1_000_000_000 + nsec %= 1_000_000_000 + + specific(sec, nsec, false, nil) + end + + def self.from_array(sec, min, hour, mday, month, year, nsec, is_dst, from_gmt, utc_offset) + Rubinius.primitive :time_s_from_array + + if sec.kind_of?(String) + sec = sec.to_i + elsif nsec + sec = Rubinius::Type.coerce_to(sec || 0, Integer, :to_int) + else + s = Rubinius::Type.coerce_to_exact_num(sec || 0) + + sec = s.to_i + nsec_frac = s % 1.0 + + if s < 0 && nsec_frac > 0 + sec -= 1 + end + + nsec = (nsec_frac * 1_000_000_000 + 0.5).to_i + end + + nsec ||= 0 + sec += nsec / 1_000_000_000 + nsec %= 1_000_000_000 + + if utc_offset + utc_offset_sec = utc_offset.to_i + utc_offset_nsec = ((utc_offset % 1.0) * 1_000_000_000 + 0.5).to_i + else + utc_offset_sec = 0 + utc_offset_nsec = 0 + end + + from_array(sec, min, hour, mday, month, year, nsec, is_dst, from_gmt, utc_offset, utc_offset_sec, utc_offset_nsec) + end + + def self.new(year=undefined, month=nil, day=nil, hour=nil, minute=nil, second=nil, utc_offset=nil) + if undefined.equal?(year) + now + elsif utc_offset == nil + compose(:local, year, month, day, hour, minute, second) + else + compose(Rubinius::Type.coerce_to_utc_offset(utc_offset), year, month, day, hour, minute, second) + end + end + + def inspect + if @is_gmt + str = strftime("%Y-%m-%d %H:%M:%S UTC") + else + str = strftime("%Y-%m-%d %H:%M:%S %z") + end + + str.force_encoding Encoding::US_ASCII + end + + alias_method :to_s, :inspect + + def nsec + Rubinius.primitive :time_nseconds + raise PrimitiveFailure, "Time#nsec primitive failed" + end + + def nsec=(nanoseconds) + Rubinius.primitive :time_set_nseconds + raise PrimitiveFailure, "Time#nsec= primitive failed" + end + private :nsec= + + alias_method :tv_nsec, :nsec + + def subsec + if nsec == 0 + 0 + else + Rational(nsec, 1_000_000_000) + end + end + + def sunday? + wday == 0 + end + + def monday? + wday == 1 + end + + def tuesday? + wday == 2 + end + + def wednesday? + wday == 3 + end + + def thursday? + wday == 4 + end + + def friday? + wday == 5 + end + + def saturday? + wday == 6 + end + + def to_r + (seconds + subsec).to_r + end + + def to_f + to_r.to_f + end + + def +(other) + raise TypeError, 'time + time?' if other.kind_of?(Time) + + case other = Rubinius::Type.coerce_to_exact_num(other) + when Integer + other_sec = other + other_nsec = 0 + else + other_sec, nsec_frac = other.divmod(1) + other_nsec = (nsec_frac * 1_000_000_000).to_i + end + + # Don't use self.class, MRI doesn't honor subclasses here + Time.specific(seconds + other_sec, nsec + other_nsec, @is_gmt, @offset) + end + + def -(other) + if other.kind_of?(Time) + return (seconds - other.seconds) + ((nsec - other.nsec) * 0.000000001) + end + + case other = Rubinius::Type.coerce_to_exact_num(other) + when Integer + other_sec = other + other_nsec = 0 + else + other_sec, nsec_frac = other.divmod(1) + other_nsec = (nsec_frac * 1_000_000_000 + 0.5).to_i + end + + # Don't use self.class, MRI doesn't honor subclasses here + Time.specific(seconds - other_sec, nsec - other_nsec, @is_gmt, @offset) + end + + def localtime(offset=nil) + @is_gmt = false + @offset = nil + @decomposed = nil + + if offset + @offset = Rubinius::Type.coerce_to_utc_offset(offset) + @zone = nil + else + @offset = gmt_offset + @zone = Rubinius.invoke_primitive(:time_env_zone, self) + end + + self + end + + def getlocal(offset=nil) + dup.localtime(offset) + end + + def eql?(other) + other.kind_of?(Time) and seconds == other.seconds and nsec == other.nsec + end + + def <=>(other) + if other.kind_of? Time + (seconds <=> other.seconds).nonzero? or (nsec <=> other.nsec) + else + r = (other <=> self) + return nil if r == nil + return -1 if r > 0 + return 1 if r < 0 + 0 + end + end + + # + # Rounds sub seconds to a given precision in decimal digits + # + # Returns a new time object. + # + # places should be nonnegative, it is 0 by default. + # + def round(places = 0) + return dup if nsec == 0 + + roundable_time = (to_i + subsec.to_r).round(places) + + sec = roundable_time.floor + nano = ((roundable_time - sec) * 1_000_000_000).floor + + Time.specific(sec, nano, @is_gmt, @offset) + end + + class << self + def compose_deal_with_year(year) + year + end + private :compose_deal_with_year + end + + #-- + # TODO: doesn't load ivars + #++ + + def self._load(data) + raise TypeError, 'marshaled time format differ' unless data.bytesize == 8 + + major, minor = data.unpack 'VV' + + if (major & (1 << 31)) == 0 + at major, minor + else + major &= ~(1 << 31) + + is_gmt = (major >> 30) & 0x1 + year = ((major >> 14) & 0xffff) + 1900 + mon = ((major >> 10) & 0xf) + 1 + mday = (major >> 5) & 0x1f + hour = major & 0x1f + + min = (minor >> 26) & 0x3f + sec = (minor >> 20) & 0x3f + isdst = false + + usec = minor & 0xfffff + + time = gm year, mon, mday, hour, min, sec, usec + time.localtime if is_gmt.zero? + time + end + end + + class << self + private :_load + end + + #-- + # TODO: doesn't dump ivars + #++ + + def _dump(limit = nil) + tm = getgm.to_a + + if (year & 0xffff) != year || year < 1900 + raise ArgumentError, "year too big to marshal: #{year}" + end + + gmt = @is_gmt ? 1 : 0 + + major = 1 << 31 | # 1 bit + gmt << 30 | # 1 bit + (tm[5] - 1900) << 14 | # 16 bits + (tm[4] - 1) << 10 | # 4 bits + tm[3] << 5 | # 5 bits + tm[2] # 5 bits + minor = tm[1] << 26 | # 6 bits + tm[0] << 20 | # 6 bits + usec # 20 bits + + [major, minor].pack 'VV' + end + + private :_dump + + def self.compose(offset, p1, p2=nil, p3=nil, p4=nil, p5=nil, p6=nil, p7=nil, + yday=undefined, is_dst=undefined, tz=undefined) + if undefined.equal?(tz) + unless undefined.equal?(is_dst) + raise ArgumentError, "wrong number of arguments (9 for 1..8)" + end + + y = p1 + m = p2 + d = p3 + hr = p4 + min = p5 + sec = p6 + usec = p7 + is_dst = -1 + else + y = p6 + m = p5 + d = p4 + hr = p3 + min = p2 + sec = p1 + usec = 0 + is_dst = is_dst ? 1 : 0 + end + + if m.kind_of?(String) or m.respond_to?(:to_str) + m = StringValue(m) + m = MonthValue[m.upcase] || m.to_i + + raise ArgumentError, "month argument out of range" unless m + else + m = Rubinius::Type.coerce_to(m || 1, Integer, :to_int) + end + + y = y.kind_of?(String) ? y.to_i : Rubinius::Type.coerce_to(y, Integer, :to_int) + d = d.kind_of?(String) ? d.to_i : Rubinius::Type.coerce_to(d || 1, Integer, :to_int) + hr = hr.kind_of?(String) ? hr.to_i : Rubinius::Type.coerce_to(hr || 0, Integer, :to_int) + min = min.kind_of?(String) ? min.to_i : Rubinius::Type.coerce_to(min || 0, Integer, :to_int) + + nsec = nil + + if usec.kind_of?(String) + nsec = usec.to_i * 1000 + elsif usec + nsec = (usec * 1000).to_i + end + + y = compose_deal_with_year(y) + + case offset + when :utc + is_dst = -1 + is_utc = true + offset = nil + when :local + is_utc = false + offset = nil + else + is_dst = -1 + is_utc = false + end + + from_array(sec, min, hr, d, m, y, nsec, is_dst, is_utc, offset) + end + + def self.local(*args) + compose(:local, *args) + end + + def self.gm(*args) + compose(:utc, *args) + end + + def succ + self + 1 + end + + def asctime + strftime("%a %b %e %H:%M:%S %Y") + end + + def sec + to_a[0] + end + + def min + to_a[1] + end + + def hour + to_a[2] + end + + def day + to_a[3] + end + + def mon + to_a[4] + end + + def year + to_a[5] + end + + def wday + to_a[6] + end + + def yday + to_a[7] + end + + def dst? + to_a[8] + end + + def zone + zone = to_a[9] + + if zone && Encoding.default_internal + zone.encode Encoding.default_internal + else + zone + end + end + + def gmt? + @is_gmt + end + + alias_method :to_i, :seconds + + def gmt_offset + Rubinius.primitive :time_utc_offset + raise PrimitiveFailure, "Time#gmt_offset primitive failed" + end + + def gmtime + unless @is_gmt + @is_gmt = true + @offset = nil + @decomposed = nil + end + + self + end + + def getgm + dup.gmtime + end + + def hash + seconds ^ usec + end + + class << self + alias_method :mktime, :local + alias_method :utc, :gm + end + + alias_method :utc?, :gmt? + alias_method :month, :mon + alias_method :ctime, :asctime + alias_method :mday, :day + alias_method :to_i, :seconds + alias_method :tv_sec, :seconds + alias_method :tv_usec, :usec + alias_method :utc, :gmtime + alias_method :isdst, :dst? + alias_method :utc_offset, :gmt_offset + alias_method :gmtoff, :gmt_offset + alias_method :getutc, :getgm +end diff --git a/gxg/core/true.rb b/gxg/core/true.rb new file mode 100644 index 0000000..81d6aac --- /dev/null +++ b/gxg/core/true.rb @@ -0,0 +1,25 @@ +class TrueClass + def &(other) + !!other + end + + def ^(other) + !other + end + + def |(other) + true + end + + def to_s + "true" + end + + def inspect + # Call #to_s rather than alias it so that people can change #to_s if they + # wish. Yes, sounds dumb. + to_s + end +end + +TRUE = true diff --git a/gxg/core/tuple.rb b/gxg/core/tuple.rb new file mode 100644 index 0000000..3f78ff1 --- /dev/null +++ b/gxg/core/tuple.rb @@ -0,0 +1,355 @@ +module Rubinius + class Tuple + include Enumerable + + def self.new(cnt) + Rubinius.primitive :tuple_allocate + raise PrimitiveFailure, "Tuple.new primitive failed" + end + + def self.pattern(size, obj) + Rubinius.primitive :tuple_pattern + raise PrimitiveFailure, "Tuple.pattern primitive failed" + end + + def [](idx) + Rubinius.primitive :tuple_at + + unless idx.kind_of? Fixnum + raise TypeError, "Only Fixnums valid for Tuple indices" + end + + raise PrimitiveFailure, "Tuple#[] primitive failed" + end + + alias_method :at, :[] + + def []=(idx, val) + Rubinius.primitive :tuple_put + + unless idx.kind_of? Fixnum + raise TypeError, "Only Fixnums valid for Tuple indices" + end + raise PrimitiveFailure, "Tuple#[]= primitive failed" + end + + alias_method :put, :[]= + + def fields + Rubinius.primitive :tuple_fields + raise PrimitiveFailure, "Tuple#fields primitive failed" + end + + def copy_from(other, start, length, dest) + Rubinius.primitive :tuple_copy_from + + unless other.kind_of? Rubinius::Tuple + raise TypeError, "Tuple#copy_from was expecting a Tuple, not a #{other.class}" + end + start = Rubinius::Type.coerce_to start, Fixnum, :to_i + length = Rubinius::Type.coerce_to length, Fixnum, :to_i + dest = Rubinius::Type.coerce_to dest, Fixnum, :to_i + + if start < 0 || start > other.fields + raise IndexError, "Start %d is out of bounds %d" % [start, other.fields] + end + + if dest < 0 || dest > self.fields + raise IndexError, "Destination %d is out of bounds %d" % [dest, self.fields] + end + + if length < 0 + raise IndexError, "length %d must be positive" % [length] + end + + if (start + length) > other.fields + raise IndexError, "end index %d can not exceed size of source %d" % [start+length, other.fields] + end + + if length > (self.fields - dest) + raise IndexError, "length %d can not exceed space %d in destination" % [length, self.fields - dest] + end + + src = start + dst = dest + while src < (start + length) + put dst, other.at(src) + src += 1 + dst += 1 + end + + self + end + + def dup + Rubinius.primitive :tuple_dup + + obj = Rubinius::Type.object_class(self).new(self.size) + + Rubinius.invoke_primitive :object_copy_object, obj, self + + Rubinius.privately do + obj.initialize_copy self + end + + obj + end + + def delete(start, length, object) + Rubinius.primitive :tuple_delete_inplace + + start = Rubinius::Type.coerce_to start, Fixnum, :to_i + length = Rubinius::Type.coerce_to length, Fixnum, :to_i + + lend = start + rend = lend + length + + return 0 if self.fields == 0 + if lend < 0 || lend >= self.fields + raise IndexError, "Index %d is not within Tuple of length %d" % [lend, self.fields] + end + + if rend < 0 || rend > self.fields + raise IndexError, "Index %d is not within Tuple of length %d" % [rend, self.fields] + end + + i = lend + while i < rend + if at(i) == object + j = i + i += 1 + while i < rend + val = at(i) + if val != object + put j, val + j += 1 + end + i += 1 + end + + # cleanup all the remaining bins in the tuple + i = j + while i < rend + put i, nil + i += 1 + end + + return rend - j + end + i += 1 + end + + return 0 + end + + def reverse!(start, total) + Rubinius.primitive :tuple_reverse + + reverse( + Rubinius::Type.coerce_to(start, Fixnum, :to_i), + Rubinius::Type.coerce_to(total, Fixnum, :to_i)) + end + + def delete_at_index(index) + return if size == 1 + + s = size - 1 + t = self.class.new s + + if index != 0 + t.copy_from self, 0, index, 0 + end + + if index < s + t.copy_from self, index+1, s-index, index + end + + t + end + + def insert_at_index(index, value) + t = self.class.new size + 1 + + if index != 0 + t.copy_from self, 0, index, 0 + end + + if index < size + t.copy_from self, index, size-index, index+1 + end + + t[index] = value + + t + end + + def self.[](*args) + args.to_tuple + end + + def to_s + "#<#{self.class}:0x#{object_id.to_s(16)} #{fields} elements>" + end + + def each + i = 0 + t = fields + while i < t + yield at(i) + i += 1 + end + self + end + + def ==(tup) + return super unless tup.kind_of?(Tuple) + + t = fields() + + return false unless t == tup.size + + i = 0 + while i < t + return false unless at(i) == tup.at(i) + i += 1 + end + + return true + end + + def +(o) + t = Tuple.new(size + o.size) + t.copy_from(self, 0, size, 0) + t.copy_from(o, 0, o.size, size) + t + end + + def inspect + str = "#<#{self.class}" + if fields == 0 + str << " empty>" + else + str << ": #{join(", ", :inspect)}>" + end + return str + end + + def join(sep, meth=:to_s) + join_upto(sep, fields, meth) + end + + def join_upto(sep, count, meth=:to_s) + str = "" + return str if count == 0 or empty? + + count = fields if count >= fields + count -= 1 + i = 0 + while i < count + str.append at(i).__send__(meth) + str.append sep.dup + i += 1 + end + + str.append at(count).__send__(meth) + return str + end + + def ===(other) + return false unless Tuple === other and fields == other.fields + i = 0 + while i < fields + return false unless at(i) === other.at(i) + i += 1 + end + true + end + + def to_a + array = Array.allocate + + m = Rubinius::Mirror.reflect array + m.tuple = dup + m.total = fields + + array + end + + alias_method :to_ary, :to_a + + def shift + return self unless fields > 0 + t = Tuple.new(fields-1) + t.copy_from self, 1, fields-1, 0 + return t + end + + # Swap elements of the two indexes. + def swap(a, b) + temp = at(a) + put a, at(b) + put b, temp + end + + alias_method :size, :fields + alias_method :length, :fields + + def empty? + size == 0 + end + + def first + at(0) + end + + def last + at(fields - 1) + end + + # Marshal support - _dump / _load are deprecated so eventually we should figure + # out a better way. + def _dump(depth) + # TODO use depth + Marshal.dump to_a + end + + def self._load(str) + ary = Marshal.load(str) + t = new(ary.size) + ary.each_with_index { |obj, idx| t[idx] = obj } + return t + end + end + + class RTuple < Tuple + include Enumerable + + def self.new(cnt) + Rubinius.primitive :rtuple_allocate + raise PrimitiveFailure, "RTuple.new primitive failed" + end + + def [](idx) + Rubinius.primitive :rtuple_at + + unless idx.kind_of? Fixnum + raise TypeError, "Only Fixnums valid for RTuple indices" + end + + raise PrimitiveFailure, "RTuple#[] primitive failed" + end + + alias_method :at, :[] + + def []=(idx, val) + Rubinius.primitive :rtuple_put + + unless idx.kind_of? Fixnum + raise TypeError, "Only Fixnums valid for RTuple indices" + end + raise PrimitiveFailure, "RTuple#[]= primitive failed" + end + + alias_method :put, :[]= + end +end diff --git a/gxg/core/type.rb b/gxg/core/type.rb new file mode 100644 index 0000000..99e2e44 --- /dev/null +++ b/gxg/core/type.rb @@ -0,0 +1,794 @@ +# The Type module provides facilities for accessing various "type" related +# data about an object, as well as providing type coercion methods. These +# facilities are independent of the object and thus are more robust in the +# face of ad hoc monkeypatching. +module Rubinius + module Type + # Performs a direct kind_of? check on the object bypassing any method + # overrides. + def self.object_kind_of?(obj, cls) + Rubinius.primitive :vm_object_kind_of + raise PrimitiveFailure, "Rubinius::Type.object_kind_of? primitive failed" + end + + def self.object_class(obj) + Rubinius.primitive :vm_object_class + raise PrimitiveFailure, "Rubinius::Type.object_class primitive failed" + end + + def self.object_singleton_class(obj) + Rubinius.primitive :vm_object_singleton_class + raise TypeError, "no singleton class available for a #{Type.object_class(obj)}" + end + + def self.singleton_class_object(mod) + Rubinius.primitive :vm_singleton_class_object + raise PrimitiveFailure, "Rubinius::Type.singleton_class_object primitive failed" + end + + def self.object_instance_of?(obj, cls) + object_class(obj) == cls + end + + def self.object_respond_to?(obj, name, include_private = false) + Rubinius.invoke_primitive :vm_object_respond_to, obj, name, include_private + end + + def self.object_equal(a, b) + Rubinius.primitive :vm_object_equal + raise PrimitiveFailure, "Rubinius::Type.object_equal primitive failed" + end + + def self.module_name(mod) + Rubinius.primitive :vm_get_module_name + raise PrimitiveFailure, "Rubinius::Type.module_name primitive failed" + end + + def self.module_inspect(mod) + sc = singleton_class_object mod + + if sc + case sc + when Class, Module + name = "#" + else + cls = object_class sc + name = "#>" + end + else + name = module_name mod + if !name or name == "" + name = "#<#{object_class(mod)}:0x#{mod.object_id.to_s(16)}>" + end + end + + name + end + + def self.set_module_name(mod, name, under) + Rubinius.primitive :vm_set_module_name + raise PrimitiveFailure, "Rubinius::Type.set_module_name primitive failed" + end + + def self.coerce_inspect(obj) + Rubinius.asm do + push_local 0 + send :inspect, 0, true + object_to_s :to_s + end + end + + def self.coerce_string_to_float(string, strict) + value = Rubinius.invoke_primitive :string_to_f, string, strict + raise ArgumentError, "invalid string for Float" if value.nil? + value + end + + def self.coerce_to_array(obj) + return [obj] unless obj + + return Rubinius.privately { obj.to_a } if object_respond_to?(obj, :to_a, true) + return obj.to_ary if obj.respond_to?(:to_ary) + + # On 1.9, #to_a is not defined on all objects, so wrap the object in a + # literal array. + return [obj] + end + + def self.coerce_to_float(obj, strict=true, must_be_numeric=true) + if !must_be_numeric && object_kind_of?(obj, String) + return coerce_string_to_float(obj, strict) + end + + case obj + when Float + obj + when Numeric + coerce_to obj, Float, :to_f + when nil, true, false + raise TypeError, "can't convert #{obj.inspect} into Float" + else + raise TypeError, "can't convert #{obj.class} into Float" + end + end + + def self.coerce_object_to_float(obj) + case obj + when Float + obj + when nil + raise TypeError, "can't convert nil into Float" + when Complex + if obj.respond_to?(:imag) && obj.imag.equal?(0) + coerce_to obj, Float, :to_f + else + raise RangeError, "can't convert #{obj} into Float" + end + else + coerce_to obj, Float, :to_f + end + end + + def self.object_encoding(obj) + Rubinius.primitive :encoding_get_object_encoding + raise PrimitiveFailure, "Rubinius::Type.object_encoding primitive failed" + end + + ## + # Returns an object of given class. If given object already is one, it is + # returned. Otherwise tries obj.meth and returns the result if it is of the + # right kind. TypeErrors are raised if the conversion method fails or the + # conversion result is wrong. + # + # Uses Rubinius::Type.object_kind_of to bypass type check overrides. + # + # Equivalent to MRI's rb_convert_type(). + + def self.coerce_to(obj, cls, meth) + return obj if object_kind_of?(obj, cls) + execute_coerce_to(obj, cls, meth) + end + + def self.execute_coerce_to(obj, cls, meth) + begin + ret = obj.__send__(meth) + rescue Exception => orig + coerce_to_failed obj, cls, meth, orig + end + + return ret if object_kind_of?(ret, cls) + + coerce_to_type_error obj, ret, meth, cls + end + + def self.coerce_to_failed(object, klass, method, exc=nil) + if object_respond_to? object, :inspect + raise TypeError, + "Coercion error: #{object.inspect}.#{method} => #{klass} failed", + exc + else + raise TypeError, + "Coercion error: #{method} => #{klass} failed", + exc + end + end + + def self.coerce_to_type_error(original, converted, method, klass) + oc = object_class original + cc = object_class converted + msg = "failed to convert #{oc} to #{klass}: #{oc}\##{method} returned #{cc}" + raise TypeError, msg + end + + ## + # Same as coerce_to but returns nil if conversion fails. + # Corresponds to MRI's rb_check_convert_type() + # + def self.check_convert_type(obj, cls, meth) + return obj if object_kind_of?(obj, cls) + return nil unless object_respond_to?(obj, meth, true) + execute_check_convert_type(obj, cls, meth) + end + + def self.execute_check_convert_type(obj, cls, meth) + begin + ret = obj.__send__(meth) + rescue Exception + return nil + end + + return ret if ret.nil? || object_kind_of?(ret, cls) + + msg = "Coercion error: obj.#{meth} did NOT return a #{cls} (was #{object_class(ret)})" + raise TypeError, msg + end + + ## + # Uses the logic of [Array, Hash, String].try_convert. + # + def self.try_convert(obj, cls, meth) + return obj if object_kind_of?(obj, cls) + return nil unless obj.respond_to?(meth) + execute_try_convert(obj, cls, meth) + end + + def self.execute_try_convert(obj, cls, meth) + ret = obj.__send__(meth) + + return ret if ret.nil? || object_kind_of?(ret, cls) + + msg = "Coercion error: obj.#{meth} did NOT return a #{cls} (was #{object_class(ret)})" + raise TypeError, msg + end + + def self.coerce_to_comparison(a, b) + unless cmp = (a <=> b) + raise ArgumentError, "comparison of #{a.inspect} with #{b.inspect} failed" + end + cmp + end + + def self.each_ancestor(mod) + sup = mod + while sup + if object_kind_of?(sup, IncludedModule) + yield sup.module + else + yield sup if sup == sup.origin + end + sup = sup.direct_superclass + end + end + + def self.coerce_to_constant_name(name) + name = Rubinius::Type.coerce_to_symbol(name) + + unless name.is_constant? + raise NameError, "wrong constant name #{name}" + end + + name + end + + def self.coerce_to_collection_index(index) + return index if object_kind_of? index, Fixnum + + method = :to_int + klass = Fixnum + + begin + idx = index.__send__ method + rescue Exception => exc + coerce_to_failed index, klass, method, exc + end + return idx if object_kind_of? idx, klass + + if object_kind_of? index, Bignum + raise RangeError, "Array index must be a Fixnum (passed Bignum)" + else + coerce_to_type_error index, idx, method, klass + end + end + + def self.coerce_to_collection_length(length) + return length if object_kind_of? length, Fixnum + + method = :to_int + klass = Fixnum + + begin + size = length.__send__ method + rescue Exception => exc + coerce_to_failed length, klass, method, exc + end + return size if object_kind_of? size, klass + + if object_kind_of? size, Bignum + raise ArgumentError, "Array size must be a Fixnum (passed Bignum)" + else + coerce_to_type_error length, size, :to_int, Fixnum + end + end + + def self.coerce_to_regexp(pattern, quote=false) + case pattern + when Regexp + return pattern + when String + # nothing + else + pattern = StringValue(pattern) + end + + pattern = Regexp.quote(pattern) if quote + Regexp.new(pattern) + end + + # Taint host if source is tainted. + def self.infect(host, source) + Rubinius.primitive :object_infect + raise PrimitiveFailure, "Object.infect primitive failed" + end + + def self.check_null_safe(string) + Rubinius.invoke_primitive(:string_check_null_safe, string) + end + + def self.const_lookup(mod, name, inherit, resolve) + parts = name.split '::' + + if name.start_with? '::' + mod = Object + parts.shift + end + + parts.each do |part| + mod = const_get mod, part, inherit, resolve + end + + mod + end + + def self.const_get(mod, name, inherit=true, resolve=true) + unless object_kind_of? name, Symbol + name = StringValue(name) + if name.index '::' and name.size > 2 + return const_lookup mod, name, inherit, resolve + end + end + + name = coerce_to_constant_name name + current = mod + constant = undefined + + while current and object_kind_of? current, Module + if bucket = current.constant_table.lookup(name) + constant = bucket.constant + + if object_kind_of? constant, Autoload + if resolve + return constant.call(current) + elsif not CodeLoader.loading? constant.path + return constant + else + return undefined + end + end + + return constant + end + + unless inherit + return resolve ? mod.const_missing(name) : undefined + end + + current = current.direct_superclass + end + + if object_instance_of? mod, Module + if bucket = Object.constant_table.lookup(name) + constant = bucket.constant + + if object_kind_of? constant, Autoload + if resolve + return constant.call(current) + elsif not CodeLoader.loading? constant.path + return constant + else + return undefined + end + end + + return constant + end + end + + resolve ? mod.const_missing(name) : undefined + end + + def self.constant_scope_defined?(scope, name) + current = scope + + until nil.equal? current + break if nil.equal? current.parent + + if bucket = current.module.constant_table.lookup(name) + constant = bucket.constant + + if object_kind_of? constant, Autoload + return constant.constant unless undefined.equal? constant.constant + end + + return constant + end + + current = current.parent + end + + check_object = true + object_seen = false + + unless nil.equal? scope + current = scope.module + + until nil.equal? current + object_seen = true if ::Object.equal? current + + if not object_seen and ::BasicObject.equal? current + check_object = false + end + + if bucket = current.constant_table.lookup(name) + constant = bucket.constant + + if object_kind_of? constant, Autoload + return undefined if CodeLoader.loading? constant.path + return constant.constant unless undefined.equal? constant.constant + end + + return constant + end + + current = current.direct_superclass + end + end + + if check_object + if bucket = Object.constant_table.lookup(name) + constant = bucket.constant + + if object_kind_of? constant, Autoload + return undefined if CodeLoader.loading? constant.path + return constant.constant unless undefined.equal? constant.constant + end + + return constant + end + end + + undefined + end + + def self.constant_path_defined?(mod, name) + current = mod + + begin + break unless object_kind_of? current, ::Module + + if bucket = current.constant_table.lookup(name) + return undefined if bucket.private? + + constant = bucket.constant + + if object_kind_of? constant, Autoload + return undefined if CodeLoader.loading? constant.path + return constant.constant unless undefined.equal? constant.constant + end + + return constant + end + end while current = current.direct_superclass + + undefined + end + + def self.const_exists?(mod, name, inherit = true) + name = coerce_to_constant_name name + + current = mod + + while current + if bucket = current.constant_table.lookup(name) + return !!bucket.constant + end + + return false unless inherit + + current = current.direct_superclass + end + + if instance_of?(Module) + if bucket = Object.constant_table.lookup(name) + return !!bucket.constant + end + end + + false + end + + def self.include_modules_from(included_module, klass) + insert_at = klass + constants_changed = false + + mod = included_module + + while mod + + # Check for a cyclic include + if mod == klass + raise ArgumentError, "cyclic include detected" + end + + if mod == mod.origin + # Try and detect check_mod in klass's heirarchy, and where. + # + # I (emp) tried to use Module#< here, but we need to also know + # where in the heirarchy the module is to change the insertion point. + # Since Module#< doesn't report that, we're going to just search directly. + # + superclass_seen = false + add = true + + k = klass.direct_superclass + while k + if k.kind_of? Rubinius::IncludedModule + # Oh, we found it. + if k == mod + # ok, if we're still within the directly included modules + # of klass, then put future things after mod, not at the + # beginning. + insert_at = k unless superclass_seen + add = false + break + end + else + superclass_seen = true + end + + k = k.direct_superclass + end + + if add + if mod.kind_of? Rubinius::IncludedModule + original_mod = mod.module + else + original_mod = mod + end + + included_module.method_table.each do |meth, obj, vis| + Rubinius::VM.reset_method_cache klass, meth + end + + im = Rubinius::IncludedModule.new(original_mod).attach_to insert_at + insert_at = im + end + + constants_changed ||= mod.constant_table.size > 0 + end + + mod = mod.direct_superclass + end + + if constants_changed + Rubinius.inc_global_serial + end + end + + def self.object_respond_to__dump?(obj) + object_respond_to? obj, :_dump + end + + def self.object_respond_to_marshal_dump?(obj) + object_respond_to? obj, :marshal_dump + end + + def self.object_respond_to_marshal_load?(obj) + object_respond_to? obj, :marshal_load + end + + def self.coerce_to_encoding(obj) + case obj + when Encoding + return obj + when String + return Encoding.find obj + else + return Encoding.find StringValue(obj) + end + end + + def self.try_convert_to_encoding(obj) + case obj + when Encoding + return obj + when String + str = obj + else + str = StringValue obj + end + + key = str.upcase.to_sym + + pair = Encoding::EncodingMap[key] + if pair + index = pair.last + return index && Encoding::EncodingList[index] + end + + return undefined + end + + def self.coerce_to_path(obj) + if object_kind_of?(obj, String) + obj + else + if object_respond_to? obj, :to_path + obj = obj.to_path + end + + StringValue(obj) + end + end + + def self.coerce_to_symbol(obj) + return obj if object_kind_of? obj, Symbol + + obj = obj.to_str if obj.respond_to?(:to_str) + coerce_to(obj, Symbol, :to_sym) + end + + def self.coerce_to_reflection_name(obj) + return obj if object_kind_of? obj, Symbol + return obj if object_kind_of? obj, String + coerce_to obj, String, :to_str + end + + def self.ivar_validate(name) + case name + when Symbol + # do nothing + when String + name = name.to_sym + else + name = Rubinius::Type.coerce_to(name, String, :to_str) + name = name.to_sym + end + + unless name.is_ivar? + raise NameError, "`#{name}' is not allowed as an instance variable name" + end + + name + end + + def self.coerce_to_binding(obj) + if obj.kind_of? Binding + binding = obj + elsif obj.kind_of? Proc + raise TypeError, 'wrong argument type Proc (expected Binding)' + elsif obj.respond_to? :to_binding + binding = obj.to_binding + else + binding = obj + end + + unless binding.kind_of? Binding + raise ArgumentError, "unknown type of binding" + end + + binding + end + + # Equivalent of num_exact in MRI's time.c; used by Time methods. + def self.coerce_to_exact_num(obj) + if obj.kind_of?(Integer) + obj + elsif obj.kind_of?(String) + raise TypeError, "can't convert #{obj} into an exact number" + elsif obj.nil? + raise TypeError, "can't convert nil into an exact number" + else + check_convert_type(obj, Rational, :to_r) || coerce_to(obj, Integer, :to_int) + end + end + + def self.coerce_to_utc_offset(offset) + offset = String.try_convert(offset) || offset + + if offset.kind_of?(String) + unless offset.encoding.ascii_compatible? && offset.match(/\A(\+|-)(\d\d):(\d\d)\z/) + raise ArgumentError, '"+HH:MM" or "-HH:MM" expected for utc_offset' + end + + offset = $2.to_i*60*60 + $3.to_i*60 + offset = -offset if $1.ord == 45 + else + offset = Rubinius::Type.coerce_to_exact_num(offset) + end + + if offset <= -86400 || offset >= 86400 + raise ArgumentError, "utc_offset out of range" + end + + offset + end + + def self.coerce_to_pid(obj) + Rubinius::Type.coerce_to obj, Integer, :to_int + end + + def self.coerce_to_bitwise_operand(obj) + unless object_kind_of? obj, Integer + raise TypeError, "can't convert non-Integer into Integer for bitwise arithmetic" + end + coerce_to obj, Integer, :to_int + end + + def self.object_initialize_dup(obj, copy) + Rubinius.privately do + copy.initialize_dup obj + end + end + + def self.object_initialize_clone(obj, copy) + Rubinius.privately do + copy.initialize_clone obj + end + end + + def self.object_respond_to_ary?(obj) + object_respond_to?(obj, :to_ary, true) + end + + def self.binary_string(string) + string.force_encoding Encoding::BINARY + end + + def self.external_string(string) + string.force_encoding Encoding.default_external + end + + def self.encode_string(string, enc) + string.force_encoding enc + end + + def self.ascii_compatible_encoding(string) + compatible_encoding string, Encoding::US_ASCII + end + + def self.compatible_encoding(a, b) + enc = Encoding.compatible? a, b + + unless enc + enc_a = object_encoding a + enc_b = object_encoding b + message = "undefined conversion " + message << "for '#{a.inspect}' " if object_kind_of?(a, String) + message << "from #{enc_a} to #{enc_b}" + + raise Encoding::CompatibilityError, message + end + + enc + end + + def self.encoding_order(a, b) + index_a = Encoding::EncodingMap[a.name.upcase][1] + index_b = Encoding::EncodingMap[b.name.upcase][1] + index_a <=> index_b + end + + def self.object_respond_to__dump?(obj) + object_respond_to? obj, :_dump, true + end + + def self.object_respond_to_marshal_dump?(obj) + object_respond_to? obj, :marshal_dump, true + end + + def self.object_respond_to_marshal_load?(obj) + object_respond_to? obj, :marshal_load, true + end + + def self.bindable_method?(source, destination) + unless object_kind_of? source, Module or + object_kind_of? source, destination + if singleton_class_object source + raise TypeError, "illegal attempt to rebind a singleton method to another object" + end + + raise TypeError, "Must be bound to an object of kind #{source}" + end + end + end +end diff --git a/gxg/core/undefined.rb b/gxg/core/undefined.rb new file mode 100644 index 0000000..f01ced4 --- /dev/null +++ b/gxg/core/undefined.rb @@ -0,0 +1,7 @@ +class << undefined + def to_s + "undefined" + end + + alias_method :inspect, :to_s +end diff --git a/gxg/core/variable_scope.rb b/gxg/core/variable_scope.rb new file mode 100644 index 0000000..9959993 --- /dev/null +++ b/gxg/core/variable_scope.rb @@ -0,0 +1,236 @@ +module Rubinius + class VariableScope + # To handle Module#private, protected + attr_accessor :method_visibility + + attr_reader :parent + attr_reader :block + attr_reader :module + attr_reader :self + attr_reader :name + + # CompiledCode this scope is for. + # + attr_reader :method + + def self.of_sender + Rubinius.primitive :variable_scope_of_sender + raise PrimitiveFailure, "Rubinius::VariableScope.of_sender primitive failed" + end + + def self.current + Rubinius.primitive :variable_scope_current + raise PrimitiveFailure, "Rubinius::VariableScope.current primitive failed" + end + + def self.allocate + Rubinius.primitive :variable_scope_allocate + raise PrimitiveFailure, "Rubinius::VariableScope.allocate primitive failed" + end + + def self.synthesize(method, module_, parent, self_, block, locals) + Rubinius.primitive :variable_scope_synthesize + raise PrimitiveFailure, "Rubinius::VariableScope.synthesize primitive failed" + end + + def set_local(slot, obj) + Rubinius.primitive :variable_scope_set_local + raise PrimitiveFailure, "Rubinius::VariableScope#set_local primitive failed" + end + + def locals + Rubinius.primitive :variable_scope_locals + raise PrimitiveFailure, "Rubinius::VariableScope#locals primitive failed" + end + + def top_level_visibility? + Rubinius.primitive :variable_scope_top_level_visibility + raise PrimitiveFailure, "Rubinius::VariableScope#top_level_visibility primitive failed" + end + + def script? + Rubinius.primitive :variable_scope_script + raise PrimitiveFailure, "Rubinius::VariableScope#script primitive failed" + end + + def locked? + Rubinius.primitive :variable_scope_locked + raise PrimitiveFailure, "Rubinius::VariableScope#locked primitive failed" + end + + def locked! + Rubinius.primitive :variable_scope_set_locked + raise PrimitiveFailure, "Rubinius::VariableScope#set_locked primitive failed" + end + + def self=(obj) + @self = obj + end + + def has_dynamic_locals? + @dynamic_locals + end + + def dynamic_locals + @dynamic_locals ||= Rubinius::LookupTable.new + end + + def set_eval_local(name, val) + scope = @parent + while scope + if scope.dynamic_locals.key? name + scope.dynamic_locals[name] = val + return val + end + + scope = scope.parent + end + + # Otherwise, put it in the highest non-eval scope + if @parent + scope = @parent + while scope.parent and scope.method.for_eval? + scope = scope.parent + end + + scope.dynamic_locals[name] = val + else + # This happens when we run some confused compiled code. + # Typically when we use eval to create the code and then + # reuse it in a non-eval context. + dynamic_locals[name] = val + end + end + + def get_eval_local(name) + scope = self + while scope + if scope.has_dynamic_locals? && scope.dynamic_locals.key?(name) + return scope.dynamic_locals[name] + end + + scope = scope.parent + end + + nil + end + + def eval_local_defined?(name, search=true) + if search + scope = self + while scope + return true if scope.dynamic_locals.key? name + + scope = scope.parent + end + else + return true if @dynamic_locals and @dynamic_locals.key? name + end + + return false + end + + def local_defined?(name) + vars = self + while vars + return true if vars.method.local_names.include? name + return true if vars.eval_local_defined?(name, false) + vars = vars.parent + end + + false + end + + def local_layout + vars = self + out = [] + + while vars + meth = vars.method.local_names.to_a + eloc = vars.dynamic_locals.keys.map { |x| x.to_s }.sort + + out << [meth, eloc] + + vars = vars.parent + end + + out + end + + # Returns the names of local variables available in this scope + # + def local_variables + locals = [] + scope = self + + # Ascend up through all applicable blocks to get all vars. + while scope + if scope.method.local_names + scope.method.local_names.each do |name| + locals << name + end + end + + if dyn = scope.dynamic_locals + dyn.keys.each do |name| + locals << name unless locals.include?(name) + end + end + + scope = scope.parent + end + + locals + end + + def exitted? + @exitted + end + + def super_method_defined? + # Minor HACK __block__ as the name is ambigious. Need a better + # way to detect that a VS is for a define_method scope. + # + # This code is so that if this VS is for a block, we walk up + # to the VS for the actual method. It's complicated by the fact + # that there may be a VS for a define_method in there, so we need + # to stop looking if we hit one. + if @parent and @method.name == :__block__ + vars = self + + while vars.method.name == :__block__ and nxt = vars.parent + vars = nxt + end + + return vars.super_method_defined? + end + + if sup = @module.direct_superclass + # This will probably break for define_method + return sup.lookup_method(@method.name) != nil + end + + return false + end + + def method_visibility + return @method_visibility if @method_visibility + + # if this scope is for a script, and there is no method_visibility + # already set, then we default to private. + # + # This is so that a script body has it's visibility default to private. + + return :private if script? + return nil if top_level_visibility? + return @parent.method_visibility if @parent + + # The default, let the caller sort it out. + return nil + end + + def inspect + "#<#{self.class.name}:0x#{self.object_id.to_s(16)} module=#{@module} method=#{@method.inspect}>" + end + end +end diff --git a/gxg/core/vm.rb b/gxg/core/vm.rb new file mode 100644 index 0000000..f496607 --- /dev/null +++ b/gxg/core/vm.rb @@ -0,0 +1,34 @@ +class Rubinius::VM + # Get an Array of Rubinius::Location objects representing the current + # call stack. +frames_to_skip+ says how many initial frames to ignore. + # If +include_vars+ is set, then the Location objects also include a + # Rubinius::VariableScope object that can be used to access the frames + # running information like locals, self, etc. + # + def self.backtrace(frames_to_skip, *args) + Rubinius.invoke_primitive :vm_backtrace, Integer(frames_to_skip) + 1 + end + + def self.stats + Rubinius.primitive :vm_stats + raise PrimitiveFailure, "Rubinius::VM.stats primitive failed" + end + + def self.load_library(path, name) + Rubinius.primitive :load_library + raise PrimitiveFailure, "Rubinius::VM.load_library primitive failed" + end + + def self.coerce_to_array(object) + array = object.respond_to?(:to_a) ? object.to_a : [object] + raise(TypeError, "`to_a' did not return Array") unless array.is_a?(Array) + array + end + + # Semantics of this are very important. ret MUST be returned. + def self.perform_hook(obj, meth, arg, ret) + # The VM already verified the method exists + obj.__send__(meth, arg) + return ret + end +end diff --git a/gxg/core/weakref.rb b/gxg/core/weakref.rb new file mode 100644 index 0000000..4ec2be6 --- /dev/null +++ b/gxg/core/weakref.rb @@ -0,0 +1,43 @@ +class WeakRef < BasicObject + class RefError < ::RuntimeError; end + + def self.new(obj) + Rubinius.primitive :weakref_new + ::Kernel.raise PrimitiveFailure, "WeakRef.new primitive failed" + end + + def __setobj__(obj) + Rubinius.primitive :weakref_set_object + ::Kernel.raise PrimitiveFailure, "WeakRef#__setobj__ primitive failed" + end + + def __object__ + Rubinius.primitive :weakref_object + ::Kernel.raise PrimitiveFailure, "WeakRef#__object__ primitive failed" + end + + def __getobj__ + obj = __object__() + ::Kernel.raise RefError, "Object has been collected as garbage" unless obj + return obj + end + + def weakref_alive? + !!__object__ + end + + def method_missing(method, *args, &block) + target = __getobj__ + if target.respond_to?(method) + target.__send__(method, *args, &block) + else + super(method, *args, &block) + end + end + + def respond_to_missing?(method, include_private) + target = __getobj__ + target.respond_to?(method, include_private) and + (!include_private || target.respond_to?(method, false)) + end +end diff --git a/gxg/core/zed.rb b/gxg/core/zed.rb new file mode 100644 index 0000000..66641b1 --- /dev/null +++ b/gxg/core/zed.rb @@ -0,0 +1,1916 @@ +# TODO: Finish adding some layer structure to the core library. This file is a +# complete mess at the moment. +# +# A proper layered structure for the core library would enable and encourage +# composition and make visible where pieces are loosely or tightly coupled. A +# reasonable layer for something as complex as the Ruby core library would be: +# +# 1. Simple values like Fixnum, Bignum, Float; +# 2. Basic data structures like Tuple, ByteArray, Array, Hash; +# 3. More complicated values like String, Regexp, Complex, Rational; +# 4. Fairly simple system-dependent facilities like IO, File, Process; +# 5. Even more complex facilities like FFI; +# 6. Something like Socket that composes multiple lower layers; +# 7. A complex facility like CodeLoader or RubyGems. + + +# The virtual concatenation file of the files given on command line (or +# from $stdin if no files were given.) Usable like an IO. +ARGF = Rubinius::ARGFClass.new + +class Module + def alias_method(new_name, current_name) + new_name = Rubinius::Type.coerce_to_symbol(new_name) + current_name = Rubinius::Type.coerce_to_symbol(current_name) + mod, entry = lookup_method(current_name, true, false) + + if entry + Rubinius::VM.reset_method_cache self, new_name + + method_visibility = visibility_for_aliased_method(new_name, entry.visibility) + + # If we're aliasing a method we contain, just reference it directly, no + # need for the alias wrapper + # + # The 'and entry.get_method' is there to force us to use the alias code + # when the original method exists only to change the visibility of a + # parent method. + if mod == self and entry.get_method + @method_table.store new_name, entry.method_id, entry.method, + entry.scope, entry.serial, method_visibility + else + @method_table.alias new_name, method_visibility, current_name, + entry.get_method, mod + end + + if ai = Rubinius::Type.singleton_class_object(self) + Rubinius.privately do + ai.singleton_method_added new_name + end + else + method_added new_name + end + + self + else + if ai = Rubinius::Type.singleton_class_object(self) + raise NameError, "Unable to find '#{current_name}' for object #{ai.inspect}" + else + thing = Rubinius::Type.object_kind_of?(self, Class) ? "class" : "module" + raise NameError, "undefined method `#{current_name}' for #{thing} `#{self.name}'" + end + end + end + private :alias_method + + def const_set(name, value) + name = Rubinius::Type.coerce_to_constant_name name + Rubinius.check_frozen + + if Rubinius::Type.object_kind_of? value, Module + Rubinius::Type.set_module_name value, name, self + + if Object.equal? self + value.constant_table.each do |n, c, _| + if Rubinius::Type.object_kind_of? c, Module and !c.name + Rubinius::Type.set_module_name c, n, value + end + end + end + end + + @constant_table.store(name, value, :public) + Rubinius.inc_global_serial + + return value + end + + def module_function(*args) + if kind_of? Class + raise TypeError, "invalid receiver class #{__class__}, expected Module" + end + + if args.empty? + vs = Rubinius::VariableScope.of_sender + vs.method_visibility = :module + else + sc = Rubinius::Type.object_singleton_class(self) + args.each do |meth| + method_name = Rubinius::Type.coerce_to_symbol meth + + Rubinius::VM.reset_method_cache sc, method_name + + mod, entry = lookup_method(method_name) + sc.method_table.store method_name, entry.method_id, entry.method, + entry.scope, entry.serial, :public + set_visibility method_name, :private + end + end + + return self + end + private :module_function + + def public(*args) + if args.empty? + vs = Rubinius::VariableScope.of_sender + vs.method_visibility = nil + else + args.each { |meth| set_visibility(meth, :public) } + end + + self + end + private :public + + def private(*args) + if args.empty? + vs = Rubinius::VariableScope.of_sender + vs.method_visibility = :private + else + args.each { |meth| set_visibility(meth, :private) } + end + + self + end + private :private + + def protected(*args) + if args.empty? + vs = Rubinius::VariableScope.of_sender + vs.method_visibility = :protected + else + args.each { |meth| set_visibility(meth, :protected) } + end + + self + end + private :protected + + def private_class_method(*args) + args.each do |meth| + set_class_visibility(meth, :private) + end + self + end + + def public_class_method(*args) + args.each do |meth| + set_class_visibility(meth, :public) + end + self + end + + # Invokes Module#append_features and + # Module#included on each argument, passing in self. + # + def include(*modules) + modules.reverse_each do |mod| + if !mod.kind_of?(Module) or mod.kind_of?(Class) + raise TypeError, "wrong argument type #{mod.class} (expected Module)" + end + + Rubinius.privately do + mod.append_features self + end + + Rubinius.privately do + mod.included self + end + end + self + end + + # Add all constants, instance methods and module variables + # of this Module and all Modules that this one includes to +klass+ + # + # This method is aliased as append_features as the default implementation + # for that method. Kernel#extend calls this method directly through + # Module#extend_object, because Kernel#extend should not use append_features. + def include_into(klass) + unless klass.kind_of? Module + raise TypeError, "invalid argument class #{klass.class}, expected Module" + end + + if kind_of? Class + raise TypeError, "invalid receiver class #{__class__}, expected Module" + end + + Rubinius::Type.include_modules_from(self, klass.origin) + Rubinius::Type.infect(klass, self) + + self + end + + # Called when this Module is being included in another Module. + # This may be overridden for custom behaviour. The default + # is to add constants, instance methods and module variables + # of this Module and all Modules that this one includes to +klass+. + # + # See also #include. + # + alias_method :append_features, :include_into + private :append_features + + def attr_reader(*names) + vis = Rubinius::VariableScope.of_sender.method_visibility + + names.each { |name| Rubinius.add_reader name, self, vis } + + return nil + end + private :attr_reader + + def attr_writer(*names) + vis = Rubinius::VariableScope.of_sender.method_visibility + + names.each { |name| Rubinius::add_writer name, self, vis } + + return nil + end + private :attr_writer + + def attr_accessor(*names) + vis = Rubinius::VariableScope.of_sender.method_visibility + + names.each do |name| + Rubinius.add_reader name, self, vis + Rubinius.add_writer name, self, vis + end + + return nil + end + private :attr_accessor + + def undef_method(*names) + return self if names.empty? + names.map!{ |name| Rubinius::Type.coerce_to_symbol name } + Rubinius.check_frozen + + names.each do |name| + # Will raise a NameError if the method doesn't exist. + instance_method name + undef_method! name + end + + self + end + private :undef_method + + # Like undef_method, but doesn't even check that the method exists. Used + # mainly to implement rb_undef_method. + def undef_method!(name) + Rubinius.check_frozen + name = Rubinius::Type.coerce_to_symbol(name) + + Rubinius::VM.reset_method_cache self, name + + @method_table.store name, nil, nil, nil, 0, :undef + + if obj = Rubinius::Type.singleton_class_object(self) + Rubinius.privately do + obj.singleton_method_undefined(name) + end + else + method_undefined(name) + end + + name + end + + def remove_method(*names) + names.each do |name| + name = Rubinius::Type.coerce_to_symbol(name) + Rubinius.check_frozen + + unless @method_table.lookup(name) + raise NameError.new("method `#{name}' not defined in #{self.name}", name) + end + + Rubinius::VM.reset_method_cache self, name + @method_table.delete name + + if obj = Rubinius::Type.singleton_class_object(self) + Rubinius.privately do + obj.singleton_method_removed(name) + end + else + method_removed(name) + end + end + + self + end + private :remove_method + + def name + Rubinius::Type.module_name self + end + + def to_s + Rubinius::Type.module_inspect self + end + + def inspect + to_s + end +end + +module Rubinius + Config = Configuration.new + Globals = GlobalVariables.new + + @add_method_hook = Rubinius::Hook.new + + def self.open_class(name, sup, scope) + if scope + under = scope.module + else + under = Object + end + + open_class_under name, sup, under + end + + def self.open_class_under(name, sup, mod) + unless Type.object_kind_of? mod, Module + raise TypeError, "'#{mod.inspect}' is not a class/module" + end + + tbl = mod.constant_table + if !tbl.has_name?(name) + # Create the class + sup = Object unless sup + obj = Class.new sup, name, mod + else + entry = tbl.lookup(name) + obj = entry.constant + if Type.object_kind_of? obj, Autoload + obj = begin + obj.call(mod, true) + rescue NameError # Constant not found in loaded file + nil + end + + # nil is returned if the autoload was abort, usually because + # the file to be required has already been loaded. In which case + # act like the autoload wasn't there. + unless obj + supr = sup ? sup : Object + obj = Class.new supr, name, mod + end + end + + if Type.object_kind_of? obj, Class + if sup and obj.superclass != sup + raise TypeError, "Superclass mismatch: #{obj.superclass} != #{sup}" + end + else + raise TypeError, "#{name} is not a class" + end + end + + return obj + end + + def self.open_module(name, scope) + if scope + under = scope.module + else + under = Object + end + + open_module_under name, under + end + + def self.open_module_under(name, mod) + unless Type.object_kind_of? mod, Module + raise TypeError, "'#{mod.inspect}' is not a class/module" + end + + tbl = mod.constant_table + found = tbl.has_name?(name) + + # Object has special behavior, we check it's included + # modules also + if !found and mod.equal? Object + check = mod.direct_superclass + + while check + tbl = check.constant_table + found = tbl.has_name?(name) + break if found + check = check.direct_superclass + end + end + + if !found + # Create the module + obj = Module.new + mod.const_set name, obj + else + entry = tbl.lookup(name) + obj = entry.constant + + if Type.object_kind_of? obj, Autoload + obj = obj.call(mod, true) + + # See comment above about autoload returning nil + unless obj + obj = Module.new + mod.const_set name, obj + end + end + + if Type.object_kind_of?(obj, Class) || !Type.object_kind_of?(obj, Module) + raise TypeError, "#{name} is not a module, but a #{obj.class}" + end + end + return obj + end + + def self.add_function(name, executable, lexical_scope) + scope = lexical_scope + while scope.parent + scope = scope.parent + end + + unless scope.functions.kind_of? Rubinius::LookupTable + scope.functions = Rubinius::LookupTable.new + end + + scope.functions[name] = executable + end + + def self.add_defn_method(name, executable, lexical_scope, vis) + # TODO: puts serial on MethodTable entry + unless Type.object_kind_of? executable, String + executable.serial = 1 + if executable.respond_to? :scope= + # If we're adding a method inside ane eval, dup it so that + # we don't share the CompiledCode with the eval, since + # we're going to mutate it. + if lexical_scope and script = lexical_scope.current_script + if script.eval? + executable = executable.dup + end + end + executable.scope = lexical_scope + end + end + + mod = lexical_scope.for_method_definition + + if ai = Type.singleton_class_object(mod) + if Type.object_kind_of? ai, Numeric + + # Such a weird protocol. If :singleton_method_added exists, allow this. + # le sigh. + unless Type.object_respond_to? ai, :singleton_method_added + raise TypeError, "Unable to define singleton methods on Numerics" + end + end + end + + add_method name, executable, mod, lexical_scope, 1, vis + name + end + + # Must be AFTER add_method, because otherwise we'll run this attach_method to add + # add_method itself and fail. + def self.attach_method(name, executable, lexical_scope, recv) + # TODO: puts serial on MethodTable entry + unless Type.object_kind_of? executable, String + executable.serial = 1 + if executable.respond_to? :scope= + # If we're adding a method inside ane eval, dup it so that + # we don't share the CompiledCode with the eval, since + # we're going to mutate it. + if lexical_scope and script = lexical_scope.current_script + if script.eval? + executable = executable.dup + end + end + executable.scope = lexical_scope + end + end + + mod = Rubinius::Type.object_singleton_class recv + + add_method name, executable, mod, lexical_scope, 1, :public + name + end + + def self.add_reader(name, mod, vis) + normalized = Rubinius::Type.coerce_to_symbol(name) + add_method normalized, AccessVariable.get_ivar(normalized), mod, nil, 0, vis + end + + def self.add_writer(name, mod, vis) + normalized = Rubinius::Type.coerce_to_symbol(name) + writer_name = "#{normalized}=".to_sym + add_method writer_name, AccessVariable.set_ivar(normalized), mod, nil, 0, vis + end + + class << self + attr_reader :add_method_hook + end + + module CAPI + INTEGER_PACK_MSWORD_FIRST = 0x01 + INTEGER_PACK_LSWORD_FIRST = 0x02 + INTEGER_PACK_MSBYTE_FIRST = 0x10 + INTEGER_PACK_LSBYTE_FIRST = 0x20 + INTEGER_PACK_NATIVE_BYTE_ORDER = 0x40 + INTEGER_PACK_2COMP = 0x80 + INTEGER_PACK_FORCE_BIGNUM = 0x100 + INTEGER_PACK_NEGATIVE = 0x200 + + INTEGER_PACK_LITTLE_ENDIAN = INTEGER_PACK_LSWORD_FIRST|INTEGER_PACK_LSBYTE_FIRST + INTEGER_PACK_BIG_ENDIAN = INTEGER_PACK_MSWORD_FIRST|INTEGER_PACK_MSBYTE_FIRST + + INTEGER_PACK_DIGITS = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0,10,11,12,13,14,15, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ] + end + + module CType + Printed = Rubinius::Tuple.new 256 + i = 0 + while i < 256 + Printed[i] = toprint(i) + i += 1 + end + + Lowered = Rubinius::Tuple.new 256 + + i = 0 + while i < 256 + Lowered[i] = tolower(i).chr + i += 1 + end + end + + module FFI + # Shorthand for the current process, i.e. all code that + # the process image itself contains. In addition to the + # Rubinius codebase, this also includes libc etc. + # + # Use this constant instead of nil directly. + # + CURRENT_PROCESS = nil + + TypeDefs = Rubinius::LookupTable.new + + # Converts a char + add_typedef TYPE_CHAR, :char + + # Converts an unsigned char + add_typedef TYPE_UCHAR, :uchar + + # The C++ boolean type + add_typedef TYPE_BOOL, :bool + + # Converts a short + add_typedef TYPE_SHORT, :short + + # Converts an unsigned short + add_typedef TYPE_USHORT, :ushort + add_typedef TYPE_USHORT, :mode_t + add_typedef TYPE_USHORT, :nlink_t + + # Converts an int + add_typedef TYPE_INT, :int + add_typedef TYPE_INT, :dev_t + add_typedef TYPE_INT, :blksize_t + add_typedef TYPE_INT, :time_t + + # Converts an unsigned int + add_typedef TYPE_UINT, :uint + add_typedef TYPE_UINT, :uid_t + add_typedef TYPE_UINT, :gid_t + add_typedef TYPE_UINT, :id_t + + # Converts a long + add_typedef TYPE_LONG, :long + add_typedef TYPE_LONG, :ssize_t + + # Converts an unsigned long + add_typedef TYPE_ULONG, :ulong + + # Converts a size_t + add_typedef TYPE_ULONG, :size_t + + # Converts a long long + add_typedef TYPE_LL, :long_long + add_typedef TYPE_LL, :blkcnt_t + add_typedef TYPE_LL, :off_t + + # Converts an unsigned long long + add_typedef TYPE_ULL, :ulong_long + add_typedef TYPE_ULL, :ino64_t + + # Converts a float + add_typedef TYPE_FLOAT, :float + + # Converts a double + add_typedef TYPE_DOUBLE, :double + + # Converts a pointer to opaque data + add_typedef TYPE_PTR, :pointer + + # For when a function has no return value + add_typedef TYPE_VOID, :void + + # Converts NULL-terminated C strings + add_typedef TYPE_STRING, :string + + # Use strptr when you need to free the result of some operation. + add_typedef TYPE_STRPTR, :strptr + add_typedef TYPE_STRPTR, :string_and_pointer + + # Use for a C struct with a char [] embedded inside. + add_typedef TYPE_CHARARR, :char_array + + # A set of unambigious integer types + add_typedef TYPE_CHAR, :int8 + add_typedef TYPE_UCHAR, :uint8 + add_typedef TYPE_SHORT, :int16 + add_typedef TYPE_USHORT, :uint16 + add_typedef TYPE_INT, :int32 + add_typedef TYPE_UINT, :uint32 + + # Converts a varargs argument + add_typedef TYPE_VARARGS, :varargs + + if Rubinius::L64 + add_typedef TYPE_LONG, :int64 + add_typedef TYPE_ULONG, :uint64 + else + add_typedef TYPE_LL, :int64 + add_typedef TYPE_ULL, :uint64 + end + + TypeSizes = Rubinius::LookupTable.new + TypeSizes[1] = :char + TypeSizes[2] = :short + TypeSizes[4] = :int + TypeSizes[8] = Rubinius::L64 ? :long : :long_long + + # Load all the platform dependent types + + Rubinius::Config.section("rbx.platform.typedef.") do |key, value| + add_typedef(find_type(value.to_sym), key.substring(21, key.length).to_sym) + end + + class DynamicLibrary + extend FFI::Library + + # Bootstrap dlsym, dlopen, and dlerror + pointer_as_function :find_symbol, FFI::Pointer::DLSYM, [:pointer, :string], :pointer + pointer_as_function :open_library, FFI::Pointer::DLOPEN, [:string, :int], :pointer + pointer_as_function :last_error, FFI::Pointer::DLERROR, [], :string + + if Rubinius.windows? + RTLD_LAZY = 0 + RTLD_NOW = 0 + RTLD_GLOBAL = 0 + RTLD_LOCAL = 0 + else + RTLD_LAZY = Rubinius::Config['rbx.platform.dlopen.RTLD_LAZY'] + RTLD_NOW = Rubinius::Config['rbx.platform.dlopen.RTLD_NOW'] + RTLD_GLOBAL = Rubinius::Config['rbx.platform.dlopen.RTLD_GLOBAL'] + RTLD_LOCAL = Rubinius::Config['rbx.platform.dlopen.RTLD_LOCAL'] + end + + class << self + alias_method :open, :new + end + + def initialize(name, flags=nil) + # Accept nil and check for ruby-ffi API compat + flags ||= RTLD_LAZY | RTLD_GLOBAL + + unless name + @name = "[current process]" + @handle = FFI::Pointer::CURRENT_PROCESS + return + end + + @name = name + @handle = DynamicLibrary.open_library name, flags + + unless @handle + orig_error = last_error + + libnames = FFI::LIB_SUFFIXES.map {|suffix| "#{name}.#{suffix}" } + libnames << "lib#{name}" + libnames += FFI::LIB_SUFFIXES.map {|suffix| "lib#{name}.#{suffix}" } + + libnames.detect do |libname| + @name = libname + @handle = DynamicLibrary.open_library libname, flags + end + end + + unless @handle + orig_error = orig_error.split("\n").first + # API Compat. LoadError is wrong here. + raise LoadError, "Could not open library #{name} - #{orig_error}" + end + end + + private :initialize + + attr_reader :name + + def find_symbol(name) + ptr = DynamicLibrary.find_symbol @handle, name + return nil unless ptr + # defined in core/pointer.rb + FFI::DynamicLibrary::Symbol.new(self, ptr, name) + end + + alias_method :find_function, :find_symbol + alias_method :find_variable, :find_symbol + + def last_error + DynamicLibrary.last_error + end + + CURRENT_PROCESS = DynamicLibrary.new(nil) + end + + module Platform + ARCH = Rubinius::CPU + + # ruby-ffi compatible + LONG_SIZE = Rubinius::SIZEOF_LONG * 8 + ADDRESS_SIZE = Rubinius::WORDSIZE + + case + when Rubinius.windows? + LIBSUFFIX = "dll" + IS_WINDOWS = true + OS = 'windows' + when Rubinius.darwin? + LIBSUFFIX = "dylib" + IS_WINDOWS = false + OS = 'darwin' + else + LIBSUFFIX = "so" + IS_WINDOWS = false + OS = 'linux' + end + + module File + SEPARATOR = '/' + ALT_SEPARATOR = nil + PATH_SEPARATOR = ':' + end + + module Math + extend FFI::Library + + attach_function :fabs, [:double], :double + attach_function :atan2, [:double, :double], :double + attach_function :cos, [:double], :double + attach_function :sin, [:double], :double + attach_function :tan, [:double], :double + attach_function :acos, [:double], :double + attach_function :asin, [:double], :double + attach_function :atan, [:double], :double + attach_function :cosh, [:double], :double + attach_function :sinh, [:double], :double + attach_function :tanh, [:double], :double + attach_function :acosh, [:double], :double + attach_function :asinh, [:double], :double + attach_function :atanh, [:double], :double + attach_function :exp, [:double], :double + attach_function :log, [:double], :double + attach_function :log10, [:double], :double + attach_function :sqrt, [:double], :double + attach_function :frexp, [:double, :pointer], :double + attach_function :ldexp, [:double, :int], :double + attach_function :hypot, [:double, :double], :double + attach_function :erf, [:double], :double + attach_function :erfc, [:double], :double + attach_function :cbrt, [:double], :double + attach_function :tgamma, [:double], :double + attach_function :lgamma_r, [:double, :pointer], :double + + # Rubinius-specific, used in Marshal + attach_function :modf, [:double, :pointer], :double + + # TODO: we need a way to determine whether a function + # is defined for a particular platform. + # + # attach_function :log2, [:double], :double + def self.log2(x) + log10(x) / log10(2.0) + end + end + + module POSIX + extend FFI::Library + + # errors + attach_function :errno, 'ffi_errno', [], :int + attach_function :errno=, 'ffi_set_errno', [:int], :void + + # memory + attach_function :malloc, [:size_t], :pointer + attach_function :realloc, [:pointer, :size_t], :pointer + attach_function :free, [:pointer], :void + attach_function :memset, [:pointer, :int, :size_t], :pointer + attach_function :memcpy, [:pointer, :pointer, :size_t], :pointer + + # file system + attach_function :access, [:string, :int], :int + attach_function :chmod, [:string, :mode_t], :int + attach_function :fchmod, [:int, :mode_t], :int + attach_function :lchmod, [:string, :mode_t], :int + attach_function :chown, [:string, :uid_t, :gid_t], :int + attach_function :fchown, [:int, :uid_t, :gid_t], :int + attach_function :lchown, [:string, :uid_t, :gid_t], :int + attach_function :unlink, [:string], :int + attach_function :getcwd, [:string, :size_t], :string + attach_function :umask, [:mode_t], :int + attach_function :link, [:string, :string], :int + attach_function :symlink, [:string, :string], :int + attach_function :readlink, [:string, :pointer, :size_t], :ssize_t + attach_function :rename, [:string, :string], :int + attach_function :utimes, [:string, :pointer], :int + attach_function :mkfifo, [:string, :mode_t], :int + + # directories + attach_function :chdir, [:string], :int + attach_function :mkdir, [:string, :mode_t], :int + attach_function :rmdir, [:string], :int + attach_function :chroot, [:string], :int + + # File/IO + attach_function :fcntl, [:int, :int, :long], :int + attach_function :ioctl, [:int, :ulong, :long], :int + attach_function :fsync, [:int], :int + attach_function :dup, [:int], :int + attach_function :dup2, [:int, :int], :int + attach_function :open, [:string, :int, :mode_t], :int + attach_function :close, [:int], :int + attach_function :lseek, [:int, :off_t, :int], :off_t + attach_function :read, [:int, :pointer, :size_t], :ssize_t + attach_function :ftruncate, [:int, :off_t], :int + attach_function :truncate, [:string, :off_t], :int + attach_function :write, [:int, :pointer, :size_t], :ssize_t + attach_function :select, [:int, :pointer, :pointer, :pointer, :pointer], :int + + # Other I/O + attach_function :pipe, [:pointer], :int + attach_function :mmap, [:pointer, :size_t, :int, :int, :int, :off_t], :pointer + attach_function :msync, [:pointer, :size_t, :int], :int + attach_function :munmap, [:pointer, :size_t], :int + attach_function :getpagesize, [], :int + attach_function :shutdown, [:int, :int], :int + attach_function :posix_fadvise, [:int, :off_t, :off_t, :int], :int + + # inspecting + attach_function :isatty, [:int], :int + attach_function :ttyname, [:int], :string + + # locking + attach_function :flock, [:int, :int], :int + + # UID/GID + attach_function :getuid, [], :uid_t + attach_function :getgid, [], :gid_t + attach_function :geteuid, [], :uid_t + attach_function :getegid, [], :gid_t + + attach_function :setgid, [:gid_t], :int + attach_function :setuid, [:uid_t], :int + attach_function :setegid, [:gid_t], :int + attach_function :seteuid, [:uid_t], :int + + attach_function :setregid, [:gid_t, :gid_t], :int + attach_function :setreuid, [:uid_t, :uid_t], :int + + attach_function :setresgid, [:gid_t, :gid_t, :gid_t], :int + attach_function :setresuid, [:uid_t, :uid_t, :uid_t], :int + + attach_function :getpriority, [:int, :id_t], :int + attach_function :setpriority, [:int, :id_t, :int], :int + + attach_function :getgroups, [:int, :pointer], :int + attach_function :setgroups, [:int, :pointer], :int + attach_function :initgroups, [:string, :gid_t], :int + + attach_function :setrlimit, [:int, :pointer], :int + attach_function :getrlimit, [:int, :pointer], :int + + # password and group file access + attach_function :getpwnam, [:string], :pointer + attach_function :getpwuid, [:uid_t], :pointer + attach_function :setpwent, [], :void + attach_function :getpwent, [], :pointer + attach_function :endpwent, [], :void + + attach_function :getgrnam, [:string], :pointer + attach_function :getgrgid, [:gid_t], :pointer + attach_function :setgrent, [], :void + attach_function :getgrent, [], :pointer + attach_function :endgrent, [], :void + + # processes and signals + attach_function :kill, [:pid_t, :int], :int + attach_function :getpgid, [:pid_t], :pid_t + attach_function :setpgid, [:pid_t, :pid_t], :int + attach_function :getpid, [], :pid_t + attach_function :getppid, [], :pid_t + attach_function :getpgrp, [], :pid_t + attach_function :setsid, [], :pid_t + + # related to stat() + attach_function :major, 'ffi_major', [:dev_t], :dev_t + attach_function :minor, 'ffi_minor', [:dev_t], :dev_t + + # time + attach_function :gettimeofday, [:pointer, :pointer], :int + + #-- + # Internal class for accessing timevals + #++ + class TimeVal < FFI::Struct + config 'rbx.platform.timeval', :tv_sec, :tv_usec + end + end + end + end + + module EnvironmentAccess + extend FFI::Library + + attach_function :getenv, [:string], :string + attach_function :putenv, [:string], :int + attach_function :setenv, [:string, :string, :int], :int + attach_function :unsetenv, [:string], :int + attach_function :environ, 'ffi_environ', [], :pointer + end + + class EnvironmentVariables + include EnvironmentAccess + end + + ::ENV = EnvironmentVariables.new + + module Metrics + class Data + MetricsHash = Hash.new + end + end + + class Stat + S_IRUSR = Rubinius::Config['rbx.platform.file.S_IRUSR'] + S_IWUSR = Rubinius::Config['rbx.platform.file.S_IWUSR'] + S_IXUSR = Rubinius::Config['rbx.platform.file.S_IXUSR'] + S_IRGRP = Rubinius::Config['rbx.platform.file.S_IRGRP'] + S_IWGRP = Rubinius::Config['rbx.platform.file.S_IWGRP'] + S_IXGRP = Rubinius::Config['rbx.platform.file.S_IXGRP'] + S_IROTH = Rubinius::Config['rbx.platform.file.S_IROTH'] + S_IWOTH = Rubinius::Config['rbx.platform.file.S_IWOTH'] + S_IXOTH = Rubinius::Config['rbx.platform.file.S_IXOTH'] + + S_IRUGO = S_IRUSR | S_IRGRP | S_IROTH + S_IWUGO = S_IWUSR | S_IWGRP | S_IWOTH + S_IXUGO = S_IXUSR | S_IXGRP | S_IXOTH + + S_IFMT = Rubinius::Config['rbx.platform.file.S_IFMT'] + S_IFIFO = Rubinius::Config['rbx.platform.file.S_IFIFO'] + S_IFCHR = Rubinius::Config['rbx.platform.file.S_IFCHR'] + S_IFDIR = Rubinius::Config['rbx.platform.file.S_IFDIR'] + S_IFBLK = Rubinius::Config['rbx.platform.file.S_IFBLK'] + S_IFREG = Rubinius::Config['rbx.platform.file.S_IFREG'] + S_IFLNK = Rubinius::Config['rbx.platform.file.S_IFLNK'] + S_IFSOCK = Rubinius::Config['rbx.platform.file.S_IFSOCK'] + S_IFWHT = Rubinius::Config['rbx.platform.file.S_IFWHT'] + S_ISUID = Rubinius::Config['rbx.platform.file.S_ISUID'] + S_ISGID = Rubinius::Config['rbx.platform.file.S_ISGID'] + S_ISVTX = Rubinius::Config['rbx.platform.file.S_ISVTX'] + end +end + +code = Class.method_table.lookup(:new).get_method +code.serial = Rubinius::CompiledCode::KernelMethodSerial + +class Class + undef_method :append_features + undef_method :extend_object + undef_method :module_function + undef_method :prepend_features +end + +class Complex < Numeric + I = Complex(0, 1) +end + +class NilClass + include ImmediateValue +end + +class FalseClass + include ImmediateValue +end + +class TrueClass + include ImmediateValue +end + +class Symbol + include ImmediateValue +end + +class Fixnum + include ImmediateValue +end + +# Default Ruby Record Separator +# Used in this file and by various methods that need to ignore $/ +DEFAULT_RECORD_SEPARATOR = "\n" + +class Dir + # This seems silly, I know. But we do this to make Dir more resistent to people + # screwing with ::File later (ie, fakefs) + PrivateFile = ::File + + FFI = Rubinius::FFI + + module Glob + total = Rubinius::Config['glob.cache'] + + case total + when Fixnum + if total == 0 + @glob_cache = nil + else + @glob_cache = Rubinius::LRUCache.new(total) + end + when false + @glob_cache = nil + else + @glob_cache = Rubinius::LRUCache.new(50) + end + end +end + +class Encoding + class Converter + class TranscodingPath + @paths = {} + @load_cache = true + @cache_valid = false + @transcoders_count = TranscodingMap.size + end + end +end + +module Errno + FFI = Rubinius::FFI +end + +class File < IO + # these will be necessary when we run on Windows + DOSISH = false # !!(RUBY_PLATFORM =~ /mswin/) + CASEFOLD_FILESYSTEM = DOSISH + FNM_SYSCASE = CASEFOLD_FILESYSTEM ? FNM_CASEFOLD : 0 + + module Constants + F_GETFL = Rubinius::Config['rbx.platform.fcntl.F_GETFL'] + F_SETFL = Rubinius::Config['rbx.platform.fcntl.F_SETFL'] + + # O_ACCMODE is /undocumented/ for fcntl() on some platforms + ACCMODE = Rubinius::Config['rbx.platform.fcntl.O_ACCMODE'] + O_ACCMODE = Rubinius::Config['rbx.platform.fcntl.O_ACCMODE'] + + F_GETFD = Rubinius::Config['rbx.platform.fcntl.F_GETFD'] + F_SETFD = Rubinius::Config['rbx.platform.fcntl.F_SETFD'] + FD_CLOEXEC = Rubinius::Config['rbx.platform.fcntl.FD_CLOEXEC'] + O_CLOEXEC = Rubinius::Config['rbx.platform.file.O_CLOEXEC'] + O_NONBLOCK = Rubinius::Config['rbx.platform.file.O_NONBLOCK'] + + RDONLY = Rubinius::Config['rbx.platform.file.O_RDONLY'] + WRONLY = Rubinius::Config['rbx.platform.file.O_WRONLY'] + RDWR = Rubinius::Config['rbx.platform.file.O_RDWR'] + + CREAT = Rubinius::Config['rbx.platform.file.O_CREAT'] + EXCL = Rubinius::Config['rbx.platform.file.O_EXCL'] + NOCTTY = Rubinius::Config['rbx.platform.file.O_NOCTTY'] + TRUNC = Rubinius::Config['rbx.platform.file.O_TRUNC'] + APPEND = Rubinius::Config['rbx.platform.file.O_APPEND'] + NONBLOCK = Rubinius::Config['rbx.platform.file.O_NONBLOCK'] + SYNC = Rubinius::Config['rbx.platform.file.O_SYNC'] + + # TODO: these flags should probably be imported from Platform + LOCK_SH = 0x01 + LOCK_EX = 0x02 + LOCK_NB = 0x04 + LOCK_UN = 0x08 + BINARY = 0x04 + + # TODO: "OK" constants aren't in File::Constants in MRI + F_OK = 0 # test for existence of file + X_OK = 1 # test for execute or search permission + W_OK = 2 # test for write permission + R_OK = 4 # test for read permission + + FNM_NOESCAPE = 0x01 + FNM_PATHNAME = 0x02 + FNM_DOTMATCH = 0x04 + FNM_CASEFOLD = 0x08 + FNM_EXTGLOB = 0x10 + FNM_SYSCASE = File::FNM_SYSCASE + + if Rubinius.windows? + NULL = 'NUL' + else + NULL = '/dev/null' + end + end + + Stat = Rubinius::Stat + + class Stat + @module_name = :"File::Stat" + + def world_readable? + if mode & S_IROTH == S_IROTH + tmp = mode & (S_IRUGO | S_IWUGO | S_IXUGO) + return Rubinius::Type.coerce_to tmp, Fixnum, :to_int + end + end + + def world_writable? + if mode & S_IWOTH == S_IWOTH + tmp = mode & (S_IRUGO | S_IWUGO | S_IXUGO) + return Rubinius::Type.coerce_to tmp, Fixnum, :to_int + end + end + end + + FFI = Rubinius::FFI + + SEPARATOR = FFI::Platform::File::SEPARATOR + Separator = FFI::Platform::File::SEPARATOR + ALT_SEPARATOR = FFI::Platform::File::ALT_SEPARATOR + PATH_SEPARATOR = FFI::Platform::File::PATH_SEPARATOR + POSIX = FFI::Platform::POSIX +end + +class Float + FFI = Rubinius::FFI +end + +class IO + include File::Constants + + FFI = Rubinius::FFI + + SEEK_SET = Rubinius::Config['rbx.platform.io.SEEK_SET'] + SEEK_CUR = Rubinius::Config['rbx.platform.io.SEEK_CUR'] + SEEK_END = Rubinius::Config['rbx.platform.io.SEEK_END'] + + # Not available on all platforms, so these constants may be nil + POSIX_FADV_NORMAL = Rubinius::Config['rbx.platform.advise.POSIX_FADV_NORMAL'] + POSIX_FADV_SEQUENTIAL = Rubinius::Config['rbx.platform.advise.POSIX_FADV_SEQUENTIAL'] + POSIX_FADV_RANDOM = Rubinius::Config['rbx.platform.advise.POSIX_FADV_RANDOM'] + POSIX_FADV_WILLNEED = Rubinius::Config['rbx.platform.advise.POSIX_FADV_WILLNEED'] + POSIX_FADV_DONTNEED = Rubinius::Config['rbx.platform.advise.POSIX_FADV_DONTNEED'] + POSIX_FADV_NOREUSE = Rubinius::Config['rbx.platform.advise.POSIX_FADV_NOREUSE'] + + class FileDescriptor + @@max_descriptors = Rubinius::AtomicReference.new(2) + + include File::Constants + + O_RDONLY = Rubinius::Config['rbx.platform.file.O_RDONLY'] + O_WRONLY = Rubinius::Config['rbx.platform.file.O_WRONLY'] + O_RDWR = Rubinius::Config['rbx.platform.file.O_RDWR'] + end + + class Select + FD_SETSIZE = Rubinius::Config['rbx.platform.select.FD_SETSIZE'] + end +end + +class NilClass + include ImmediateValue +end + +class FalseClass + include ImmediateValue +end + +class TrueClass + include ImmediateValue +end + +class Symbol + include ImmediateValue +end + +class Fixnum + include ImmediateValue +end + +module Rubinius + class Mirror + module Process + class Execute + # Mapping of string open modes to integer oflag versions. + OFLAGS = { + "r" => ::File::RDONLY, + "r+" => ::File::RDWR | ::File::CREAT, + "w" => ::File::WRONLY | ::File::CREAT | ::File::TRUNC, + "w+" => ::File::RDWR | ::File::CREAT | ::File::TRUNC, + "a" => ::File::WRONLY | ::File::APPEND | ::File::CREAT, + "a+" => ::File::RDWR | ::File::APPEND | ::File::CREAT + } + end + end + end +end + +class Regexp + IGNORECASE = 1 + EXTENDED = 2 + MULTILINE = 4 + FIXEDENCODING = 16 + NOENCODING = 32 + DONT_CAPTURE_GROUP = 128 + CAPTURE_GROUP = 256 + + KCODE_NONE = (1 << 9) + KCODE_EUC = (2 << 9) + KCODE_SJIS = (3 << 9) + KCODE_UTF8 = (4 << 9) + KCODE_MASK = KCODE_NONE | KCODE_EUC | KCODE_SJIS | KCODE_UTF8 + + OPTION_MASK = IGNORECASE | EXTENDED | MULTILINE | FIXEDENCODING | NOENCODING | DONT_CAPTURE_GROUP | CAPTURE_GROUP + + ESCAPE_TABLE = Rubinius::Tuple.new(256) + + # Seed it with direct replacements + i = 0 + while i < 256 + ESCAPE_TABLE[i] = i.chr + i += 1 + end + + # The character literals (?x) are Fixnums in 1.8 and Strings in 1.9 + # so we use literal values instead so this is 1.8/1.9 compatible. + ESCAPE_TABLE[9] = '\\t' + ESCAPE_TABLE[10] = '\\n' + ESCAPE_TABLE[11] = '\\v' + ESCAPE_TABLE[12] = '\\f' + ESCAPE_TABLE[13] = '\\r' + ESCAPE_TABLE[32] = '\\ ' + ESCAPE_TABLE[35] = '\\#' + ESCAPE_TABLE[36] = '\\$' + ESCAPE_TABLE[40] = '\\(' + ESCAPE_TABLE[41] = '\\)' + ESCAPE_TABLE[42] = '\\*' + ESCAPE_TABLE[43] = '\\+' + ESCAPE_TABLE[45] = '\\-' + ESCAPE_TABLE[46] = '\\.' + ESCAPE_TABLE[63] = '\\?' + ESCAPE_TABLE[91] = '\\[' + ESCAPE_TABLE[92] = '\\\\' + ESCAPE_TABLE[93] = '\\]' + ESCAPE_TABLE[94] = '\\^' + ESCAPE_TABLE[123] = '\\{' + ESCAPE_TABLE[124] = '\\|' + ESCAPE_TABLE[125] = '\\}' + + class SourceParser + class Part + OPTIONS_MAP = { + 'm' => Regexp::MULTILINE, + 'i' => Regexp::IGNORECASE, + 'x' => Regexp::EXTENDED + } + end + + PossibleOptions = [[MULTILINE, "m"], [IGNORECASE, "i"], [EXTENDED, "x"]] + end +end + +class String + class Rationalizer + SPACE = "\\s*" + DIGITS = "(?:[0-9](?:_[0-9]|[0-9])*)" + NUMERATOR = "(?:#{DIGITS}?\\.)?#{DIGITS}(?:[eE][-+]?#{DIGITS})?" + DENOMINATOR = DIGITS + RATIONAL = "\\A#{SPACE}([-+])?(#{NUMERATOR})(?:\\/(#{DENOMINATOR}))?#{SPACE}" + PATTERN = Regexp.new RATIONAL + end + + class Complexifier + SPACE = Rationalizer::SPACE + NUMERATOR = Rationalizer::NUMERATOR + DENOMINATOR = Rationalizer::DENOMINATOR + NUMBER = "[-+]?#{NUMERATOR}(?:\\/#{DENOMINATOR})?" + NUMBERNOS = "#{NUMERATOR}(?:\\/#{DENOMINATOR})?" + PATTERN0 = Regexp.new "\\A#{SPACE}(#{NUMBER})@(#{NUMBER})#{SPACE}" + PATTERN1 = Regexp.new "\\A#{SPACE}([-+])?(#{NUMBER})?[iIjJ]#{SPACE}" + PATTERN2 = Regexp.new "\\A#{SPACE}(#{NUMBER})(([-+])(#{NUMBERNOS})?[iIjJ])?#{SPACE}" + end +end + +module Signal + Names = { + "EXIT" => 0 + } + + Numbers = { + 0 => "EXIT" + } + + @handlers = {} + + Rubinius::Config.section 'rbx.platform.signal.' do |key, value| + name = key[23, key.length] + number = value.to_i + Names[name] = number + Numbers[number] = name + end + + # special case of signal.c + if Names["CHLD"] + Names["CLD"] = Names["CHLD"] + end +end + +class Integer < Numeric + alias_method :magnitude, :abs +end + +module Kernel + def extend(*modules) + raise ArgumentError, "wrong number of arguments (0 for 1+)" if modules.empty? + Rubinius.check_frozen + + modules.reverse_each do |mod| + Rubinius.privately do + mod.extend_object self + end + + Rubinius.privately do + mod.extended self + end + end + self + end + + alias_method :__extend__, :extend + + def method_missing(meth, *args) + cls = NoMethodError + + case Rubinius.method_missing_reason + when :private + msg = "private method `#{meth}' called" + when :protected + msg = "protected method `#{meth}' called" + when :super + msg = "no superclass method `#{meth}'" + when :vcall + msg = "undefined local variable or method `#{meth}'" + cls = NameError + else + msg = "undefined method `#{meth}'" + end + + object_class = Rubinius::Type.object_class(self) + + if Rubinius::Type.object_kind_of?(self, Module) + msg << " on #{self} (#{object_class})" + + # A separate case for nil, because people like to patch methods to + # nil, so we can't call methods on it reliably. + elsif nil.equal?(self) + msg << " on nil:NilClass" + + if $rbx_show_nil_location + m = Rubinius::Mirror::Object.reflect self + code_id = m.nil_code_id + + # TODO: implement in CodeDB + code = nil + + ObjectSpace.each_object(Rubinius::CompiledCode).each do |c| + code = c if c.code_id.start_with? code_id + end + + + if code + ip = m.nil_ip + msg << " originating in '#{code.name}' at #{code.file}:#{code.line_from_ip(ip)}+#{ip}" + end + end + + msg << "." + elsif ImmediateValue === self + msg << " on #{self}:#{object_class}." + else + msg << " on an instance of #{object_class}." + end + + Kernel.raise(cls.new(msg, meth, args, receiver: self)) + end + + private :method_missing + + def raise(exc=undefined, msg=undefined, ctx=nil, cause: nil) + if undefined.equal? exc + exc = $! + exc = RuntimeError.new("No current exception") unless exc + exc.cause = cause unless exc.cause or exc == cause + else + if exc.respond_to? :exception + if undefined.equal? msg + exc = exc.exception + else + exc = exc.exception msg + end + raise ::TypeError, 'exception class/object expected' unless exc.kind_of?(::Exception) + elsif exc.kind_of? String + exc = ::RuntimeError.exception exc + else + raise ::TypeError, 'exception class/object expected' + end + + exc.cause = cause unless exc.cause or exc == cause + exc.cause = $! unless exc.cause or exc == $! + end + + if ctx.kind_of? Array + exc.set_backtrace ctx + else + exc.capture_backtrace!(2) unless exc.backtrace? + end + + if $DEBUG and $VERBOSE != nil + if loc = exc.locations and loc[1] + pos = loc[1].position + else + pos = Rubinius::VM.backtrace(1)[0].position + end + + STDERR.puts "Exception: `#{exc.class}' #{pos} - #{exc.message}" + end + + Rubinius.raise_exception exc + end + module_function :raise + + alias_method :fail, :raise + module_function :fail + + def to_s + Rubinius::Type.infect("#<#{self.class}:0x#{self.__id__.to_s(16)}>", self) + end + + # Add in $! in as a hook, to just do $!. This is for accesses to $! + # that the compiler can't see. + Rubinius::Globals.set_hook(:$!) { $! } + + get = proc do + # We raise an exception here because Regexp.last_match won't work + raise TypeError, "Unable to handle $~ in this context" + end + set = proc do |key, val| + if val.nil? || val.kind_of?(MatchData) + Rubinius.invoke_primitive :regexp_set_last_match, val + else + raise TypeError, "Cannot assign #{val.class}, expexted nil or instance MatchData." + end + end + Rubinius::Globals.set_hook(:$~, get, set) + + Rubinius::Globals.set_hook(:$*) { ARGV } + + Rubinius::Globals.set_hook(:$@) { $! ? $!.backtrace : nil } + + Rubinius::Globals.set_hook(:$$) { Process.pid } + + prc = proc { ARGF.filename } + Rubinius::Globals.set_hook(:$FILENAME, prc, :raise_readonly) + + write_filter = proc do |key, io| + unless io.respond_to? :write + raise ::TypeError, "#{key} must have write method, #{io.class} given" + end + io + end + + get = proc { |key| Thread.current[:$_] } + set = proc { |key, val| Thread.current[:$_] = val } + Rubinius::Globals.set_hook(:$_, get, set) + + Rubinius::Globals.add_alias :$stdout, :$> + Rubinius::Globals.set_filter(:$stdout, write_filter) + Rubinius::Globals.set_filter(:$stderr, write_filter) + + get = proc do + warn "$KCODE is unused in Ruby 1.9 and later" + nil + end + + set = proc do |key, io| + warn "$KCODE is unused in Ruby 1.9 and later, changes are ignored" + nil + end + + Rubinius::Globals.set_hook(:$KCODE, get, set) + + set = proc do |key, val| + val = Rubinius::Type.coerce_to val, String, :to_str + Rubinius.invoke_primitive :vm_set_process_title, val + end + Rubinius::Globals.set_hook(:$0, :[], set) + + set = proc { |key, val| STDERR.puts("WARNING: $SAFE is not supported on Rubinius."); val } + Rubinius::Globals.set_hook(:$SAFE, :[], set) + + # Alias $0 $PROGRAM_NAME + Rubinius::Globals.add_alias(:$0, :$PROGRAM_NAME) + + Rubinius::Globals.read_only :$:, :$LOAD_PATH, :$-I + Rubinius::Globals.read_only :$", :$LOADED_FEATURES + Rubinius::Globals.read_only :$< + + Rubinius::Globals[:$-a] = false + Rubinius::Globals[:$-l] = false + Rubinius::Globals[:$-p] = false + Rubinius::Globals.read_only :$-a, :$-l, :$-p + + Rubinius::Globals.add_alias :$DEBUG, :$-d + Rubinius::Globals.add_alias :$VERBOSE, :$-v + Rubinius::Globals.add_alias :$VERBOSE, :$-w + + set_string = proc do |key, value| + unless value.nil? or value.kind_of? String + raise TypeError, "value of #{key} must be a String" + end + + Rubinius::Globals.set! key, value + end + + Rubinius::Globals.set_filter :$/, set_string + Rubinius::Globals.add_alias :$/, :$-0 + + Rubinius::Globals.set_filter :$,, set_string +end + +module Math + FFI = Rubinius::FFI + + # Constants + PI = 3.14159_26535_89793_23846 + E = 2.71828_18284_59045_23536 + + FactorialTable = [ + 1.0, + 1.0, + 2.0, + 6.0, + 24.0, + 120.0, + 720.0, + 5040.0, + 40320.0, + 362880.0, + 3628800.0, + 39916800.0, + 479001600.0, + 6227020800.0, + 87178291200.0, + 1307674368000.0, + 20922789888000.0, + 355687428096000.0, + 6402373705728000.0, + 121645100408832000.0, + 2432902008176640000.0, + 51090942171709440000.0, + 1124000727777607680000.0 + ] + + if Errno.const_defined? :EDOM + DomainError = Errno::EDOM + elsif Errno.const_defined? :ERANGE + DomainError = Errno::ERANGE + else + class DomainError < SystemCallError + end + end +end + +module Process + module Constants + EXIT_SUCCESS = Rubinius::Config['rbx.platform.process.EXIT_SUCCESS'] || 0 + EXIT_FAILURE = Rubinius::Config['rbx.platform.process.EXIT_FAILURE'] || 1 + + PRIO_PGRP = Rubinius::Config['rbx.platform.process.PRIO_PGRP'] + PRIO_PROCESS = Rubinius::Config['rbx.platform.process.PRIO_PROCESS'] + PRIO_USER = Rubinius::Config['rbx.platform.process.PRIO_USER'] + + RLIM_INFINITY = Rubinius::Config['rbx.platform.process.RLIM_INFINITY'] + RLIM_SAVED_MAX = Rubinius::Config['rbx.platform.process.RLIM_SAVED_MAX'] + RLIM_SAVED_CUR = Rubinius::Config['rbx.platform.process.RLIM_SAVED_CUR'] + + RLIMIT_AS = Rubinius::Config['rbx.platform.process.RLIMIT_AS'] + RLIMIT_CORE = Rubinius::Config['rbx.platform.process.RLIMIT_CORE'] + RLIMIT_CPU = Rubinius::Config['rbx.platform.process.RLIMIT_CPU'] + RLIMIT_DATA = Rubinius::Config['rbx.platform.process.RLIMIT_DATA'] + RLIMIT_FSIZE = Rubinius::Config['rbx.platform.process.RLIMIT_FSIZE'] + RLIMIT_MEMLOCK = Rubinius::Config['rbx.platform.process.RLIMIT_MEMLOCK'] + RLIMIT_NOFILE = Rubinius::Config['rbx.platform.process.RLIMIT_NOFILE'] + RLIMIT_NPROC = Rubinius::Config['rbx.platform.process.RLIMIT_NPROC'] + RLIMIT_RSS = Rubinius::Config['rbx.platform.process.RLIMIT_RSS'] + RLIMIT_SBSIZE = Rubinius::Config['rbx.platform.process.RLIMIT_SBSIZE'] + RLIMIT_STACK = Rubinius::Config['rbx.platform.process.RLIMIT_STACK'] + + RLIMIT_RTPRIO = Rubinius::Config['rbx.platform.process.RLIMIT_RTPRIO'] + RLIMIT_RTTIME = Rubinius::Config['rbx.platform.process.RLIMIT_RTTIME'] + RLIMIT_SIGPENDING = Rubinius::Config['rbx.platform.process.RLIMIT_SIGPENDING'] + RLIMIT_MSGQUEUE = Rubinius::Config['rbx.platform.process.RLIMIT_MSGQUEUE'] + RLIMIT_NICE = Rubinius::Config['rbx.platform.process.RLIMIT_NICE'] + + WNOHANG = 1 + WUNTRACED = 2 + end + include Constants + + FFI = Rubinius::FFI + + class Rlimit < FFI::Struct + config "rbx.platform.rlimit", :rlim_cur, :rlim_max + end + + Rubinius::Globals.read_only :$? + Rubinius::Globals.set_hook(:$?) { Thread.current[:$?] } +end + + +STDIN = Rubinius::IOUtility.redefine_io(0, :read_only) +STDOUT = Rubinius::IOUtility.redefine_io(1, :write_only) +STDERR = Rubinius::IOUtility.redefine_io(2, :write_only) + +Rubinius::Globals.set!(:$stdin, STDIN) +Rubinius::Globals.set!(:$stdout, STDOUT) +Rubinius::Globals.set!(:$stderr, STDERR) + +module Rubinius + begin + is_tty = STDIN.tty? + rescue IOError + is_tty = false + end + + Terminal = is_tty + AtExit = [] + + class Sprinter + total = Rubinius::Config['printf.cache'] + + case total + when Fixnum + if total == 0 + @cache = nil + else + @cache = Rubinius::LRUCache.new(total) + end + when false + @cache = nil + else + @cache = Rubinius::LRUCache.new(50) + end + + class Builder + RADIX = { 'u' => 10, 'x' => 16, 'X' => 16, 'o' => 8, 'b' => 2, 'B' => 2 } + PREFIX = { 'o' => '0', 'x' => '0x', 'X' => '0X', 'b' => '0b', 'B' => '0B' } + + AtomMap = Rubinius::LookupTable.new + + AtomMap[?c] = CharAtom + AtomMap[?s] = StringAtom + AtomMap[?p] = InspectAtom + AtomMap[?e] = FloatAtom + AtomMap[?E] = FloatAtom + AtomMap[?g] = FloatAtom + AtomMap[?G] = FloatAtom + AtomMap[?f] = FloatAtom + AtomMap[?d] = IntegerAtom + AtomMap[?i] = IntegerAtom + AtomMap[?u] = IntegerAtom + AtomMap[?b] = ExtIntegerAtom + AtomMap[?B] = ExtIntegerAtom + AtomMap[?x] = ExtIntegerAtom + AtomMap[?X] = ExtIntegerAtom + AtomMap[?o] = ExtIntegerAtom + + RE = / + ([^%]+|%(?:[\n\0]|\z)) # 1 + | + % + ( # 2 + (<[^>]+>)? # 3 + ([0# +-]*) # 4 + (?:([0-9]+)\$)? # 5 + ([0# +-]*) # 6 + (?: + (\*(?:([0-9]+)\$)?|([1-9][0-9]*))? # 7 8 9 + (?:\.(\*(?:([0-9]+)\$)?|([0-9][0-9]*))?)? # 10 11 12 + ) + (?:([0-9]+)\$)? # 13 + ([BbcdEefGgiopsuXx]) # 14 + ) + | + (%)(?:%|[-+0-9# *.$]+\$[0-9.]*\z) # 15 + | + (%{[^}]+}) #16 + | + (%) # 17 + /x + end + end +end + +class Struct + Struct.new 'Tms', :utime, :stime, :cutime, :cstime, :tutime, :tstime + + class Tms + def initialize(utime=nil, stime=nil, cutime=nil, cstime=nil, + tutime=nil, tstime=nil) + @utime = utime + @stime = stime + @cutime = cutime + @cstime = cstime + @tutime = tutime + @tstime = tstime + end + + private :initialize + end + + def self._specialize(attrs) + # Because people are crazy, they subclass Struct directly, ie. + # class Craptastic < Struct + # + # NOT + # + # class Fine < Struct.new(:x, :y) + # + # When they do this craptastic act, they'll sometimes define their + # own #initialize and super into Struct#initialize. + # + # When they do this and then do Craptastic.new(:x, :y), this code + # will accidentally shadow their #initialize. So for now, only run + # the specialize if we're trying new Struct's directly from Struct itself, + # not a craptastic Struct subclass. + + return unless superclass.equal? Struct + + # To allow for optimization, we generate code with normal ivar + # references for all attributes whose names can be written as + # tIVAR tokens. For example, of the following struct attributes + # + # Struct.new(:a, :@b, :c?, :'d-e') + # + # only the first, :a, can be written as a valid tIVAR token: + # + # * :a can be written as @a + # * :@b becomes @@b and would be interpreted as a tCVAR + # * :c? becomes @c? and be interpreted as the beginning of + # a ternary expression + # * :'d-e' becomes @d-e and would be interpreted as a method + # invocation + # + # Attribute names that cannot be written as tIVAR tokens will + # fall back to using #instance_variable_(get|set). + + args, assigns, hashes, vars = [], [], [], [] + + attrs.each_with_index do |name, i| + name = "@#{name}" + + if name =~ /^@[a-z_]\w*$/i + assigns << "#{name} = a#{i}" + vars << name + else + assigns << "instance_variable_set(:#{name.inspect}, a#{i})" + vars << "instance_variable_get(:#{name.inspect})" + end + + args << "a#{i} = nil" + hashes << "#{vars[-1]}.hash" + end + + code = <<-CODE + def initialize(#{args.join(", ")}) + #{assigns.join(';')} + self + end + + private :initialize + + def hash + hash = #{hashes.size} + + return hash if Thread.detect_outermost_recursion(self) do + hash = hash ^ #{hashes.join(' ^ ')} + end + + hash + end + + def to_a + [#{vars.join(', ')}] + end + + def length + #{vars.size} + end + CODE + + begin + mod = Module.new do + module_eval code + end + include mod + rescue SyntaxError + # SyntaxError means that something is wrong with the + # specialization code. Just eat the error and don't specialize. + end + end +end + +class Thread + MUTEX_FOR_THREAD_EXCLUSIVE = Mutex.new + + Thread.initialize_main_thread(Thread.current) + ThreadGroup::Default.add Thread.current + + sm = Rubinius::Mirror.reflect Rubinius.signal_thread + sm.group = ThreadGroup::Default +end + +class Time + MonthValue = { + 'JAN' => 1, 'FEB' => 2, 'MAR' => 3, 'APR' => 4, 'MAY' => 5, 'JUN' => 6, + 'JUL' => 7, 'AUG' => 8, 'SEP' => 9, 'OCT' =>10, 'NOV' =>11, 'DEC' =>12 + } +end + +class Random + DEFAULT = new +end + +module Rubinius + class Prediction + def valid? + Rubinius.primitive :prediction_valid_p + raise PrimitiveFailure, "Rubinius::Prediction#valid? primitive failed" + end + end + + class CodeDB + class << self + undef :new + end + end + + module FFI + module Library + # Once the core library is loaded, we want to raise an error if + # attempting to attach to a non-existent function. + def ffi_function_missing(name, *args) + raise FFI::NotFoundError, "Unable to find foreign function '#{name}'" + end + end + end +end + diff --git a/gxg/deps/chronic.rb b/gxg/deps/chronic.rb new file mode 100644 index 0000000..7a90210 --- /dev/null +++ b/gxg/deps/chronic.rb @@ -0,0 +1,3448 @@ +# +require 'time' +require 'date' +# +module Chronic + class Anchor + # + end + # + module Handlers + module_function + + # Handle month/day + def handle_m_d(month, day, time_tokens, options) + month.start = self.now + span = month.this(options[:context]) + year, month = span.begin.year, span.begin.month + day_start = Chronic.time_class.local(year, month, day) + + day_or_time(day_start, time_tokens, options) + end + + # Handle repeater-month-name/scalar-day + def handle_rmn_sd(tokens, options) + month = tokens[0].get_tag(RepeaterMonthName) + day = tokens[1].get_tag(ScalarDay).type + + return if month_overflow?(self.now.year, month.index, day) + + handle_m_d(month, day, tokens[2..tokens.size], options) + end + + # Handle repeater-month-name/scalar-day with separator-on + def handle_rmn_sd_on(tokens, options) + if tokens.size > 3 + month = tokens[2].get_tag(RepeaterMonthName) + day = tokens[3].get_tag(ScalarDay).type + token_range = 0..1 + else + month = tokens[1].get_tag(RepeaterMonthName) + day = tokens[2].get_tag(ScalarDay).type + token_range = 0..0 + end + + return if month_overflow?(self.now.year, month.index, day) + + handle_m_d(month, day, tokens[token_range], options) + end + + # Handle repeater-month-name/ordinal-day + def handle_rmn_od(tokens, options) + month = tokens[0].get_tag(RepeaterMonthName) + day = tokens[1].get_tag(OrdinalDay).type + + return if month_overflow?(self.now.year, month.index, day) + + handle_m_d(month, day, tokens[2..tokens.size], options) + end + + # Handle ordinal this month + def handle_od_rm(tokens, options) + day = tokens[0].get_tag(OrdinalDay).type + month = tokens[2].get_tag(RepeaterMonth) + handle_m_d(month, day, tokens[3..tokens.size], options) + end + + # Handle ordinal-day/repeater-month-name + def handle_od_rmn(tokens, options) + month = tokens[1].get_tag(RepeaterMonthName) + day = tokens[0].get_tag(OrdinalDay).type + + return if month_overflow?(self.now.year, month.index, day) + + handle_m_d(month, day, tokens[2..tokens.size], options) + end + + def handle_sy_rmn_od(tokens, options) + year = tokens[0].get_tag(ScalarYear).type + month = tokens[1].get_tag(RepeaterMonthName).index + day = tokens[2].get_tag(OrdinalDay).type + time_tokens = tokens.last(tokens.size - 3) + + return if month_overflow?(year, month, day) + + begin + day_start = Chronic.time_class.local(year, month, day) + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + # Handle scalar-day/repeater-month-name + def handle_sd_rmn(tokens, options) + month = tokens[1].get_tag(RepeaterMonthName) + day = tokens[0].get_tag(ScalarDay).type + + return if month_overflow?(self.now.year, month.index, day) + + handle_m_d(month, day, tokens[2..tokens.size], options) + end + + # Handle repeater-month-name/ordinal-day with separator-on + def handle_rmn_od_on(tokens, options) + if tokens.size > 3 + month = tokens[2].get_tag(RepeaterMonthName) + day = tokens[3].get_tag(OrdinalDay).type + token_range = 0..1 + else + month = tokens[1].get_tag(RepeaterMonthName) + day = tokens[2].get_tag(OrdinalDay).type + token_range = 0..0 + end + + return if month_overflow?(self.now.year, month.index, day) + + handle_m_d(month, day, tokens[token_range], options) + end + + # Handle repeater-month-name/scalar-year + def handle_rmn_sy(tokens, options) + month = tokens[0].get_tag(RepeaterMonthName).index + year = tokens[1].get_tag(ScalarYear).type + + if month == 12 + next_month_year = year + 1 + next_month_month = 1 + else + next_month_year = year + next_month_month = month + 1 + end + + begin + end_time = Chronic.time_class.local(next_month_year, next_month_month) + Span.new(Chronic.time_class.local(year, month), end_time) + rescue ArgumentError + nil + end + end + + # Handle generic timestamp (ruby 1.8) + def handle_generic(tokens, options) + t = Chronic.time_class.parse(options[:text]) + Span.new(t, t + 1) + rescue ArgumentError => e + raise e unless e.message =~ /out of range/ + end + + # Handle repeater-month-name/scalar-day/scalar-year + def handle_rmn_sd_sy(tokens, options) + month = tokens[0].get_tag(RepeaterMonthName).index + day = tokens[1].get_tag(ScalarDay).type + year = tokens[2].get_tag(ScalarYear).type + time_tokens = tokens.last(tokens.size - 3) + + return if month_overflow?(year, month, day) + + begin + day_start = Chronic.time_class.local(year, month, day) + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + # Handle repeater-month-name/ordinal-day/scalar-year + def handle_rmn_od_sy(tokens, options) + month = tokens[0].get_tag(RepeaterMonthName).index + day = tokens[1].get_tag(OrdinalDay).type + year = tokens[2].get_tag(ScalarYear).type + time_tokens = tokens.last(tokens.size - 3) + + return if month_overflow?(year, month, day) + + begin + day_start = Chronic.time_class.local(year, month, day) + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + # Handle oridinal-day/repeater-month-name/scalar-year + def handle_od_rmn_sy(tokens, options) + day = tokens[0].get_tag(OrdinalDay).type + month = tokens[1].get_tag(RepeaterMonthName).index + year = tokens[2].get_tag(ScalarYear).type + time_tokens = tokens.last(tokens.size - 3) + + return if month_overflow?(year, month, day) + + begin + day_start = Chronic.time_class.local(year, month, day) + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + # Handle scalar-day/repeater-month-name/scalar-year + def handle_sd_rmn_sy(tokens, options) + new_tokens = [tokens[1], tokens[0], tokens[2]] + time_tokens = tokens.last(tokens.size - 3) + handle_rmn_sd_sy(new_tokens + time_tokens, options) + end + + # Handle scalar-month/scalar-day/scalar-year (endian middle) + def handle_sm_sd_sy(tokens, options) + month = tokens[0].get_tag(ScalarMonth).type + day = tokens[1].get_tag(ScalarDay).type + year = tokens[2].get_tag(ScalarYear).type + time_tokens = tokens.last(tokens.size - 3) + + return if month_overflow?(year, month, day) + + begin + day_start = Chronic.time_class.local(year, month, day) + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + # Handle scalar-day/scalar-month/scalar-year (endian little) + def handle_sd_sm_sy(tokens, options) + new_tokens = [tokens[1], tokens[0], tokens[2]] + time_tokens = tokens.last(tokens.size - 3) + handle_sm_sd_sy(new_tokens + time_tokens, options) + end + + # Handle scalar-year/scalar-month/scalar-day + def handle_sy_sm_sd(tokens, options) + new_tokens = [tokens[1], tokens[2], tokens[0]] + time_tokens = tokens.last(tokens.size - 3) + handle_sm_sd_sy(new_tokens + time_tokens, options) + end + + # Handle scalar-month/scalar-day + def handle_sm_sd(tokens, options) + month = tokens[0].get_tag(ScalarMonth).type + day = tokens[1].get_tag(ScalarDay).type + year = self.now.year + time_tokens = tokens.last(tokens.size - 2) + + return if month_overflow?(year, month, day) + + begin + day_start = Chronic.time_class.local(year, month, day) + day_start = Chronic.time_class.local(year + 1, month, day) if options[:context] == :future && day_start < now + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + # Handle scalar-day/scalar-month + def handle_sd_sm(tokens, options) + new_tokens = [tokens[1], tokens[0]] + time_tokens = tokens.last(tokens.size - 2) + handle_sm_sd(new_tokens + time_tokens, options) + end + + def handle_year_and_month(year, month) + if month == 12 + next_month_year = year + 1 + next_month_month = 1 + else + next_month_year = year + next_month_month = month + 1 + end + + begin + end_time = Chronic.time_class.local(next_month_year, next_month_month) + Span.new(Chronic.time_class.local(year, month), end_time) + rescue ArgumentError + nil + end + end + + # Handle scalar-month/scalar-year + def handle_sm_sy(tokens, options) + month = tokens[0].get_tag(ScalarMonth).type + year = tokens[1].get_tag(ScalarYear).type + handle_year_and_month(year, month) + end + + # Handle scalar-year/scalar-month + def handle_sy_sm(tokens, options) + year = tokens[0].get_tag(ScalarYear).type + month = tokens[1].get_tag(ScalarMonth).type + handle_year_and_month(year, month) + end + + # Handle RepeaterDayName RepeaterMonthName OrdinalDay + def handle_rdn_rmn_od(tokens, options) + month = tokens[1].get_tag(RepeaterMonthName) + day = tokens[2].get_tag(OrdinalDay).type + time_tokens = tokens.last(tokens.size - 3) + year = self.now.year + + return if month_overflow?(year, month.index, day) + + begin + if time_tokens.empty? + start_time = Chronic.time_class.local(year, month.index, day) + end_time = time_with_rollover(year, month.index, day + 1) + Span.new(start_time, end_time) + else + day_start = Chronic.time_class.local(year, month.index, day) + day_or_time(day_start, time_tokens, options) + end + rescue ArgumentError + nil + end + end + + # Handle RepeaterDayName RepeaterMonthName OrdinalDay ScalarYear + def handle_rdn_rmn_od_sy(tokens, options) + month = tokens[1].get_tag(RepeaterMonthName) + day = tokens[2].get_tag(OrdinalDay).type + year = tokens[3].get_tag(ScalarYear).type + + return if month_overflow?(year, month.index, day) + + begin + start_time = Chronic.time_class.local(year, month.index, day) + end_time = time_with_rollover(year, month.index, day + 1) + Span.new(start_time, end_time) + rescue ArgumentError + nil + end + end + + # Handle RepeaterDayName OrdinalDay + def handle_rdn_od(tokens, options) + day = tokens[1].get_tag(OrdinalDay).type + time_tokens = tokens.last(tokens.size - 2) + year = self.now.year + month = self.now.month + if options[:context] == :future + self.now.day > day ? month += 1 : month + end + + return if month_overflow?(year, month, day) + + begin + if time_tokens.empty? + start_time = Chronic.time_class.local(year, month, day) + end_time = time_with_rollover(year, month, day + 1) + Span.new(start_time, end_time) + else + day_start = Chronic.time_class.local(year, month, day) + day_or_time(day_start, time_tokens, options) + end + rescue ArgumentError + nil + end + end + + # Handle RepeaterDayName RepeaterMonthName ScalarDay + def handle_rdn_rmn_sd(tokens, options) + month = tokens[1].get_tag(RepeaterMonthName) + day = tokens[2].get_tag(ScalarDay).type + time_tokens = tokens.last(tokens.size - 3) + year = self.now.year + + return if month_overflow?(year, month.index, day) + + begin + if time_tokens.empty? + start_time = Chronic.time_class.local(year, month.index, day) + end_time = time_with_rollover(year, month.index, day + 1) + Span.new(start_time, end_time) + else + day_start = Chronic.time_class.local(year, month.index, day) + day_or_time(day_start, time_tokens, options) + end + rescue ArgumentError + nil + end + end + + # Handle RepeaterDayName RepeaterMonthName ScalarDay ScalarYear + def handle_rdn_rmn_sd_sy(tokens, options) + month = tokens[1].get_tag(RepeaterMonthName) + day = tokens[2].get_tag(ScalarDay).type + year = tokens[3].get_tag(ScalarYear).type + + return if month_overflow?(year, month.index, day) + + begin + start_time = Chronic.time_class.local(year, month.index, day) + end_time = time_with_rollover(year, month.index, day + 1) + Span.new(start_time, end_time) + rescue ArgumentError + nil + end + end + + def handle_sm_rmn_sy(tokens, options) + day = tokens[0].get_tag(ScalarDay).type + month = tokens[1].get_tag(RepeaterMonthName).index + year = tokens[2].get_tag(ScalarYear).type + if tokens.size > 3 + time = get_anchor([tokens.last], options).begin + h, m, s = time.hour, time.min, time.sec + time = Chronic.time_class.local(year, month, day, h, m, s) + end_time = Chronic.time_class.local(year, month, day + 1, h, m, s) + else + time = Chronic.time_class.local(year, month, day) + day += 1 unless day >= 31 + end_time = Chronic.time_class.local(year, month, day) + end + Span.new(time, end_time) + end + + # anchors + + # Handle repeaters + def handle_r(tokens, options) + dd_tokens = dealias_and_disambiguate_times(tokens, options) + get_anchor(dd_tokens, options) + end + + # Handle repeater/grabber/repeater + def handle_r_g_r(tokens, options) + new_tokens = [tokens[1], tokens[0], tokens[2]] + handle_r(new_tokens, options) + end + + # arrows + + # Handle scalar/repeater/pointer helper + def handle_srp(tokens, span, options) + distance = tokens[0].get_tag(Scalar).type + repeater = tokens[1].get_tag(Repeater) + pointer = tokens[2].get_tag(Pointer).type + + repeater.offset(span, distance, pointer) if repeater.respond_to?(:offset) + end + + # Handle scalar/repeater/pointer + def handle_s_r_p(tokens, options) + span = Span.new(self.now, self.now + 1) + + handle_srp(tokens, span, options) + end + + # Handle pointer/scalar/repeater + def handle_p_s_r(tokens, options) + new_tokens = [tokens[1], tokens[2], tokens[0]] + handle_s_r_p(new_tokens, options) + end + + # Handle scalar/repeater/pointer/anchor + def handle_s_r_p_a(tokens, options) + anchor_span = get_anchor(tokens[3..tokens.size - 1], options) + handle_srp(tokens, anchor_span, options) + end + + def handle_s_r_a_s_r_p_a(tokens, options) + anchor_span = get_anchor(tokens[4..tokens.size - 1], options) + + span = handle_srp(tokens[0..1]+tokens[4..6], anchor_span, options) + handle_srp(tokens[2..3]+tokens[4..6], span, options) + end + + # narrows + + # Handle oridinal repeaters + def handle_orr(tokens, outer_span, options) + repeater = tokens[1].get_tag(Repeater) + repeater.start = outer_span.begin - 1 + ordinal = tokens[0].get_tag(Ordinal).type + span = nil + + ordinal.times do + span = repeater.next(:future) + + if span.begin >= outer_span.end + span = nil + break + end + end + + span + end + + # Handle ordinal/repeater/separator/repeater + def handle_o_r_s_r(tokens, options) + outer_span = get_anchor([tokens[3]], options) + handle_orr(tokens[0..1], outer_span, options) + end + + # Handle ordinal/repeater/grabber/repeater + def handle_o_r_g_r(tokens, options) + outer_span = get_anchor(tokens[2..3], options) + handle_orr(tokens[0..1], outer_span, options) + end + + # support methods + + def day_or_time(day_start, time_tokens, options) + outer_span = Span.new(day_start, day_start + (24 * 60 * 60)) + + unless time_tokens.empty? + self.now = outer_span.begin + get_anchor(dealias_and_disambiguate_times(time_tokens, options), options.merge(:context => :future)) + else + outer_span + end + end + + def get_anchor(tokens, options) + grabber = Grabber.new(:this) + pointer = :future + repeaters = get_repeaters(tokens) + repeaters.size.times { tokens.pop } + + if tokens.first && tokens.first.get_tag(Grabber) + grabber = tokens.shift.get_tag(Grabber) + end + + head = repeaters.shift + head.start = self.now + + case grabber.type + when :last + outer_span = head.next(:past) + when :this + if options[:context] != :past and repeaters.size > 0 + outer_span = head.this(:none) + else + outer_span = head.this(options[:context]) + end + when :next + outer_span = head.next(:future) + else + raise "Invalid grabber" + end + + if Chronic.debug + puts "Handler-class: #{head.class}" + puts "--#{outer_span}" + end + + find_within(repeaters, outer_span, pointer) + end + + def get_repeaters(tokens) + tokens.map { |token| token.get_tag(Repeater) }.compact.sort.reverse + end + + def month_overflow?(year, month, day) + if ::Date.leap?(year) + day > RepeaterMonth::MONTH_DAYS_LEAP[month - 1] + else + day > RepeaterMonth::MONTH_DAYS[month - 1] + end + rescue ArgumentError + false + end + + # Recursively finds repeaters within other repeaters. + # Returns a Span representing the innermost time span + # or nil if no repeater union could be found + def find_within(tags, span, pointer) + puts "--#{span}" if Chronic.debug + return span if tags.empty? + + head = tags.shift + head.start = (pointer == :future ? span.begin : span.end) + h = head.this(:none) + + if span.cover?(h.begin) || span.cover?(h.end) + find_within(tags, h, pointer) + end + end + + def time_with_rollover(year, month, day) + date_parts = + if month_overflow?(year, month, day) + if month == 12 + [year + 1, 1, 1] + else + [year, month + 1, 1] + end + else + [year, month, day] + end + Chronic.time_class.local(*date_parts) + end + + def dealias_and_disambiguate_times(tokens, options) + # handle aliases of am/pm + # 5:00 in the morning -> 5:00 am + # 7:00 in the evening -> 7:00 pm + + day_portion_index = nil + tokens.each_with_index do |t, i| + if t.get_tag(RepeaterDayPortion) + day_portion_index = i + break + end + end + + time_index = nil + tokens.each_with_index do |t, i| + if t.get_tag(RepeaterTime) + time_index = i + break + end + end + + if day_portion_index && time_index + t1 = tokens[day_portion_index] + t1tag = t1.get_tag(RepeaterDayPortion) + + case t1tag.type + when :morning + puts '--morning->am' if Chronic.debug + t1.untag(RepeaterDayPortion) + t1.tag(RepeaterDayPortion.new(:am)) + when :afternoon, :evening, :night + puts "--#{t1tag.type}->pm" if Chronic.debug + t1.untag(RepeaterDayPortion) + t1.tag(RepeaterDayPortion.new(:pm)) + end + end + + # handle ambiguous times if :ambiguous_time_range is specified + if options[:ambiguous_time_range] != :none + ambiguous_tokens = [] + + tokens.each_with_index do |token, i| + ambiguous_tokens << token + next_token = tokens[i + 1] + + if token.get_tag(RepeaterTime) && token.get_tag(RepeaterTime).type.ambiguous? && (!next_token || !next_token.get_tag(RepeaterDayPortion)) + distoken = Token.new('disambiguator') + + distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range])) + ambiguous_tokens << distoken + end + end + + tokens = ambiguous_tokens + end + + tokens + end + + end + +end +# +module Chronic + class Parser + include Handlers + + # Hash of default configuration options. + DEFAULT_OPTIONS = { + :context => :future, + :now => nil, + :hours24 => nil, + :guess => true, + :ambiguous_time_range => 6, + :endian_precedence => [:middle, :little], + :ambiguous_year_future_bias => 50 + } + + attr_accessor :now + attr_reader :options + + # options - An optional Hash of configuration options: + # :context - If your string represents a birthday, you can set + # this value to :past and if an ambiguous string is + # given, it will assume it is in the past. + # :now - Time, all computations will be based off of time + # instead of Time.now. + # :hours24 - Time will be parsed as it would be 24 hour clock. + # :guess - By default the parser will guess a single point in time + # for the given date or time. If you'd rather have the + # entire time span returned, set this to false + # and a Chronic::Span will be returned. Setting :guess to :end + # will return last time from Span, to :middle for middle (same as just true) + # and :begin for first time from span. + # :ambiguous_time_range - If an Integer is given, ambiguous times + # (like 5:00) will be assumed to be within the range of + # that time in the AM to that time in the PM. For + # example, if you set it to `7`, then the parser will + # look for the time between 7am and 7pm. In the case of + # 5:00, it would assume that means 5:00pm. If `:none` + # is given, no assumption will be made, and the first + # matching instance of that time will be used. + # :endian_precedence - By default, Chronic will parse "03/04/2011" + # as the fourth day of the third month. Alternatively you + # can tell Chronic to parse this as the third day of the + # fourth month by setting this to [:little, :middle]. + # :ambiguous_year_future_bias - When parsing two digit years + # (ie 79) unlike Rubys Time class, Chronic will attempt + # to assume the full year using this figure. Chronic will + # look x amount of years into the future and past. If the + # two digit year is `now + x years` it's assumed to be the + # future, `now - x years` is assumed to be the past. + def initialize(options = {}) + @options = DEFAULT_OPTIONS.merge(options) + @now = options.delete(:now) || Chronic.time_class.now + end + + # Parse "text" with the given options + # Returns either a Time or Chronic::Span, depending on the value of options[:guess] + def parse(text) + tokens = tokenize(text, options) + span = tokens_to_span(tokens, options.merge(:text => text)) + + puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}" if Chronic.debug + + guess(span, options[:guess]) if span + end + + # Clean up the specified text ready for parsing. + # + # Clean up the string by stripping unwanted characters, converting + # idioms to their canonical form, converting number words to numbers + # (three => 3), and converting ordinal words to numeric + # ordinals (third => 3rd) + # + # text - The String text to normalize. + # + # Examples: + # + # Chronic.pre_normalize('first day in May') + # #=> "1st day in may" + # + # Chronic.pre_normalize('tomorrow after noon') + # #=> "next day future 12:00" + # + # Chronic.pre_normalize('one hundred and thirty six days from now') + # #=> "136 days future this second" + # + # Returns a new String ready for Chronic to parse. + def pre_normalize(text) + text = text.to_s.downcase + text = text.gsub(/\b(\d{2})\.(\d{2})\.(\d{4})\b/, '\3 / \2 / \1') + text = text.gsub(/\b([ap])\.m\.?/, '\1m') + text = text.gsub(/(\s+|:\d{2}|:\d{2}\.\d{3})\-(\d{2}:?\d{2})\b/, '\1tzminus\2') + text = text.gsub(/\./, ':') + text = text.gsub(/([ap]):m:?/, '\1m') + text = text.gsub(/['"]/, '') + text = text.gsub(/,/, ' ') + text = text.gsub(/^second /, '2nd ') + text = text.gsub(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1') + text = Numerizer.numerize(text) + text = text.gsub(/([\/\-\,\@])/) { ' ' + $1 + ' ' } + text = text.gsub(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, ' \1') + text = text.gsub(/\btoday\b/, 'this day') + text = text.gsub(/\btomm?orr?ow\b/, 'next day') + text = text.gsub(/\byesterday\b/, 'last day') + text = text.gsub(/\bnoon\b/, '12:00pm') + text = text.gsub(/\bmidnight\b/, '24:00') + text = text.gsub(/\bnow\b/, 'this second') + text = text.gsub('quarter', '15') + text = text.gsub('half', '30') + text = text.gsub(/(\d{1,2}) (to|till|prior to|before)\b/, '\1 minutes past') + text = text.gsub(/(\d{1,2}) (after|past)\b/, '\1 minutes future') + text = text.gsub(/\b(?:ago|before(?: now)?)\b/, 'past') + text = text.gsub(/\bthis (?:last|past)\b/, 'last') + text = text.gsub(/\b(?:in|during) the (morning)\b/, '\1') + text = text.gsub(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1') + text = text.gsub(/\btonight\b/, 'this night') + text = text.gsub(/\b\d+:?\d*[ap]\b/,'\0m') + text = text.gsub(/\b(\d{2})(\d{2})(am|pm)\b/, '\1:\2\3') + text = text.gsub(/(\d)([ap]m|oclock)\b/, '\1 \2') + text = text.gsub(/\b(hence|after|from)\b/, 'future') + text = text.gsub(/^\s?an? /i, '1 ') + text = text.gsub(/\b(\d{4}):(\d{2}):(\d{2})\b/, '\1 / \2 / \3') # DTOriginal + text = text.gsub(/\b0(\d+):(\d{2}):(\d{2}) ([ap]m)\b/, '\1:\2:\3 \4') + text + end + + # Guess a specific time within the given span. + # + # span - The Chronic::Span object to calcuate a guess from. + # + # Returns a new Time object. + def guess(span, mode = :middle) + return span if not mode + return span.begin + span.width / 2 if span.width > 1 and (mode == true or mode == :middle) + return span.end if mode == :end + span.begin + end + + # List of Handler definitions. See Chronic.parse for a list of options this + # method accepts. + # + # options - An optional Hash of configuration options. + # + # Returns a Hash of Handler definitions. + def definitions(options = {}) + options[:endian_precedence] ||= [:middle, :little] + + @@definitions ||= { + :time => [ + Handler.new([:repeater_time, :repeater_day_portion?], nil) + ], + + :date => [ + Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, [:separator_slash?, :separator_dash?], :time_zone, :scalar_year], :handle_generic), + Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day], :handle_rdn_rmn_sd), + Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :scalar_year], :handle_rdn_rmn_sd_sy), + Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day], :handle_rdn_rmn_od), + Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :scalar_year], :handle_rdn_rmn_od_sy), + Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rdn_rmn_sd), + Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_rmn_od), + Handler.new([:repeater_day_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_od), + Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_day, :repeater_time, :time_zone], :handle_generic), + Handler.new([:ordinal_day], :handle_generic), + Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy), + Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy), + Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy), + Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy), + Handler.new([:repeater_month_name, [:separator_slash?, :separator_dash?], :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd), + Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on), + Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od), + Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy), + Handler.new([:ordinal_day, :repeater_month_name, :separator_at?, 'time?'], :handle_od_rmn), + Handler.new([:ordinal_day, :grabber?, :repeater_month, :separator_at?, 'time?'], :handle_od_rm), + Handler.new([:scalar_year, :repeater_month_name, :ordinal_day], :handle_sy_rmn_od), + Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on), + Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy), + Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy), + Handler.new([:scalar_day, [:separator_slash?, :separator_dash?], :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn), + Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd), + Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month], :handle_sy_sm), + Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_year], :handle_sm_sy), + Handler.new([:scalar_day, [:separator_slash, :separator_dash], :repeater_month_name, [:separator_slash, :separator_dash], :scalar_year, :repeater_time?], :handle_sm_rmn_sy), + Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar?, :time_zone], :handle_generic), + ], + + :anchor => [ + Handler.new([:separator_on?, :grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r), + Handler.new([:grabber?, :repeater, :repeater, :separator?, :repeater?, :repeater?], :handle_r), + Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r) + ], + + :arrow => [ + Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p), + Handler.new([:scalar, :repeater, :separator_and?, :scalar, :repeater, :pointer, :separator_at?, 'anchor'], :handle_s_r_a_s_r_p_a), + Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r), + Handler.new([:scalar, :repeater, :pointer, :separator_at?, 'anchor'], :handle_s_r_p_a) + ], + + :narrow => [ + Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r), + Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r) + ] + } + + endians = [ + Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_day, [:separator_slash, :separator_dash], :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy), + Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_day, :separator_at?, 'time?'], :handle_sm_sd), + Handler.new([:scalar_day, [:separator_slash, :separator_dash], :scalar_month, :separator_at?, 'time?'], :handle_sd_sm), + Handler.new([:scalar_day, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy) + ] + + case endian = Array(options[:endian_precedence]).first + when :little + @@definitions.merge(:endian => endians.reverse) + when :middle + @@definitions.merge(:endian => endians) + else + raise ArgumentError, "Unknown endian option '#{endian}'" + end + end + + private + + def tokenize(text, options) + text = pre_normalize(text) + tokens = text.split(' ').map { |word| Token.new(word) } + [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, Sign, TimeZone].each do |tok| + tok.scan(tokens, options) + end + tokens.select { |token| token.tagged? } + end + + def tokens_to_span(tokens, options) + definitions = definitions(options) + + (definitions[:endian] + definitions[:date]).each do |handler| + if handler.match(tokens, definitions) + good_tokens = tokens.select { |o| !o.get_tag Separator } + return handler.invoke(:date, good_tokens, self, options) + end + end + + definitions[:anchor].each do |handler| + if handler.match(tokens, definitions) + good_tokens = tokens.select { |o| !o.get_tag Separator } + return handler.invoke(:anchor, good_tokens, self, options) + end + end + + definitions[:arrow].each do |handler| + if handler.match(tokens, definitions) + good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlash) || o.get_tag(SeparatorDash) || o.get_tag(SeparatorComma) || o.get_tag(SeparatorAnd) } + return handler.invoke(:arrow, good_tokens, self, options) + end + end + + definitions[:narrow].each do |handler| + if handler.match(tokens, definitions) + return handler.invoke(:narrow, tokens, self, options) + end + end + + puts "-none" if Chronic.debug + return nil + end + end +end +# +module Chronic + class Time + HOUR_SECONDS = 3600 # 60 * 60 + MINUTE_SECONDS = 60 + SECOND_SECONDS = 1 # haha, awesome + SUBSECOND_SECONDS = 0.001 + + # Checks if given number could be hour + def self.could_be_hour?(hour) + hour >= 0 && hour <= 24 + end + + # Checks if given number could be minute + def self.could_be_minute?(minute) + minute >= 0 && minute <= 60 + end + + # Checks if given number could be second + def self.could_be_second?(second) + second >= 0 && second <= 60 + end + + # Checks if given number could be subsecond + def self.could_be_subsecond?(subsecond) + subsecond >= 0 && subsecond <= 999999 + end + + # normalize offset in seconds to offset as string +mm:ss or -mm:ss + def self.normalize_offset(offset) + return offset if offset.is_a?(String) + offset = Chronic.time_class.now.to_time.utc_offset unless offset # get current system's UTC offset if offset is nil + sign = '+' + sign = '-' if offset < 0 + hours = (offset.abs / 3600).to_i.to_s.rjust(2,'0') + minutes = (offset.abs % 3600).to_s.rjust(2,'0') + sign + hours + minutes + end + + end +end +# +module Chronic + class Date + YEAR_MONTHS = 12 + MONTH_DAYS = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + MONTH_DAYS_LEAP = [nil, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + YEAR_SECONDS = 31_536_000 # 365 * 24 * 60 * 60 + SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60 + MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60 + FORTNIGHT_SECONDS = 1_209_600 # 14 * 24 * 60 * 60 + WEEK_SECONDS = 604_800 # 7 * 24 * 60 * 60 + WEEKEND_SECONDS = 172_800 # 2 * 24 * 60 * 60 + DAY_SECONDS = 86_400 # 24 * 60 * 60 + MONTHS = { + :january => 1, + :february => 2, + :march => 3, + :april => 4, + :may => 5, + :june => 6, + :july => 7, + :august => 8, + :september => 9, + :october => 10, + :november => 11, + :december => 12 + } + DAYS = { + :sunday => 0, + :monday => 1, + :tuesday => 2, + :wednesday => 3, + :thursday => 4, + :friday => 5, + :saturday => 6 + } + + # Checks if given number could be day + def self.could_be_day?(day) + day >= 1 && day <= 31 + end + + # Checks if given number could be month + def self.could_be_month?(month) + month >= 1 && month <= 12 + end + + # Checks if given number could be year + def self.could_be_year?(year) + year >= 1 && year <= 9999 + end + + # Build a year from a 2 digit suffix. + # + # year - The two digit Integer year to build from. + # bias - The Integer amount of future years to bias. + # + # Examples: + # + # make_year(96, 50) #=> 1996 + # make_year(79, 20) #=> 2079 + # make_year(00, 50) #=> 2000 + # + # Returns The Integer 4 digit year. + def self.make_year(year, bias) + return year if year.to_s.size > 2 + start_year = Chronic.time_class.now.year - bias + century = (start_year / 100) * 100 + full_year = century + year + full_year += 100 if full_year < start_year + full_year + end + + def self.month_overflow?(year, month, day) + if ::Date.leap?(year) + day > Date::MONTH_DAYS_LEAP[month] + else + day > Date::MONTH_DAYS[month] + end + end + + end +end +# +module Chronic + class Handler + + attr_reader :pattern + + attr_reader :handler_method + + # pattern - An Array of patterns to match tokens against. + # handler_method - A Symbol representing the method to be invoked + # when a pattern matches. + def initialize(pattern, handler_method) + @pattern = pattern + @handler_method = handler_method + end + + # tokens - An Array of tokens to process. + # definitions - A Hash of definitions to check against. + # + # Returns true if a match is found. + def match(tokens, definitions) + token_index = 0 + @pattern.each do |elements| + was_optional = false + elements = [elements] unless elements.is_a?(Array) + + elements.each_index do |i| + name = elements[i].to_s + optional = name[-1, 1] == '?' + name = name.chop if optional + + case elements[i] + when Symbol + if tags_match?(name, tokens, token_index) + token_index += 1 + break + else + if optional + was_optional = true + next + elsif i + 1 < elements.count + next + else + return false unless was_optional + end + end + + when String + return true if optional && token_index == tokens.size + + if definitions.key?(name.to_sym) + sub_handlers = definitions[name.to_sym] + else + raise "Invalid subset #{name} specified" + end + + sub_handlers.each do |sub_handler| + return true if sub_handler.match(tokens[token_index..tokens.size], definitions) + end + else + raise "Invalid match type: #{elements[i].class}" + end + end + + end + + return false if token_index != tokens.size + return true + end + + def invoke(type, tokens, parser, options) + if Chronic.debug + puts "-#{type}" + puts "Handler: #{@handler_method}" + end + + parser.send(@handler_method, tokens, options) + end + + # other - The other Handler object to compare. + # + # Returns true if these Handlers match. + def ==(other) + @pattern == other.pattern + end + + private + + def tags_match?(name, tokens, token_index) + klass = Chronic.const_get(name.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }) + + if tokens[token_index] + !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty? + end + end + + end +end +# +module Chronic + class MiniDate + attr_accessor :month, :day + + def self.from_time(time) + new(time.month, time.day) + end + + def initialize(month, day) + unless (1..12).include?(month) + raise ArgumentError, "1..12 are valid months" + end + + @month = month + @day = day + end + + def is_between?(md_start, md_end) + return false if (@month == md_start.month && @month == md_end.month) && + (@day < md_start.day || @day > md_end.day) + return true if (@month == md_start.month && @day >= md_start.day) || + (@month == md_end.month && @day <= md_end.day) + + i = (md_start.month % 12) + 1 + + until i == md_end.month + return true if @month == i + i = (i % 12) + 1 + end + + return false + end + + def equals?(other) + @month == other.month and @day == other.day + end + end +end +# +module Chronic + # Tokens are tagged with subclassed instances of this class when + # they match specific criteria. + class Tag + + attr_accessor :type + + # type - The Symbol type of this tag. + def initialize(type, options = {}) + @type = type + @options = options + end + + # time - Set the start Time for this Tag. + def start=(time) + @now = time + end + + class << self + private + + def scan_for(token, klass, items={}, options = {}) + case items + when Regexp + return klass.new(token.word, options) if items =~ token.word + when Hash + items.each do |item, symbol| + return klass.new(symbol, options) if item =~ token.word + end + end + nil + end + + end + + end +end +# +module Chronic + # A Span represents a range of time. Since this class extends + # Range, you can use #begin and #end to get the beginning and + # ending times of the span (they will be of class Time) + class Span < Range + # Returns the width of this span in seconds + def width + (self.end - self.begin).to_i + end + + # Add a number of seconds to this span, returning the + # resulting Span + def +(seconds) + Span.new(self.begin + seconds, self.end + seconds) + end + + # Subtract a number of seconds to this span, returning the + # resulting Span + def -(seconds) + self + -seconds + end + + # Prints this span in a nice fashion + def to_s + ('(' + self.begin.to_s + '..' + self.end.to_s + ')') + end + + alias :cover? :include? if RUBY_VERSION =~ /^1.8/ + + end +end +# +module Chronic + class Token + + attr_accessor :word + attr_accessor :tags + + def initialize(word) + @word = word + @tags = [] + end + + # Tag this token with the specified tag. + # + # new_tag - The new Tag object. + # + # Returns nothing. + def tag(new_tag) + @tags << new_tag + end + + # Remove all tags of the given class. + # + # tag_class - The tag Class to remove. + # + # Returns nothing. + def untag(tag_class) + @tags.delete_if { |m| m.kind_of? tag_class } + end + + # Returns true if this token has any tags. + def tagged? + @tags.size > 0 + end + + # tag_class - The tag Class to search for. + # + # Returns The first Tag that matches the given class. + def get_tag(tag_class) + @tags.find { |m| m.kind_of? tag_class } + end + + # Print this Token in a pretty way + def to_s + @word = (@word + '(' + @tags.join(', ') + ') ') + end + + def inspect + to_s + end + end +end +# +module Chronic + class Grabber < Tag + + # Scan an Array of Tokens and apply any necessary Grabber tags to + # each token. + # + # tokens - An Array of Token objects to scan. + # options - The Hash of options specified in Chronic::parse. + # + # Returns an Array of Token objects. + def self.scan(tokens, options) + tokens.each do |token| + if t = scan_for_all(token) then token.tag(t); next end + end + end + + # token - The Token object to scan. + # + # Returns a new Grabber object. + def self.scan_for_all(token) + scan_for token, self, + { + /last/ => :last, + /this/ => :this, + /next/ => :next + } + end + + def to_s + ('grabber-' + @type.to_s) + end + end +end +# +module Chronic + class Pointer < Tag + + # Scan an Array of Token objects and apply any necessary Pointer + # tags to each token. + # + # tokens - An Array of tokens to scan. + # options - The Hash of options specified in Chronic::parse. + # + # Returns an Array of tokens. + def self.scan(tokens, options) + tokens.each do |token| + if t = scan_for_all(token) then token.tag(t) end + end + end + + # token - The Token object we want to scan. + # + # Returns a new Pointer object. + def self.scan_for_all(token) + scan_for token, self, + { + /\bpast\b/ => :past, + /\b(?:future|in)\b/ => :future, + } + end + + def to_s + ('pointer-' + @type.to_s) + end + end +end +# +module Chronic + class Scalar < Tag + DAY_PORTIONS = %w( am pm morning afternoon evening night ) + + # Scan an Array of Token objects and apply any necessary Scalar + # tags to each token. + # + # tokens - An Array of tokens to scan. + # options - The Hash of options specified in Chronic::parse. + # + # Returns an Array of tokens. + def self.scan(tokens, options) + tokens.each_index do |i| + token = tokens[i] + post_token = tokens[i + 1] + if token.word =~ /^\d+$/ + scalar = token.word.to_i + token.tag(Scalar.new(scalar)) + token.tag(ScalarSubsecond.new(scalar)) if Chronic::Time::could_be_subsecond?(scalar) + token.tag(ScalarSecond.new(scalar)) if Chronic::Time::could_be_second?(scalar) + token.tag(ScalarMinute.new(scalar)) if Chronic::Time::could_be_minute?(scalar) + token.tag(ScalarHour.new(scalar)) if Chronic::Time::could_be_hour?(scalar) + unless post_token and DAY_PORTIONS.include?(post_token.word) + token.tag(ScalarDay.new(scalar)) if Chronic::Date::could_be_day?(scalar) + token.tag(ScalarMonth.new(scalar)) if Chronic::Date::could_be_month?(scalar) + if Chronic::Date::could_be_year?(scalar) + year = Chronic::Date::make_year(scalar, options[:ambiguous_year_future_bias]) + token.tag(ScalarYear.new(year.to_i)) + end + end + end + end + end + + def to_s + 'scalar' + end + end + + class ScalarSubsecond < Scalar #:nodoc: + def to_s + (super + '-subsecond-' + @type.to_s) + end + end + + class ScalarSecond < Scalar #:nodoc: + def to_s + (super + '-second-' + @type.to_s) + end + end + + class ScalarMinute < Scalar #:nodoc: + def to_s + (super + '-minute-' + @type.to_s) + end + end + + class ScalarHour < Scalar #:nodoc: + def to_s + (super + '-hour-' + @type.to_s) + end + end + + class ScalarDay < Scalar #:nodoc: + def to_s + (super + '-day-' + @type.to_s) + end + end + + class ScalarMonth < Scalar #:nodoc: + def to_s + (super + '-month-' + @type.to_s) + end + end + + class ScalarYear < Scalar #:nodoc: + def to_s + (super + '-year-' + @type.to_s) + end + end +end +# +module Chronic + class Ordinal < Tag + + # Scan an Array of Token objects and apply any necessary Ordinal + # tags to each token. + # + # tokens - An Array of tokens to scan. + # options - The Hash of options specified in Chronic::parse. + # + # Returns an Array of tokens. + def self.scan(tokens, options) + tokens.each_index do |i| + if tokens[i].word =~ /^(\d+)(st|nd|rd|th|\.)$/ + ordinal = $1.to_i + tokens[i].tag(Ordinal.new(ordinal)) + tokens[i].tag(OrdinalDay.new(ordinal)) if Chronic::Date::could_be_day?(ordinal) + tokens[i].tag(OrdinalMonth.new(ordinal)) if Chronic::Date::could_be_month?(ordinal) + if Chronic::Date::could_be_year?(ordinal) + year = Chronic::Date::make_year(ordinal, options[:ambiguous_year_future_bias]) + tokens[i].tag(OrdinalYear.new(year.to_i)) + end + end + end + end + + def to_s + 'ordinal' + end + end + + class OrdinalDay < Ordinal #:nodoc: + def to_s + (super + '-day-' + @type.to_s) + end + end + + class OrdinalMonth < Ordinal #:nodoc: + def to_s + (super + '-month-' + @type.to_s) + end + end + + class OrdinalYear < Ordinal #:nodoc: + def to_s + (super + '-year-' + @type.to_s) + end + end + +end +# +module Chronic + class Separator < Tag + + # Scan an Array of Token objects and apply any necessary Separator + # tags to each token. + # + # tokens - An Array of tokens to scan. + # options - The Hash of options specified in Chronic::parse. + # + # Returns an Array of tokens. + def self.scan(tokens, options) + tokens.each do |token| + if t = scan_for_commas(token) then token.tag(t); next end + if t = scan_for_dots(token) then token.tag(t); next end + if t = scan_for_colon(token) then token.tag(t); next end + if t = scan_for_space(token) then token.tag(t); next end + if t = scan_for_slash(token) then token.tag(t); next end + if t = scan_for_dash(token) then token.tag(t); next end + if t = scan_for_quote(token) then token.tag(t); next end + if t = scan_for_at(token) then token.tag(t); next end + if t = scan_for_in(token) then token.tag(t); next end + if t = scan_for_on(token) then token.tag(t); next end + if t = scan_for_and(token) then token.tag(t); next end + if t = scan_for_t(token) then token.tag(t); next end + if t = scan_for_w(token) then token.tag(t); next end + end + end + + # token - The Token object we want to scan. + # + # Returns a new SeparatorComma object. + def self.scan_for_commas(token) + scan_for token, SeparatorComma, { /^,$/ => :comma } + end + + # token - The Token object we want to scan. + # + # Returns a new SeparatorDot object. + def self.scan_for_dots(token) + scan_for token, SeparatorDot, { /^\.$/ => :dot } + end + + # token - The Token object we want to scan. + # + # Returns a new SeparatorColon object. + def self.scan_for_colon(token) + scan_for token, SeparatorColon, { /^:$/ => :colon } + end + + # token - The Token object we want to scan. + # + # Returns a new SeparatorSpace object. + def self.scan_for_space(token) + scan_for token, SeparatorSpace, { /^ $/ => :space } + end + + # token - The Token object we want to scan. + # + # Returns a new SeparatorSlash object. + def self.scan_for_slash(token) + scan_for token, SeparatorSlash, { /^\/$/ => :slash } + end + + # token - The Token object we want to scan. + # + # Returns a new SeparatorDash object. + def self.scan_for_dash(token) + scan_for token, SeparatorDash, { /^-$/ => :dash } + end + + # token - The Token object we want to scan. + # + # Returns a new SeparatorQuote object. + def self.scan_for_quote(token) + scan_for token, SeparatorQuote, + { + /^'$/ => :single_quote, + /^"$/ => :double_quote + } + end + + # token - The Token object we want to scan. + # + # Returns a new SeparatorAt object. + def self.scan_for_at(token) + scan_for token, SeparatorAt, { /^(at|@)$/ => :at } + end + + # token - The Token object we want to scan. + # + # Returns a new SeparatorIn object. + def self.scan_for_in(token) + scan_for token, SeparatorIn, { /^in$/ => :in } + end + + # token - The Token object we want to scan. + # + # Returns a new SeparatorOn object. + def self.scan_for_on(token) + scan_for token, SeparatorOn, { /^on$/ => :on } + end + + # token - The Token object we want to scan. + # + # Returns a new SeperatorAnd Object object. + def self.scan_for_and(token) + scan_for token, SeparatorAnd, { /^and$/ => :and } + end + + # token - The Token object we want to scan. + # + # Returns a new SeperatorT Object object. + def self.scan_for_t(token) + scan_for token, SeparatorT, { /^t$/ => :T } + end + + # token - The Token object we want to scan. + # + # Returns a new SeperatorW Object object. + def self.scan_for_w(token) + scan_for token, SeparatorW, { /^w$/ => :W } + end + + def to_s + 'separator' + end + end + + class SeparatorComma < Separator #:nodoc: + def to_s + (super + '-comma') + end + end + + class SeparatorDot < Separator #:nodoc: + def to_s + (super + '-dot') + end + end + + class SeparatorColon < Separator #:nodoc: + def to_s + (super + '-colon') + end + end + + class SeparatorSpace < Separator #:nodoc: + def to_s + (super + '-space') + end + end + + class SeparatorSlash < Separator #:nodoc: + def to_s + (super + '-slash') + end + end + + class SeparatorDash < Separator #:nodoc: + def to_s + (super + '-dash') + end + end + + class SeparatorQuote < Separator #:nodoc: + def to_s + (super + '-quote-' + @type.to_s) + end + end + + class SeparatorAt < Separator #:nodoc: + def to_s + (super + '-at') + end + end + + class SeparatorIn < Separator #:nodoc: + def to_s + (super + '-in') + end + end + + class SeparatorOn < Separator #:nodoc: + def to_s + (super + '-on') + end + end + + class SeparatorAnd < Separator #:nodoc: + def to_s + (super + '-and') + end + end + + class SeparatorT < Separator #:nodoc: + def to_s + (super + '-T') + end + end + + class SeparatorW < Separator #:nodoc: + def to_s + (super + '-W') + end + end + +end +# +module Chronic + class Sign < Tag + + # Scan an Array of Token objects and apply any necessary Sign + # tags to each token. + # + # tokens - An Array of tokens to scan. + # options - The Hash of options specified in Chronic::parse. + # + # Returns an Array of tokens. + def self.scan(tokens, options) + tokens.each do |token| + if t = scan_for_plus(token) then token.tag(t); next end + if t = scan_for_minus(token) then token.tag(t); next end + end + end + + # token - The Token object we want to scan. + # + # Returns a new SignPlus object. + def self.scan_for_plus(token) + scan_for token, SignPlus, { /^\+$/ => :plus } + end + + # token - The Token object we want to scan. + # + # Returns a new SignMinus object. + def self.scan_for_minus(token) + scan_for token, SignMinus, { /^-$/ => :minus } + end + + def to_s + 'sign' + end + end + + class SignPlus < Sign #:nodoc: + def to_s + (super + '-plus') + end + end + + class SignMinus < Sign #:nodoc: + def to_s + (super + '-minus') + end + end + +end +# +module Chronic + class TimeZone < Tag + + # Scan an Array of Token objects and apply any necessary TimeZone + # tags to each token. + # + # tokens - An Array of tokens to scan. + # options - The Hash of options specified in Chronic::parse. + # + # Returns an Array of tokens. + def self.scan(tokens, options) + tokens.each do |token| + if t = scan_for_all(token) then token.tag(t); next end + end + end + + # token - The Token object we want to scan. + # + # Returns a new Pointer object. + def self.scan_for_all(token) + scan_for token, self, + { + /[PMCE][DS]T|UTC/i => :tz, + /(tzminus)?\d{2}:?\d{2}/ => :tz + } + end + + def to_s + 'timezone' + end + end +end +# +require 'strscan' + +module Chronic + class Numerizer + + DIRECT_NUMS = [ + ['eleven', '11'], + ['twelve', '12'], + ['thirteen', '13'], + ['fourteen', '14'], + ['fifteen', '15'], + ['sixteen', '16'], + ['seventeen', '17'], + ['eighteen', '18'], + ['nineteen', '19'], + ['ninteen', '19'], # Common mis-spelling + ['zero', '0'], + ['one', '1'], + ['two', '2'], + ['three', '3'], + ['four(\W|$)', '4\1'], # The weird regex is so that it matches four but not fourty + ['five', '5'], + ['six(\W|$)', '6\1'], + ['seven(\W|$)', '7\1'], + ['eight(\W|$)', '8\1'], + ['nine(\W|$)', '9\1'], + ['ten', '10'], + ['\ba[\b^$]', '1'] # doesn't make sense for an 'a' at the end to be a 1 + ] + + ORDINALS = [ + ['first', '1'], + ['third', '3'], + ['fourth', '4'], + ['fifth', '5'], + ['sixth', '6'], + ['seventh', '7'], + ['eighth', '8'], + ['ninth', '9'], + ['tenth', '10'], + ['twelfth', '12'], + ['twentieth', '20'], + ['thirtieth', '30'], + ['fourtieth', '40'], + ['fiftieth', '50'], + ['sixtieth', '60'], + ['seventieth', '70'], + ['eightieth', '80'], + ['ninetieth', '90'] + ] + + TEN_PREFIXES = [ + ['twenty', 20], + ['thirty', 30], + ['forty', 40], + ['fourty', 40], # Common mis-spelling + ['fifty', 50], + ['sixty', 60], + ['seventy', 70], + ['eighty', 80], + ['ninety', 90] + ] + + BIG_PREFIXES = [ + ['hundred', 100], + ['thousand', 1000], + ['million', 1_000_000], + ['billion', 1_000_000_000], + ['trillion', 1_000_000_000_000], + ] + + def self.numerize(string) + string = string.dup + + # preprocess + string = string.gsub(/ +|([^\d])-([^\d])/, '\1 \2') # will mutilate hyphenated-words but shouldn't matter for date extraction + string = string.gsub(/a half/, 'haAlf') # take the 'a' out so it doesn't turn into a 1, save the half for the end + + # easy/direct replacements + + DIRECT_NUMS.each do |dn| + string = string.gsub(/#{dn[0]}/i, '' + dn[1]) + end + + ORDINALS.each do |on| + string = string.gsub(/#{on[0]}/i, '' + on[1] + on[0][-2, 2]) + end + + # ten, twenty, etc. + + TEN_PREFIXES.each do |tp| + string = string.gsub(/(?:#{tp[0]}) *(\d(?=[^\d]|$))*/i) { '' + (tp[1] + $1.to_i).to_s } + end + + TEN_PREFIXES.each do |tp| + string = string.gsub(/#{tp[0]}/i) { '' + tp[1].to_s } + end + + # hundreds, thousands, millions, etc. + + BIG_PREFIXES.each do |bp| + string = string.gsub(/(?:)?(\d*) *#{bp[0]}/i) { $1.empty? ? bp[1] : '' + (bp[1] * $1.to_i).to_s} + andition(string) + end + + # fractional addition + # I'm not combining this with the previous block as using float addition complicates the strings + # (with extraneous .0's and such ) + string = string.gsub(/(\d+)(?: | and |-)*haAlf/i) { ($1.to_f + 0.5).to_s } + + string.gsub(//, '') + end + + class << self + private + + def andition(string) + sc = StringScanner.new(string) + + while sc.scan_until(/(\d+)( | and )(\d+)(?=[^\w]|$)/i) + if sc[2] =~ /and/ || sc[1].size > sc[3].size + string[(sc.pos - sc.matched_size)..(sc.pos-1)] = '' + (sc[1].to_i + sc[3].to_i).to_s + sc.reset + end + end + end + + end + end +end +# +module Chronic + class Season + + attr_reader :start + attr_reader :end + + def initialize(start_date, end_date) + @start = start_date + @end = end_date + end + + def self.find_next_season(season, pointer) + lookup = [:spring, :summer, :autumn, :winter] + next_season_num = (lookup.index(season) + 1 * pointer) % 4 + lookup[next_season_num] + end + + def self.season_after(season) + find_next_season(season, +1) + end + + def self.season_before(season) + find_next_season(season, -1) + end + end +end +# +module Chronic + class Repeater < Tag + + # Scan an Array of Token objects and apply any necessary Repeater + # tags to each token. + # + # tokens - An Array of tokens to scan. + # options - The Hash of options specified in Chronic::parse. + # + # Returns an Array of tokens. + def self.scan(tokens, options) + tokens.each do |token| + if t = scan_for_season_names(token, options) then token.tag(t); next end + if t = scan_for_month_names(token, options) then token.tag(t); next end + if t = scan_for_day_names(token, options) then token.tag(t); next end + if t = scan_for_day_portions(token, options) then token.tag(t); next end + if t = scan_for_times(token, options) then token.tag(t); next end + if t = scan_for_units(token, options) then token.tag(t); next end + end + end + + # token - The Token object we want to scan. + # + # Returns a new Repeater object. + def self.scan_for_season_names(token, options = {}) + scan_for token, RepeaterSeasonName, + { + /^springs?$/ => :spring, + /^summers?$/ => :summer, + /^(autumn)|(fall)s?$/ => :autumn, + /^winters?$/ => :winter + }, options + end + + # token - The Token object we want to scan. + # + # Returns a new Repeater object. + def self.scan_for_month_names(token, options = {}) + scan_for token, RepeaterMonthName, + { + /^jan[:\.]?(uary)?$/ => :january, + /^feb[:\.]?(ruary)?$/ => :february, + /^mar[:\.]?(ch)?$/ => :march, + /^apr[:\.]?(il)?$/ => :april, + /^may$/ => :may, + /^jun[:\.]?e?$/ => :june, + /^jul[:\.]?y?$/ => :july, + /^aug[:\.]?(ust)?$/ => :august, + /^sep[:\.]?(t[:\.]?|tember)?$/ => :september, + /^oct[:\.]?(ober)?$/ => :october, + /^nov[:\.]?(ember)?$/ => :november, + /^dec[:\.]?(ember)?$/ => :december + }, options + end + + # token - The Token object we want to scan. + # + # Returns a new Repeater object. + def self.scan_for_day_names(token, options = {}) + scan_for token, RepeaterDayName, + { + /^m[ou]n(day)?$/ => :monday, + /^t(ue|eu|oo|u|)s?(day)?$/ => :tuesday, + /^we(d|dnes|nds|nns)(day)?$/ => :wednesday, + /^th(u|ur|urs|ers)(day)?$/ => :thursday, + /^fr[iy](day)?$/ => :friday, + /^sat(t?[ue]rday)?$/ => :saturday, + /^su[nm](day)?$/ => :sunday + }, options + end + + # token - The Token object we want to scan. + # + # Returns a new Repeater object. + def self.scan_for_day_portions(token, options = {}) + scan_for token, RepeaterDayPortion, + { + /^ams?$/ => :am, + /^pms?$/ => :pm, + /^mornings?$/ => :morning, + /^afternoons?$/ => :afternoon, + /^evenings?$/ => :evening, + /^(night|nite)s?$/ => :night + }, options + end + + # token - The Token object we want to scan. + # + # Returns a new Repeater object. + def self.scan_for_times(token, options = {}) + scan_for token, RepeaterTime, /^\d{1,2}(:?\d{1,2})?([\.:]?\d{1,2}([\.:]\d{1,6})?)?$/, options + end + + # token - The Token object we want to scan. + # + # Returns a new Repeater object. + def self.scan_for_units(token, options = {}) + { + /^years?$/ => :year, + /^seasons?$/ => :season, + /^months?$/ => :month, + /^fortnights?$/ => :fortnight, + /^weeks?$/ => :week, + /^weekends?$/ => :weekend, + /^(week|business)days?$/ => :weekday, + /^days?$/ => :day, + /^hrs?$/ => :hour, + /^hours?$/ => :hour, + /^mins?$/ => :minute, + /^minutes?$/ => :minute, + /^secs?$/ => :second, + /^seconds?$/ => :second + }.each do |item, symbol| + if item =~ token.word + klass_name = 'Repeater' + symbol.to_s.capitalize + klass = Chronic.const_get(klass_name) + return klass.new(symbol, options) + end + end + return nil + end + + def <=>(other) + width <=> other.width + end + + # returns the width (in seconds or months) of this repeatable. + def width + raise("Repeater#width must be overridden in subclasses") + end + + # returns the next occurance of this repeatable. + def next(pointer) + raise("Start point must be set before calling #next") unless @now + end + + def this(pointer) + raise("Start point must be set before calling #this") unless @now + end + + def to_s + 'repeater' + end + end +end +# +module Chronic + class RepeaterYear < Repeater #:nodoc: + YEAR_SECONDS = 31536000 # 365 * 24 * 60 * 60 + + def initialize(type, options = {}) + super + @current_year_start = nil + end + + def next(pointer) + super + + unless @current_year_start + case pointer + when :future + @current_year_start = Chronic.construct(@now.year + 1) + when :past + @current_year_start = Chronic.construct(@now.year - 1) + end + else + diff = pointer == :future ? 1 : -1 + @current_year_start = Chronic.construct(@current_year_start.year + diff) + end + + Span.new(@current_year_start, Chronic.construct(@current_year_start.year + 1)) + end + + def this(pointer = :future) + super + + case pointer + when :future + this_year_start = Chronic.construct(@now.year, @now.month, @now.day + 1) + this_year_end = Chronic.construct(@now.year + 1, 1, 1) + when :past + this_year_start = Chronic.construct(@now.year, 1, 1) + this_year_end = Chronic.construct(@now.year, @now.month, @now.day) + when :none + this_year_start = Chronic.construct(@now.year, 1, 1) + this_year_end = Chronic.construct(@now.year + 1, 1, 1) + end + + Span.new(this_year_start, this_year_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + new_begin = build_offset_time(span.begin, amount, direction) + new_end = build_offset_time(span.end, amount, direction) + Span.new(new_begin, new_end) + end + + def width + YEAR_SECONDS + end + + def to_s + (super + '-year') + end + + private + + def build_offset_time(time, amount, direction) + year = time.year + (amount * direction) + days = month_days(year, time.month) + day = time.day > days ? days : time.day + Chronic.construct(year, time.month, day, time.hour, time.min, time.sec) + end + + def month_days(year, month) + if ::Date.leap?(year) + RepeaterMonth::MONTH_DAYS_LEAP[month - 1] + else + RepeaterMonth::MONTH_DAYS[month - 1] + end + end + end +end +# +module Chronic + class RepeaterSeason < Repeater #:nodoc: + SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60 + SEASONS = { + :spring => Season.new(MiniDate.new(3,20), MiniDate.new(6,20)), + :summer => Season.new(MiniDate.new(6,21), MiniDate.new(9,22)), + :autumn => Season.new(MiniDate.new(9,23), MiniDate.new(12,21)), + :winter => Season.new(MiniDate.new(12,22), MiniDate.new(3,19)) + } + + def initialize(type, options = {}) + super + @next_season_start = nil + @next_season_end = nil + end + + def next(pointer) + super + + direction = pointer == :future ? 1 : -1 + next_season = Season.find_next_season(find_current_season(MiniDate.from_time(@now)), direction) + + find_next_season_span(direction, next_season) + end + + def this(pointer = :future) + super + + direction = pointer == :future ? 1 : -1 + + today = Chronic.construct(@now.year, @now.month, @now.day) + this_ssn = find_current_season(MiniDate.from_time(@now)) + case pointer + when :past + this_ssn_start = today + direction * num_seconds_til_start(this_ssn, direction) + this_ssn_end = today + when :future + this_ssn_start = today + RepeaterDay::DAY_SECONDS + this_ssn_end = today + direction * num_seconds_til_end(this_ssn, direction) + when :none + this_ssn_start = today + direction * num_seconds_til_start(this_ssn, direction) + this_ssn_end = today + direction * num_seconds_til_end(this_ssn, direction) + end + + construct_season(this_ssn_start, this_ssn_end) + end + + def offset(span, amount, pointer) + Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer)) + end + + def offset_by(time, amount, pointer) + direction = pointer == :future ? 1 : -1 + time + amount * direction * SEASON_SECONDS + end + + def width + SEASON_SECONDS + end + + def to_s + (super + '-season') + end + + private + + def find_next_season_span(direction, next_season) + unless @next_season_start || @next_season_end + @next_season_start = Chronic.construct(@now.year, @now.month, @now.day) + @next_season_end = Chronic.construct(@now.year, @now.month, @now.day) + end + + @next_season_start += direction * num_seconds_til_start(next_season, direction) + @next_season_end += direction * num_seconds_til_end(next_season, direction) + + construct_season(@next_season_start, @next_season_end) + end + + def find_current_season(md) + [:spring, :summer, :autumn, :winter].find do |season| + md.is_between?(SEASONS[season].start, SEASONS[season].end) + end + end + + def num_seconds_til(goal, direction) + start = Chronic.construct(@now.year, @now.month, @now.day) + seconds = 0 + + until MiniDate.from_time(start + direction * seconds).equals?(goal) + seconds += RepeaterDay::DAY_SECONDS + end + + seconds + end + + def num_seconds_til_start(season_symbol, direction) + num_seconds_til(SEASONS[season_symbol].start, direction) + end + + def num_seconds_til_end(season_symbol, direction) + num_seconds_til(SEASONS[season_symbol].end, direction) + end + + def construct_season(start, finish) + Span.new( + Chronic.construct(start.year, start.month, start.day), + Chronic.construct(finish.year, finish.month, finish.day) + ) + end + end +end +# +module Chronic + class RepeaterSeasonName < RepeaterSeason #:nodoc: + SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60 + DAY_SECONDS = 86_400 # (24 * 60 * 60) + + def next(pointer) + direction = pointer == :future ? 1 : -1 + find_next_season_span(direction, @type) + end + + def this(pointer = :future) + direction = pointer == :future ? 1 : -1 + + today = Chronic.construct(@now.year, @now.month, @now.day) + goal_ssn_start = today + direction * num_seconds_til_start(@type, direction) + goal_ssn_end = today + direction * num_seconds_til_end(@type, direction) + curr_ssn = find_current_season(MiniDate.from_time(@now)) + case pointer + when :past + this_ssn_start = goal_ssn_start + this_ssn_end = (curr_ssn == @type) ? today : goal_ssn_end + when :future + this_ssn_start = (curr_ssn == @type) ? today + RepeaterDay::DAY_SECONDS : goal_ssn_start + this_ssn_end = goal_ssn_end + when :none + this_ssn_start = goal_ssn_start + this_ssn_end = goal_ssn_end + end + + construct_season(this_ssn_start, this_ssn_end) + end + + def offset(span, amount, pointer) + Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer)) + end + + def offset_by(time, amount, pointer) + direction = pointer == :future ? 1 : -1 + time + amount * direction * RepeaterYear::YEAR_SECONDS + end + + end +end +# +module Chronic + class RepeaterMonth < Repeater #:nodoc: + MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60 + YEAR_MONTHS = 12 + MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + MONTH_DAYS_LEAP = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + + def initialize(type, options = {}) + super + @current_month_start = nil + end + + def next(pointer) + super + + unless @current_month_start + @current_month_start = offset_by(Chronic.construct(@now.year, @now.month), 1, pointer) + else + @current_month_start = offset_by(Chronic.construct(@current_month_start.year, @current_month_start.month), 1, pointer) + end + + Span.new(@current_month_start, Chronic.construct(@current_month_start.year, @current_month_start.month + 1)) + end + + def this(pointer = :future) + super + + case pointer + when :future + month_start = Chronic.construct(@now.year, @now.month, @now.day + 1) + month_end = self.offset_by(Chronic.construct(@now.year, @now.month), 1, :future) + when :past + month_start = Chronic.construct(@now.year, @now.month) + month_end = Chronic.construct(@now.year, @now.month, @now.day) + when :none + month_start = Chronic.construct(@now.year, @now.month) + month_end = self.offset_by(Chronic.construct(@now.year, @now.month), 1, :future) + end + + Span.new(month_start, month_end) + end + + def offset(span, amount, pointer) + Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer)) + end + + def offset_by(time, amount, pointer) + direction = pointer == :future ? 1 : -1 + + amount_years = direction * amount / YEAR_MONTHS + amount_months = direction * amount % YEAR_MONTHS + + new_year = time.year + amount_years + new_month = time.month + amount_months + if new_month > YEAR_MONTHS + new_year += 1 + new_month -= YEAR_MONTHS + end + + days = month_days(new_year, new_month) + new_day = time.day > days ? days : time.day + + Chronic.construct(new_year, new_month, new_day, time.hour, time.min, time.sec) + end + + def width + MONTH_SECONDS + end + + def to_s + (super + '-month') + end + + private + + def month_days(year, month) + ::Date.leap?(year) ? MONTH_DAYS_LEAP[month - 1] : MONTH_DAYS[month - 1] + end + end +end +# +module Chronic + class RepeaterMonthName < Repeater #:nodoc: + MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60 + MONTHS = { + :january => 1, + :february => 2, + :march => 3, + :april => 4, + :may => 5, + :june => 6, + :july => 7, + :august => 8, + :september => 9, + :october => 10, + :november => 11, + :december => 12 + } + + def initialize(type, options = {}) + super + @current_month_begin = nil + end + + def next(pointer) + super + + unless @current_month_begin + case pointer + when :future + if @now.month < index + @current_month_begin = Chronic.construct(@now.year, index) + elsif @now.month > index + @current_month_begin = Chronic.construct(@now.year + 1, index) + end + when :none + if @now.month <= index + @current_month_begin = Chronic.construct(@now.year, index) + elsif @now.month > index + @current_month_begin = Chronic.construct(@now.year + 1, index) + end + when :past + if @now.month >= index + @current_month_begin = Chronic.construct(@now.year, index) + elsif @now.month < index + @current_month_begin = Chronic.construct(@now.year - 1, index) + end + end + @current_month_begin || raise("Current month should be set by now") + else + case pointer + when :future + @current_month_begin = Chronic.construct(@current_month_begin.year + 1, @current_month_begin.month) + when :past + @current_month_begin = Chronic.construct(@current_month_begin.year - 1, @current_month_begin.month) + end + end + + cur_month_year = @current_month_begin.year + cur_month_month = @current_month_begin.month + + if cur_month_month == 12 + next_month_year = cur_month_year + 1 + next_month_month = 1 + else + next_month_year = cur_month_year + next_month_month = cur_month_month + 1 + end + + Span.new(@current_month_begin, Chronic.construct(next_month_year, next_month_month)) + end + + def this(pointer = :future) + super + + case pointer + when :past + self.next(pointer) + when :future, :none + self.next(:none) + end + end + + def width + MONTH_SECONDS + end + + def index + @index ||= MONTHS[@type] + end + + def to_s + (super + '-monthname-' + @type.to_s) + end + end +end +# +module Chronic + class RepeaterFortnight < Repeater #:nodoc: + FORTNIGHT_SECONDS = 1_209_600 # (14 * 24 * 60 * 60) + + def initialize(type, options = {}) + super + @current_fortnight_start = nil + end + + def next(pointer) + super + + unless @current_fortnight_start + case pointer + when :future + sunday_repeater = RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + next_sunday_span = sunday_repeater.next(:future) + @current_fortnight_start = next_sunday_span.begin + when :past + sunday_repeater = RepeaterDayName.new(:sunday) + sunday_repeater.start = (@now + RepeaterDay::DAY_SECONDS) + 2.times { sunday_repeater.next(:past) } + last_sunday_span = sunday_repeater.next(:past) + @current_fortnight_start = last_sunday_span.begin + end + else + direction = pointer == :future ? 1 : -1 + @current_fortnight_start += direction * FORTNIGHT_SECONDS + end + + Span.new(@current_fortnight_start, @current_fortnight_start + FORTNIGHT_SECONDS) + end + + def this(pointer = :future) + super + + pointer = :future if pointer == :none + + case pointer + when :future + this_fortnight_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour) + RepeaterHour::HOUR_SECONDS + sunday_repeater = RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + sunday_repeater.this(:future) + this_sunday_span = sunday_repeater.this(:future) + this_fortnight_end = this_sunday_span.begin + Span.new(this_fortnight_start, this_fortnight_end) + when :past + this_fortnight_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour) + sunday_repeater = RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_fortnight_start = last_sunday_span.begin + Span.new(this_fortnight_start, this_fortnight_end) + end + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * FORTNIGHT_SECONDS + end + + def width + FORTNIGHT_SECONDS + end + + def to_s + (super + '-fortnight') + end + end +end +# +module Chronic + class RepeaterWeek < Repeater #:nodoc: + WEEK_SECONDS = 604800 # (7 * 24 * 60 * 60) + + def initialize(type, options = {}) + super + @current_week_start = nil + end + + def next(pointer) + super + + unless @current_week_start + case pointer + when :future + sunday_repeater = RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + next_sunday_span = sunday_repeater.next(:future) + @current_week_start = next_sunday_span.begin + when :past + sunday_repeater = RepeaterDayName.new(:sunday) + sunday_repeater.start = (@now + RepeaterDay::DAY_SECONDS) + sunday_repeater.next(:past) + last_sunday_span = sunday_repeater.next(:past) + @current_week_start = last_sunday_span.begin + end + else + direction = pointer == :future ? 1 : -1 + @current_week_start += direction * WEEK_SECONDS + end + + Span.new(@current_week_start, @current_week_start + WEEK_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + this_week_start = Chronic.time_class.local(@now.year, @now.month, @now.day, @now.hour) + RepeaterHour::HOUR_SECONDS + sunday_repeater = RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + this_sunday_span = sunday_repeater.this(:future) + this_week_end = this_sunday_span.begin + Span.new(this_week_start, this_week_end) + when :past + this_week_end = Chronic.time_class.local(@now.year, @now.month, @now.day, @now.hour) + sunday_repeater = RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_week_start = last_sunday_span.begin + Span.new(this_week_start, this_week_end) + when :none + sunday_repeater = RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_week_start = last_sunday_span.begin + Span.new(this_week_start, this_week_start + WEEK_SECONDS) + end + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * WEEK_SECONDS + end + + def width + WEEK_SECONDS + end + + def to_s + (super + '-week') + end + end +end +# +module Chronic + class RepeaterWeekend < Repeater #:nodoc: + WEEKEND_SECONDS = 172_800 # (2 * 24 * 60 * 60) + + def initialize(type, options = {}) + super + @current_week_start = nil + end + + def next(pointer) + super + + unless @current_week_start + case pointer + when :future + saturday_repeater = RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + next_saturday_span = saturday_repeater.next(:future) + @current_week_start = next_saturday_span.begin + when :past + saturday_repeater = RepeaterDayName.new(:saturday) + saturday_repeater.start = (@now + RepeaterDay::DAY_SECONDS) + last_saturday_span = saturday_repeater.next(:past) + @current_week_start = last_saturday_span.begin + end + else + direction = pointer == :future ? 1 : -1 + @current_week_start += direction * RepeaterWeek::WEEK_SECONDS + end + + Span.new(@current_week_start, @current_week_start + WEEKEND_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future, :none + saturday_repeater = RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + this_saturday_span = saturday_repeater.this(:future) + Span.new(this_saturday_span.begin, this_saturday_span.begin + WEEKEND_SECONDS) + when :past + saturday_repeater = RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + last_saturday_span = saturday_repeater.this(:past) + Span.new(last_saturday_span.begin, last_saturday_span.begin + WEEKEND_SECONDS) + end + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + weekend = RepeaterWeekend.new(:weekend) + weekend.start = span.begin + start = weekend.next(pointer).begin + (amount - 1) * direction * RepeaterWeek::WEEK_SECONDS + Span.new(start, start + (span.end - span.begin)) + end + + def width + WEEKEND_SECONDS + end + + def to_s + (super + '-weekend') + end + end +end +# +module Chronic + class RepeaterWeekday < Repeater #:nodoc: + DAY_SECONDS = 86400 # (24 * 60 * 60) + DAYS = { + :sunday => 0, + :monday => 1, + :tuesday => 2, + :wednesday => 3, + :thursday => 4, + :friday => 5, + :saturday => 6 + } + + def initialize(type, options = {}) + super + @current_weekday_start = nil + end + + def next(pointer) + super + + direction = pointer == :future ? 1 : -1 + + unless @current_weekday_start + @current_weekday_start = Chronic.construct(@now.year, @now.month, @now.day) + @current_weekday_start += direction * DAY_SECONDS + + until is_weekday?(@current_weekday_start.wday) + @current_weekday_start += direction * DAY_SECONDS + end + else + loop do + @current_weekday_start += direction * DAY_SECONDS + break if is_weekday?(@current_weekday_start.wday) + end + end + + Span.new(@current_weekday_start, @current_weekday_start + DAY_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :past + self.next(:past) + when :future, :none + self.next(:future) + end + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + + num_weekdays_passed = 0; offset = 0 + until num_weekdays_passed == amount + offset += direction * DAY_SECONDS + num_weekdays_passed += 1 if is_weekday?((span.begin+offset).wday) + end + + span + offset + end + + def width + DAY_SECONDS + end + + def to_s + (super + '-weekday') + end + + private + + def is_weekend?(day) + day == symbol_to_number(:saturday) || day == symbol_to_number(:sunday) + end + + def is_weekday?(day) + !is_weekend?(day) + end + + def symbol_to_number(sym) + DAYS[sym] || raise("Invalid symbol specified") + end + end +end +# +module Chronic + class RepeaterDay < Repeater #:nodoc: + DAY_SECONDS = 86_400 # (24 * 60 * 60) + + def initialize(type, options = {}) + super + @current_day_start = nil + end + + def next(pointer) + super + + unless @current_day_start + @current_day_start = Chronic.time_class.local(@now.year, @now.month, @now.day) + end + + direction = pointer == :future ? 1 : -1 + @current_day_start += direction * DAY_SECONDS + + Span.new(@current_day_start, @current_day_start + DAY_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + day_begin = Chronic.construct(@now.year, @now.month, @now.day, @now.hour) + day_end = Chronic.construct(@now.year, @now.month, @now.day) + DAY_SECONDS + when :past + day_begin = Chronic.construct(@now.year, @now.month, @now.day) + day_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour) + when :none + day_begin = Chronic.construct(@now.year, @now.month, @now.day) + day_end = Chronic.construct(@now.year, @now.month, @now.day) + DAY_SECONDS + end + + Span.new(day_begin, day_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * DAY_SECONDS + end + + def width + DAY_SECONDS + end + + def to_s + (super + '-day') + end + end +end +# +module Chronic + class RepeaterDayName < Repeater #:nodoc: + DAY_SECONDS = 86400 # (24 * 60 * 60) + + def initialize(type, options = {}) + super + @current_date = nil + end + + def next(pointer) + super + + direction = pointer == :future ? 1 : -1 + + unless @current_date + @current_date = ::Date.new(@now.year, @now.month, @now.day) + @current_date += direction + + day_num = symbol_to_number(@type) + + while @current_date.wday != day_num + @current_date += direction + end + else + @current_date += direction * 7 + end + next_date = @current_date.succ + Span.new(Chronic.construct(@current_date.year, @current_date.month, @current_date.day), Chronic.construct(next_date.year, next_date.month, next_date.day)) + end + + def this(pointer = :future) + super + + pointer = :future if pointer == :none + self.next(pointer) + end + + def width + DAY_SECONDS + end + + def to_s + (super + '-dayname-' + @type.to_s) + end + + private + + def symbol_to_number(sym) + lookup = {:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6} + lookup[sym] || raise("Invalid symbol specified") + end + end +end +# +module Chronic + class RepeaterDayPortion < Repeater #:nodoc: + PORTIONS = { + :am => 0..(12 * 60 * 60 - 1), + :pm => (12 * 60 * 60)..(24 * 60 * 60 - 1), + :morning => (6 * 60 * 60)..(12 * 60 * 60), # 6am-12am, + :afternoon => (13 * 60 * 60)..(17 * 60 * 60), # 1pm-5pm, + :evening => (17 * 60 * 60)..(20 * 60 * 60), # 5pm-8pm, + :night => (20 * 60 * 60)..(24 * 60 * 60), # 8pm-12pm + } + + def initialize(type, options = {}) + super + @current_span = nil + + if type.kind_of? Integer + @range = (@type * 60 * 60)..((@type + 12) * 60 * 60) + else + @range = PORTIONS[type] + @range || raise("Invalid type '#{type}' for RepeaterDayPortion") + end + + @range || raise("Range should have been set by now") + end + + def next(pointer) + super + + unless @current_span + now_seconds = @now - Chronic.construct(@now.year, @now.month, @now.day) + if now_seconds < @range.begin + case pointer + when :future + range_start = Chronic.construct(@now.year, @now.month, @now.day) + @range.begin + when :past + range_start = Chronic.construct(@now.year, @now.month, @now.day - 1) + @range.begin + end + elsif now_seconds > @range.end + case pointer + when :future + range_start = Chronic.construct(@now.year, @now.month, @now.day + 1) + @range.begin + when :past + range_start = Chronic.construct(@now.year, @now.month, @now.day) + @range.begin + end + else + case pointer + when :future + range_start = Chronic.construct(@now.year, @now.month, @now.day + 1) + @range.begin + when :past + range_start = Chronic.construct(@now.year, @now.month, @now.day - 1) + @range.begin + end + end + offset = (@range.end - @range.begin) + range_end = construct_date_from_reference_and_offset(range_start, offset) + @current_span = Span.new(range_start, range_end) + else + days_to_shift_window = + case pointer + when :future + 1 + when :past + -1 + end + + new_begin = Chronic.construct(@current_span.begin.year, @current_span.begin.month, @current_span.begin.day + days_to_shift_window, @current_span.begin.hour, @current_span.begin.min, @current_span.begin.sec) + new_end = Chronic.construct(@current_span.end.year, @current_span.end.month, @current_span.end.day + days_to_shift_window, @current_span.end.hour, @current_span.end.min, @current_span.end.sec) + @current_span = Span.new(new_begin, new_end) + end + end + + def this(context = :future) + super + + range_start = Chronic.construct(@now.year, @now.month, @now.day) + @range.begin + range_end = construct_date_from_reference_and_offset(range_start) + @current_span = Span.new(range_start, range_end) + end + + def offset(span, amount, pointer) + @now = span.begin + portion_span = self.next(pointer) + direction = pointer == :future ? 1 : -1 + portion_span + (direction * (amount - 1) * RepeaterDay::DAY_SECONDS) + end + + def width + @range || raise("Range has not been set") + return @current_span.width if @current_span + if @type.kind_of? Integer + return (12 * 60 * 60) + else + @range.end - @range.begin + end + end + + def to_s + (super + '-dayportion-' + @type.to_s) + end + + private + def construct_date_from_reference_and_offset(reference, offset = nil) + elapsed_seconds_for_range = offset || (@range.end - @range.begin) + second_hand = ((elapsed_seconds_for_range - (12 * 60))) % 60 + minute_hand = (elapsed_seconds_for_range - second_hand) / (60) % 60 + hour_hand = (elapsed_seconds_for_range - minute_hand - second_hand) / (60 * 60) + reference.hour % 24 + Chronic.construct(reference.year, reference.month, reference.day, hour_hand, minute_hand, second_hand) + end + end +end +# +module Chronic + class RepeaterHour < Repeater #:nodoc: + HOUR_SECONDS = 3600 # 60 * 60 + + def initialize(type, options = {}) + super + @current_hour_start = nil + end + + def next(pointer) + super + + unless @current_hour_start + case pointer + when :future + @current_hour_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour + 1) + when :past + @current_hour_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour - 1) + end + else + direction = pointer == :future ? 1 : -1 + @current_hour_start += direction * HOUR_SECONDS + end + + Span.new(@current_hour_start, @current_hour_start + HOUR_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + hour_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1) + hour_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour + 1) + when :past + hour_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour) + hour_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + when :none + hour_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour) + hour_end = hour_start + HOUR_SECONDS + end + + Span.new(hour_start, hour_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * HOUR_SECONDS + end + + def width + HOUR_SECONDS + end + + def to_s + (super + '-hour') + end + end +end +# +module Chronic + class RepeaterMinute < Repeater #:nodoc: + MINUTE_SECONDS = 60 + + def initialize(type, options = {}) + super + @current_minute_start = nil + end + + def next(pointer = :future) + super + + unless @current_minute_start + case pointer + when :future + @current_minute_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1) + when :past + @current_minute_start = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min - 1) + end + else + direction = pointer == :future ? 1 : -1 + @current_minute_start += direction * MINUTE_SECONDS + end + + Span.new(@current_minute_start, @current_minute_start + MINUTE_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + minute_begin = @now + minute_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + when :past + minute_begin = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + minute_end = @now + when :none + minute_begin = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + minute_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + MINUTE_SECONDS + end + + Span.new(minute_begin, minute_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * MINUTE_SECONDS + end + + def width + MINUTE_SECONDS + end + + def to_s + (super + '-minute') + end + end +end +# +module Chronic + class RepeaterSecond < Repeater #:nodoc: + SECOND_SECONDS = 1 # haha, awesome + + def initialize(type, options = {}) + super + @second_start = nil + end + + def next(pointer = :future) + super + + direction = pointer == :future ? 1 : -1 + + unless @second_start + @second_start = @now + (direction * SECOND_SECONDS) + else + @second_start += SECOND_SECONDS * direction + end + + Span.new(@second_start, @second_start + SECOND_SECONDS) + end + + def this(pointer = :future) + super + + Span.new(@now, @now + 1) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * SECOND_SECONDS + end + + def width + SECOND_SECONDS + end + + def to_s + (super + '-second') + end + end +end +# +module Chronic + class RepeaterTime < Repeater #:nodoc: + class Tick #:nodoc: + attr_accessor :time + + def initialize(time, ambiguous = false) + @time = time + @ambiguous = ambiguous + end + + def ambiguous? + @ambiguous + end + + def *(other) + Tick.new(@time * other, @ambiguous) + end + + def to_f + @time.to_f + end + + def to_s + @time.to_s + (@ambiguous ? '?' : '') + end + + end + + def initialize(time, options = {}) + @current_time = nil + @options = options + time_parts = time.split(':') + raise ArgumentError, "Time cannot have more than 4 groups of ':'" if time_parts.count > 4 + + if time_parts.first.length > 2 and time_parts.count == 1 + if time_parts.first.length > 4 + second_index = time_parts.first.length - 2 + time_parts.insert(1, time_parts.first[second_index..time_parts.first.length]) + time_parts[0] = time_parts.first[0..second_index - 1] + end + minute_index = time_parts.first.length - 2 + time_parts.insert(1, time_parts.first[minute_index..time_parts.first.length]) + time_parts[0] = time_parts.first[0..minute_index - 1] + end + + ambiguous = false + hours = time_parts.first.to_i + + if @options[:hours24].nil? or (not @options[:hours24].nil? and @options[:hours24] != true) + ambiguous = true if (time_parts.first.length == 1 and hours > 0) or (hours >= 10 and hours <= 12) or (@options[:hours24] == false and hours > 0) + hours = 0 if hours == 12 and ambiguous + end + + hours *= 60 * 60 + minutes = 0 + seconds = 0 + subseconds = 0 + + minutes = time_parts[1].to_i * 60 if time_parts.count > 1 + seconds = time_parts[2].to_i if time_parts.count > 2 + subseconds = time_parts[3].to_f / (10 ** time_parts[3].length) if time_parts.count > 3 + + @type = Tick.new(hours + minutes + seconds + subseconds, ambiguous) + end + + # Return the next past or future Span for the time that this Repeater represents + # pointer - Symbol representing which temporal direction to fetch the next day + # must be either :past or :future + def next(pointer) + super + + half_day = 60 * 60 * 12 + full_day = 60 * 60 * 24 + + first = false + + unless @current_time + first = true + midnight = Chronic.time_class.local(@now.year, @now.month, @now.day) + + yesterday_midnight = midnight - full_day + tomorrow_midnight = midnight + full_day + + offset_fix = midnight.gmt_offset - tomorrow_midnight.gmt_offset + tomorrow_midnight += offset_fix + + catch :done do + if pointer == :future + if @type.ambiguous? + [midnight + @type.time + offset_fix, midnight + half_day + @type.time + offset_fix, tomorrow_midnight + @type.time].each do |t| + (@current_time = t; throw :done) if t >= @now + end + else + [midnight + @type.time + offset_fix, tomorrow_midnight + @type.time].each do |t| + (@current_time = t; throw :done) if t >= @now + end + end + else # pointer == :past + if @type.ambiguous? + [midnight + half_day + @type.time + offset_fix, midnight + @type.time + offset_fix, yesterday_midnight + @type.time + half_day].each do |t| + (@current_time = t; throw :done) if t <= @now + end + else + [midnight + @type.time + offset_fix, yesterday_midnight + @type.time].each do |t| + (@current_time = t; throw :done) if t <= @now + end + end + end + end + + @current_time || raise("Current time cannot be nil at this point") + end + + unless first + increment = @type.ambiguous? ? half_day : full_day + @current_time += pointer == :future ? increment : -increment + end + + Span.new(@current_time, @current_time + width) + end + + def this(context = :future) + super + + context = :future if context == :none + + self.next(context) + end + + def width + 1 + end + + def to_s + (super + '-time-' + @type.to_s) + end + end +end +# + +# Parse natural language dates and times into Time or Chronic::Span objects. +# +# Examples: +# +# require 'chronic' +# +# Time.now #=> Sun Aug 27 23:18:25 PDT 2006 +# +# Chronic.parse('tomorrow') +# #=> Mon Aug 28 12:00:00 PDT 2006 +# +# Chronic.parse('monday', :context => :past) +# #=> Mon Aug 21 12:00:00 PDT 2006 +module Chronic + VERSION = "0.10.2" + + class << self + + # Returns true when debug mode is enabled. + attr_accessor :debug + + # Examples: + # + # require 'chronic' + # require 'active_support/time' + # + # Time.zone = 'UTC' + # Chronic.time_class = Time.zone + # Chronic.parse('June 15 2006 at 5:54 AM') + # # => Thu, 15 Jun 2006 05:45:00 UTC +00:00 + # + # Returns The Time class Chronic uses internally. + attr_accessor :time_class + end + + self.debug = false + self.time_class = ::Time + + + # Parses a string containing a natural language date or time. + # + # If the parser can find a date or time, either a Time or Chronic::Span + # will be returned (depending on the value of `:guess`). If no + # date or time can be found, `nil` will be returned. + # + # text - The String text to parse. + # opts - An optional Hash of configuration options passed to Parser::new. + def self.parse(text, options = {}) + Parser.new(options).parse(text) + end + + # Construct a new time object determining possible month overflows + # and leap years. + # + # year - Integer year. + # month - Integer month. + # day - Integer day. + # hour - Integer hour. + # minute - Integer minute. + # second - Integer second. + # + # Returns a new Time object constructed from these params. + def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0, offset = nil) + if second >= 60 + minute += second / 60 + second = second % 60 + end + + if minute >= 60 + hour += minute / 60 + minute = minute % 60 + end + + if hour >= 24 + day += hour / 24 + hour = hour % 24 + end + + # determine if there is a day overflow. this is complicated by our crappy calendar + # system (non-constant number of days per month) + day <= 56 || raise("day must be no more than 56 (makes month resolution easier)") + if day > 28 # no month ever has fewer than 28 days, so only do this if necessary + days_this_month = ::Date.leap?(year) ? Date::MONTH_DAYS_LEAP[month] : Date::MONTH_DAYS[month] + if day > days_this_month + month += day / days_this_month + day = day % days_this_month + end + end + + if month > 12 + if month % 12 == 0 + year += (month - 12) / 12 + month = 12 + else + year += month / 12 + month = month % 12 + end + end + if Chronic.time_class.name == "Date" + Chronic.time_class.new(year, month, day) + elsif not Chronic.time_class.respond_to?(:new) or (RUBY_VERSION.to_f < 1.9 and Chronic.time_class.name == "Time") + Chronic.time_class.local(year, month, day, hour, minute, second) + else + offset = Time::normalize_offset(offset) if Chronic.time_class.name == "DateTime" + Chronic.time_class.new(year, month, day, hour, minute, second, offset) + end + end + +end diff --git a/gxg/deps/mimemagic.rb b/gxg/deps/mimemagic.rb new file mode 100644 index 0000000..547d89c --- /dev/null +++ b/gxg/deps/mimemagic.rb @@ -0,0 +1,2043 @@ +# -*- coding: binary -*- +# Generated from script/freedesktop.org.xml +class MimeMagic + # @private + # :nodoc: + EXTENSIONS = { + '123' => 'application/vnd.lotus-1-2-3', + '32x' => 'application/x-genesis-rom', + '3ds' => 'image/x-3ds', + '3g2' => 'video/3gpp2', + '3ga' => 'video/3gpp', + '3gp' => 'video/3gpp', + '3gp2' => 'video/3gpp2', + '3gpp' => 'video/3gpp', + '3gpp2' => 'video/3gpp2', + '602' => 'application/x-t602', + '669' => 'audio/x-mod', + '7z' => 'application/x-7z-compressed', + 'a' => 'application/x-archive', + 'aac' => 'audio/aac', + 'abw' => 'application/x-abiword', + 'abw.crashed' => 'application/x-abiword', + 'abw.gz' => 'application/x-abiword', + 'ac3' => 'audio/ac3', + 'ace' => 'application/x-ace', + 'adb' => 'text/x-adasrc', + 'adf' => 'application/x-amiga-disk-format', + 'ads' => 'text/x-adasrc', + 'afm' => 'application/x-font-afm', + 'ag' => 'image/x-applix-graphics', + 'agb' => 'application/x-gba-rom', + 'ai' => 'application/illustrator', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aifc', + 'aiff' => 'audio/x-aiff', + 'aiffc' => 'audio/x-aifc', + 'al' => 'application/x-perl', + 'alz' => 'application/x-alz', + 'amr' => 'audio/AMR', + 'amz' => 'audio/x-amzxml', + 'ani' => 'application/x-navi-animation', + 'anx' => 'application/annodex', + 'ape' => 'audio/x-ape', + 'apk' => 'application/vnd.android.package-archive', + 'ar' => 'application/x-archive', + 'arj' => 'application/x-arj', + 'arw' => 'image/x-sony-arw', + 'as' => 'application/x-applix-spreadsheet', + 'asc' => 'application/pgp-encrypted', + 'asf' => 'application/vnd.ms-asf', + 'asp' => 'application/x-asp', + 'ass' => 'text/x-ssa', + 'asx' => 'audio/x-ms-asx', + 'atom' => 'application/atom+xml', + 'au' => 'audio/basic', + 'avf' => 'video/x-msvideo', + 'avi' => 'video/x-msvideo', + 'aw' => 'application/x-applix-word', + 'awb' => 'audio/AMR-WB', + 'awk' => 'application/x-awk', + 'axa' => 'audio/annodex', + 'axv' => 'video/annodex', + 'bak' => 'application/x-trash', + 'bcpio' => 'application/x-bcpio', + 'bdf' => 'application/x-font-bdf', + 'bdm' => 'video/mp2t', + 'bdmv' => 'video/mp2t', + 'bib' => 'text/x-bibtex', + 'bin' => 'application/octet-stream', + 'blend' => 'application/x-blender', + 'blender' => 'application/x-blender', + 'bmp' => 'image/bmp', + 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip', + 'c' => 'text/x-c++src', + 'c++' => 'text/x-c++src', + 'cab' => 'application/vnd.ms-cab-compressed', + 'cap' => 'application/vnd.tcpdump.pcap', + 'cb7' => 'application/x-cb7', + 'cbl' => 'text/x-cobol', + 'cbr' => 'application/x-cbr', + 'cbt' => 'application/x-cbt', + 'cbz' => 'application/x-cbz', + 'cc' => 'text/x-c++src', + 'ccmx' => 'application/x-ccmx', + 'cdf' => 'application/x-netcdf', + 'cdr' => 'application/vnd.corel-draw', + 'cer' => 'application/pkix-cert', + 'cert' => 'application/x-x509-ca-cert', + 'cgb' => 'application/x-gameboy-rom', + 'cgm' => 'image/cgm', + 'chm' => 'application/vnd.ms-htmlhelp', + 'chrt' => 'application/x-kchart', + 'class' => 'application/x-java', + 'clpi' => 'video/mp2t', + 'cls' => 'text/x-tex', + 'cmake' => 'text/x-cmake', + 'cob' => 'text/x-cobol', + 'coffee' => 'application/vnd.coffeescript', + 'cpi' => 'video/mp2t', + 'cpio' => 'application/x-cpio', + 'cpio.gz' => 'application/x-cpio-compressed', + 'cpp' => 'text/x-c++src', + 'cr2' => 'image/x-canon-cr2', + 'crdownload' => 'application/x-partial-download', + 'crl' => 'application/pkix-crl', + 'crt' => 'application/x-x509-ca-cert', + 'crw' => 'image/x-canon-crw', + 'cs' => 'text/x-csharp', + 'csh' => 'application/x-csh', + 'css' => 'text/css', + 'csv' => 'text/csv', + 'csvs' => 'text/csv-schema', + 'cue' => 'application/x-cue', + 'cur' => 'image/x-win-bitmap', + 'cxx' => 'text/x-c++src', + 'd' => 'text/x-dsrc', + 'dar' => 'application/x-dar', + 'dbf' => 'application/x-dbf', + 'dbk' => 'application/x-docbook+xml', + 'dc' => 'application/x-dc-rom', + 'dcl' => 'text/x-dcl', + 'dcm' => 'application/dicom', + 'dcr' => 'image/x-kodak-dcr', + 'dds' => 'image/x-dds', + 'deb' => 'application/vnd.debian.binary-package', + 'der' => 'application/x-x509-ca-cert', + 'desktop' => 'application/x-desktop', + 'di' => 'text/x-dsrc', + 'dia' => 'application/x-dia-diagram', + 'diff' => 'text/x-patch', + 'divx' => 'video/x-msvideo', + 'djv' => 'image/vnd.djvu', + 'djvu' => 'image/vnd.djvu', + 'dmg' => 'application/x-apple-diskimage', + 'dmp' => 'application/vnd.tcpdump.pcap', + 'dng' => 'image/x-adobe-dng', + 'doc' => 'application/msword', + 'docbook' => 'application/x-docbook+xml', + 'docm' => 'application/vnd.ms-word.document.macroEnabled.12', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dot' => 'application/msword-template', + 'dotm' => 'application/vnd.ms-word.template.macroEnabled.12', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'dsl' => 'text/x-dsl', + 'dtd' => 'application/xml-dtd', + 'dts' => 'audio/vnd.dts', + 'dtshd' => 'audio/vnd.dts.hd', + 'dtx' => 'text/x-tex', + 'dv' => 'video/dv', + 'dvi' => 'application/x-dvi', + 'dvi.bz2' => 'application/x-bzdvi', + 'dvi.gz' => 'application/x-gzdvi', + 'dwg' => 'image/vnd.dwg', + 'dxf' => 'image/vnd.dxf', + 'e' => 'text/x-eiffel', + 'egon' => 'application/x-egon', + 'eif' => 'text/x-eiffel', + 'el' => 'text/x-emacs-lisp', + 'emf' => 'image/x-emf', + 'eml' => 'message/rfc822', + 'emp' => 'application/vnd.emusic-emusic_package', + 'ent' => 'application/xml-external-parsed-entity', + 'eps' => 'image/x-eps', + 'eps.bz2' => 'image/x-bzeps', + 'eps.gz' => 'image/x-gzeps', + 'epsf' => 'image/x-eps', + 'epsf.bz2' => 'image/x-bzeps', + 'epsf.gz' => 'image/x-gzeps', + 'epsi' => 'image/x-eps', + 'epsi.bz2' => 'image/x-bzeps', + 'epsi.gz' => 'image/x-gzeps', + 'epub' => 'application/epub+zip', + 'erl' => 'text/x-erlang', + 'es' => 'application/ecmascript', + 'etheme' => 'application/x-e-theme', + 'etx' => 'text/x-setext', + 'exe' => 'application/x-ms-dos-executable', + 'exr' => 'image/x-exr', + 'ez' => 'application/andrew-inset', + 'f' => 'text/x-fortran', + 'f4a' => 'audio/mp4', + 'f4b' => 'audio/x-m4b', + 'f4v' => 'video/mp4', + 'f90' => 'text/x-fortran', + 'f95' => 'text/x-fortran', + 'fb2' => 'application/x-fictionbook+xml', + 'fb2.zip' => 'application/x-zip-compressed-fb2', + 'fig' => 'image/x-xfig', + 'fits' => 'image/fits', + 'fl' => 'application/x-fluid', + 'flac' => 'audio/flac', + 'flc' => 'video/x-flic', + 'fli' => 'video/x-flic', + 'flv' => 'video/x-flv', + 'flw' => 'application/x-kivio', + 'fo' => 'text/x-xslfo', + 'fodg' => 'application/vnd.oasis.opendocument.graphics-flat-xml', + 'fodp' => 'application/vnd.oasis.opendocument.presentation-flat-xml', + 'fods' => 'application/vnd.oasis.opendocument.spreadsheet-flat-xml', + 'fodt' => 'application/vnd.oasis.opendocument.text-flat-xml', + 'for' => 'text/x-fortran', + 'fxm' => 'video/x-javafx', + 'g3' => 'image/fax-g3', + 'gb' => 'application/x-gameboy-rom', + 'gba' => 'application/x-gba-rom', + 'gbc' => 'application/x-gameboy-rom', + 'gcrd' => 'text/vcard', + 'ged' => 'application/x-gedcom', + 'gedcom' => 'application/x-gedcom', + 'gem' => 'application/x-tar', + 'gen' => 'application/x-genesis-rom', + 'geo.json' => 'application/vnd.geo+json', + 'geojson' => 'application/vnd.geo+json', + 'gf' => 'application/x-tex-gf', + 'gg' => 'application/x-sms-rom', + 'gif' => 'image/gif', + 'glade' => 'application/x-glade', + 'gml' => 'application/gml+xml', + 'gmo' => 'application/x-gettext-translation', + 'gnc' => 'application/x-gnucash', + 'gnd' => 'application/gnunet-directory', + 'gnucash' => 'application/x-gnucash', + 'gnumeric' => 'application/x-gnumeric', + 'gnuplot' => 'application/x-gnuplot', + 'go' => 'text/x-go', + 'gp' => 'application/x-gnuplot', + 'gpg' => 'application/pgp-encrypted', + 'gplt' => 'application/x-gnuplot', + 'gpx' => 'application/gpx+xml', + 'gra' => 'application/x-graphite', + 'gs' => 'text/x-genie', + 'gsf' => 'application/x-font-type1', + 'gsm' => 'audio/x-gsm', + 'gtar' => 'application/x-tar', + 'gv' => 'text/vnd.graphviz', + 'gvp' => 'text/x-google-video-pointer', + 'gz' => 'application/gzip', + 'h' => 'text/x-chdr', + 'h++' => 'text/x-c++hdr', + 'h4' => 'application/x-hdf', + 'h5' => 'application/x-hdf', + 'hdf' => 'application/x-hdf', + 'hdf4' => 'application/x-hdf', + 'hdf5' => 'application/x-hdf', + 'hh' => 'text/x-c++hdr', + 'hlp' => 'application/winhlp', + 'hp' => 'text/x-c++hdr', + 'hpgl' => 'application/vnd.hp-hpgl', + 'hpp' => 'text/x-c++hdr', + 'hs' => 'text/x-haskell', + 'htm' => 'text/html', + 'html' => 'text/html', + 'hwp' => 'application/x-hwp', + 'hwt' => 'application/x-hwt', + 'hxx' => 'text/x-c++hdr', + 'ica' => 'application/x-ica', + 'icb' => 'image/x-tga', + 'icc' => 'application/vnd.iccprofile', + 'icm' => 'application/vnd.iccprofile', + 'icns' => 'image/x-icns', + 'ico' => 'image/vnd.microsoft.icon', + 'ics' => 'text/calendar', + 'idl' => 'text/x-idl', + 'ief' => 'image/ief', + 'iff' => 'image/x-ilbm', + 'ilbm' => 'image/x-ilbm', + 'ime' => 'text/x-iMelody', + 'img' => 'application/x-raw-disk-image', + 'img.xz' => 'application/x-raw-disk-image-xz-compressed', + 'imy' => 'text/x-iMelody', + 'ins' => 'text/x-tex', + 'iptables' => 'text/x-iptables', + 'iso' => 'application/x-cd-image', + 'iso9660' => 'application/x-cd-image', + 'it' => 'audio/x-it', + 'it87' => 'application/x-it87', + 'jad' => 'text/vnd.sun.j2me.app-descriptor', + 'jar' => 'application/x-java-archive', + 'java' => 'text/x-java', + 'jceks' => 'application/x-java-jce-keystore', + 'jks' => 'application/x-java-keystore', + 'jng' => 'image/x-jng', + 'jnlp' => 'application/x-java-jnlp-file', + 'jp2' => 'image/jp2', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpf' => 'image/jp2', + 'jpg' => 'image/jpeg', + 'jpr' => 'application/x-jbuilder-project', + 'jpx' => 'application/x-jbuilder-project', + 'jrd' => 'application/jrd+json', + 'js' => 'application/javascript', + 'jsm' => 'application/javascript', + 'json' => 'application/json', + 'json-patch' => 'application/json-patch+json', + 'jsonld' => 'application/ld+json', + 'k25' => 'image/x-kodak-k25', + 'kar' => 'audio/midi', + 'karbon' => 'application/x-karbon', + 'kdc' => 'image/x-kodak-kdc', + 'kdelnk' => 'application/x-desktop', + 'kexi' => 'application/x-kexiproject-sqlite2', + 'kexic' => 'application/x-kexi-connectiondata', + 'kexis' => 'application/x-kexiproject-shortcut', + 'key' => 'application/x-iwork-keynote-sffkey', + 'kfo' => 'application/x-kformula', + 'kil' => 'application/x-killustrator', + 'kino' => 'application/smil+xml', + 'kml' => 'application/vnd.google-earth.kml+xml', + 'kmz' => 'application/vnd.google-earth.kmz', + 'kon' => 'application/x-kontour', + 'kpm' => 'application/x-kpovmodeler', + 'kpr' => 'application/x-kpresenter', + 'kpt' => 'application/x-kpresenter', + 'kra' => 'application/x-krita', + 'ks' => 'application/x-java-keystore', + 'ksp' => 'application/x-kspread', + 'kud' => 'application/x-kugar', + 'kwd' => 'application/x-kword', + 'kwt' => 'application/x-kword', + 'la' => 'application/x-shared-library-la', + 'latex' => 'text/x-tex', + 'lbm' => 'image/x-ilbm', + 'ldif' => 'text/x-ldif', + 'lha' => 'application/x-lha', + 'lhs' => 'text/x-literate-haskell', + 'lhz' => 'application/x-lhz', + 'log' => 'text/x-log', + 'lrv' => 'video/mp4', + 'lrz' => 'application/x-lrzip', + 'ltx' => 'text/x-tex', + 'lua' => 'text/x-lua', + 'lwo' => 'image/x-lwo', + 'lwob' => 'image/x-lwo', + 'lwp' => 'application/vnd.lotus-wordpro', + 'lws' => 'image/x-lws', + 'ly' => 'text/x-lilypond', + 'lyx' => 'application/x-lyx', + 'lz' => 'application/x-lzip', + 'lz4' => 'application/x-lz4', + 'lzh' => 'application/x-lha', + 'lzma' => 'application/x-lzma', + 'lzo' => 'application/x-lzop', + 'm' => 'text/x-objcsrc', + 'm15' => 'audio/x-mod', + 'm1u' => 'video/vnd.mpegurl', + 'm2t' => 'video/mp2t', + 'm2ts' => 'video/mp2t', + 'm3u' => 'audio/x-mpegurl', + 'm3u8' => 'audio/x-mpegurl', + 'm4' => 'application/x-m4', + 'm4a' => 'audio/mp4', + 'm4b' => 'audio/x-m4b', + 'm4u' => 'video/vnd.mpegurl', + 'm4v' => 'video/mp4', + 'mab' => 'application/x-markaby', + 'mak' => 'text/x-makefile', + 'man' => 'application/x-troff-man', + 'manifest' => 'text/cache-manifest', + 'markdown' => 'text/markdown', + 'mbox' => 'application/mbox', + 'md' => 'text/markdown', + 'mdb' => 'application/vnd.ms-access', + 'mdi' => 'image/vnd.ms-modi', + 'mdx' => 'application/x-genesis-rom', + 'me' => 'text/x-troff-me', + 'med' => 'audio/x-mod', + 'meta4' => 'application/metalink4+xml', + 'metalink' => 'application/metalink+xml', + 'mgp' => 'application/x-magicpoint', + 'mht' => 'application/x-mimearchive', + 'mhtml' => 'application/x-mimearchive', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mif' => 'application/x-mif', + 'minipsf' => 'audio/x-minipsf', + 'mk' => 'text/x-makefile', + 'mk3d' => 'video/x-matroska-3d', + 'mka' => 'audio/x-matroska', + 'mkd' => 'text/markdown', + 'mkv' => 'video/x-matroska', + 'ml' => 'text/x-ocaml', + 'mli' => 'text/x-ocaml', + 'mm' => 'text/x-troff-mm', + 'mmf' => 'application/x-smaf', + 'mml' => 'application/mathml+xml', + 'mng' => 'video/x-mng', + 'mo' => 'application/x-gettext-translation', + 'mo3' => 'audio/x-mo3', + 'mobi' => 'application/x-mobipocket-ebook', + 'moc' => 'text/x-moc', + 'mod' => 'audio/x-mod', + 'mof' => 'text/x-mof', + 'moov' => 'video/quicktime', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp+' => 'audio/x-musepack', + 'mp2' => 'audio/mp2', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mpc' => 'audio/x-musepack', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpga' => 'audio/mpeg', + 'mpl' => 'video/mp2t', + 'mpls' => 'video/mp2t', + 'mpp' => 'audio/x-musepack', + 'mrl' => 'text/x-mrml', + 'mrml' => 'text/x-mrml', + 'mrw' => 'image/x-minolta-mrw', + 'ms' => 'text/x-troff-ms', + 'msi' => 'application/x-msi', + 'msod' => 'image/x-msod', + 'msx' => 'application/x-msx-rom', + 'mtm' => 'audio/x-mod', + 'mts' => 'video/mp2t', + 'mup' => 'text/x-mup', + 'mxf' => 'application/mxf', + 'mxu' => 'video/vnd.mpegurl', + 'n64' => 'application/x-n64-rom', + 'nb' => 'application/mathematica', + 'nc' => 'application/x-netcdf', + 'nds' => 'application/x-nintendo-ds-rom', + 'nef' => 'image/x-nikon-nef', + 'nes' => 'application/x-nes-rom', + 'nez' => 'application/x-nes-rom', + 'nfo' => 'text/x-nfo', + 'not' => 'text/x-mup', + 'nsc' => 'application/x-netshow-channel', + 'nsv' => 'video/x-nsv', + 'nzb' => 'application/x-nzb', + 'o' => 'application/x-object', + 'obj' => 'application/x-tgif', + 'ocl' => 'text/x-ocl', + 'oda' => 'application/oda', + 'odb' => 'application/vnd.oasis.opendocument.database', + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'odf' => 'application/vnd.oasis.opendocument.formula', + 'odg' => 'application/vnd.oasis.opendocument.graphics', + 'odi' => 'application/vnd.oasis.opendocument.image', + 'odm' => 'application/vnd.oasis.opendocument.text-master', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'oga' => 'audio/ogg', + 'ogg' => 'audio/ogg', + 'ogm' => 'video/x-ogm+ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'old' => 'application/x-trash', + 'oleo' => 'application/x-oleo', + 'ooc' => 'text/x-ooc', + 'opml' => 'text/x-opml+xml', + 'oprc' => 'application/vnd.palm', + 'opus' => 'audio/ogg', + 'ora' => 'image/openraster', + 'orf' => 'image/x-olympus-orf', + 'otc' => 'application/vnd.oasis.opendocument.chart-template', + 'otf' => 'application/vnd.oasis.opendocument.formula-template', + 'otg' => 'application/vnd.oasis.opendocument.graphics-template', + 'oth' => 'application/vnd.oasis.opendocument.text-web', + 'otp' => 'application/vnd.oasis.opendocument.presentation-template', + 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', + 'ott' => 'application/vnd.oasis.opendocument.text-template', + 'owl' => 'application/rdf+xml', + 'owx' => 'application/owl+xml', + 'oxps' => 'application/oxps', + 'oxt' => 'application/vnd.openofficeorg.extension', + 'p' => 'text/x-pascal', + 'p10' => 'application/pkcs10', + 'p12' => 'application/pkcs12', + 'p65' => 'application/x-pagemaker', + 'p7b' => 'application/x-pkcs7-certificates', + 'p7c' => 'application/pkcs7-mime', + 'p7m' => 'application/pkcs7-mime', + 'p7s' => 'application/pkcs7-signature', + 'p8' => 'application/pkcs8', + 'pack' => 'application/x-java-pack200', + 'pak' => 'application/x-pak', + 'par2' => 'application/x-par2', + 'part' => 'application/x-partial-download', + 'pas' => 'text/x-pascal', + 'patch' => 'text/x-patch', + 'pbm' => 'image/x-portable-bitmap', + 'pcap' => 'application/vnd.tcpdump.pcap', + 'pcd' => 'image/x-photo-cd', + 'pce' => 'application/x-pc-engine-rom', + 'pcf' => 'application/x-font-pcf', + 'pcf.gz' => 'application/x-font-pcf', + 'pcf.z' => 'application/x-font-pcf', + 'pcl' => 'application/vnd.hp-pcl', + 'pct' => 'image/x-pict', + 'pcx' => 'image/vnd.zbrush.pcx', + 'pdb' => 'application/x-aportisdoc', + 'pdc' => 'application/x-aportisdoc', + 'pdf' => 'application/pdf', + 'pdf.bz2' => 'application/x-bzpdf', + 'pdf.gz' => 'application/x-gzpdf', + 'pdf.xz' => 'application/x-xzpdf', + 'pef' => 'image/x-pentax-pef', + 'pem' => 'application/x-x509-ca-cert', + 'perl' => 'application/x-perl', + 'pfa' => 'application/x-font-type1', + 'pfb' => 'application/x-font-type1', + 'pfx' => 'application/pkcs12', + 'pgm' => 'image/x-portable-graymap', + 'pgn' => 'application/x-chess-pgn', + 'pgp' => 'application/pgp-encrypted', + 'php' => 'application/x-php', + 'php3' => 'application/x-php', + 'php4' => 'application/x-php', + 'php5' => 'application/x-php', + 'phps' => 'application/x-php', + 'pict' => 'image/x-pict', + 'pict1' => 'image/x-pict', + 'pict2' => 'image/x-pict', + 'pk' => 'application/x-tex-pk', + 'pkg' => 'application/x-xar', + 'pkipath' => 'application/pkix-pkipath', + 'pkr' => 'application/pgp-keys', + 'pl' => 'application/x-perl', + 'pla' => 'audio/x-iriver-pla', + 'pln' => 'application/x-planperfect', + 'pls' => 'audio/x-scpls', + 'pm' => 'application/x-perl', + 'pm6' => 'application/x-pagemaker', + 'pmd' => 'application/x-pagemaker', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'pntg' => 'image/x-macpaint', + 'po' => 'text/x-gettext-translation', + 'pod' => 'application/x-perl', + 'por' => 'application/x-spss-por', + 'pot' => 'application/vnd.ms-powerpoint', + 'potm' => 'application/vnd.ms-powerpoint.template.macroEnabled.12', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12', + 'ppm' => 'image/x-portable-pixmap', + 'pps' => 'application/vnd.ms-powerpoint', + 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ppz' => 'application/vnd.ms-powerpoint', + 'pqa' => 'application/vnd.palm', + 'prc' => 'application/x-mobipocket-ebook', + 'ps' => 'application/postscript', + 'ps.bz2' => 'application/x-bzpostscript', + 'ps.gz' => 'application/x-gzpostscript', + 'psd' => 'image/vnd.adobe.photoshop', + 'psf' => 'application/x-font-linux-psf', + 'psf.gz' => 'application/x-gz-font-linux-psf', + 'psflib' => 'audio/x-psflib', + 'psid' => 'audio/prs.sid', + 'psw' => 'application/x-pocket-word', + 'pub' => 'application/vnd.ms-publisher', + 'pw' => 'application/x-pw', + 'py' => 'text/x-python', + 'pyc' => 'application/x-python-bytecode', + 'pyo' => 'application/x-python-bytecode', + 'pyx' => 'text/x-python', + 'qif' => 'application/x-qw', + 'qml' => 'text/x-qml', + 'qmlproject' => 'text/x-qml', + 'qmltypes' => 'text/x-qml', + 'qp' => 'application/x-qpress', + 'qt' => 'video/quicktime', + 'qti' => 'application/x-qtiplot', + 'qti.gz' => 'application/x-qtiplot', + 'qtif' => 'image/x-quicktime', + 'qtl' => 'application/x-quicktime-media-link', + 'qtvr' => 'video/quicktime', + 'ra' => 'audio/vnd.rn-realaudio', + 'raf' => 'image/x-fuji-raf', + 'ram' => 'application/ram', + 'rar' => 'application/x-rar', + 'ras' => 'image/x-cmu-raster', + 'raw' => 'image/x-panasonic-raw', + 'raw-disk-image' => 'application/x-raw-disk-image', + 'raw-disk-image.xz' => 'application/x-raw-disk-image-xz-compressed', + 'rax' => 'audio/vnd.rn-realaudio', + 'rb' => 'application/x-ruby', + 'rdf' => 'application/rdf+xml', + 'rdfs' => 'application/rdf+xml', + 'reg' => 'text/x-ms-regedit', + 'rej' => 'text/x-reject', + 'rgb' => 'image/x-rgb', + 'rle' => 'image/rle', + 'rm' => 'application/vnd.rn-realmedia', + 'rmj' => 'application/vnd.rn-realmedia', + 'rmm' => 'application/vnd.rn-realmedia', + 'rms' => 'application/vnd.rn-realmedia', + 'rmvb' => 'application/vnd.rn-realmedia', + 'rmx' => 'application/vnd.rn-realmedia', + 'rnc' => 'application/relax-ng-compact-syntax', + 'rng' => 'application/xml', + 'roff' => 'text/troff', + 'rp' => 'image/vnd.rn-realpix', + 'rpm' => 'application/x-rpm', + 'rs' => 'text/rust', + 'rss' => 'application/rss+xml', + 'rt' => 'text/vnd.rn-realtext', + 'rtf' => 'application/rtf', + 'rtx' => 'text/richtext', + 'rv' => 'video/vnd.rn-realvideo', + 'rvx' => 'video/vnd.rn-realvideo', + 'rw2' => 'image/x-panasonic-raw2', + 's3m' => 'audio/x-s3m', + 'sam' => 'application/x-amipro', + 'sami' => 'application/x-sami', + 'sav' => 'application/x-spss-sav', + 'scala' => 'text/x-scala', + 'scm' => 'text/x-scheme', + 'sda' => 'application/vnd.stardivision.draw', + 'sdc' => 'application/vnd.stardivision.calc', + 'sdd' => 'application/vnd.stardivision.impress', + 'sdp' => 'application/vnd.stardivision.impress', + 'sds' => 'application/vnd.stardivision.chart', + 'sdw' => 'application/vnd.stardivision.writer', + 'sfc' => 'application/vnd.nintendo.snes.rom', + 'sg' => 'application/x-sms-rom', + 'sgb' => 'application/x-gameboy-rom', + 'sgf' => 'application/x-go-sgf', + 'sgi' => 'image/x-sgi', + 'sgl' => 'application/vnd.stardivision.writer', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'sh' => 'application/x-shellscript', + 'shape' => 'application/x-dia-shape', + 'shar' => 'application/x-shar', + 'shn' => 'application/x-shorten', + 'siag' => 'application/x-siag', + 'sid' => 'audio/prs.sid', + 'sig' => 'application/pgp-signature', + 'sik' => 'application/x-trash', + 'sis' => 'application/vnd.symbian.install', + 'sisx' => 'x-epoc/x-sisx-app', + 'sit' => 'application/x-stuffit', + 'siv' => 'application/sieve', + 'sk' => 'image/x-skencil', + 'sk1' => 'image/x-skencil', + 'skr' => 'application/pgp-keys', + 'sldm' => 'application/vnd.ms-powerpoint.slide.macroEnabled.12', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'slk' => 'text/spreadsheet', + 'smaf' => 'application/x-smaf', + 'smc' => 'application/vnd.nintendo.snes.rom', + 'smd' => 'application/vnd.stardivision.mail', + 'smf' => 'application/vnd.stardivision.math', + 'smi' => 'application/smil+xml', + 'smil' => 'application/smil+xml', + 'sml' => 'application/smil+xml', + 'sms' => 'application/x-sms-rom', + 'snd' => 'audio/basic', + 'so' => 'application/x-sharedlib', + 'spc' => 'application/x-pkcs7-certificates', + 'spd' => 'application/x-font-speedo', + 'spec' => 'text/x-rpm-spec', + 'spl' => 'application/vnd.adobe.flash.movie', + 'spm' => 'application/x-source-rpm', + 'spx' => 'audio/x-speex', + 'sql' => 'application/sql', + 'sr2' => 'image/x-sony-sr2', + 'src' => 'application/x-wais-source', + 'src.rpm' => 'application/x-source-rpm', + 'srf' => 'image/x-sony-srf', + 'srt' => 'application/x-subrip', + 'ss' => 'text/x-scheme', + 'ssa' => 'text/x-ssa', + 'stc' => 'application/vnd.sun.xml.calc.template', + 'std' => 'application/vnd.sun.xml.draw.template', + 'sti' => 'application/vnd.sun.xml.impress.template', + 'stm' => 'audio/x-stm', + 'stw' => 'application/vnd.sun.xml.writer.template', + 'sty' => 'text/x-tex', + 'sub' => 'text/x-microdvd', + 'sun' => 'image/x-sun-raster', + 'sv' => 'text/x-svsrc', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml-compressed', + 'svh' => 'text/x-svhdr', + 'swf' => 'application/vnd.adobe.flash.movie', + 'swm' => 'application/x-ms-wim', + 'sxc' => 'application/vnd.sun.xml.calc', + 'sxd' => 'application/vnd.sun.xml.draw', + 'sxg' => 'application/vnd.sun.xml.writer.global', + 'sxi' => 'application/vnd.sun.xml.impress', + 'sxm' => 'application/vnd.sun.xml.math', + 'sxw' => 'application/vnd.sun.xml.writer', + 'sylk' => 'text/spreadsheet', + 't' => 'application/x-perl', + 't2t' => 'text/x-txt2tags', + 'tar' => 'application/x-tar', + 'tar.bz' => 'application/x-bzip-compressed-tar', + 'tar.bz2' => 'application/x-bzip-compressed-tar', + 'tar.gz' => 'application/x-compressed-tar', + 'tar.lrz' => 'application/x-lrzip-compressed-tar', + 'tar.lzma' => 'application/x-lzma-compressed-tar', + 'tar.lzo' => 'application/x-tzo', + 'tar.xz' => 'application/x-xz-compressed-tar', + 'tar.z' => 'application/x-tarz', + 'taz' => 'application/x-tarz', + 'tb2' => 'application/x-bzip-compressed-tar', + 'tbz' => 'application/x-bzip-compressed-tar', + 'tbz2' => 'application/x-bzip-compressed-tar', + 'tcl' => 'text/x-tcl', + 'tex' => 'text/x-tex', + 'texi' => 'text/x-texinfo', + 'texinfo' => 'text/x-texinfo', + 'tga' => 'image/x-tga', + 'tgz' => 'application/x-compressed-tar', + 'theme' => 'application/x-theme', + 'themepack' => 'application/x-windows-themepack', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'tk' => 'text/x-tcl', + 'tlrz' => 'application/x-lrzip-compressed-tar', + 'tlz' => 'application/x-lzma-compressed-tar', + 'tnef' => 'application/vnd.ms-tnef', + 'tnf' => 'application/vnd.ms-tnef', + 'toc' => 'application/x-cdrdao-toc', + 'torrent' => 'application/x-bittorrent', + 'tpic' => 'image/x-tga', + 'tr' => 'text/troff', + 'trig' => 'application/x-trig', + 'ts' => 'text/vnd.trolltech.linguist', + 'tsv' => 'text/tab-separated-values', + 'tta' => 'audio/x-tta', + 'ttc' => 'application/x-font-ttf', + 'ttf' => 'application/x-font-ttf', + 'ttl' => 'text/turtle', + 'ttx' => 'application/x-font-ttx', + 'txt' => 'text/plain', + 'txz' => 'application/x-xz-compressed-tar', + 'tzo' => 'application/x-tzo', + 'udeb' => 'application/vnd.debian.binary-package', + 'ufraw' => 'application/x-ufraw', + 'ui' => 'application/x-designer', + 'uil' => 'text/x-uil', + 'ult' => 'audio/x-mod', + 'unf' => 'application/x-nes-rom', + 'uni' => 'audio/x-mod', + 'unif' => 'application/x-nes-rom', + 'url' => 'application/x-mswinurl', + 'ustar' => 'application/x-ustar', + 'uue' => 'text/x-uuencode', + 'v' => 'text/x-verilog', + 'v64' => 'application/x-n64-rom', + 'vala' => 'text/x-vala', + 'vapi' => 'text/x-vala', + 'vcard' => 'text/vcard', + 'vcf' => 'text/vcard', + 'vcs' => 'text/calendar', + 'vct' => 'text/vcard', + 'vda' => 'image/x-tga', + 'vhd' => 'text/x-vhdl', + 'vhdl' => 'text/x-vhdl', + 'viv' => 'video/vnd.vivo', + 'vivo' => 'video/vnd.vivo', + 'vlc' => 'audio/x-mpegurl', + 'vob' => 'video/mpeg', + 'voc' => 'audio/x-voc', + 'vor' => 'application/vnd.stardivision.writer', + 'vrm' => 'model/vrml', + 'vrml' => 'model/vrml', + 'vsd' => 'application/vnd.visio', + 'vsdm' => 'application/vnd.ms-visio.drawing.macroEnabled.main+xml', + 'vsdx' => 'application/vnd.ms-visio.drawing.main+xml', + 'vss' => 'application/vnd.visio', + 'vssm' => 'application/vnd.ms-visio.stencil.macroEnabled.main+xml', + 'vssx' => 'application/vnd.ms-visio.stencil.main+xml', + 'vst' => 'application/vnd.visio', + 'vstm' => 'application/vnd.ms-visio.template.macroEnabled.main+xml', + 'vstx' => 'application/vnd.ms-visio.template.main+xml', + 'vsw' => 'application/vnd.visio', + 'vtt' => 'text/vtt', + 'wad' => 'application/x-wii-wad', + 'wav' => 'audio/x-wav', + 'wax' => 'audio/x-ms-asx', + 'wb1' => 'application/x-quattropro', + 'wb2' => 'application/x-quattropro', + 'wb3' => 'application/x-quattropro', + 'wbmp' => 'image/vnd.wap.wbmp', + 'wcm' => 'application/vnd.ms-works', + 'wdb' => 'application/vnd.ms-works', + 'webm' => 'video/webm', + 'webp' => 'image/webp', + 'wim' => 'application/x-ms-wim', + 'wk1' => 'application/vnd.lotus-1-2-3', + 'wk3' => 'application/vnd.lotus-1-2-3', + 'wk4' => 'application/vnd.lotus-1-2-3', + 'wkdownload' => 'application/x-partial-download', + 'wks' => 'application/vnd.lotus-1-2-3', + 'wma' => 'audio/x-ms-wma', + 'wmf' => 'image/x-wmf', + 'wml' => 'text/vnd.wap.wml', + 'wmls' => 'text/vnd.wap.wmlscript', + 'wmv' => 'video/x-ms-wmv', + 'wmx' => 'audio/x-ms-asx', + 'woff' => 'application/font-woff', + 'wp' => 'application/vnd.wordperfect', + 'wp4' => 'application/vnd.wordperfect', + 'wp5' => 'application/vnd.wordperfect', + 'wp6' => 'application/vnd.wordperfect', + 'wpd' => 'application/vnd.wordperfect', + 'wpg' => 'application/x-wpg', + 'wpl' => 'application/vnd.ms-wpl', + 'wpp' => 'application/vnd.wordperfect', + 'wps' => 'application/vnd.ms-works', + 'wri' => 'application/x-mswrite', + 'wrl' => 'model/vrml', + 'wsgi' => 'text/x-python', + 'wv' => 'audio/x-wavpack', + 'wvc' => 'audio/x-wavpack-correction', + 'wvp' => 'audio/x-wavpack', + 'wvx' => 'audio/x-ms-asx', + 'wwf' => 'application/x-wwf', + 'x3f' => 'image/x-sigma-x3f', + 'xac' => 'application/x-gnucash', + 'xar' => 'application/x-xar', + 'xbel' => 'application/x-xbel', + 'xbl' => 'application/xml', + 'xbm' => 'image/x-xbitmap', + 'xcf' => 'image/x-xcf', + 'xcf.bz2' => 'image/x-compressed-xcf', + 'xcf.gz' => 'image/x-compressed-xcf', + 'xdgapp' => 'application/vnd.xdgapp', + 'xht' => 'application/xhtml+xml', + 'xhtml' => 'application/xhtml+xml', + 'xi' => 'audio/x-xi', + 'xla' => 'application/vnd.ms-excel', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'xlc' => 'application/vnd.ms-excel', + 'xld' => 'application/vnd.ms-excel', + 'xlf' => 'application/x-xliff', + 'xliff' => 'application/x-xliff', + 'xll' => 'application/vnd.ms-excel', + 'xlm' => 'application/vnd.ms-excel', + 'xlr' => 'application/vnd.ms-works', + 'xls' => 'application/vnd.ms-excel', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xlt' => 'application/vnd.ms-excel', + 'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'xlw' => 'application/vnd.ms-excel', + 'xm' => 'audio/x-xm', + 'xmf' => 'audio/x-xmf', + 'xmi' => 'text/x-xmi', + 'xml' => 'application/xml', + 'xpi' => 'application/x-xpinstall', + 'xpm' => 'image/x-xpixmap', + 'xps' => 'application/oxps', + 'xsd' => 'application/xml', + 'xsl' => 'application/xslt+xml', + 'xslfo' => 'text/x-xslfo', + 'xslt' => 'application/xslt+xml', + 'xspf' => 'application/xspf+xml', + 'xul' => 'application/vnd.mozilla.xul+xml', + 'xwd' => 'image/x-xwindowdump', + 'xz' => 'application/x-xz', + 'yaml' => 'application/x-yaml', + 'yml' => 'application/x-yaml', + 'z' => 'application/x-compress', + 'z64' => 'application/x-n64-rom', + 'zabw' => 'application/x-abiword', + 'zip' => 'application/zip', + 'zoo' => 'application/x-zoo', + 'zsav' => 'application/x-spss-sav', + 'zz' => 'application/zlib', + } + # @private + # :nodoc: + TYPES = { + 'application/andrew-inset' => [%w(ez), %w(), 'ATK inset'], + 'application/annodex' => [%w(anx), %w(), 'Annodex exchange format'], + 'application/atom+xml' => [%w(atom), %w(application/xml), 'Atom syndication feed'], + 'application/dicom' => [%w(dcm), %w(), 'DICOM image'], + 'application/ecmascript' => [%w(es), %w(text/plain), 'ECMAScript program'], + 'application/epub+zip' => [%w(epub), %w(application/zip), 'electronic book document'], + 'application/font-woff' => [%w(woff), %w(), 'WOFF font'], + 'application/gml+xml' => [%w(gml), %w(application/xml), 'GML document'], + 'application/gnunet-directory' => [%w(gnd), %w(), 'GNUnet search file'], + 'application/gpx+xml' => [%w(gpx), %w(application/xml), 'GPX geographic data'], + 'application/gzip' => [%w(gz), %w(), 'Gzip archive'], + 'application/illustrator' => [%w(ai), %w(), 'Adobe Illustrator document'], + 'application/javascript' => [%w(js jsm), %w(application/ecmascript), 'JavaScript program'], + 'application/jrd+json' => [%w(jrd), %w(application/json), 'JRD document'], + 'application/json' => [%w(json), %w(application/javascript), 'JSON document'], + 'application/json-patch+json' => [%w(json-patch), %w(application/json), 'JSON patch'], + 'application/ld+json' => [%w(jsonld), %w(application/json), 'JSON-LD document'], + 'application/mathematica' => [%w(nb), %w(text/plain), 'Mathematica Notebook'], + 'application/mathml+xml' => [%w(mml), %w(application/xml), 'MathML document'], + 'application/mbox' => [%w(mbox), %w(text/plain), 'mailbox file'], + 'application/metalink+xml' => [%w(metalink), %w(application/xml), 'Metalink file'], + 'application/metalink4+xml' => [%w(meta4), %w(application/xml), 'Metalink file'], + 'application/msword' => [%w(doc), %w(application/x-ole-storage), 'Word document'], + 'application/msword-template' => [%w(dot), %w(application/msword), 'Word template'], + 'application/mxf' => [%w(mxf), %w(), 'MXF video'], + 'application/octet-stream' => [%w(bin), %w(), 'unknown'], + 'application/oda' => [%w(oda), %w(), 'ODA document'], + 'application/ogg' => [%w(ogx), %w(), 'Ogg multimedia file'], + 'application/owl+xml' => [%w(owx), %w(application/xml), 'OWL XML file'], + 'application/oxps' => [%w(oxps xps), %w(application/zip), 'XPS document'], + 'application/pdf' => [%w(pdf), %w(), 'PDF document'], + 'application/pgp-encrypted' => [%w(asc gpg pgp), %w(text/plain), 'PGP/MIME-encrypted message header'], + 'application/pgp-keys' => [%w(asc gpg pgp pkr skr), %w(text/plain), 'PGP keys'], + 'application/pgp-signature' => [%w(asc gpg pgp sig), %w(text/plain), 'detached OpenPGP signature'], + 'application/pkcs10' => [%w(p10), %w(), 'PKCS#10 certification request'], + 'application/pkcs12' => [%w(p12 pfx), %w(), 'PKCS#12 certificate bundle'], + 'application/pkcs7-mime' => [%w(p7c p7m), %w(), 'PKCS#7 Message or Certificate'], + 'application/pkcs7-signature' => [%w(p7s), %w(text/plain), 'detached S/MIME signature'], + 'application/pkcs8' => [%w(p8), %w(), 'PKCS#8 private key'], + 'application/pkix-cert' => [%w(cer), %w(), 'X.509 certificate'], + 'application/pkix-crl' => [%w(crl), %w(), 'Certificate revocation list'], + 'application/pkix-pkipath' => [%w(pkipath), %w(), 'PkiPath certification path'], + 'application/postscript' => [%w(ps), %w(text/plain), 'PS document'], + 'application/ram' => [%w(ram), %w(), 'RealMedia Metafile'], + 'application/rdf+xml' => [%w(owl rdf rdfs), %w(application/xml), 'RDF file'], + 'application/relax-ng-compact-syntax' => [%w(rnc), %w(text/plain), 'RELAX NG XML schema'], + 'application/rss+xml' => [%w(rss), %w(application/xml), 'RSS summary'], + 'application/rtf' => [%w(rtf), %w(text/plain), 'RTF document'], + 'application/sdp' => [%w(sdp), %w(text/plain), 'SDP multicast stream file'], + 'application/sieve' => [%w(siv), %w(application/xml), 'Sieve mail filter script'], + 'application/smil+xml' => [%w(kino smi smil sml), %w(application/xml), 'SMIL document'], + 'application/sql' => [%w(sql), %w(text/plain), 'SQL code'], + 'application/vnd.adobe.flash.movie' => [%w(spl swf), %w(), 'Shockwave Flash file'], + 'application/vnd.android.package-archive' => [%w(apk), %w(application/x-java-archive), 'Android package'], + 'application/vnd.apple.mpegurl' => [%w(m3u m3u8), %w(text/plain), 'HTTP Live Streaming playlist'], + 'application/vnd.coffeescript' => [%w(coffee), %w(text/plain), 'CoffeeScript document'], + 'application/vnd.corel-draw' => [%w(cdr), %w(), 'Corel Draw drawing'], + 'application/vnd.debian.binary-package' => [%w(deb udeb), %w(), 'Debian package'], + 'application/vnd.emusic-emusic_package' => [%w(emp), %w(), 'eMusic download package'], + 'application/vnd.geo+json' => [%w(geo.json geojson), %w(application/json), 'GeoJSON geospatial data'], + 'application/vnd.google-earth.kml+xml' => [%w(kml), %w(application/xml), 'KML geographic data'], + 'application/vnd.google-earth.kmz' => [%w(kmz), %w(application/zip), 'KML geographic compressed data'], + 'application/vnd.hp-hpgl' => [%w(hpgl), %w(), 'HPGL file'], + 'application/vnd.hp-pcl' => [%w(pcl), %w(), 'PCL file'], + 'application/vnd.iccprofile' => [%w(icc icm), %w(), 'ICC profile'], + 'application/vnd.lotus-1-2-3' => [%w(123 wk1 wk3 wk4 wks), %w(), 'Lotus 1-2-3 spreadsheet'], + 'application/vnd.lotus-wordpro' => [%w(lwp), %w(), 'Lotus Word Pro'], + 'application/vnd.mozilla.xul+xml' => [%w(xul), %w(application/xml), 'XUL interface document'], + 'application/vnd.ms-access' => [%w(mdb), %w(), 'JET database'], + 'application/vnd.ms-asf' => [%w(asf), %w(), 'ASF video'], + 'application/vnd.ms-cab-compressed' => [%w(cab), %w(), 'Microsoft Cabinet archive'], + 'application/vnd.ms-excel' => [%w(xla xlc xld xll xlm xls xlt xlw), %w(), 'Excel spreadsheet'], + 'application/vnd.ms-excel.addin.macroEnabled.12' => [%w(xlam), %w(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet), 'Excel add-in'], + 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' => [%w(xlsb), %w(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet), 'Excel 2007 binary spreadsheet'], + 'application/vnd.ms-excel.sheet.macroEnabled.12' => [%w(xlsm), %w(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet), 'Excel spreadsheet'], + 'application/vnd.ms-excel.template.macroEnabled.12' => [%w(xltm), %w(application/vnd.openxmlformats-officedocument.spreadsheetml.template), 'Excel spreadsheet template'], + 'application/vnd.ms-htmlhelp' => [%w(chm), %w(), 'CHM document'], + 'application/vnd.ms-powerpoint' => [%w(pot pps ppt ppz), %w(), 'PowerPoint presentation'], + 'application/vnd.ms-powerpoint.addin.macroEnabled.12' => [%w(ppam), %w(), 'PowerPoint add-in'], + 'application/vnd.ms-powerpoint.presentation.macroEnabled.12' => [%w(pptm), %w(application/vnd.openxmlformats-officedocument.presentationml.presentation), 'PowerPoint presentation'], + 'application/vnd.ms-powerpoint.slide.macroEnabled.12' => [%w(sldm), %w(application/vnd.openxmlformats-officedocument.presentationml.slide), 'PowerPoint slide'], + 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' => [%w(ppsm), %w(application/vnd.openxmlformats-officedocument.presentationml.slideshow), 'PowerPoint presentation'], + 'application/vnd.ms-powerpoint.template.macroEnabled.12' => [%w(potm), %w(application/vnd.openxmlformats-officedocument.presentationml.template), 'PowerPoint presentation template'], + 'application/vnd.ms-publisher' => [%w(pub), %w(application/x-ole-storage), 'Microsoft Publisher document'], + 'application/vnd.ms-tnef' => [%w(tnef tnf), %w(), 'TNEF message'], + 'application/vnd.ms-visio.drawing.macroEnabled.main+xml' => [%w(vsdm), %w(application/zip), 'Office Open XML Visio Drawing'], + 'application/vnd.ms-visio.drawing.main+xml' => [%w(vsdx), %w(application/zip), 'Office Open XML Visio Drawing'], + 'application/vnd.ms-visio.stencil.macroEnabled.main+xml' => [%w(vssm), %w(application/zip), 'Office Open XML Visio Stencil'], + 'application/vnd.ms-visio.stencil.main+xml' => [%w(vssx), %w(application/zip), 'Office Open XML Visio Stencil'], + 'application/vnd.ms-visio.template.macroEnabled.main+xml' => [%w(vstm), %w(application/zip), 'Office Open XML Visio Template'], + 'application/vnd.ms-visio.template.main+xml' => [%w(vstx), %w(application/zip), 'Office Open XML Visio Template'], + 'application/vnd.ms-word.document.macroEnabled.12' => [%w(docm), %w(application/vnd.openxmlformats-officedocument.wordprocessingml.document), 'Word document'], + 'application/vnd.ms-word.template.macroEnabled.12' => [%w(dotm), %w(application/vnd.openxmlformats-officedocument.wordprocessingml.template), 'Word document template'], + 'application/vnd.ms-works' => [%w(wcm wdb wks wps xlr), %w(application/x-ole-storage), 'Microsoft Works document'], + 'application/vnd.ms-wpl' => [%w(wpl), %w(), 'WPL playlist'], + 'application/vnd.nintendo.snes.rom' => [%w(sfc smc), %w(), 'Super NES ROM'], + 'application/vnd.oasis.opendocument.chart' => [%w(odc), %w(application/zip), 'ODC chart'], + 'application/vnd.oasis.opendocument.chart-template' => [%w(otc), %w(application/zip), 'ODC template'], + 'application/vnd.oasis.opendocument.database' => [%w(odb), %w(application/zip), 'ODB database'], + 'application/vnd.oasis.opendocument.formula' => [%w(odf), %w(application/zip), 'ODF formula'], + 'application/vnd.oasis.opendocument.formula-template' => [%w(otf), %w(application/zip), 'ODF template'], + 'application/vnd.oasis.opendocument.graphics' => [%w(odg), %w(application/zip), 'ODG drawing'], + 'application/vnd.oasis.opendocument.graphics-flat-xml' => [%w(fodg), %w(application/xml), 'ODG drawing (Flat XML)'], + 'application/vnd.oasis.opendocument.graphics-template' => [%w(otg), %w(application/zip), 'ODG template'], + 'application/vnd.oasis.opendocument.image' => [%w(odi), %w(application/zip), 'ODI image'], + 'application/vnd.oasis.opendocument.presentation' => [%w(odp), %w(application/zip), 'ODP presentation'], + 'application/vnd.oasis.opendocument.presentation-flat-xml' => [%w(fodp), %w(application/xml), 'ODP presentation (Flat XML)'], + 'application/vnd.oasis.opendocument.presentation-template' => [%w(otp), %w(application/zip), 'ODP template'], + 'application/vnd.oasis.opendocument.spreadsheet' => [%w(ods), %w(application/zip), 'ODS spreadsheet'], + 'application/vnd.oasis.opendocument.spreadsheet-flat-xml' => [%w(fods), %w(application/xml), 'ODS spreadsheet (Flat XML)'], + 'application/vnd.oasis.opendocument.spreadsheet-template' => [%w(ots), %w(application/zip), 'ODS template'], + 'application/vnd.oasis.opendocument.text' => [%w(odt), %w(application/zip), 'ODT document'], + 'application/vnd.oasis.opendocument.text-flat-xml' => [%w(fodt), %w(application/xml), 'ODT document (Flat XML)'], + 'application/vnd.oasis.opendocument.text-master' => [%w(odm), %w(application/zip), 'ODM document'], + 'application/vnd.oasis.opendocument.text-template' => [%w(ott), %w(application/zip), 'ODT template'], + 'application/vnd.oasis.opendocument.text-web' => [%w(oth), %w(application/zip), 'OTH template'], + 'application/vnd.openofficeorg.extension' => [%w(oxt), %w(application/zip), 'OpenOffice.org extension'], + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => [%w(pptx), %w(application/zip), 'PowerPoint 2007 presentation'], + 'application/vnd.openxmlformats-officedocument.presentationml.slide' => [%w(sldx), %w(application/zip), 'PowerPoint 2007 slide'], + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => [%w(ppsx), %w(application/zip), 'PowerPoint 2007 show'], + 'application/vnd.openxmlformats-officedocument.presentationml.template' => [%w(potx), %w(application/zip), 'PowerPoint 2007 presentation template'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => [%w(xlsx), %w(application/zip), 'Excel 2007 spreadsheet'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => [%w(xltx), %w(application/zip), 'Excel 2007 spreadsheet template'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => [%w(docx), %w(application/zip), 'Word 2007 document'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => [%w(dotx), %w(application/zip), 'Word 2007 document template'], + 'application/vnd.palm' => [%w(oprc pdb pqa prc), %w(), 'Palm OS database'], + 'application/vnd.rn-realmedia' => [%w(rm rmj rmm rms rmvb rmx), %w(), 'RealMedia document'], + 'application/vnd.stardivision.calc' => [%w(sdc), %w(), 'StarCalc spreadsheet'], + 'application/vnd.stardivision.chart' => [%w(sds), %w(), 'StarChart chart'], + 'application/vnd.stardivision.draw' => [%w(sda), %w(), 'StarDraw drawing'], + 'application/vnd.stardivision.impress' => [%w(sdd sdp), %w(), 'StarImpress presentation'], + 'application/vnd.stardivision.mail' => [%w(smd), %w(), 'StarMail email'], + 'application/vnd.stardivision.math' => [%w(smf), %w(), 'StarMath formula'], + 'application/vnd.stardivision.writer' => [%w(sdw sgl vor), %w(), 'StarWriter document'], + 'application/vnd.sun.xml.calc' => [%w(sxc), %w(application/zip), 'OpenOffice Calc spreadsheet'], + 'application/vnd.sun.xml.calc.template' => [%w(stc), %w(application/zip), 'OpenOffice Calc template'], + 'application/vnd.sun.xml.draw' => [%w(sxd), %w(application/zip), 'OpenOffice Draw drawing'], + 'application/vnd.sun.xml.draw.template' => [%w(std), %w(application/zip), 'OpenOffice Draw template'], + 'application/vnd.sun.xml.impress' => [%w(sxi), %w(application/zip), 'OpenOffice Impress presentation'], + 'application/vnd.sun.xml.impress.template' => [%w(sti), %w(application/zip), 'OpenOffice Impress template'], + 'application/vnd.sun.xml.math' => [%w(sxm), %w(application/zip), 'OpenOffice Math formula'], + 'application/vnd.sun.xml.writer' => [%w(sxw), %w(application/zip), 'OpenOffice Writer document'], + 'application/vnd.sun.xml.writer.global' => [%w(sxg), %w(application/zip), 'OpenOffice Writer global document'], + 'application/vnd.sun.xml.writer.template' => [%w(stw), %w(application/zip), 'OpenOffice Writer template'], + 'application/vnd.symbian.install' => [%w(sis), %w(), 'SIS package'], + 'application/vnd.tcpdump.pcap' => [%w(cap dmp pcap), %w(), 'Network Packet Capture'], + 'application/vnd.visio' => [%w(vsd vss vst vsw), %w(application/x-ole-storage), 'Microsoft Visio document'], + 'application/vnd.wordperfect' => [%w(wp wp4 wp5 wp6 wpd wpp), %w(), 'WordPerfect document'], + 'application/vnd.xdgapp' => [%w(xdgapp), %w(), 'XDG application bundle'], + 'application/winhlp' => [%w(hlp), %w(), 'WinHelp help file'], + 'application/x-7z-compressed' => [%w(7z), %w(), '7-zip archive'], + 'application/x-abiword' => [%w(abw abw.crashed abw.gz zabw), %w(application/xml), 'AbiWord document'], + 'application/x-ace' => [%w(ace), %w(), 'ACE archive'], + 'application/x-alz' => [%w(alz), %w(), 'Alzip archive'], + 'application/x-amiga-disk-format' => [%w(adf), %w(), 'Amiga disk image'], + 'application/x-amipro' => [%w(sam), %w(), 'Lotus AmiPro document'], + 'application/x-aportisdoc' => [%w(pdb pdc), %w(application/x-palm-database), 'AportisDoc document'], + 'application/x-apple-diskimage' => [%w(dmg), %w(), 'Apple disk image'], + 'application/x-applix-spreadsheet' => [%w(as), %w(), 'Applix Spreadsheets spreadsheet'], + 'application/x-applix-word' => [%w(aw), %w(), 'Applix Words document'], + 'application/x-archive' => [%w(a ar), %w(), 'AR archive'], + 'application/x-arj' => [%w(arj), %w(), 'ARJ archive'], + 'application/x-asp' => [%w(asp), %w(text/plain), 'ASP page'], + 'application/x-awk' => [%w(awk), %w(application/x-executable text/plain), 'AWK script'], + 'application/x-bcpio' => [%w(bcpio), %w(), 'BCPIO document'], + 'application/x-bittorrent' => [%w(torrent), %w(), 'BitTorrent seed file'], + 'application/x-blender' => [%w(blend blend blender), %w(), 'Blender scene'], + 'application/x-bzdvi' => [%w(dvi.bz2), %w(application/x-bzip), 'TeX DVI document (bzip-compressed)'], + 'application/x-bzip' => [%w(bz bz2), %w(), 'Bzip archive'], + 'application/x-bzip-compressed-tar' => [%w(tar.bz tar.bz2 tb2 tbz tbz2), %w(application/x-bzip), 'Tar archive (bzip-compressed)'], + 'application/x-bzpdf' => [%w(pdf.bz2), %w(application/x-bzip), 'PDF document (bzip-compressed)'], + 'application/x-bzpostscript' => [%w(ps.bz2), %w(application/x-bzip), 'PostScript document (bzip-compressed)'], + 'application/x-cb7' => [%w(cb7), %w(application/x-7z-compressed), 'comic book archive'], + 'application/x-cbr' => [%w(cbr), %w(application/x-rar), 'comic book archive'], + 'application/x-cbt' => [%w(cbt), %w(application/x-tar), 'comic book archive'], + 'application/x-cbz' => [%w(cbz), %w(application/zip), 'comic book archive'], + 'application/x-ccmx' => [%w(ccmx), %w(text/plain), 'CCMX color correction file'], + 'application/x-cd-image' => [%w(iso iso9660), %w(application/x-raw-disk-image), 'raw CD image'], + 'application/x-cdrdao-toc' => [%w(toc), %w(text/plain), 'CD Table Of Contents'], + 'application/x-chess-pgn' => [%w(pgn), %w(text/plain), 'PGN chess game notation'], + 'application/x-cisco-vpn-settings' => [%w(pcf), %w(), 'Cisco VPN Settings'], + 'application/x-compress' => [%w(z), %w(), 'UNIX-compressed file'], + 'application/x-compressed-tar' => [%w(tar.gz tgz), %w(application/gzip), 'Tar archive (gzip-compressed)'], + 'application/x-cpio' => [%w(cpio), %w(), 'CPIO archive'], + 'application/x-cpio-compressed' => [%w(cpio.gz), %w(application/gzip), 'CPIO archive (gzip-compressed)'], + 'application/x-csh' => [%w(csh), %w(application/x-shellscript text/plain), 'C shell script'], + 'application/x-cue' => [%w(cue), %w(text/plain), 'CD image cuesheet'], + 'application/x-dar' => [%w(dar), %w(), 'DAR archive'], + 'application/x-dbf' => [%w(dbf), %w(), 'Xbase document'], + 'application/x-dc-rom' => [%w(dc), %w(), 'Dreamcast GD-ROM'], + 'application/x-designer' => [%w(ui), %w(application/xml), 'Qt Designer file'], + 'application/x-desktop' => [%w(desktop kdelnk), %w(text/plain), 'desktop configuration file'], + 'application/x-dia-diagram' => [%w(dia), %w(application/xml), 'Dia diagram'], + 'application/x-dia-shape' => [%w(shape), %w(application/xml), 'Dia shape'], + 'application/x-docbook+xml' => [%w(dbk docbook), %w(application/xml), 'DocBook document'], + 'application/x-doom-wad' => [%w(wad), %w(), 'Doom WAD'], + 'application/x-dvi' => [%w(dvi), %w(), 'TeX DVI document'], + 'application/x-e-theme' => [%w(etheme), %w(), 'Enlightenment theme'], + 'application/x-egon' => [%w(egon), %w(), 'Egon Animator animation'], + 'application/x-fictionbook+xml' => [%w(fb2), %w(application/xml), 'FictionBook document'], + 'application/x-fluid' => [%w(fl), %w(text/plain), 'FLTK Fluid file'], + 'application/x-font-afm' => [%w(afm), %w(), 'Adobe font metrics'], + 'application/x-font-bdf' => [%w(bdf), %w(), 'BDF font'], + 'application/x-font-linux-psf' => [%w(psf), %w(), 'Linux PSF console font'], + 'application/x-font-otf' => [%w(otf), %w(application/x-font-ttf), 'OpenType font'], + 'application/x-font-pcf' => [%w(pcf pcf.gz pcf.z), %w(), 'PCF font'], + 'application/x-font-speedo' => [%w(spd), %w(), 'Speedo font'], + 'application/x-font-ttf' => [%w(ttc ttf), %w(), 'TrueType font'], + 'application/x-font-ttx' => [%w(ttx), %w(text/xml), 'TrueType XML font'], + 'application/x-font-type1' => [%w(gsf pfa pfb), %w(application/postscript), 'Postscript type-1 font'], + 'application/x-gameboy-rom' => [%w(cgb gb gbc sgb), %w(), 'Game Boy ROM'], + 'application/x-gamecube-rom' => [%w(iso), %w(), 'GameCube disc image'], + 'application/x-gba-rom' => [%w(agb gba), %w(), 'Game Boy Advance ROM'], + 'application/x-gedcom' => [%w(ged gedcom), %w(), 'GEDCOM family history'], + 'application/x-genesis-rom' => [%w(32x gen mdx smd), %w(), 'Genesis ROM'], + 'application/x-gettext-translation' => [%w(gmo mo), %w(), 'translated messages (machine-readable)'], + 'application/x-glade' => [%w(glade), %w(application/xml), 'Glade project'], + 'application/x-gnucash' => [%w(gnc gnucash xac), %w(), 'GnuCash financial data'], + 'application/x-gnumeric' => [%w(gnumeric), %w(), 'Gnumeric spreadsheet'], + 'application/x-gnuplot' => [%w(gnuplot gp gplt), %w(text/plain), 'Gnuplot document'], + 'application/x-go-sgf' => [%w(sgf), %w(text/plain), 'SGF record'], + 'application/x-graphite' => [%w(gra), %w(), 'Graphite scientific graph'], + 'application/x-gtk-builder' => [%w(ui), %w(application/xml), 'GTK+ Builder'], + 'application/x-gz-font-linux-psf' => [%w(psf.gz), %w(application/gzip), 'Linux PSF console font (gzip-compressed)'], + 'application/x-gzdvi' => [%w(dvi.gz), %w(application/gzip), 'TeX DVI document (gzip-compressed)'], + 'application/x-gzpdf' => [%w(pdf.gz), %w(application/gzip), 'PDF document (gzip-compressed)'], + 'application/x-gzpostscript' => [%w(ps.gz), %w(application/gzip), 'PostScript document (gzip-compressed)'], + 'application/x-hdf' => [%w(h4 h5 hdf hdf4 hdf5), %w(), 'HDF document'], + 'application/x-hwp' => [%w(hwp), %w(), 'Haansoft Hangul document'], + 'application/x-hwt' => [%w(hwt), %w(), 'Haansoft Hangul document template'], + 'application/x-ica' => [%w(ica), %w(text/plain), 'Citrix ICA settings file'], + 'application/x-it87' => [%w(it87), %w(text/plain), 'IT 8.7 color calibration file'], + 'application/x-iwork-keynote-sffkey' => [%w(key), %w(application/zip), 'Apple Keynote 5 presentation'], + 'application/x-java' => [%w(class), %w(), 'Java class'], + 'application/x-java-archive' => [%w(jar), %w(application/zip), 'Java archive'], + 'application/x-java-jce-keystore' => [%w(jceks), %w(), 'Java JCE keystore'], + 'application/x-java-jnlp-file' => [%w(jnlp), %w(application/xml), 'JNLP file'], + 'application/x-java-keystore' => [%w(jks ks), %w(), 'Java keystore'], + 'application/x-java-pack200' => [%w(pack), %w(), 'Pack200 Java archive'], + 'application/x-jbuilder-project' => [%w(jpr jpx), %w(), 'JBuilder project'], + 'application/x-karbon' => [%w(karbon), %w(), 'Karbon14 drawing'], + 'application/x-kchart' => [%w(chrt), %w(), 'KChart chart'], + 'application/x-kexi-connectiondata' => [%w(kexic), %w(), 'Kexi settings for database server connection'], + 'application/x-kexiproject-shortcut' => [%w(kexis), %w(), 'shortcut to Kexi project on database server'], + 'application/x-kexiproject-sqlite2' => [%w(kexi), %w(application/x-sqlite2), 'Kexi database file-based project'], + 'application/x-kexiproject-sqlite3' => [%w(kexi), %w(application/x-sqlite3), 'Kexi database file-based project'], + 'application/x-kformula' => [%w(kfo), %w(), 'KFormula formula'], + 'application/x-killustrator' => [%w(kil), %w(), 'KIllustrator drawing'], + 'application/x-kivio' => [%w(flw), %w(), 'Kivio flowchart'], + 'application/x-kontour' => [%w(kon), %w(), 'Kontour drawing'], + 'application/x-kpovmodeler' => [%w(kpm), %w(), 'KPovModeler scene'], + 'application/x-kpresenter' => [%w(kpr kpt), %w(), 'KPresenter presentation'], + 'application/x-krita' => [%w(kra), %w(), 'Krita document'], + 'application/x-kspread' => [%w(ksp), %w(), 'KSpread spreadsheet'], + 'application/x-kugar' => [%w(kud), %w(), 'Kugar document'], + 'application/x-kword' => [%w(kwd kwt), %w(), 'KWord document'], + 'application/x-lha' => [%w(lha lzh), %w(), 'LHA archive'], + 'application/x-lhz' => [%w(lhz), %w(), 'LHZ archive'], + 'application/x-lrzip' => [%w(lrz), %w(), 'Lrzip archive'], + 'application/x-lrzip-compressed-tar' => [%w(tar.lrz tlrz), %w(application/x-lrzip), 'Tar archive (lrzip-compressed)'], + 'application/x-lyx' => [%w(lyx), %w(text/plain), 'LyX document'], + 'application/x-lz4' => [%w(lz4), %w(), 'LZ4 archive'], + 'application/x-lzip' => [%w(lz), %w(), 'Lzip archive'], + 'application/x-lzma' => [%w(lzma), %w(), 'LZMA archive'], + 'application/x-lzma-compressed-tar' => [%w(tar.lzma tlz), %w(application/x-lzma), 'Tar archive (LZMA-compressed)'], + 'application/x-lzop' => [%w(lzo), %w(), 'LZO archive'], + 'application/x-m4' => [%w(m4), %w(text/plain), 'M4 macro'], + 'application/x-magicpoint' => [%w(mgp), %w(text/plain), 'MagicPoint presentation'], + 'application/x-markaby' => [%w(mab), %w(application/x-ruby), 'Markaby script'], + 'application/x-mif' => [%w(mif), %w(), 'Adobe FrameMaker MIF document'], + 'application/x-mimearchive' => [%w(mht mhtml), %w(multipart/related), 'MHTML web archive'], + 'application/x-mobipocket-ebook' => [%w(mobi prc), %w(application/x-palm-database), 'Mobipocket e-book'], + 'application/x-ms-dos-executable' => [%w(exe), %w(), 'DOS/Windows executable'], + 'application/x-ms-wim' => [%w(swm wim), %w(), 'WIM disk Image'], + 'application/x-msi' => [%w(msi), %w(application/x-ole-storage), 'Windows Installer package'], + 'application/x-mswinurl' => [%w(url), %w(), 'Internet shortcut'], + 'application/x-mswrite' => [%w(wri), %w(), 'WRI document'], + 'application/x-msx-rom' => [%w(msx), %w(), 'MSX ROM'], + 'application/x-n64-rom' => [%w(n64 v64 z64), %w(), 'Nintendo64 ROM'], + 'application/x-navi-animation' => [%w(ani), %w(), 'Windows animated cursor'], + 'application/x-nes-rom' => [%w(nes nez unf unif), %w(), 'NES ROM'], + 'application/x-netcdf' => [%w(cdf nc), %w(), 'Unidata NetCDF document'], + 'application/x-netshow-channel' => [%w(nsc), %w(application/vnd.ms-asf), 'Windows Media Station file'], + 'application/x-nintendo-ds-rom' => [%w(nds), %w(), 'Nintendo DS ROM'], + 'application/x-nzb' => [%w(nzb), %w(application/xml), 'NewzBin usenet index'], + 'application/x-object' => [%w(o), %w(), 'object code'], + 'application/x-oleo' => [%w(oleo), %w(), 'GNU Oleo spreadsheet'], + 'application/x-pagemaker' => [%w(p65 pm pm6 pmd), %w(application/x-ole-storage), 'Adobe PageMaker'], + 'application/x-pak' => [%w(pak), %w(), 'PAK archive'], + 'application/x-par2' => [%w(par2 par2), %w(), 'Parchive archive'], + 'application/x-partial-download' => [%w(crdownload part wkdownload), %w(), 'Partially downloaded file'], + 'application/x-pc-engine-rom' => [%w(pce), %w(), 'PC Engine ROM'], + 'application/x-perl' => [%w(al perl pl pl pm pod t), %w(application/x-executable text/plain), 'Perl script'], + 'application/x-php' => [%w(php php3 php4 php5 phps), %w(text/plain), 'PHP script'], + 'application/x-pkcs7-certificates' => [%w(p7b spc), %w(), 'PKCS#7 certificate bundle'], + 'application/x-planperfect' => [%w(pln), %w(), 'PlanPerfect spreadsheet'], + 'application/x-pocket-word' => [%w(psw), %w(), 'Pocket Word document'], + 'application/x-pw' => [%w(pw), %w(), 'Pathetic Writer document'], + 'application/x-python-bytecode' => [%w(pyc pyo), %w(), 'Python bytecode'], + 'application/x-qpress' => [%w(qp), %w(), 'Qpress archive'], + 'application/x-qtiplot' => [%w(qti qti.gz), %w(text/plain), 'QtiPlot document'], + 'application/x-quattropro' => [%w(wb1 wb2 wb3), %w(), 'Quattro Pro spreadsheet'], + 'application/x-quicktime-media-link' => [%w(qtl), %w(video/quicktime), 'QuickTime metalink playlist'], + 'application/x-qw' => [%w(qif), %w(), 'Quicken document'], + 'application/x-rar' => [%w(rar), %w(), 'RAR archive'], + 'application/x-raw-disk-image' => [%w(img raw-disk-image), %w(), 'Raw disk image'], + 'application/x-raw-disk-image-xz-compressed' => [%w(img.xz raw-disk-image.xz), %w(application/x-xz), 'Raw disk image (XZ-compressed)'], + 'application/x-rpm' => [%w(rpm), %w(), 'RPM package'], + 'application/x-ruby' => [%w(rb), %w(application/x-executable text/plain), 'Ruby script'], + 'application/x-sami' => [%w(sami smi), %w(text/plain), 'SAMI subtitles'], + 'application/x-saturn-rom' => [%w(bin iso), %w(), 'Sega Saturn disc image'], + 'application/x-shar' => [%w(shar), %w(), 'shell archive'], + 'application/x-shared-library-la' => [%w(la), %w(text/plain), 'libtool shared library'], + 'application/x-sharedlib' => [%w(so), %w(), 'shared library'], + 'application/x-shellscript' => [%w(sh), %w(application/x-executable text/plain), 'shell script'], + 'application/x-shorten' => [%w(shn), %w(), 'Shorten audio'], + 'application/x-siag' => [%w(siag), %w(), 'Siag spreadsheet'], + 'application/x-smaf' => [%w(mmf smaf), %w(), 'SMAF audio'], + 'application/x-sms-rom' => [%w(gg sg sms), %w(), 'Sega Master System/Game Gear ROM'], + 'application/x-source-rpm' => [%w(spm src.rpm), %w(application/x-rpm), 'Source RPM package'], + 'application/x-spss-por' => [%w(por), %w(), 'SPSS Portable Data File'], + 'application/x-spss-sav' => [%w(sav zsav), %w(), 'SPSS Data File'], + 'application/x-stuffit' => [%w(sit), %w(), 'StuffIt archive'], + 'application/x-subrip' => [%w(srt), %w(text/plain), 'SubRip subtitles'], + 'application/x-sv4cpio' => [%w(sv4cpio), %w(), 'SV4 CPIO archive'], + 'application/x-sv4crc' => [%w(sv4crc), %w(), 'SV4 CPIO archive (with CRC)'], + 'application/x-t602' => [%w(602), %w(), 'T602 document'], + 'application/x-tar' => [%w(gem gtar tar), %w(), 'Tar archive'], + 'application/x-tarz' => [%w(tar.z taz), %w(application/x-compress), 'Tar archive (compressed)'], + 'application/x-tex-gf' => [%w(gf), %w(), 'generic font file'], + 'application/x-tex-pk' => [%w(pk), %w(), 'packed font file'], + 'application/x-tgif' => [%w(obj), %w(), 'TGIF document'], + 'application/x-theme' => [%w(theme), %w(application/x-desktop), 'theme'], + 'application/x-trash' => [%w(bak old sik), %w(), 'backup file'], + 'application/x-trig' => [%w(trig), %w(text/plain), 'TriG RDF document'], + 'application/x-troff-man' => [%w(man), %w(text/plain), 'Manpage manual document'], + 'application/x-tzo' => [%w(tar.lzo tzo), %w(application/x-lzop), 'Tar archive (LZO-compressed)'], + 'application/x-ufraw' => [%w(ufraw), %w(text/xml), 'UFRaw ID image'], + 'application/x-ustar' => [%w(ustar), %w(), 'Ustar archive'], + 'application/x-wais-source' => [%w(src), %w(text/plain), 'WAIS source code'], + 'application/x-wii-rom' => [%w(iso), %w(), 'Wii disc image'], + 'application/x-wii-wad' => [%w(wad), %w(), 'WiiWare bundle'], + 'application/x-windows-themepack' => [%w(themepack), %w(application/vnd.ms-cab-compressed), 'Microsoft Windows theme pack'], + 'application/x-wpg' => [%w(wpg), %w(), 'WordPerfect/Drawperfect image'], + 'application/x-wwf' => [%w(wwf), %w(application/pdf), 'WWF document'], + 'application/x-x509-ca-cert' => [%w(cert crt der pem), %w(), 'DER/PEM/Netscape-encoded X.509 certificate'], + 'application/x-xar' => [%w(pkg xar), %w(), 'XAR archive'], + 'application/x-xbel' => [%w(xbel), %w(application/xml), 'XBEL bookmarks'], + 'application/x-xliff' => [%w(xlf xliff), %w(application/xml), 'XLIFF translation file'], + 'application/x-xpinstall' => [%w(xpi), %w(application/zip), 'XPInstall installer module'], + 'application/x-xz' => [%w(xz), %w(), 'XZ archive'], + 'application/x-xz-compressed-tar' => [%w(tar.xz txz), %w(application/x-xz), 'Tar archive (XZ-compressed)'], + 'application/x-xzpdf' => [%w(pdf.xz), %w(application/x-xz), 'PDF document (XZ-compressed)'], + 'application/x-yaml' => [%w(yaml yml), %w(text/plain), 'YAML document'], + 'application/x-zip-compressed-fb2' => [%w(fb2.zip), %w(application/zip), 'Compressed FictionBook document'], + 'application/x-zoo' => [%w(zoo), %w(), 'Zoo archive'], + 'application/xhtml+xml' => [%w(xht xhtml), %w(application/xml), 'XHTML page'], + 'application/xml' => [%w(rng xbl xml xsd), %w(text/plain), 'XML document'], + 'application/xml-dtd' => [%w(dtd), %w(text/plain), 'DTD file'], + 'application/xml-external-parsed-entity' => [%w(ent), %w(application/xml), 'XML entities document'], + 'application/xslt+xml' => [%w(xsl xslt), %w(application/xml), 'XSLT stylesheet'], + 'application/xspf+xml' => [%w(xspf), %w(application/xml), 'XSPF playlist'], + 'application/zip' => [%w(zip), %w(), 'Zip archive'], + 'application/zlib' => [%w(zz), %w(), 'Zlib archive'], + 'audio/AMR' => [%w(amr), %w(), 'AMR audio'], + 'audio/AMR-WB' => [%w(awb), %w(), 'AMR-WB audio'], + 'audio/aac' => [%w(aac), %w(), 'AAC audio'], + 'audio/ac3' => [%w(ac3), %w(), 'Dolby Digital audio'], + 'audio/annodex' => [%w(axa), %w(application/annodex), 'Annodex Audio'], + 'audio/basic' => [%w(au snd), %w(), 'ULAW (Sun) audio'], + 'audio/flac' => [%w(flac), %w(), 'FLAC audio'], + 'audio/midi' => [%w(kar mid midi), %w(), 'MIDI audio'], + 'audio/mp2' => [%w(mp2), %w(), 'MP2 audio'], + 'audio/mp4' => [%w(f4a m4a), %w(), 'MPEG-4 audio'], + 'audio/mpeg' => [%w(mp3 mpga), %w(), 'MP3 audio'], + 'audio/ogg' => [%w(oga ogg opus), %w(application/ogg), 'Ogg Audio'], + 'audio/prs.sid' => [%w(psid sid), %w(), 'Commodore 64 audio'], + 'audio/vnd.dts' => [%w(dts), %w(), 'DTS audio'], + 'audio/vnd.dts.hd' => [%w(dtshd), %w(audio/vnd.dts), 'DTSHD audio'], + 'audio/vnd.rn-realaudio' => [%w(ra rax), %w(), 'RealAudio document'], + 'audio/x-aifc' => [%w(aifc aiffc), %w(application/x-iff), 'AIFC audio'], + 'audio/x-aiff' => [%w(aif aiff), %w(application/x-iff), 'AIFF/Amiga/Mac audio'], + 'audio/x-amzxml' => [%w(amz), %w(), 'AmazonMP3 download file'], + 'audio/x-ape' => [%w(ape), %w(), "Monkey's audio"], + 'audio/x-flac+ogg' => [%w(oga ogg), %w(audio/ogg), 'Ogg FLAC audio'], + 'audio/x-gsm' => [%w(gsm), %w(), 'GSM 06.10 audio'], + 'audio/x-iriver-pla' => [%w(pla), %w(), 'iRiver Playlist'], + 'audio/x-it' => [%w(it), %w(), 'Impulse Tracker audio'], + 'audio/x-m4b' => [%w(f4b m4b), %w(audio/mp4), 'MPEG-4 audio book'], + 'audio/x-matroska' => [%w(mka), %w(application/x-matroska), 'Matroska audio'], + 'audio/x-minipsf' => [%w(minipsf), %w(audio/x-psf), 'MiniPSF audio'], + 'audio/x-mo3' => [%w(mo3), %w(), 'compressed Tracker audio'], + 'audio/x-mod' => [%w(669 m15 med mod mtm ult uni), %w(), 'Amiga SoundTracker audio'], + 'audio/x-mpegurl' => [%w(m3u m3u8 vlc), %w(text/plain), 'MP3 audio (streamed)'], + 'audio/x-ms-asx' => [%w(asx wax wmx wvx), %w(), 'Microsoft ASX playlist'], + 'audio/x-ms-wma' => [%w(wma), %w(application/vnd.ms-asf), 'Windows Media audio'], + 'audio/x-musepack' => [%w(mp+ mpc mpp), %w(), 'Musepack audio'], + 'audio/x-opus+ogg' => [%w(opus), %w(audio/ogg), 'Opus audio'], + 'audio/x-psf' => [%w(psf), %w(), 'PSF audio'], + 'audio/x-psflib' => [%w(psflib), %w(audio/x-psf), 'PSFlib audio library'], + 'audio/x-s3m' => [%w(s3m), %w(), 'Scream Tracker 3 audio'], + 'audio/x-scpls' => [%w(pls), %w(), 'MP3 ShoutCast playlist'], + 'audio/x-speex' => [%w(spx), %w(), 'Speex audio'], + 'audio/x-speex+ogg' => [%w(oga ogg), %w(audio/ogg), 'Ogg Speex audio'], + 'audio/x-stm' => [%w(stm), %w(), 'Scream Tracker audio'], + 'audio/x-tta' => [%w(tta), %w(), 'TrueAudio audio'], + 'audio/x-voc' => [%w(voc), %w(), 'VOC audio'], + 'audio/x-vorbis+ogg' => [%w(oga ogg), %w(audio/ogg), 'Ogg Vorbis audio'], + 'audio/x-wav' => [%w(wav), %w(), 'WAV audio'], + 'audio/x-wavpack' => [%w(wv wvp), %w(), 'WavPack audio'], + 'audio/x-wavpack-correction' => [%w(wvc), %w(), 'WavPack audio correction file'], + 'audio/x-xi' => [%w(xi), %w(), 'Scream Tracker instrument'], + 'audio/x-xm' => [%w(xm), %w(), 'FastTracker II audio'], + 'audio/x-xmf' => [%w(xmf), %w(), 'XMF audio'], + 'image/bmp' => [%w(bmp), %w(), 'Windows BMP image'], + 'image/cgm' => [%w(cgm), %w(), 'Computer Graphics Metafile'], + 'image/fax-g3' => [%w(g3), %w(), 'CCITT G3 fax'], + 'image/fits' => [%w(fits), %w(), 'FITS document'], + 'image/gif' => [%w(gif), %w(), 'GIF image'], + 'image/ief' => [%w(ief), %w(), 'IEF image'], + 'image/jp2' => [%w(jp2 jpf jpx), %w(), 'JPEG-2000 image'], + 'image/jpeg' => [%w(jpe jpeg jpg), %w(), 'JPEG image'], + 'image/openraster' => [%w(ora), %w(), 'OpenRaster archiving image'], + 'image/png' => [%w(png), %w(), 'PNG image'], + 'image/rle' => [%w(rle), %w(), 'Run Length Encoded bitmap image'], + 'image/svg+xml' => [%w(svg), %w(application/xml), 'SVG image'], + 'image/svg+xml-compressed' => [%w(svgz), %w(application/gzip), 'compressed SVG image'], + 'image/tiff' => [%w(tif tiff), %w(), 'TIFF image'], + 'image/vnd.adobe.photoshop' => [%w(psd), %w(), 'Photoshop image'], + 'image/vnd.djvu' => [%w(djv djvu), %w(), 'DjVu image'], + 'image/vnd.dwg' => [%w(dwg), %w(), 'AutoCAD image'], + 'image/vnd.dxf' => [%w(dxf), %w(), 'DXF vector image'], + 'image/vnd.microsoft.icon' => [%w(ico), %w(), 'Windows icon'], + 'image/vnd.ms-modi' => [%w(mdi), %w(), 'Microsoft Document Imaging format'], + 'image/vnd.rn-realpix' => [%w(rp), %w(), 'RealPix document'], + 'image/vnd.wap.wbmp' => [%w(wbmp), %w(), 'WBMP image'], + 'image/vnd.zbrush.pcx' => [%w(pcx), %w(), 'PCX image'], + 'image/webp' => [%w(webp), %w(), 'WebP image'], + 'image/x-3ds' => [%w(3ds), %w(), '3D Studio image'], + 'image/x-adobe-dng' => [%w(dng), %w(image/tiff image/x-dcraw), 'Adobe DNG negative'], + 'image/x-applix-graphics' => [%w(ag), %w(), 'Applix Graphics image'], + 'image/x-bzeps' => [%w(eps.bz2 epsf.bz2 epsi.bz2), %w(application/x-bzip), 'EPS image (bzip-compressed)'], + 'image/x-canon-cr2' => [%w(cr2), %w(image/tiff image/x-dcraw), 'Canon CR2 raw image'], + 'image/x-canon-crw' => [%w(crw), %w(image/x-dcraw), 'Canon CRW raw image'], + 'image/x-cmu-raster' => [%w(ras), %w(), 'CMU raster image'], + 'image/x-compressed-xcf' => [%w(xcf.bz2 xcf.gz), %w(), 'compressed GIMP image'], + 'image/x-dds' => [%w(dds), %w(), 'DirectDraw surface'], + 'image/x-emf' => [%w(emf), %w(), 'EMF image'], + 'image/x-eps' => [%w(eps epsf epsi), %w(application/postscript), 'EPS image'], + 'image/x-exr' => [%w(exr), %w(), 'EXR image'], + 'image/x-fuji-raf' => [%w(raf), %w(image/x-dcraw), 'Fuji RAF raw image'], + 'image/x-gzeps' => [%w(eps.gz epsf.gz epsi.gz), %w(application/gzip), 'EPS image (gzip-compressed)'], + 'image/x-icns' => [%w(icns), %w(), 'MacOS X icon'], + 'image/x-ilbm' => [%w(iff ilbm lbm), %w(application/x-iff), 'ILBM image'], + 'image/x-jng' => [%w(jng), %w(), 'JNG image'], + 'image/x-kodak-dcr' => [%w(dcr), %w(image/tiff image/x-dcraw), 'Kodak DCR raw image'], + 'image/x-kodak-k25' => [%w(k25), %w(image/tiff image/x-dcraw), 'Kodak K25 raw image'], + 'image/x-kodak-kdc' => [%w(kdc), %w(image/tiff image/x-dcraw), 'Kodak KDC raw image'], + 'image/x-lwo' => [%w(lwo lwob), %w(), 'LightWave object'], + 'image/x-lws' => [%w(lws), %w(), 'LightWave scene'], + 'image/x-macpaint' => [%w(pntg), %w(), 'MacPaint Bitmap image'], + 'image/x-minolta-mrw' => [%w(mrw), %w(image/x-dcraw), 'Minolta MRW raw image'], + 'image/x-msod' => [%w(msod), %w(), 'Office drawing'], + 'image/x-nikon-nef' => [%w(nef), %w(image/tiff image/x-dcraw), 'Nikon NEF raw image'], + 'image/x-olympus-orf' => [%w(orf), %w(image/x-dcraw), 'Olympus ORF raw image'], + 'image/x-panasonic-raw' => [%w(raw), %w(image/x-dcraw), 'Panasonic raw image'], + 'image/x-panasonic-raw2' => [%w(rw2), %w(image/x-dcraw), 'Panasonic raw2 image'], + 'image/x-pentax-pef' => [%w(pef), %w(image/tiff image/x-dcraw), 'Pentax PEF raw image'], + 'image/x-photo-cd' => [%w(pcd), %w(), 'PCD image'], + 'image/x-pict' => [%w(pct pict pict1 pict2), %w(), 'Macintosh Quickdraw/PICT drawing'], + 'image/x-portable-anymap' => [%w(pnm), %w(), 'PNM image'], + 'image/x-portable-bitmap' => [%w(pbm), %w(image/x-portable-anymap), 'PBM image'], + 'image/x-portable-graymap' => [%w(pgm), %w(image/x-portable-anymap), 'PGM image'], + 'image/x-portable-pixmap' => [%w(ppm), %w(image/x-portable-anymap), 'PPM image'], + 'image/x-quicktime' => [%w(qif qtif), %w(), 'QuickTime image'], + 'image/x-rgb' => [%w(rgb), %w(), 'RGB image'], + 'image/x-sgi' => [%w(sgi), %w(), 'SGI image'], + 'image/x-sigma-x3f' => [%w(x3f), %w(image/x-dcraw), 'Sigma X3F raw image'], + 'image/x-skencil' => [%w(sk sk1), %w(), 'Skencil document'], + 'image/x-sony-arw' => [%w(arw), %w(image/tiff image/x-dcraw), 'Sony ARW raw image'], + 'image/x-sony-sr2' => [%w(sr2), %w(image/tiff image/x-dcraw), 'Sony SR2 raw image'], + 'image/x-sony-srf' => [%w(srf), %w(image/tiff image/x-dcraw), 'Sony SRF raw image'], + 'image/x-sun-raster' => [%w(sun), %w(), 'Sun raster image'], + 'image/x-tga' => [%w(icb tga tpic vda vst), %w(), 'TGA image'], + 'image/x-win-bitmap' => [%w(cur), %w(), 'Windows cursor'], + 'image/x-wmf' => [%w(wmf), %w(), 'WMF image'], + 'image/x-xbitmap' => [%w(xbm), %w(), 'XBM image'], + 'image/x-xcf' => [%w(xcf), %w(), 'GIMP image'], + 'image/x-xfig' => [%w(fig), %w(), 'XFig image'], + 'image/x-xpixmap' => [%w(xpm), %w(), 'XPM image'], + 'image/x-xwindowdump' => [%w(xwd), %w(), 'X window image'], + 'message/rfc822' => [%w(eml), %w(text/plain), 'email message'], + 'model/vrml' => [%w(vrm vrml wrl), %w(text/plain), 'VRML document'], + 'text/cache-manifest' => [%w(manifest), %w(text/plain), 'Web application cache manifest'], + 'text/calendar' => [%w(ics vcs), %w(text/plain), 'VCS/ICS calendar'], + 'text/css' => [%w(css), %w(text/plain), 'CSS stylesheet'], + 'text/csv' => [%w(csv), %w(text/plain), 'CSV document'], + 'text/csv-schema' => [%w(csvs), %w(text/plain), 'CSV Schema document'], + 'text/html' => [%w(htm html), %w(text/plain), 'HTML document'], + 'text/markdown' => [%w(markdown md mkd), %w(text/plain), 'Markdown document'], + 'text/plain' => [%w(asc txt), %w(), 'plain text document'], + 'text/richtext' => [%w(rtx), %w(text/plain), 'rich text document'], + 'text/rust' => [%w(rs), %w(text/plain), 'Rust source code'], + 'text/sgml' => [%w(sgm sgml), %w(text/plain), 'SGML document'], + 'text/spreadsheet' => [%w(slk sylk), %w(text/plain), 'spreadsheet interchange document'], + 'text/tab-separated-values' => [%w(tsv), %w(text/plain), 'TSV document'], + 'text/troff' => [%w(roff t tr), %w(text/plain), 'Troff document'], + 'text/turtle' => [%w(ttl), %w(text/plain), 'Turtle document'], + 'text/vcard' => [%w(gcrd vcard vcf vct), %w(text/plain), 'electronic business card'], + 'text/vnd.graphviz' => [%w(dot gv), %w(), 'Graphviz DOT graph'], + 'text/vnd.rn-realtext' => [%w(rt), %w(), 'RealText document'], + 'text/vnd.sun.j2me.app-descriptor' => [%w(jad), %w(), 'JAD document'], + 'text/vnd.trolltech.linguist' => [%w(ts), %w(application/xml), 'message catalog'], + 'text/vnd.wap.wml' => [%w(wml), %w(application/xml), 'WML document'], + 'text/vnd.wap.wmlscript' => [%w(wmls), %w(), 'WMLScript program'], + 'text/vtt' => [%w(vtt), %w(text/plain), 'WebVTT subtitles'], + 'text/x-adasrc' => [%w(adb ads), %w(text/plain), 'Ada source code'], + 'text/x-bibtex' => [%w(bib), %w(text/plain), 'BibTeX document'], + 'text/x-c++hdr' => [%w(h++ hh hp hpp hxx), %w(text/x-chdr), 'C++ header'], + 'text/x-c++src' => [%w(c c++ cc cpp cxx), %w(text/x-csrc), 'C++ source code'], + 'text/x-chdr' => [%w(h), %w(text/x-csrc), 'C header'], + 'text/x-cmake' => [%w(cmake), %w(text/plain), 'CMake source code'], + 'text/x-cobol' => [%w(cbl cob), %w(text/plain), 'COBOL source file'], + 'text/x-csharp' => [%w(cs), %w(text/x-csrc), 'C# source code'], + 'text/x-csrc' => [%w(c), %w(text/plain), 'C source code'], + 'text/x-dcl' => [%w(dcl), %w(text/plain), 'DCL script'], + 'text/x-dsl' => [%w(dsl), %w(text/plain), 'DSSSL document'], + 'text/x-dsrc' => [%w(d di), %w(text/x-csrc), 'D source code'], + 'text/x-eiffel' => [%w(e eif), %w(text/plain), 'Eiffel source code'], + 'text/x-emacs-lisp' => [%w(el), %w(text/plain), 'Emacs Lisp source code'], + 'text/x-erlang' => [%w(erl), %w(text/plain), 'Erlang source code'], + 'text/x-fortran' => [%w(f f90 f95 for), %w(text/plain), 'Fortran source code'], + 'text/x-genie' => [%w(gs), %w(text/plain), 'Genie source code'], + 'text/x-gettext-translation' => [%w(po), %w(text/plain), 'translation file'], + 'text/x-gettext-translation-template' => [%w(pot), %w(text/plain), 'translation template'], + 'text/x-go' => [%w(go), %w(text/plain), 'Go source code'], + 'text/x-google-video-pointer' => [%w(gvp), %w(), 'Google Video Pointer'], + 'text/x-haskell' => [%w(hs), %w(text/plain), 'Haskell source code'], + 'text/x-iMelody' => [%w(ime imy), %w(), 'iMelody ringtone'], + 'text/x-idl' => [%w(idl), %w(text/plain), 'IDL document'], + 'text/x-iptables' => [%w(iptables), %w(text/plain), 'iptables configuration file'], + 'text/x-java' => [%w(java), %w(text/x-csrc), 'Java source code'], + 'text/x-ldif' => [%w(ldif), %w(text/plain), 'LDIF address book'], + 'text/x-lilypond' => [%w(ly), %w(text/plain), 'Lilypond music sheet'], + 'text/x-literate-haskell' => [%w(lhs), %w(text/plain), 'LHS source code'], + 'text/x-log' => [%w(log), %w(text/plain), 'application log'], + 'text/x-lua' => [%w(lua), %w(application/x-executable text/plain), 'Lua script'], + 'text/x-makefile' => [%w(mak mk), %w(text/plain), 'Makefile'], + 'text/x-matlab' => [%w(m), %w(text/plain), 'MATLAB script/function'], + 'text/x-microdvd' => [%w(sub), %w(text/plain), 'MicroDVD subtitles'], + 'text/x-moc' => [%w(moc), %w(text/plain), 'Qt MOC file'], + 'text/x-modelica' => [%w(mo), %w(text/plain), 'Modelica model'], + 'text/x-mof' => [%w(mof), %w(text/x-csrc), 'Managed Object Format'], + 'text/x-mpsub' => [%w(sub), %w(text/plain), 'MPSub subtitles'], + 'text/x-mrml' => [%w(mrl mrml), %w(), 'MRML playlist'], + 'text/x-ms-regedit' => [%w(reg), %w(text/plain), 'Windows Registry extract'], + 'text/x-mup' => [%w(mup not), %w(text/plain), 'Mup publication'], + 'text/x-nfo' => [%w(nfo), %w(text/x-readme), 'NFO document'], + 'text/x-objcsrc' => [%w(m), %w(text/x-csrc), 'Objective-C source code'], + 'text/x-ocaml' => [%w(ml mli), %w(), 'OCaml source code'], + 'text/x-ocl' => [%w(ocl), %w(text/plain), 'OCL file'], + 'text/x-ooc' => [%w(ooc), %w(text/x-csrc), 'OOC source code'], + 'text/x-opml+xml' => [%w(opml), %w(application/xml), 'OPML syndication feed'], + 'text/x-pascal' => [%w(p pas), %w(text/plain), 'Pascal source code'], + 'text/x-patch' => [%w(diff patch), %w(text/plain), 'differences between files'], + 'text/x-python' => [%w(py pyx wsgi), %w(application/x-executable text/plain), 'Python script'], + 'text/x-qml' => [%w(qml qmlproject qmltypes), %w(), 'Qt Markup Language file'], + 'text/x-reject' => [%w(rej), %w(text/plain), 'rejected patch'], + 'text/x-rpm-spec' => [%w(spec), %w(text/plain), 'RPM spec file'], + 'text/x-scala' => [%w(scala), %w(text/plain), 'Scala source code'], + 'text/x-scheme' => [%w(scm ss), %w(text/plain), 'Scheme source code'], + 'text/x-setext' => [%w(etx), %w(text/plain), 'Setext document'], + 'text/x-ssa' => [%w(ass ssa), %w(text/plain), 'SSA subtitles'], + 'text/x-subviewer' => [%w(sub), %w(text/plain), 'SubViewer subtitles'], + 'text/x-svhdr' => [%w(svh), %w(text/x-verilog), 'SystemVerilog header'], + 'text/x-svsrc' => [%w(sv), %w(text/x-verilog), 'SystemVerilog source code'], + 'text/x-tcl' => [%w(tcl tk), %w(text/plain), 'Tcl script'], + 'text/x-tex' => [%w(cls dtx ins latex ltx sty tex), %w(text/plain), 'TeX document'], + 'text/x-texinfo' => [%w(texi texinfo), %w(text/plain), 'TeXInfo document'], + 'text/x-troff-me' => [%w(me), %w(text/plain), 'Troff ME input document'], + 'text/x-troff-mm' => [%w(mm), %w(text/plain), 'Troff MM input document'], + 'text/x-troff-ms' => [%w(ms), %w(text/plain), 'Troff MS input document'], + 'text/x-txt2tags' => [%w(t2t), %w(text/plain), 'txt2tags document'], + 'text/x-uil' => [%w(uil), %w(text/plain), 'X-Motif UIL table'], + 'text/x-uuencode' => [%w(uue), %w(text/plain), 'uuencoded file'], + 'text/x-vala' => [%w(vala vapi), %w(text/x-csrc), 'Vala source code'], + 'text/x-verilog' => [%w(v), %w(text/plain), 'Verilog source code'], + 'text/x-vhdl' => [%w(vhd vhdl), %w(text/plain), 'VHDL source code'], + 'text/x-xmi' => [%w(xmi), %w(application/xml), 'XMI file'], + 'text/x-xslfo' => [%w(fo xslfo), %w(application/xml), 'XSL FO file'], + 'video/3gpp' => [%w(3ga 3gp 3gpp), %w(video/mp4), '3GPP multimedia file'], + 'video/3gpp2' => [%w(3g2 3gp2 3gpp2), %w(video/mp4), '3GPP2 multimedia file'], + 'video/annodex' => [%w(axv), %w(application/annodex), 'Annodex Video'], + 'video/dv' => [%w(dv), %w(), 'DV video'], + 'video/mp2t' => [%w(bdm bdmv clpi cpi m2t m2ts mpl mpls mts ts), %w(), 'MPEG-2 transport stream'], + 'video/mp4' => [%w(f4v lrv m4v mp4), %w(), 'MPEG-4 video'], + 'video/mpeg' => [%w(mp2 mpe mpeg mpg vob), %w(), 'MPEG video'], + 'video/ogg' => [%w(ogg ogv), %w(application/ogg), 'Ogg Video'], + 'video/quicktime' => [%w(moov mov qt qtvr), %w(), 'QuickTime video'], + 'video/vnd.mpegurl' => [%w(m1u m4u mxu), %w(text/plain), 'MPEG video (streamed)'], + 'video/vnd.rn-realvideo' => [%w(rv rvx), %w(), 'RealVideo document'], + 'video/vnd.vivo' => [%w(viv vivo), %w(), 'Vivo video'], + 'video/webm' => [%w(webm), %w(), 'WebM video'], + 'video/x-flic' => [%w(flc fli), %w(), 'FLIC animation'], + 'video/x-flv' => [%w(flv), %w(), 'Flash video'], + 'video/x-javafx' => [%w(fxm), %w(video/x-flv), 'JavaFX video'], + 'video/x-matroska' => [%w(mkv), %w(application/x-matroska), 'Matroska video'], + 'video/x-matroska-3d' => [%w(mk3d), %w(application/x-matroska), 'Matroska 3D video'], + 'video/x-mng' => [%w(mng), %w(), 'MNG animation'], + 'video/x-ms-wmv' => [%w(wmv), %w(application/vnd.ms-asf), 'Windows Media video'], + 'video/x-msvideo' => [%w(avf avi divx), %w(), 'AVI video'], + 'video/x-nsv' => [%w(nsv), %w(), 'NullSoft video'], + 'video/x-ogm+ogg' => [%w(ogm), %w(video/ogg), 'OGM video'], + 'video/x-sgi-movie' => [%w(movie), %w(), 'SGI video'], + 'video/x-theora+ogg' => [%w(ogg), %w(video/ogg), 'Ogg Theora video'], + 'x-epoc/x-sisx-app' => [%w(sisx), %w(), 'SISX package'], + } + # @private + # :nodoc: + MAGIC = [ + ['image/jpeg', [[0, "\377\330\377"], [0, "\377\330"]]], + ['image/png', [[0, "\211PNG"]]], + ['image/gif', [[0, 'GIF8']]], + ['image/tiff', [[0, "MM\000*"], [0, "II*\000"]]], + ['image/bmp', [[0, 'BM', [[14, "\f"], [14, '@'], [14, '(']]]]], + ['image/vnd.adobe.photoshop', []], + ['image/webp', [[0, 'RIFF', [[8, 'WEBP']]]]], + ['image/svg+xml', [[0..256, '']]], + ['application/x-nzb', [[0..256, '', [[8, 'debian']]]]], + ['application/vnd.emusic-emusic_package', [[0, 'nF7YLao']]], + ['application/vnd.iccprofile', [[36, 'acsp']]], + ['application/vnd.lotus-1-2-3', [[0, "\000\000\002\000\006\004\006\000\b\000\000\000\000\000"]]], + ['application/vnd.lotus-wordpro', [[0, 'WordPro']]], + ['application/vnd.ms-access', [[0, "\000\001\000\000Standard Jet DB"]]], + ['application/vnd.ms-asf', [[0, "0&\262u"], [0, '[Reference]']]], + ['application/vnd.ms-tnef', [[0, "x\237>\""]]], + ['application/vnd.oasis.opendocument.chart', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.chart']]]]]]], + ['application/vnd.oasis.opendocument.chart-template', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.chart-template']]]]]]], + ['application/vnd.oasis.opendocument.database', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.base']]]]]]], + ['application/vnd.oasis.opendocument.formula', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.formula']]]]]]], + ['application/vnd.oasis.opendocument.formula-template', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.formula-template']]]]]]], + ['application/vnd.oasis.opendocument.graphics', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.graphics']]]]]]], + ['application/vnd.oasis.opendocument.graphics-template', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.graphics-template']]]]]]], + ['application/vnd.oasis.opendocument.image', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.image']]]]]]], + ['application/vnd.oasis.opendocument.presentation', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.presentation']]]]]]], + ['application/vnd.oasis.opendocument.presentation-template', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.presentation-template']]]]]]], + ['application/vnd.oasis.opendocument.spreadsheet', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.spreadsheet']]]]]]], + ['application/vnd.oasis.opendocument.spreadsheet-template', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.spreadsheet-template']]]]]]], + ['application/vnd.oasis.opendocument.text', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.text']]]]]]], + ['application/vnd.oasis.opendocument.text-master', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.text-master']]]]]]], + ['application/vnd.oasis.opendocument.text-template', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.text-template']]]]]]], + ['application/vnd.oasis.opendocument.text-web', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.oasis.opendocument.text-web']]]]]]], + ['application/vnd.rn-realmedia', [[0, '.RMF']]], + ['application/vnd.sun.xml.calc', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.sun.xml.calc']]]]]]], + ['application/vnd.sun.xml.calc.template', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.sun.xml.calc']]]]]]], + ['application/vnd.sun.xml.draw', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.sun.xml.draw']]]]]]], + ['application/vnd.sun.xml.draw.template', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.sun.xml.draw']]]]]]], + ['application/vnd.sun.xml.impress', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.sun.xml.impress']]]]]]], + ['application/vnd.sun.xml.impress.template', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.sun.xml.impress']]]]]]], + ['application/vnd.sun.xml.math', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.sun.xml.math']]]]]]], + ['application/vnd.sun.xml.writer', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.sun.xml.writer']]]]]]], + ['application/vnd.sun.xml.writer.global', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.sun.xml.writer']]]]]]], + ['application/vnd.sun.xml.writer.template', [[0, "PK\003\004", [[30, 'mimetype', [[38, 'application/vnd.sun.xml.writer']]]]]]], + ['application/vnd.symbian.install', [[8, "\031\004\000\020"]]], + ['application/vnd.tcpdump.pcap', [[0, "\324\303\262\241"], [0, "\241\262\303\324"]]], + ['application/vnd.wordperfect', [[1, 'WPC']]], + ['application/vnd.xdgapp', [[0, "xdg-app\000\001\000\211\345"]]], + ['application/winhlp', [[0, "?_\003\000"]]], + ['application/x-abiword', [[0..256, '']]], + ['application/x-saturn-rom', [[0, 'SEGA SEGASATURN'], [16, 'SEGA SEGASATURN']]], + ['application/x-sc', [[38, 'Spreadsheet']]], + ['application/x-sharedlib', [[0, "\177ELF", [[5, "\001", [[16, "\003\000"]]]]], [0, "\177ELF", [[5, "\002", [[16, "\000\003"]]]]], [0, "\203\001"]]], + ['application/x-shellscript', [[10, '# This is a shell archive'], [2..16, '/bin/bash'], [2..16, '/bin/nawk'], [2..16, '/bin/zsh'], [2..16, '/bin/sh'], [2..16, '/bin/ksh'], [2..16, '/bin/dash'], [2..16, '/bin/env sh'], [2..16, '/bin/env bash'], [2..16, '/bin/env zsh'], [2..16, '/bin/env ksh']]], + ['application/x-shorten', [[0, 'ajkg']]], + ['application/x-smaf', [[0, 'MMMD']]], + ['application/x-spss-por', [[40, 'ASCII SPSS PORT FILE']]], + ['application/x-spss-sav', [[0, '$FL2'], [0, '$FL3']]], + ['application/x-sqlite2', [[0, '** This file contains an SQLite']]], + ['application/x-sqlite3', [[0, 'SQLite format 3']]], + ['application/x-subrip', [[0, '1', [[0..256, ' --> ']]]]], + ['application/x-t602', [[0, '@CT 0'], [0, '@CT 1'], [0, '@CT 2']]], + ['application/x-tgif', [[0, '%TGIF']]], + ['application/x-wii-rom', [[24, "]\034\236\243"], [0, 'WBFS'], [0, "WII\001DISC"]]], + ['application/x-wii-wad', [[4, "Is\000\000"], [4, "ib\000\000"], [4, "Bk\000\000"]]], + ['application/x-xbel', [[0..256, ''], [0, '!']]], + ['application/x-riff', [[0, 'RIFF']]], + ['application/x-executable', [[0, "\177ELF", [[5, "\001", [[16, "\002\000"]]]]], [0, "\177ELF", [[5, "\002", [[16, "\000\002"]]]]], [0, 'MZ'], [0, "\034R"], [0, "\020\001"], [0, "\021\001"], [0, "\203\001"]]], + ['application/x-iff', [[0, 'FORM']]], + ['application/x-perl', [[0..256, "\n=pod"], [0..256, "\n=head1 NAME"], [0..256, "\n=head1 DESCRIPTION"]]], + ['application/xml', [[0, 'type: Mime type + # * options: Options hash + # + # Option keys: + # * :extensions: String list or single string of file extensions + # * :parents: String list or single string of parent mime types + # * :magic: Mime magic specification + # * :comment: Comment string + def self.add(type, options) + extensions = [options[:extensions]].flatten.compact + TYPES[type] = [extensions, + [options[:parents]].flatten.compact, + options[:comment]] + extensions.each {|ext| EXTENSIONS[ext] = type } + MAGIC.unshift [type, options[:magic]] if options[:magic] + end + + # Removes a mime type from the dictionary. You might want to do this if + # you're seeing impossible conflicts (for instance, application/x-gmc-link). + # * type: The mime type to remove. All associated extensions and magic are removed too. + def self.remove(type) + EXTENSIONS.delete_if {|ext, t| t == type } + MAGIC.delete_if {|t, m| t == type } + TYPES.delete(type) + end + + # Returns true if type is a text format + def text?; mediatype == 'text' || child_of?('text/plain'); end + + # Mediatype shortcuts + def image?; mediatype == 'image'; end + def audio?; mediatype == 'audio'; end + def video?; mediatype == 'video'; end + + # Returns true if type is child of parent type + def child_of?(parent) + MimeMagic.child?(type, parent) + end + + # Get string list of file extensions + def extensions + TYPES.key?(type) ? TYPES[type][0] : [] + end + + # Get mime comment + def comment + (TYPES.key?(type) ? TYPES[type][2] : nil).to_s + end + + # Lookup mime type by file extension + def self.by_extension(ext) + ext = ext.to_s.downcase + mime = ext[0..0] == '.' ? EXTENSIONS[ext[1..-1]] : EXTENSIONS[ext] + mime && new(mime) + end + + # Lookup mime type by filename + def self.by_path(path) + ext = path.split(".").last + by_extension(ext) + end + + # Lookup mime type by magic content analysis. + # This is a slow operation. + def self.by_magic(io) + mime = magic_match(io, :find) + mime && new(mime[0]) + end + + # Lookup all mime types by magic content analysis. + # This is a slower operation. + def self.all_by_magic(io) + magic_match(io, :select).map { |mime| new(mime[0]) } + end + + # Return type as string + def to_s + type + end + + # Allow comparison with string + def eql?(other) + type == other.to_s + end + + def hash + type.hash + end + + alias == eql? + + def self.child?(child, parent) + child == parent || TYPES.key?(child) && TYPES[child][1].any? {|p| child?(p, parent) } + end + + def self.magic_match(io, method) + return magic_match(StringIO.new(cast_to_ascii(io.to_s)), method) unless io.respond_to?(:read) + + # io.binmode if io.respond_to?(:binmode) + # io.set_encoding(Encoding::BINARY) if io.respond_to?(:set_encoding) + # buffer = "".force_encoding(Encoding::BINARY) + buffer = cast_to_ascii("") + MAGIC.send(method) { |type, matches| magic_match_io(io, matches, buffer) } + end + + def self.magic_match_io(io, matches, buffer) + matches.any? do |offset, value, children| + match = + if Range === offset + io.read(offset.begin, buffer) + x = io.read((offset.end - offset.begin + value.bytesize), buffer) + x && x.include?(value) + else + io.read(offset, buffer) + io.read(value.bytesize, buffer) == value + end + io.rewind + match && (!children || magic_match_io(io, children, buffer)) + end + end + + private_class_method :magic_match, :magic_match_io +end +# +[['application/vnd.openxmlformats-officedocument.presentationml.presentation', [[0, "PK\003\004", [[30, '[Content_Types].xml', [[0..5000, 'ppt/']]]]]]], + ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', [[0, "PK\003\004", [[30, '[Content_Types].xml', [[0..5000, 'xl/']]]]]]], + ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', [[0, "PK\003\004", [[30, '[Content_Types].xml', [[0..5000, 'word/']]]]]]]].each do |magic| + MimeMagic.add(magic[0], magic: magic[1]) +end diff --git a/gxg/deps/rexml/attlistdecl.rb b/gxg/deps/rexml/attlistdecl.rb new file mode 100644 index 0000000..540d6c5 --- /dev/null +++ b/gxg/deps/rexml/attlistdecl.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: false +#vim:ts=2 sw=2 noexpandtab: +require 'deps/rexml/child' +require 'deps/rexml/source' + +module REXML + # This class needs: + # * Documentation + # * Work! Not all types of attlists are intelligently parsed, so we just + # spew back out what we get in. This works, but it would be better if + # we formatted the output ourselves. + # + # AttlistDecls provide *just* enough support to allow namespace + # declarations. If you need some sort of generalized support, or have an + # interesting idea about how to map the hideous, terrible design of DTD + # AttlistDecls onto an intuitive Ruby interface, let me know. I'm desperate + # for anything to make DTDs more palateable. + class AttlistDecl < Child + include Enumerable + + # What is this? Got me. + attr_reader :element_name + + # Create an AttlistDecl, pulling the information from a Source. Notice + # that this isn't very convenient; to create an AttlistDecl, you basically + # have to format it yourself, and then have the initializer parse it. + # Sorry, but for the foreseeable future, DTD support in REXML is pretty + # weak on convenience. Have I mentioned how much I hate DTDs? + def initialize(source) + super() + if (source.kind_of? Array) + @element_name, @pairs, @contents = *source + end + end + + # Access the attlist attribute/value pairs. + # value = attlist_decl[ attribute_name ] + def [](key) + @pairs[key] + end + + # Whether an attlist declaration includes the given attribute definition + # if attlist_decl.include? "xmlns:foobar" + def include?(key) + @pairs.keys.include? key + end + + # Iterate over the key/value pairs: + # attlist_decl.each { |attribute_name, attribute_value| ... } + def each(&block) + @pairs.each(&block) + end + + # Write out exactly what we got in. + def write out, indent=-1 + out << @contents + end + + def node_type + :attlistdecl + end + end +end diff --git a/gxg/deps/rexml/attribute.rb b/gxg/deps/rexml/attribute.rb new file mode 100644 index 0000000..f7148b2 --- /dev/null +++ b/gxg/deps/rexml/attribute.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: false +require "deps/rexml/namespace" +require 'deps/rexml/text' + +module REXML + # Defines an Element Attribute; IE, a attribute=value pair, as in: + # . Attributes can be in their own + # namespaces. General users of REXML will not interact with the + # Attribute class much. + class Attribute + include Node + include Namespace + + # The element to which this attribute belongs + attr_reader :element + # The normalized value of this attribute. That is, the attribute with + # entities intact. + attr_writer :normalized + PATTERN = /\s*(#{NAME_STR})\s*=\s*(["'])(.*?)\2/um + + NEEDS_A_SECOND_CHECK = /(<|&((#{Entity::NAME});|(#0*((?:\d+)|(?:x[a-fA-F0-9]+)));)?)/um + + # Constructor. + # FIXME: The parser doesn't catch illegal characters in attributes + # + # first:: + # Either: an Attribute, which this new attribute will become a + # clone of; or a String, which is the name of this attribute + # second:: + # If +first+ is an Attribute, then this may be an Element, or nil. + # If nil, then the Element parent of this attribute is the parent + # of the +first+ Attribute. If the first argument is a String, + # then this must also be a String, and is the content of the attribute. + # If this is the content, it must be fully normalized (contain no + # illegal characters). + # parent:: + # Ignored unless +first+ is a String; otherwise, may be the Element + # parent of this attribute, or nil. + # + # + # Attribute.new( attribute_to_clone ) + # Attribute.new( attribute_to_clone, parent_element ) + # Attribute.new( "attr", "attr_value" ) + # Attribute.new( "attr", "attr_value", parent_element ) + def initialize( first, second=nil, parent=nil ) + @normalized = @unnormalized = @element = nil + if first.kind_of? Attribute + self.name = first.expanded_name + @unnormalized = first.value + if second.kind_of? Element + @element = second + else + @element = first.element + end + elsif first.kind_of? String + @element = parent + self.name = first + @normalized = second.to_s + else + raise "illegal argument #{first.class.name} to Attribute constructor" + end + end + + # Returns the namespace of the attribute. + # + # e = Element.new( "elns:myelement" ) + # e.add_attribute( "nsa:a", "aval" ) + # e.add_attribute( "b", "bval" ) + # e.attributes.get_attribute( "a" ).prefix # -> "nsa" + # e.attributes.get_attribute( "b" ).prefix # -> "elns" + # a = Attribute.new( "x", "y" ) + # a.prefix # -> "" + def prefix + pf = super + if pf == "" + pf = @element.prefix if @element + end + pf + end + + # Returns the namespace URL, if defined, or nil otherwise + # + # e = Element.new("el") + # e.add_namespace("ns", "http://url") + # e.add_attribute("ns:a", "b") + # e.add_attribute("nsx:a", "c") + # e.attribute("ns:a").namespace # => "http://url" + # e.attribute("nsx:a").namespace # => nil + def namespace arg=nil + arg = prefix if arg.nil? + @element.namespace arg + end + + # Returns true if other is an Attribute and has the same name and value, + # false otherwise. + def ==( other ) + other.kind_of?(Attribute) and other.name==name and other.value==value + end + + # Creates (and returns) a hash from both the name and value + def hash + name.hash + value.hash + end + + # Returns this attribute out as XML source, expanding the name + # + # a = Attribute.new( "x", "y" ) + # a.to_string # -> "x='y'" + # b = Attribute.new( "ns:x", "y" ) + # b.to_string # -> "ns:x='y'" + def to_string + if @element and @element.context and @element.context[:attribute_quote] == :quote + %Q^#@expanded_name="#{to_s().gsub(/"/, '"')}"^ + else + "#@expanded_name='#{to_s().gsub(/'/, ''')}'" + end + end + + def doctype + if @element + doc = @element.document + doc.doctype if doc + end + end + + # Returns the attribute value, with entities replaced + def to_s + return @normalized if @normalized + + @normalized = Text::normalize( @unnormalized, doctype ) + @unnormalized = nil + @normalized + end + + # Returns the UNNORMALIZED value of this attribute. That is, entities + # have been expanded to their values + def value + return @unnormalized if @unnormalized + @unnormalized = Text::unnormalize( @normalized, doctype ) + @normalized = nil + @unnormalized + end + + # Returns a copy of this attribute + def clone + Attribute.new self + end + + # Sets the element of which this object is an attribute. Normally, this + # is not directly called. + # + # Returns this attribute + def element=( element ) + @element = element + + if @normalized + Text.check( @normalized, NEEDS_A_SECOND_CHECK, doctype ) + end + + self + end + + # Removes this Attribute from the tree, and returns true if successful + # + # This method is usually not called directly. + def remove + @element.attributes.delete self.name unless @element.nil? + end + + # Writes this attribute (EG, puts 'key="value"' to the output) + def write( output, indent=-1 ) + output << to_string + end + + def node_type + :attribute + end + + def inspect + rv = "" + write( rv ) + rv + end + + def xpath + path = @element.xpath + path += "/@#{self.expanded_name}" + return path + end + end +end +#vim:ts=2 sw=2 noexpandtab: diff --git a/gxg/deps/rexml/cdata.rb b/gxg/deps/rexml/cdata.rb new file mode 100644 index 0000000..24bf597 --- /dev/null +++ b/gxg/deps/rexml/cdata.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: false +require "deps/rexml/text" + +module REXML + class CData < Text + START = '' + ILLEGAL = /(\]\]>)/ + + # Constructor. CData is data between + # + # _Examples_ + # CData.new( source ) + # CData.new( "Here is some CDATA" ) + # CData.new( "Some unprocessed data", respect_whitespace_TF, parent_element ) + def initialize( first, whitespace=true, parent=nil ) + super( first, whitespace, parent, false, true, ILLEGAL ) + end + + # Make a copy of this object + # + # _Examples_ + # c = CData.new( "Some text" ) + # d = c.clone + # d.to_s # -> "Some text" + def clone + CData.new self + end + + # Returns the content of this CData object + # + # _Examples_ + # c = CData.new( "Some text" ) + # c.to_s # -> "Some text" + def to_s + @string + end + + def value + @string + end + + # == DEPRECATED + # See the rexml/formatters package + # + # Generates XML output of this object + # + # output:: + # Where to write the string. Defaults to $stdout + # indent:: + # The amount to indent this node by + # transitive:: + # Ignored + # ie_hack:: + # Ignored + # + # _Examples_ + # c = CData.new( " Some text " ) + # c.write( $stdout ) #-> + def write( output=$stdout, indent=-1, transitive=false, ie_hack=false ) + Kernel.warn( "#{self.class.name}.write is deprecated" ) + indent( output, indent ) + output << START + output << @string + output << STOP + end + end +end diff --git a/gxg/deps/rexml/child.rb b/gxg/deps/rexml/child.rb new file mode 100644 index 0000000..57013ff --- /dev/null +++ b/gxg/deps/rexml/child.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: false +require "deps/rexml/node" + +module REXML + ## + # A Child object is something contained by a parent, and this class + # contains methods to support that. Most user code will not use this + # class directly. + class Child + include Node + attr_reader :parent # The Parent of this object + + # Constructor. Any inheritors of this class should call super to make + # sure this method is called. + # parent:: + # if supplied, the parent of this child will be set to the + # supplied value, and self will be added to the parent + def initialize( parent = nil ) + @parent = nil + # Declare @parent, but don't define it. The next line sets the + # parent. + parent.add( self ) if parent + end + + # Replaces this object with another object. Basically, calls + # Parent.replace_child + # + # Returns:: self + def replace_with( child ) + @parent.replace_child( self, child ) + self + end + + # Removes this child from the parent. + # + # Returns:: self + def remove + unless @parent.nil? + @parent.delete self + end + self + end + + # Sets the parent of this child to the supplied argument. + # + # other:: + # Must be a Parent object. If this object is the same object as the + # existing parent of this child, no action is taken. Otherwise, this + # child is removed from the current parent (if one exists), and is added + # to the new parent. + # Returns:: The parent added + def parent=( other ) + return @parent if @parent == other + @parent.delete self if defined? @parent and @parent + @parent = other + end + + alias :next_sibling :next_sibling_node + alias :previous_sibling :previous_sibling_node + + # Sets the next sibling of this child. This can be used to insert a child + # after some other child. + # a = Element.new("a") + # b = a.add_element("b") + # c = Element.new("c") + # b.next_sibling = c + # # => + def next_sibling=( other ) + parent.insert_after self, other + end + + # Sets the previous sibling of this child. This can be used to insert a + # child before some other child. + # a = Element.new("a") + # b = a.add_element("b") + # c = Element.new("c") + # b.previous_sibling = c + # # => + def previous_sibling=(other) + parent.insert_before self, other + end + + # Returns:: the document this child belongs to, or nil if this child + # belongs to no document + def document + return parent.document unless parent.nil? + nil + end + + # This doesn't yet handle encodings + def bytes + document.encoding + + to_s + end + end +end diff --git a/gxg/deps/rexml/comment.rb b/gxg/deps/rexml/comment.rb new file mode 100644 index 0000000..3887193 --- /dev/null +++ b/gxg/deps/rexml/comment.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: false +require "deps/rexml/child" + +module REXML + ## + # Represents an XML comment; that is, text between \ + class Comment < Child + include Comparable + START = "" + + # The content text + + attr_accessor :string + + ## + # Constructor. The first argument can be one of three types: + # @param first If String, the contents of this comment are set to the + # argument. If Comment, the argument is duplicated. If + # Source, the argument is scanned for a comment. + # @param second If the first argument is a Source, this argument + # should be nil, not supplied, or a Parent to be set as the parent + # of this object + def initialize( first, second = nil ) + super(second) + if first.kind_of? String + @string = first + elsif first.kind_of? Comment + @string = first.string + end + end + + def clone + Comment.new self + end + + # == DEPRECATED + # See REXML::Formatters + # + # output:: + # Where to write the string + # indent:: + # An integer. If -1, no indenting will be used; otherwise, the + # indentation will be this number of spaces, and children will be + # indented an additional amount. + # transitive:: + # Ignored by this class. The contents of comments are never modified. + # ie_hack:: + # Needed for conformity to the child API, but not used by this class. + def write( output, indent=-1, transitive=false, ie_hack=false ) + Kernel.warn("Comment.write is deprecated. See REXML::Formatters") + indent( output, indent ) + output << START + output << @string + output << STOP + end + + alias :to_s :string + + ## + # Compares this Comment to another; the contents of the comment are used + # in the comparison. + def <=>(other) + other.to_s <=> @string + end + + ## + # Compares this Comment to another; the contents of the comment are used + # in the comparison. + def ==( other ) + other.kind_of? Comment and + (other <=> self) == 0 + end + + def node_type + :comment + end + end +end +#vim:ts=2 sw=2 noexpandtab: diff --git a/gxg/deps/rexml/doctype.rb b/gxg/deps/rexml/doctype.rb new file mode 100644 index 0000000..de965ed --- /dev/null +++ b/gxg/deps/rexml/doctype.rb @@ -0,0 +1,270 @@ +# frozen_string_literal: false +require "deps/rexml/parent" +require "deps/rexml/parseexception" +require "deps/rexml/namespace" +require 'deps/rexml/entity' +require 'deps/rexml/attlistdecl' +require 'deps/rexml/xmltokens' + +module REXML + # Represents an XML DOCTYPE declaration; that is, the contents of . DOCTYPES can be used to declare the DTD of a document, as well as + # being used to declare entities used in the document. + class DocType < Parent + include XMLTokens + START = "" + SYSTEM = "SYSTEM" + PUBLIC = "PUBLIC" + DEFAULT_ENTITIES = { + 'gt'=>EntityConst::GT, + 'lt'=>EntityConst::LT, + 'quot'=>EntityConst::QUOT, + "apos"=>EntityConst::APOS + } + + # name is the name of the doctype + # external_id is the referenced DTD, if given + attr_reader :name, :external_id, :entities, :namespaces + + # Constructor + # + # dt = DocType.new( 'foo', '-//I/Hate/External/IDs' ) + # # + # dt = DocType.new( doctype_to_clone ) + # # Incomplete. Shallow clone of doctype + # + # +Note+ that the constructor: + # + # Doctype.new( Source.new( "" ) ) + # + # is _deprecated_. Do not use it. It will probably disappear. + def initialize( first, parent=nil ) + @entities = DEFAULT_ENTITIES + @long_name = @uri = nil + if first.kind_of? String + super() + @name = first + @external_id = parent + elsif first.kind_of? DocType + super( parent ) + @name = first.name + @external_id = first.external_id + elsif first.kind_of? Array + super( parent ) + @name = first[0] + @external_id = first[1] + @long_name = first[2] + @uri = first[3] + elsif first.kind_of? Source + super( parent ) + parser = Parsers::BaseParser.new( first ) + event = parser.pull + if event[0] == :start_doctype + @name, @external_id, @long_name, @uri, = event[1..-1] + end + else + super() + end + end + + def node_type + :doctype + end + + def attributes_of element + rv = [] + each do |child| + child.each do |key,val| + rv << Attribute.new(key,val) + end if child.kind_of? AttlistDecl and child.element_name == element + end + rv + end + + def attribute_of element, attribute + att_decl = find do |child| + child.kind_of? AttlistDecl and + child.element_name == element and + child.include? attribute + end + return nil unless att_decl + att_decl[attribute] + end + + def clone + DocType.new self + end + + # output:: + # Where to write the string + # indent:: + # An integer. If -1, no indentation will be used; otherwise, the + # indentation will be this number of spaces, and children will be + # indented an additional amount. + # transitive:: + # Ignored + # ie_hack:: + # Ignored + def write( output, indent=0, transitive=false, ie_hack=false ) + f = REXML::Formatters::Default.new + indent( output, indent ) + output << START + output << ' ' + output << @name + output << " #@external_id" if @external_id + output << " #{@long_name.inspect}" if @long_name + output << " #{@uri.inspect}" if @uri + unless @children.empty? + output << ' [' + @children.each { |child| + output << "\n" + f.write( child, output ) + } + output << "\n]" + end + output << STOP + end + + def context + @parent.context + end + + def entity( name ) + @entities[name].unnormalized if @entities[name] + end + + def add child + super(child) + @entities = DEFAULT_ENTITIES.clone if @entities == DEFAULT_ENTITIES + @entities[ child.name ] = child if child.kind_of? Entity + end + + # This method retrieves the public identifier identifying the document's + # DTD. + # + # Method contributed by Henrik Martensson + def public + case @external_id + when "SYSTEM" + nil + when "PUBLIC" + strip_quotes(@long_name) + end + end + + # This method retrieves the system identifier identifying the document's DTD + # + # Method contributed by Henrik Martensson + def system + case @external_id + when "SYSTEM" + strip_quotes(@long_name) + when "PUBLIC" + @uri.kind_of?(String) ? strip_quotes(@uri) : nil + end + end + + # This method returns a list of notations that have been declared in the + # _internal_ DTD subset. Notations in the external DTD subset are not + # listed. + # + # Method contributed by Henrik Martensson + def notations + children().select {|node| node.kind_of?(REXML::NotationDecl)} + end + + # Retrieves a named notation. Only notations declared in the internal + # DTD subset can be retrieved. + # + # Method contributed by Henrik Martensson + def notation(name) + notations.find { |notation_decl| + notation_decl.name == name + } + end + + private + + # Method contributed by Henrik Martensson + def strip_quotes(quoted_string) + quoted_string =~ /^[\'\"].*[\'\"]$/ ? + quoted_string[1, quoted_string.length-2] : + quoted_string + end + end + + # We don't really handle any of these since we're not a validating + # parser, so we can be pretty dumb about them. All we need to be able + # to do is spew them back out on a write() + + # This is an abstract class. You never use this directly; it serves as a + # parent class for the specific declarations. + class Declaration < Child + def initialize src + super() + @string = src + end + + def to_s + @string+'>' + end + + # == DEPRECATED + # See REXML::Formatters + # + def write( output, indent ) + output << to_s + end + end + + public + class ElementDecl < Declaration + def initialize( src ) + super + end + end + + class ExternalEntity < Child + def initialize( src ) + super() + @entity = src + end + def to_s + @entity + end + def write( output, indent ) + output << @entity + end + end + + class NotationDecl < Child + attr_accessor :public, :system + def initialize name, middle, pub, sys + super(nil) + @name = name + @middle = middle + @public = pub + @system = sys + end + + def to_s + notation = "" + notation + end + + def write( output, indent=-1 ) + output << to_s + end + + # This method retrieves the name of the notation. + # + # Method contributed by Henrik Martensson + def name + @name + end + end +end diff --git a/gxg/deps/rexml/document.rb b/gxg/deps/rexml/document.rb new file mode 100644 index 0000000..0b939ea --- /dev/null +++ b/gxg/deps/rexml/document.rb @@ -0,0 +1,291 @@ +# frozen_string_literal: false +require "deps/rexml/security" +require "deps/rexml/element" +require "deps/rexml/xmldecl" +require "deps/rexml/source" +require "deps/rexml/comment" +require "deps/rexml/doctype" +require "deps/rexml/instruction" +require "deps/rexml/rexml" +require "deps/rexml/parseexception" +require "deps/rexml/output" +require "deps/rexml/parsers/baseparser" +require "deps/rexml/parsers/streamparser" +require "deps/rexml/parsers/treeparser" + +module REXML + # Represents a full XML document, including PIs, a doctype, etc. A + # Document has a single child that can be accessed by root(). + # Note that if you want to have an XML declaration written for a document + # you create, you must add one; REXML documents do not write a default + # declaration for you. See |DECLARATION| and |write|. + class Document < Element + # A convenient default XML declaration. If you want an XML declaration, + # the easiest way to add one is mydoc << Document::DECLARATION + # +DEPRECATED+ + # Use: mydoc << XMLDecl.default + DECLARATION = XMLDecl.default + + # Constructor + # @param source if supplied, must be a Document, String, or IO. + # Documents have their context and Element attributes cloned. + # Strings are expected to be valid XML documents. IOs are expected + # to be sources of valid XML documents. + # @param context if supplied, contains the context of the document; + # this should be a Hash. + def initialize( source = nil, context = {} ) + @entity_expansion_count = 0 + super() + @context = context + return if source.nil? + if source.kind_of? Document + @context = source.context + super source + else + build( source ) + end + end + + def node_type + :document + end + + # Should be obvious + def clone + Document.new self + end + + # According to the XML spec, a root node has no expanded name + def expanded_name + '' + #d = doc_type + #d ? d.name : "UNDEFINED" + end + + alias :name :expanded_name + + # We override this, because XMLDecls and DocTypes must go at the start + # of the document + def add( child ) + if child.kind_of? XMLDecl + if @children[0].kind_of? XMLDecl + @children[0] = child + else + @children.unshift child + end + child.parent = self + elsif child.kind_of? DocType + # Find first Element or DocType node and insert the decl right + # before it. If there is no such node, just insert the child at the + # end. If there is a child and it is an DocType, then replace it. + insert_before_index = @children.find_index { |x| + x.kind_of?(Element) || x.kind_of?(DocType) + } + if insert_before_index # Not null = not end of list + if @children[ insert_before_index ].kind_of? DocType + @children[ insert_before_index ] = child + else + @children[ insert_before_index-1, 0 ] = child + end + else # Insert at end of list + @children << child + end + child.parent = self + else + rv = super + raise "attempted adding second root element to document" if @elements.size > 1 + rv + end + end + alias :<< :add + + def add_element(arg=nil, arg2=nil) + rv = super + raise "attempted adding second root element to document" if @elements.size > 1 + rv + end + + # @return the root Element of the document, or nil if this document + # has no children. + def root + elements[1] + #self + #@children.find { |item| item.kind_of? Element } + end + + # @return the DocType child of the document, if one exists, + # and nil otherwise. + def doctype + @children.find { |item| item.kind_of? DocType } + end + + # @return the XMLDecl of this document; if no XMLDecl has been + # set, the default declaration is returned. + def xml_decl + rv = @children[0] + return rv if rv.kind_of? XMLDecl + @children.unshift(XMLDecl.default)[0] + end + + # @return the XMLDecl version of this document as a String. + # If no XMLDecl has been set, returns the default version. + def version + xml_decl().version + end + + # @return the XMLDecl encoding of this document as an + # Encoding object. + # If no XMLDecl has been set, returns the default encoding. + def encoding + xml_decl().encoding + end + + # @return the XMLDecl standalone value of this document as a String. + # If no XMLDecl has been set, returns the default setting. + def stand_alone? + xml_decl().stand_alone? + end + + # :call-seq: + # doc.write(output=$stdout, indent=-1, transtive=false, ie_hack=false, encoding=nil) + # doc.write(options={:output => $stdout, :indent => -1, :transtive => false, :ie_hack => false, :encoding => nil}) + # + # Write the XML tree out, optionally with indent. This writes out the + # entire XML document, including XML declarations, doctype declarations, + # and processing instructions (if any are given). + # + # A controversial point is whether Document should always write the XML + # declaration () whether or not one is given by the + # user (or source document). REXML does not write one if one was not + # specified, because it adds unnecessary bandwidth to applications such + # as XML-RPC. + # + # Accept Nth argument style and options Hash style as argument. + # The recommended style is options Hash style for one or more + # arguments case. + # + # _Examples_ + # Document.new("").write + # + # output = "" + # Document.new("").write(output) + # + # output = "" + # Document.new("").write(:output => output, :indent => 2) + # + # See also the classes in the rexml/formatters package for the proper way + # to change the default formatting of XML output. + # + # _Examples_ + # + # output = "" + # tr = Transitive.new + # tr.write(Document.new(""), output) + # + # output:: + # output an object which supports '<< string'; this is where the + # document will be written. + # indent:: + # An integer. If -1, no indenting will be used; otherwise, the + # indentation will be twice this number of spaces, and children will be + # indented an additional amount. For a value of 3, every item will be + # indented 3 more levels, or 6 more spaces (2 * 3). Defaults to -1 + # transitive:: + # If transitive is true and indent is >= 0, then the output will be + # pretty-printed in such a way that the added whitespace does not affect + # the absolute *value* of the document -- that is, it leaves the value + # and number of Text nodes in the document unchanged. + # ie_hack:: + # This hack inserts a space before the /> on empty tags to address + # a limitation of Internet Explorer. Defaults to false + # encoding:: + # Encoding name as String. Change output encoding to specified encoding + # instead of encoding in XML declaration. + # Defaults to nil. It means encoding in XML declaration is used. + def write(*arguments) + if arguments.size == 1 and arguments[0].class == Hash + options = arguments[0] + + output = options[:output] + indent = options[:indent] + transitive = options[:transitive] + ie_hack = options[:ie_hack] + encoding = options[:encoding] + else + output, indent, transitive, ie_hack, encoding, = *arguments + end + + output ||= $stdout + indent ||= -1 + transitive = false if transitive.nil? + ie_hack = false if ie_hack.nil? + encoding ||= xml_decl.encoding + + if encoding != 'UTF-8' && !output.kind_of?(Output) + output = Output.new( output, encoding ) + end + formatter = if indent > -1 + if transitive + require "deps/rexml/formatters/transitive" + REXML::Formatters::Transitive.new( indent, ie_hack ) + else + REXML::Formatters::Pretty.new( indent, ie_hack ) + end + else + REXML::Formatters::Default.new( ie_hack ) + end + formatter.write( self, output ) + end + + + def Document::parse_stream( source, listener ) + Parsers::StreamParser.new( source, listener ).parse + end + + # Set the entity expansion limit. By default the limit is set to 10000. + # + # Deprecated. Use REXML::Security.entity_expansion_limit= instead. + def Document::entity_expansion_limit=( val ) + Security.entity_expansion_limit = val + end + + # Get the entity expansion limit. By default the limit is set to 10000. + # + # Deprecated. Use REXML::Security.entity_expansion_limit= instead. + def Document::entity_expansion_limit + return Security.entity_expansion_limit + end + + # Set the entity expansion limit. By default the limit is set to 10240. + # + # Deprecated. Use REXML::Security.entity_expansion_text_limit= instead. + def Document::entity_expansion_text_limit=( val ) + Security.entity_expansion_text_limit = val + end + + # Get the entity expansion limit. By default the limit is set to 10240. + # + # Deprecated. Use REXML::Security.entity_expansion_text_limit instead. + def Document::entity_expansion_text_limit + return Security.entity_expansion_text_limit + end + + attr_reader :entity_expansion_count + + def record_entity_expansion + @entity_expansion_count += 1 + if @entity_expansion_count > Security.entity_expansion_limit + raise "number of entity expansions exceeded, processing aborted." + end + end + + def document + self + end + + private + def build( source ) + Parsers::TreeParser.new( source, self ).parse + end + end +end diff --git a/gxg/deps/rexml/dtd/attlistdecl.rb b/gxg/deps/rexml/dtd/attlistdecl.rb new file mode 100644 index 0000000..27213c7 --- /dev/null +++ b/gxg/deps/rexml/dtd/attlistdecl.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: false +require "deps/rexml/child" +module REXML + module DTD + class AttlistDecl < Child + START = ")/um + end + end +end diff --git a/gxg/deps/rexml/dtd/dtd.rb b/gxg/deps/rexml/dtd/dtd.rb new file mode 100644 index 0000000..2962a8c --- /dev/null +++ b/gxg/deps/rexml/dtd/dtd.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: false +require "deps/rexml/dtd/elementdecl" +require "deps/rexml/dtd/entitydecl" +require "deps/rexml/comment" +require "deps/rexml/dtd/notationdecl" +require "deps/rexml/dtd/attlistdecl" +require "deps/rexml/parent" + +module REXML + module DTD + class Parser + def Parser.parse( input ) + case input + when String + parse_helper input + when File + parse_helper input.read + end + end + + # Takes a String and parses it out + def Parser.parse_helper( input ) + contents = Parent.new + while input.size > 0 + case input + when ElementDecl.PATTERN_RE + match = $& + contents << ElementDecl.new( match ) + when AttlistDecl.PATTERN_RE + matchdata = $~ + contents << AttlistDecl.new( matchdata ) + when EntityDecl.PATTERN_RE + matchdata = $~ + contents << EntityDecl.new( matchdata ) + when Comment.PATTERN_RE + matchdata = $~ + contents << Comment.new( matchdata ) + when NotationDecl.PATTERN_RE + matchdata = $~ + contents << NotationDecl.new( matchdata ) + end + end + contents + end + end + end +end diff --git a/gxg/deps/rexml/dtd/elementdecl.rb b/gxg/deps/rexml/dtd/elementdecl.rb new file mode 100644 index 0000000..01ea13a --- /dev/null +++ b/gxg/deps/rexml/dtd/elementdecl.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: false +require "deps/rexml/child" +module REXML + module DTD + class ElementDecl < Child + START = "/um + PATTERN_RE = /^\s*#{START}\s+((?:[:\w][-\.\w]*:)?[-!\*\.\w]*)(.*?)>/ + #\s*((((["']).*?\5)|[^\/'">]*)*?)(\/)?>/um, true) + + def initialize match + @name = match[1] + @rest = match[2] + end + end + end +end diff --git a/gxg/deps/rexml/dtd/entitydecl.rb b/gxg/deps/rexml/dtd/entitydecl.rb new file mode 100644 index 0000000..450cd99 --- /dev/null +++ b/gxg/deps/rexml/dtd/entitydecl.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: false +require "deps/rexml/child" +module REXML + module DTD + class EntityDecl < Child + START = "/um + SYSTEM = /^\s*#{START}\s+(?:%\s+)?(\w+)\s+SYSTEM\s+((["']).*?\3)(?:\s+NDATA\s+\w+)?\s*>/um + PLAIN = /^\s*#{START}\s+(\w+)\s+((["']).*?\3)\s*>/um + PERCENT = /^\s*#{START}\s+%\s+(\w+)\s+((["']).*?\3)\s*>/um + # + # + def initialize src + super() + md = nil + if src.match( PUBLIC ) + md = src.match( PUBLIC, true ) + @middle = "PUBLIC" + @content = "#{md[2]} #{md[4]}" + elsif src.match( SYSTEM ) + md = src.match( SYSTEM, true ) + @middle = "SYSTEM" + @content = md[2] + elsif src.match( PLAIN ) + md = src.match( PLAIN, true ) + @middle = "" + @content = md[2] + elsif src.match( PERCENT ) + md = src.match( PERCENT, true ) + @middle = "" + @content = md[2] + end + raise ParseException.new("failed Entity match", src) if md.nil? + @name = md[1] + end + + def to_s + rv = " 0 + rv << @content + rv + end + + def write( output, indent ) + indent( output, indent ) + output << to_s + end + + def EntityDecl.parse_source source, listener + md = source.match( PATTERN_RE, true ) + thing = md[0].squeeze(" \t\n\r") + listener.send inspect.downcase, thing + end + end + end +end diff --git a/gxg/deps/rexml/dtd/notationdecl.rb b/gxg/deps/rexml/dtd/notationdecl.rb new file mode 100644 index 0000000..1a1ccf2 --- /dev/null +++ b/gxg/deps/rexml/dtd/notationdecl.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: false +require "deps/rexml/child" +module REXML + module DTD + class NotationDecl < Child + START = "/um + SYSTEM = /^\s*#{START}\s+(\w[\w-]*)\s+(SYSTEM)\s+((["']).*?\4)\s*>/um + def initialize src + super() + if src.match( PUBLIC ) + md = src.match( PUBLIC, true ) + elsif src.match( SYSTEM ) + md = src.match( SYSTEM, true ) + else + raise ParseException.new( "error parsing notation: no matching pattern", src ) + end + @name = md[1] + @middle = md[2] + @rest = md[3] + end + + def to_s + "" + end + + def write( output, indent ) + indent( output, indent ) + output << to_s + end + + def NotationDecl.parse_source source, listener + md = source.match( PATTERN_RE, true ) + thing = md[0].squeeze(" \t\n\r") + listener.send inspect.downcase, thing + end + end + end +end diff --git a/gxg/deps/rexml/element.rb b/gxg/deps/rexml/element.rb new file mode 100644 index 0000000..880c78f --- /dev/null +++ b/gxg/deps/rexml/element.rb @@ -0,0 +1,1265 @@ +# frozen_string_literal: false +require "deps/rexml/parent" +require "deps/rexml/namespace" +require "deps/rexml/attribute" +require "deps/rexml/cdata" +require "deps/rexml/xpath" +require "deps/rexml/parseexception" + +module REXML + # An implementation note about namespaces: + # As we parse, when we find namespaces we put them in a hash and assign + # them a unique ID. We then convert the namespace prefix for the node + # to the unique ID. This makes namespace lookup much faster for the + # cost of extra memory use. We save the namespace prefix for the + # context node and convert it back when we write it. + @@namespaces = {} + + # Represents a tagged XML element. Elements are characterized by + # having children, attributes, and names, and can themselves be + # children. + class Element < Parent + include Namespace + + UNDEFINED = "UNDEFINED"; # The default name + + # Mechanisms for accessing attributes and child elements of this + # element. + attr_reader :attributes, :elements + # The context holds information about the processing environment, such as + # whitespace handling. + attr_accessor :context + + # Constructor + # arg:: + # if not supplied, will be set to the default value. + # If a String, the name of this object will be set to the argument. + # If an Element, the object will be shallowly cloned; name, + # attributes, and namespaces will be copied. Children will +not+ be + # copied. + # parent:: + # if supplied, must be a Parent, and will be used as + # the parent of this object. + # context:: + # If supplied, must be a hash containing context items. Context items + # include: + # * :respect_whitespace the value of this is :+all+ or an array of + # strings being the names of the elements to respect + # whitespace for. Defaults to :+all+. + # * :compress_whitespace the value can be :+all+ or an array of + # strings being the names of the elements to ignore whitespace on. + # Overrides :+respect_whitespace+. + # * :ignore_whitespace_nodes the value can be :+all+ or an array + # of strings being the names of the elements in which to ignore + # whitespace-only nodes. If this is set, Text nodes which contain only + # whitespace will not be added to the document tree. + # * :raw can be :+all+, or an array of strings being the names of + # the elements to process in raw mode. In raw mode, special + # characters in text is not converted to or from entities. + def initialize( arg = UNDEFINED, parent=nil, context=nil ) + super(parent) + + @elements = Elements.new(self) + @attributes = Attributes.new(self) + @context = context + + if arg.kind_of? String + self.name = arg + elsif arg.kind_of? Element + self.name = arg.expanded_name + arg.attributes.each_attribute{ |attribute| + @attributes << Attribute.new( attribute ) + } + @context = arg.context + end + end + + def inspect + rv = "<#@expanded_name" + + @attributes.each_attribute do |attr| + rv << " " + attr.write( rv, 0 ) + end + + if children.size > 0 + rv << "> ... " + else + rv << "/>" + end + end + + + # Creates a shallow copy of self. + # d = Document.new "" + # new_a = d.root.clone + # puts new_a # => "" + def clone + self.class.new self + end + + # Evaluates to the root node of the document that this element + # belongs to. If this element doesn't belong to a document, but does + # belong to another Element, the parent's root will be returned, until the + # earliest ancestor is found. + # + # Note that this is not the same as the document element. + # In the following example, is the document element, and the root + # node is the parent node of the document element. You may ask yourself + # why the root node is useful: consider the doctype and XML declaration, + # and any processing instructions before the document element... they + # are children of the root node, or siblings of the document element. + # The only time this isn't true is when an Element is created that is + # not part of any Document. In this case, the ancestor that has no + # parent acts as the root node. + # d = Document.new '' + # a = d[1] ; c = a[1][1] + # d.root_node == d # TRUE + # a.root_node # namely, d + # c.root_node # again, d + def root_node + parent.nil? ? self : parent.root_node + end + + def root + return elements[1] if self.kind_of? Document + return self if parent.kind_of? Document or parent.nil? + return parent.root + end + + # Evaluates to the document to which this element belongs, or nil if this + # element doesn't belong to a document. + def document + rt = root + rt.parent if rt + end + + # Evaluates to +true+ if whitespace is respected for this element. This + # is the case if: + # 1. Neither :+respect_whitespace+ nor :+compress_whitespace+ has any value + # 2. The context has :+respect_whitespace+ set to :+all+ or + # an array containing the name of this element, and + # :+compress_whitespace+ isn't set to :+all+ or an array containing the + # name of this element. + # The evaluation is tested against +expanded_name+, and so is namespace + # sensitive. + def whitespace + @whitespace = nil + if @context + if @context[:respect_whitespace] + @whitespace = (@context[:respect_whitespace] == :all or + @context[:respect_whitespace].include? expanded_name) + end + @whitespace = false if (@context[:compress_whitespace] and + (@context[:compress_whitespace] == :all or + @context[:compress_whitespace].include? expanded_name) + ) + end + @whitespace = true unless @whitespace == false + @whitespace + end + + def ignore_whitespace_nodes + @ignore_whitespace_nodes = false + if @context + if @context[:ignore_whitespace_nodes] + @ignore_whitespace_nodes = + (@context[:ignore_whitespace_nodes] == :all or + @context[:ignore_whitespace_nodes].include? expanded_name) + end + end + end + + # Evaluates to +true+ if raw mode is set for this element. This + # is the case if the context has :+raw+ set to :+all+ or + # an array containing the name of this element. + # + # The evaluation is tested against +expanded_name+, and so is namespace + # sensitive. + def raw + @raw = (@context and @context[:raw] and + (@context[:raw] == :all or + @context[:raw].include? expanded_name)) + @raw + end + + #once :whitespace, :raw, :ignore_whitespace_nodes + + ################################################# + # Namespaces # + ################################################# + + # Evaluates to an +Array+ containing the prefixes (names) of all defined + # namespaces at this context node. + # doc = Document.new("") + # doc.elements['//b'].prefixes # -> ['x', 'y'] + def prefixes + prefixes = [] + prefixes = parent.prefixes if parent + prefixes |= attributes.prefixes + return prefixes + end + + def namespaces + namespaces = {} + namespaces = parent.namespaces if parent + namespaces = namespaces.merge( attributes.namespaces ) + return namespaces + end + + # Evaluates to the URI for a prefix, or the empty string if no such + # namespace is declared for this element. Evaluates recursively for + # ancestors. Returns the default namespace, if there is one. + # prefix:: + # the prefix to search for. If not supplied, returns the default + # namespace if one exists + # Returns:: + # the namespace URI as a String, or nil if no such namespace + # exists. If the namespace is undefined, returns an empty string + # doc = Document.new("") + # b = doc.elements['//b'] + # b.namespace # -> '1' + # b.namespace("y") # -> '2' + def namespace(prefix=nil) + if prefix.nil? + prefix = prefix() + end + if prefix == '' + prefix = "xmlns" + else + prefix = "xmlns:#{prefix}" unless prefix[0,5] == 'xmlns' + end + ns = attributes[ prefix ] + ns = parent.namespace(prefix) if ns.nil? and parent + ns = '' if ns.nil? and prefix == 'xmlns' + return ns + end + + # Adds a namespace to this element. + # prefix:: + # the prefix string, or the namespace URI if +uri+ is not + # supplied + # uri:: + # the namespace URI. May be nil, in which +prefix+ is used as + # the URI + # Evaluates to: this Element + # a = Element.new("a") + # a.add_namespace("xmlns:foo", "bar" ) + # a.add_namespace("foo", "bar") # shorthand for previous line + # a.add_namespace("twiddle") + # puts a #-> + def add_namespace( prefix, uri=nil ) + unless uri + @attributes["xmlns"] = prefix + else + prefix = "xmlns:#{prefix}" unless prefix =~ /^xmlns:/ + @attributes[ prefix ] = uri + end + self + end + + # Removes a namespace from this node. This only works if the namespace is + # actually declared in this node. If no argument is passed, deletes the + # default namespace. + # + # Evaluates to: this element + # doc = Document.new "" + # doc.root.delete_namespace + # puts doc # -> + # doc.root.delete_namespace 'foo' + # puts doc # -> + def delete_namespace namespace="xmlns" + namespace = "xmlns:#{namespace}" unless namespace == 'xmlns' + attribute = attributes.get_attribute(namespace) + attribute.remove unless attribute.nil? + self + end + + ################################################# + # Elements # + ################################################# + + # Adds a child to this element, optionally setting attributes in + # the element. + # element:: + # optional. If Element, the element is added. + # Otherwise, a new Element is constructed with the argument (see + # Element.initialize). + # attrs:: + # If supplied, must be a Hash containing String name,value + # pairs, which will be used to set the attributes of the new Element. + # Returns:: the Element that was added + # el = doc.add_element 'my-tag' + # el = doc.add_element 'my-tag', {'attr1'=>'val1', 'attr2'=>'val2'} + # el = Element.new 'my-tag' + # doc.add_element el + def add_element element, attrs=nil + raise "First argument must be either an element name, or an Element object" if element.nil? + el = @elements.add(element) + attrs.each do |key, value| + el.attributes[key]=value + end if attrs.kind_of? Hash + el + end + + # Deletes a child element. + # element:: + # Must be an +Element+, +String+, or +Integer+. If Element, + # the element is removed. If String, the element is found (via XPath) + # and removed. This means that any parent can remove any + # descendant. If Integer, the Element indexed by that number will be + # removed. + # Returns:: the element that was removed. + # doc.delete_element "/a/b/c[@id='4']" + # doc.delete_element doc.elements["//k"] + # doc.delete_element 1 + def delete_element element + @elements.delete element + end + + # Evaluates to +true+ if this element has at least one child Element + # doc = Document.new "Text" + # doc.root.has_elements # -> true + # doc.elements["/a/b"].has_elements # -> false + # doc.elements["/a/c"].has_elements # -> false + def has_elements? + !@elements.empty? + end + + # Iterates through the child elements, yielding for each Element that + # has a particular attribute set. + # key:: + # the name of the attribute to search for + # value:: + # the value of the attribute + # max:: + # (optional) causes this method to return after yielding + # for this number of matching children + # name:: + # (optional) if supplied, this is an XPath that filters + # the children to check. + # + # doc = Document.new "" + # # Yields b, c, d + # doc.root.each_element_with_attribute( 'id' ) {|e| p e} + # # Yields b, d + # doc.root.each_element_with_attribute( 'id', '1' ) {|e| p e} + # # Yields b + # doc.root.each_element_with_attribute( 'id', '1', 1 ) {|e| p e} + # # Yields d + # doc.root.each_element_with_attribute( 'id', '1', 0, 'd' ) {|e| p e} + def each_element_with_attribute( key, value=nil, max=0, name=nil, &block ) # :yields: Element + each_with_something( proc {|child| + if value.nil? + child.attributes[key] != nil + else + child.attributes[key]==value + end + }, max, name, &block ) + end + + # Iterates through the children, yielding for each Element that + # has a particular text set. + # text:: + # the text to search for. If nil, or not supplied, will iterate + # over all +Element+ children that contain at least one +Text+ node. + # max:: + # (optional) causes this method to return after yielding + # for this number of matching children + # name:: + # (optional) if supplied, this is an XPath that filters + # the children to check. + # + # doc = Document.new 'bbd' + # # Yields b, c, d + # doc.each_element_with_text {|e|p e} + # # Yields b, c + # doc.each_element_with_text('b'){|e|p e} + # # Yields b + # doc.each_element_with_text('b', 1){|e|p e} + # # Yields d + # doc.each_element_with_text(nil, 0, 'd'){|e|p e} + def each_element_with_text( text=nil, max=0, name=nil, &block ) # :yields: Element + each_with_something( proc {|child| + if text.nil? + child.has_text? + else + child.text == text + end + }, max, name, &block ) + end + + # Synonym for Element.elements.each + def each_element( xpath=nil, &block ) # :yields: Element + @elements.each( xpath, &block ) + end + + # Synonym for Element.to_a + # This is a little slower than calling elements.each directly. + # xpath:: any XPath by which to search for elements in the tree + # Returns:: an array of Elements that match the supplied path + def get_elements( xpath ) + @elements.to_a( xpath ) + end + + # Returns the next sibling that is an element, or nil if there is + # no Element sibling after this one + # doc = Document.new 'text' + # doc.root.elements['b'].next_element #-> + # doc.root.elements['c'].next_element #-> nil + def next_element + element = next_sibling + element = element.next_sibling until element.nil? or element.kind_of? Element + return element + end + + # Returns the previous sibling that is an element, or nil if there is + # no Element sibling prior to this one + # doc = Document.new 'text' + # doc.root.elements['c'].previous_element #-> + # doc.root.elements['b'].previous_element #-> nil + def previous_element + element = previous_sibling + element = element.previous_sibling until element.nil? or element.kind_of? Element + return element + end + + + ################################################# + # Text # + ################################################# + + # Evaluates to +true+ if this element has at least one Text child + def has_text? + not text().nil? + end + + # A convenience method which returns the String value of the _first_ + # child text element, if one exists, and +nil+ otherwise. + # + # Note that an element may have multiple Text elements, perhaps + # separated by other children. Be aware that this method only returns + # the first Text node. + # + # This method returns the +value+ of the first text child node, which + # ignores the +raw+ setting, so always returns normalized text. See + # the Text::value documentation. + # + # doc = Document.new "

some text this is bold! more text

" + # # The element 'p' has two text elements, "some text " and " more text". + # doc.root.text #-> "some text " + def text( path = nil ) + rv = get_text(path) + return rv.value unless rv.nil? + nil + end + + # Returns the first child Text node, if any, or +nil+ otherwise. + # This method returns the actual +Text+ node, rather than the String content. + # doc = Document.new "

some text this is bold! more text

" + # # The element 'p' has two text elements, "some text " and " more text". + # doc.root.get_text.value #-> "some text " + def get_text path = nil + rv = nil + if path + element = @elements[ path ] + rv = element.get_text unless element.nil? + else + rv = @children.find { |node| node.kind_of? Text } + end + return rv + end + + # Sets the first Text child of this object. See text() for a + # discussion about Text children. + # + # If a Text child already exists, the child is replaced by this + # content. This means that Text content can be deleted by calling + # this method with a nil argument. In this case, the next Text + # child becomes the first Text child. In no case is the order of + # any siblings disturbed. + # text:: + # If a String, a new Text child is created and added to + # this Element as the first Text child. If Text, the text is set + # as the first Child element. If nil, then any existing first Text + # child is removed. + # Returns:: this Element. + # doc = Document.new '' + # doc.root.text = 'Sean' #-> 'Sean' + # doc.root.text = 'Elliott' #-> 'Elliott' + # doc.root.add_element 'c' #-> 'Elliott' + # doc.root.text = 'Russell' #-> 'Russell' + # doc.root.text = nil #-> '' + def text=( text ) + if text.kind_of? String + text = Text.new( text, whitespace(), nil, raw() ) + elsif !text.nil? and !text.kind_of? Text + text = Text.new( text.to_s, whitespace(), nil, raw() ) + end + old_text = get_text + if text.nil? + old_text.remove unless old_text.nil? + else + if old_text.nil? + self << text + else + old_text.replace_with( text ) + end + end + return self + end + + # A helper method to add a Text child. Actual Text instances can + # be added with regular Parent methods, such as add() and <<() + # text:: + # if a String, a new Text instance is created and added + # to the parent. If Text, the object is added directly. + # Returns:: this Element + # e = Element.new('a') #-> + # e.add_text 'foo' #-> foo + # e.add_text Text.new(' bar') #-> foo bar + # Note that at the end of this example, the branch has 3 nodes; the 'e' + # element and 2 Text node children. + def add_text( text ) + if text.kind_of? String + if @children[-1].kind_of? Text + @children[-1] << text + return + end + text = Text.new( text, whitespace(), nil, raw() ) + end + self << text unless text.nil? + return self + end + + def node_type + :element + end + + def xpath + path_elements = [] + cur = self + path_elements << __to_xpath_helper( self ) + while cur.parent + cur = cur.parent + path_elements << __to_xpath_helper( cur ) + end + return path_elements.reverse.join( "/" ) + end + + ################################################# + # Attributes # + ################################################# + + # Fetches an attribute value or a child. + # + # If String or Symbol is specified, it's treated as attribute + # name. Attribute value as String or +nil+ is returned. This case + # is shortcut of +attributes[name]+. + # + # If Integer is specified, it's treated as the index of + # child. It returns Nth child. + # + # doc = REXML::Document.new("") + # doc.root["attr"] # => "1" + # doc.root.attributes["attr"] # => "1" + # doc.root[1] # => + def [](name_or_index) + case name_or_index + when String + attributes[name_or_index] + when Symbol + attributes[name_or_index.to_s] + else + super + end + end + + def attribute( name, namespace=nil ) + prefix = nil + if namespaces.respond_to? :key + prefix = namespaces.key(namespace) if namespace + else + prefix = namespaces.index(namespace) if namespace + end + prefix = nil if prefix == 'xmlns' + + ret_val = + attributes.get_attribute( "#{prefix ? prefix + ':' : ''}#{name}" ) + + return ret_val unless ret_val.nil? + return nil if prefix.nil? + + # now check that prefix'es namespace is not the same as the + # default namespace + return nil unless ( namespaces[ prefix ] == namespaces[ 'xmlns' ] ) + + attributes.get_attribute( name ) + + end + + # Evaluates to +true+ if this element has any attributes set, false + # otherwise. + def has_attributes? + return !@attributes.empty? + end + + # Adds an attribute to this element, overwriting any existing attribute + # by the same name. + # key:: + # can be either an Attribute or a String. If an Attribute, + # the attribute is added to the list of Element attributes. If String, + # the argument is used as the name of the new attribute, and the value + # parameter must be supplied. + # value:: + # Required if +key+ is a String, and ignored if the first argument is + # an Attribute. This is a String, and is used as the value + # of the new Attribute. This should be the unnormalized value of the + # attribute (without entities). + # Returns:: the Attribute added + # e = Element.new 'e' + # e.add_attribute( 'a', 'b' ) #-> + # e.add_attribute( 'x:a', 'c' ) #-> + # e.add_attribute Attribute.new('b', 'd') #-> + def add_attribute( key, value=nil ) + if key.kind_of? Attribute + @attributes << key + else + @attributes[key] = value + end + end + + # Add multiple attributes to this element. + # hash:: is either a hash, or array of arrays + # el.add_attributes( {"name1"=>"value1", "name2"=>"value2"} ) + # el.add_attributes( [ ["name1","value1"], ["name2"=>"value2"] ] ) + def add_attributes hash + if hash.kind_of? Hash + hash.each_pair {|key, value| @attributes[key] = value } + elsif hash.kind_of? Array + hash.each { |value| @attributes[ value[0] ] = value[1] } + end + end + + # Removes an attribute + # key:: + # either an Attribute or a String. In either case, the + # attribute is found by matching the attribute name to the argument, + # and then removed. If no attribute is found, no action is taken. + # Returns:: + # the attribute removed, or nil if this Element did not contain + # a matching attribute + # e = Element.new('E') + # e.add_attribute( 'name', 'Sean' ) #-> + # r = e.add_attribute( 'sur:name', 'Russell' ) #-> + # e.delete_attribute( 'name' ) #-> + # e.delete_attribute( r ) #-> + def delete_attribute(key) + attr = @attributes.get_attribute(key) + attr.remove unless attr.nil? + end + + ################################################# + # Other Utilities # + ################################################# + + # Get an array of all CData children. + # IMMUTABLE + def cdatas + find_all { |child| child.kind_of? CData }.freeze + end + + # Get an array of all Comment children. + # IMMUTABLE + def comments + find_all { |child| child.kind_of? Comment }.freeze + end + + # Get an array of all Instruction children. + # IMMUTABLE + def instructions + find_all { |child| child.kind_of? Instruction }.freeze + end + + # Get an array of all Text children. + # IMMUTABLE + def texts + find_all { |child| child.kind_of? Text }.freeze + end + + # == DEPRECATED + # See REXML::Formatters + # + # Writes out this element, and recursively, all children. + # output:: + # output an object which supports '<< string'; this is where the + # document will be written. + # indent:: + # An integer. If -1, no indenting will be used; otherwise, the + # indentation will be this number of spaces, and children will be + # indented an additional amount. Defaults to -1 + # transitive:: + # If transitive is true and indent is >= 0, then the output will be + # pretty-printed in such a way that the added whitespace does not affect + # the parse tree of the document + # ie_hack:: + # This hack inserts a space before the /> on empty tags to address + # a limitation of Internet Explorer. Defaults to false + # + # out = '' + # doc.write( out ) #-> doc is written to the string 'out' + # doc.write( $stdout ) #-> doc written to the console + def write(output=$stdout, indent=-1, transitive=false, ie_hack=false) + Kernel.warn("#{self.class.name}.write is deprecated. See REXML::Formatters") + formatter = if indent > -1 + if transitive + require "deps/rexml/formatters/transitive" + REXML::Formatters::Transitive.new( indent, ie_hack ) + else + REXML::Formatters::Pretty.new( indent, ie_hack ) + end + else + REXML::Formatters::Default.new( ie_hack ) + end + formatter.write( self, output ) + end + + + private + def __to_xpath_helper node + rv = node.expanded_name.clone + if node.parent + results = node.parent.find_all {|n| + n.kind_of?(REXML::Element) and n.expanded_name == node.expanded_name + } + if results.length > 1 + idx = results.index( node ) + rv << "[#{idx+1}]" + end + end + rv + end + + # A private helper method + def each_with_something( test, max=0, name=nil ) + num = 0 + @elements.each( name ){ |child| + yield child if test.call(child) and num += 1 + return if max>0 and num == max + } + end + end + + ######################################################################## + # ELEMENTS # + ######################################################################## + + # A class which provides filtering of children for Elements, and + # XPath search support. You are expected to only encounter this class as + # the element.elements object. Therefore, you are + # _not_ expected to instantiate this yourself. + class Elements + include Enumerable + # Constructor + # parent:: the parent Element + def initialize parent + @element = parent + end + + # Fetches a child element. Filters only Element children, regardless of + # the XPath match. + # index:: + # the search parameter. This is either an Integer, which + # will be used to find the index'th child Element, or an XPath, + # which will be used to search for the Element. Because + # of the nature of XPath searches, any element in the connected XML + # document can be fetched through any other element. The + # Integer index is 1-based, not 0-based. This means that the first + # child element is at index 1, not 0, and the +n+th element is at index + # +n+, not n-1. This is because XPath indexes element children + # starting from 1, not 0, and the indexes should be the same. + # name:: + # optional, and only used in the first argument is an + # Integer. In that case, the index'th child Element that has the + # supplied name will be returned. Note again that the indexes start at 1. + # Returns:: the first matching Element, or nil if no child matched + # doc = Document.new '' + # doc.root.elements[1] #-> + # doc.root.elements['c'] #-> + # doc.root.elements[2,'c'] #-> + def []( index, name=nil) + if index.kind_of? Integer + raise "index (#{index}) must be >= 1" if index < 1 + name = literalize(name) if name + num = 0 + @element.find { |child| + child.kind_of? Element and + (name.nil? ? true : child.has_name?( name )) and + (num += 1) == index + } + else + return XPath::first( @element, index ) + #{ |element| + # return element if element.kind_of? Element + #} + #return nil + end + end + + # Sets an element, replacing any previous matching element. If no + # existing element is found ,the element is added. + # index:: Used to find a matching element to replace. See [](). + # element:: + # The element to replace the existing element with + # the previous element + # Returns:: nil if no previous element was found. + # + # doc = Document.new '' + # doc.root.elements[10] = Element.new('b') #-> + # doc.root.elements[1] #-> + # doc.root.elements[1] = Element.new('c') #-> + # doc.root.elements['c'] = Element.new('d') #-> + def []=( index, element ) + previous = self[index] + if previous.nil? + @element.add element + else + previous.replace_with element + end + return previous + end + + # Returns +true+ if there are no +Element+ children, +false+ otherwise + def empty? + @element.find{ |child| child.kind_of? Element}.nil? + end + + # Returns the index of the supplied child (starting at 1), or -1 if + # the element is not a child + # element:: an +Element+ child + def index element + rv = 0 + found = @element.find do |child| + child.kind_of? Element and + (rv += 1) and + child == element + end + return rv if found == element + return -1 + end + + # Deletes a child Element + # element:: + # Either an Element, which is removed directly; an + # xpath, where the first matching child is removed; or an Integer, + # where the n'th Element is removed. + # Returns:: the removed child + # doc = Document.new '' + # b = doc.root.elements[1] + # doc.root.elements.delete b #-> + # doc.elements.delete("a/c[@id='1']") #-> + # doc.root.elements.delete 1 #-> + def delete element + if element.kind_of? Element + @element.delete element + else + el = self[element] + el.remove if el + end + end + + # Removes multiple elements. Filters for Element children, regardless of + # XPath matching. + # xpath:: all elements matching this String path are removed. + # Returns:: an Array of Elements that have been removed + # doc = Document.new '' + # deleted = doc.elements.delete_all 'a/c' #-> [, , , ] + def delete_all( xpath ) + rv = [] + XPath::each( @element, xpath) {|element| + rv << element if element.kind_of? Element + } + rv.each do |element| + @element.delete element + element.remove + end + return rv + end + + # Adds an element + # element:: + # if supplied, is either an Element, String, or + # Source (see Element.initialize). If not supplied or nil, a + # new, default Element will be constructed + # Returns:: the added Element + # a = Element.new('a') + # a.elements.add(Element.new('b')) #-> + # a.elements.add('c') #-> + def add element=nil + if element.nil? + Element.new("", self, @element.context) + elsif not element.kind_of?(Element) + Element.new(element, self, @element.context) + else + @element << element + element.context = @element.context + element + end + end + + alias :<< :add + + # Iterates through all of the child Elements, optionally filtering + # them by a given XPath + # xpath:: + # optional. If supplied, this is a String XPath, and is used to + # filter the children, so that only matching children are yielded. Note + # that XPaths are automatically filtered for Elements, so that + # non-Element children will not be yielded + # doc = Document.new 'sean' + # doc.root.elements.each {|e|p e} #-> Yields b, c, d, b, c, d elements + # doc.root.elements.each('b') {|e|p e} #-> Yields b, b elements + # doc.root.elements.each('child::node()') {|e|p e} + # #-> Yields , , , , , + # XPath.each(doc.root, 'child::node()', &block) + # #-> Yields , , , sean, , , + def each( xpath=nil ) + XPath::each( @element, xpath ) {|e| yield e if e.kind_of? Element } + end + + def collect( xpath=nil ) + collection = [] + XPath::each( @element, xpath ) {|e| + collection << yield(e) if e.kind_of?(Element) + } + collection + end + + def inject( xpath=nil, initial=nil ) + first = true + XPath::each( @element, xpath ) {|e| + if (e.kind_of? Element) + if (first and initial == nil) + initial = e + first = false + else + initial = yield( initial, e ) if e.kind_of? Element + end + end + } + initial + end + + # Returns the number of +Element+ children of the parent object. + # doc = Document.new 'seanelliottrussell' + # doc.root.size #-> 6, 3 element and 3 text nodes + # doc.root.elements.size #-> 3 + def size + count = 0 + @element.each {|child| count+=1 if child.kind_of? Element } + count + end + + # Returns an Array of Element children. An XPath may be supplied to + # filter the children. Only Element children are returned, even if the + # supplied XPath matches non-Element children. + # doc = Document.new 'seanelliott' + # doc.root.elements.to_a #-> [ , ] + # doc.root.elements.to_a("child::node()") #-> [ , ] + # XPath.match(doc.root, "child::node()") #-> [ sean, , elliott, ] + def to_a( xpath=nil ) + rv = XPath.match( @element, xpath ) + return rv.find_all{|e| e.kind_of? Element} if xpath + rv + end + + private + # Private helper class. Removes quotes from quoted strings + def literalize name + name = name[1..-2] if name[0] == ?' or name[0] == ?" #' + name + end + end + + ######################################################################## + # ATTRIBUTES # + ######################################################################## + + # A class that defines the set of Attributes of an Element and provides + # operations for accessing elements in that set. + class Attributes < Hash + # Constructor + # element:: the Element of which this is an Attribute + def initialize element + @element = element + end + + # Fetches an attribute value. If you want to get the Attribute itself, + # use get_attribute() + # name:: an XPath attribute name. Namespaces are relevant here. + # Returns:: + # the String value of the matching attribute, or +nil+ if no + # matching attribute was found. This is the unnormalized value + # (with entities expanded). + # + # doc = Document.new "" + # doc.root.attributes['att'] #-> '<' + # doc.root.attributes['bar:att'] #-> '2' + def [](name) + attr = get_attribute(name) + return attr.value unless attr.nil? + return nil + end + + def to_a + enum_for(:each_attribute).to_a + end + + # Returns the number of attributes the owning Element contains. + # doc = Document "" + # doc.root.attributes.length #-> 3 + def length + c = 0 + each_attribute { c+=1 } + c + end + alias :size :length + + # Iterates over the attributes of an Element. Yields actual Attribute + # nodes, not String values. + # + # doc = Document.new '' + # doc.root.attributes.each_attribute {|attr| + # p attr.expanded_name+" => "+attr.value + # } + def each_attribute # :yields: attribute + each_value do |val| + if val.kind_of? Attribute + yield val + else + val.each_value { |atr| yield atr } + end + end + end + + # Iterates over each attribute of an Element, yielding the expanded name + # and value as a pair of Strings. + # + # doc = Document.new '' + # doc.root.attributes.each {|name, value| p name+" => "+value } + def each + each_attribute do |attr| + yield [attr.expanded_name, attr.value] + end + end + + # Fetches an attribute + # name:: + # the name by which to search for the attribute. Can be a + # prefix:name namespace name. + # Returns:: The first matching attribute, or nil if there was none. This + # value is an Attribute node, not the String value of the attribute. + # doc = Document.new '' + # doc.root.attributes.get_attribute("foo").value #-> "2" + # doc.root.attributes.get_attribute("x:foo").value #-> "1" + def get_attribute( name ) + attr = fetch( name, nil ) + if attr.nil? + return nil if name.nil? + # Look for prefix + name =~ Namespace::NAMESPLIT + prefix, n = $1, $2 + if prefix + attr = fetch( n, nil ) + # check prefix + if attr == nil + elsif attr.kind_of? Attribute + return attr if prefix == attr.prefix + else + attr = attr[ prefix ] + return attr + end + end + element_document = @element.document + if element_document and element_document.doctype + expn = @element.expanded_name + expn = element_document.doctype.name if expn.size == 0 + attr_val = element_document.doctype.attribute_of(expn, name) + return Attribute.new( name, attr_val ) if attr_val + end + return nil + end + if attr.kind_of? Hash + attr = attr[ @element.prefix ] + end + return attr + end + + # Sets an attribute, overwriting any existing attribute value by the + # same name. Namespace is significant. + # name:: the name of the attribute + # value:: + # (optional) If supplied, the value of the attribute. If + # nil, any existing matching attribute is deleted. + # Returns:: + # Owning element + # doc = Document.new "" + # doc.root.attributes['y:foo'] = '2' + # doc.root.attributes['foo'] = '4' + # doc.root.attributes['x:foo'] = nil + def []=( name, value ) + if value.nil? # Delete the named attribute + attr = get_attribute(name) + delete attr + return + end + + unless value.kind_of? Attribute + if @element.document and @element.document.doctype + value = Text::normalize( value, @element.document.doctype ) + else + value = Text::normalize( value, nil ) + end + value = Attribute.new(name, value) + end + value.element = @element + old_attr = fetch(value.name, nil) + if old_attr.nil? + store(value.name, value) + elsif old_attr.kind_of? Hash + old_attr[value.prefix] = value + elsif old_attr.prefix != value.prefix + # Check for conflicting namespaces + raise ParseException.new( + "Namespace conflict in adding attribute \"#{value.name}\": "+ + "Prefix \"#{old_attr.prefix}\" = "+ + "\"#{@element.namespace(old_attr.prefix)}\" and prefix "+ + "\"#{value.prefix}\" = \"#{@element.namespace(value.prefix)}\"") if + value.prefix != "xmlns" and old_attr.prefix != "xmlns" and + @element.namespace( old_attr.prefix ) == + @element.namespace( value.prefix ) + store value.name, { old_attr.prefix => old_attr, + value.prefix => value } + else + store value.name, value + end + return @element + end + + # Returns an array of Strings containing all of the prefixes declared + # by this set of # attributes. The array does not include the default + # namespace declaration, if one exists. + # doc = Document.new("") + # prefixes = doc.root.attributes.prefixes #-> ['x', 'y'] + def prefixes + ns = [] + each_attribute do |attribute| + ns << attribute.name if attribute.prefix == 'xmlns' + end + if @element.document and @element.document.doctype + expn = @element.expanded_name + expn = @element.document.doctype.name if expn.size == 0 + @element.document.doctype.attributes_of(expn).each { + |attribute| + ns << attribute.name if attribute.prefix == 'xmlns' + } + end + ns + end + + def namespaces + namespaces = {} + each_attribute do |attribute| + namespaces[attribute.name] = attribute.value if attribute.prefix == 'xmlns' or attribute.name == 'xmlns' + end + if @element.document and @element.document.doctype + expn = @element.expanded_name + expn = @element.document.doctype.name if expn.size == 0 + @element.document.doctype.attributes_of(expn).each { + |attribute| + namespaces[attribute.name] = attribute.value if attribute.prefix == 'xmlns' or attribute.name == 'xmlns' + } + end + namespaces + end + + # Removes an attribute + # attribute:: + # either a String, which is the name of the attribute to remove -- + # namespaces are significant here -- or the attribute to remove. + # Returns:: the owning element + # doc = Document.new "" + # doc.root.attributes.delete 'foo' #-> " + # doc.root.attributes.delete 'x:foo' #-> " + # attr = doc.root.attributes.get_attribute('y:foo') + # doc.root.attributes.delete attr #-> " + def delete( attribute ) + name = nil + prefix = nil + if attribute.kind_of? Attribute + name = attribute.name + prefix = attribute.prefix + else + attribute =~ Namespace::NAMESPLIT + prefix, name = $1, $2 + prefix = '' unless prefix + end + old = fetch(name, nil) + if old.kind_of? Hash # the supplied attribute is one of many + old.delete(prefix) + if old.size == 1 + repl = nil + old.each_value{|v| repl = v} + store name, repl + end + elsif old.nil? + return @element + else # the supplied attribute is a top-level one + super(name) + end + @element + end + + # Adds an attribute, overriding any existing attribute by the + # same name. Namespaces are significant. + # attribute:: An Attribute + def add( attribute ) + self[attribute.name] = attribute + end + + alias :<< :add + + # Deletes all attributes matching a name. Namespaces are significant. + # name:: + # A String; all attributes that match this path will be removed + # Returns:: an Array of the Attributes that were removed + def delete_all( name ) + rv = [] + each_attribute { |attribute| + rv << attribute if attribute.expanded_name == name + } + rv.each{ |attr| attr.remove } + return rv + end + + # The +get_attribute_ns+ method retrieves a method by its namespace + # and name. Thus it is possible to reliably identify an attribute + # even if an XML processor has changed the prefix. + # + # Method contributed by Henrik Martensson + def get_attribute_ns(namespace, name) + result = nil + each_attribute() { |attribute| + if name == attribute.name && + namespace == attribute.namespace() && + ( !namespace.empty? || !attribute.fully_expanded_name.index(':') ) + # foo will match xmlns:foo, but only if foo isn't also an attribute + result = attribute if !result or !namespace.empty? or + !attribute.fully_expanded_name.index(':') + end + } + result + end + end +end diff --git a/gxg/deps/rexml/encoding.rb b/gxg/deps/rexml/encoding.rb new file mode 100644 index 0000000..da2d70d --- /dev/null +++ b/gxg/deps/rexml/encoding.rb @@ -0,0 +1,51 @@ +# coding: US-ASCII +# frozen_string_literal: false +module REXML + module Encoding + # ID ---> Encoding name + attr_reader :encoding + def encoding=(encoding) + encoding = encoding.name if encoding.is_a?(Encoding) + if encoding.is_a?(String) + original_encoding = encoding + encoding = find_encoding(encoding) + unless encoding + raise ArgumentError, "Bad encoding name #{original_encoding}" + end + end + return false if defined?(@encoding) and encoding == @encoding + if encoding + @encoding = encoding.upcase + else + @encoding = 'UTF-8' + end + true + end + + def encode(string) + string.encode(@encoding) + end + + def decode(string) + string.encode(::Encoding::UTF_8, @encoding) + end + + private + def find_encoding(name) + case name + when /\Ashift-jis\z/i + return "SHIFT_JIS" + when /\ACP-(\d+)\z/ + name = "CP#{$1}" + when /\AUTF-8\z/i + return name + end + begin + ::Encoding::Converter.search_convpath(name, 'UTF-8') + rescue ::Encoding::ConverterNotFoundError + return nil + end + name + end + end +end diff --git a/gxg/deps/rexml/entity.rb b/gxg/deps/rexml/entity.rb new file mode 100644 index 0000000..144b4c5 --- /dev/null +++ b/gxg/deps/rexml/entity.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: false +require 'deps/rexml/child' +require 'deps/rexml/source' +require 'deps/rexml/xmltokens' + +module REXML + # God, I hate DTDs. I really do. Why this idiot standard still + # plagues us is beyond me. + class Entity < Child + include XMLTokens + PUBIDCHAR = "\x20\x0D\x0Aa-zA-Z0-9\\-()+,./:=?;!*@$_%#" + SYSTEMLITERAL = %Q{((?:"[^"]*")|(?:'[^']*'))} + PUBIDLITERAL = %Q{("[#{PUBIDCHAR}']*"|'[#{PUBIDCHAR}]*')} + EXTERNALID = "(?:(?:(SYSTEM)\\s+#{SYSTEMLITERAL})|(?:(PUBLIC)\\s+#{PUBIDLITERAL}\\s+#{SYSTEMLITERAL}))" + NDATADECL = "\\s+NDATA\\s+#{NAME}" + PEREFERENCE = "%#{NAME};" + ENTITYVALUE = %Q{((?:"(?:[^%&"]|#{PEREFERENCE}|#{REFERENCE})*")|(?:'([^%&']|#{PEREFERENCE}|#{REFERENCE})*'))} + PEDEF = "(?:#{ENTITYVALUE}|#{EXTERNALID})" + ENTITYDEF = "(?:#{ENTITYVALUE}|(?:#{EXTERNALID}(#{NDATADECL})?))" + PEDECL = "" + GEDECL = "" + ENTITYDECL = /\s*(?:#{GEDECL})|(?:#{PEDECL})/um + + attr_reader :name, :external, :ref, :ndata, :pubid + + # Create a new entity. Simple entities can be constructed by passing a + # name, value to the constructor; this creates a generic, plain entity + # reference. For anything more complicated, you have to pass a Source to + # the constructor with the entity definition, or use the accessor methods. + # +WARNING+: There is no validation of entity state except when the entity + # is read from a stream. If you start poking around with the accessors, + # you can easily create a non-conformant Entity. The best thing to do is + # dump the stupid DTDs and use XMLSchema instead. + # + # e = Entity.new( 'amp', '&' ) + def initialize stream, value=nil, parent=nil, reference=false + super(parent) + @ndata = @pubid = @value = @external = nil + if stream.kind_of? Array + @name = stream[1] + if stream[-1] == '%' + @reference = true + stream.pop + else + @reference = false + end + if stream[2] =~ /SYSTEM|PUBLIC/ + @external = stream[2] + if @external == 'SYSTEM' + @ref = stream[3] + @ndata = stream[4] if stream.size == 5 + else + @pubid = stream[3] + @ref = stream[4] + end + else + @value = stream[2] + end + else + @reference = reference + @external = nil + @name = stream + @value = value + end + end + + # Evaluates whether the given string matches an entity definition, + # returning true if so, and false otherwise. + def Entity::matches? string + (ENTITYDECL =~ string) == 0 + end + + # Evaluates to the unnormalized value of this entity; that is, replacing + # all entities -- both %ent; and &ent; entities. This differs from + # +value()+ in that +value+ only replaces %ent; entities. + def unnormalized + document.record_entity_expansion unless document.nil? + v = value() + return nil if v.nil? + @unnormalized = Text::unnormalize(v, parent) + @unnormalized + end + + #once :unnormalized + + # Returns the value of this entity unprocessed -- raw. This is the + # normalized value; that is, with all %ent; and &ent; entities intact + def normalized + @value + end + + # Write out a fully formed, correct entity definition (assuming the Entity + # object itself is valid.) + # + # out:: + # An object implementing << to which the entity will be + # output + # indent:: + # *DEPRECATED* and ignored + def write out, indent=-1 + out << '' + end + + # Returns this entity as a string. See write(). + def to_s + rv = '' + write rv + rv + end + + PEREFERENCE_RE = /#{PEREFERENCE}/um + # Returns the value of this entity. At the moment, only internal entities + # are processed. If the value contains internal references (IE, + # %blah;), those are replaced with their values. IE, if the doctype + # contains: + # + # + # then: + # doctype.entity('yada').value #-> "nanoo bar nanoo" + def value + if @value + matches = @value.scan(PEREFERENCE_RE) + rv = @value.clone + if @parent + sum = 0 + matches.each do |entity_reference| + entity_value = @parent.entity( entity_reference[0] ) + if sum + entity_value.bytesize > Security.entity_expansion_text_limit + raise "entity expansion has grown too large" + else + sum += entity_value.bytesize + end + rv.gsub!( /%#{entity_reference.join};/um, entity_value ) + end + end + return rv + end + nil + end + end + + # This is a set of entity constants -- the ones defined in the XML + # specification. These are +gt+, +lt+, +amp+, +quot+ and +apos+. + # CAUTION: these entities does not have parent and document + module EntityConst + # +>+ + GT = Entity.new( 'gt', '>' ) + # +<+ + LT = Entity.new( 'lt', '<' ) + # +&+ + AMP = Entity.new( 'amp', '&' ) + # +"+ + QUOT = Entity.new( 'quot', '"' ) + # +'+ + APOS = Entity.new( 'apos', "'" ) + end +end diff --git a/gxg/deps/rexml/formatters/default.rb b/gxg/deps/rexml/formatters/default.rb new file mode 100644 index 0000000..b84759d --- /dev/null +++ b/gxg/deps/rexml/formatters/default.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: false +module REXML + module Formatters + class Default + # Prints out the XML document with no formatting -- except if id_hack is + # set. + # + # ie_hack:: + # If set to true, then inserts whitespace before the close of an empty + # tag, so that IE's bad XML parser doesn't choke. + def initialize( ie_hack=false ) + @ie_hack = ie_hack + end + + # Writes the node to some output. + # + # node:: + # The node to write + # output:: + # A class implementing <<. Pass in an Output object to + # change the output encoding. + def write( node, output ) + case node + + when Document + if node.xml_decl.encoding != 'UTF-8' && !output.kind_of?(Output) + output = Output.new( output, node.xml_decl.encoding ) + end + write_document( node, output ) + + when Element + write_element( node, output ) + + when Declaration, ElementDecl, NotationDecl, ExternalEntity, Entity, + Attribute, AttlistDecl + node.write( output,-1 ) + + when Instruction + write_instruction( node, output ) + + when DocType, XMLDecl + node.write( output ) + + when Comment + write_comment( node, output ) + + when CData + write_cdata( node, output ) + + when Text + write_text( node, output ) + + else + raise Exception.new("XML FORMATTING ERROR") + + end + end + + protected + def write_document( node, output ) + node.children.each { |child| write( child, output ) } + end + + def write_element( node, output ) + output << "<#{node.expanded_name}" + + node.attributes.to_a.map { |a| + Hash === a ? a.values : a + }.flatten.sort_by {|attr| attr.name}.each do |attr| + output << " " + attr.write( output ) + end unless node.attributes.empty? + + if node.children.empty? + output << " " if @ie_hack + output << "/" + else + output << ">" + node.children.each { |child| + write( child, output ) + } + output << "" + end + + def write_text( node, output ) + output << node.to_s() + end + + def write_comment( node, output ) + output << Comment::START + output << node.to_s + output << Comment::STOP + end + + def write_cdata( node, output ) + output << CData::START + output << node.to_s + output << CData::STOP + end + + def write_instruction( node, output ) + output << Instruction::START.sub(/\\/u, '') + output << node.target + output << ' ' + output << node.content + output << Instruction::STOP.sub(/\\/u, '') + end + end + end +end diff --git a/gxg/deps/rexml/formatters/pretty.rb b/gxg/deps/rexml/formatters/pretty.rb new file mode 100644 index 0000000..18aec42 --- /dev/null +++ b/gxg/deps/rexml/formatters/pretty.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: false +require 'deps/rexml/formatters/default' + +module REXML + module Formatters + # Pretty-prints an XML document. This destroys whitespace in text nodes + # and will insert carriage returns and indentations. + # + # TODO: Add an option to print attributes on new lines + class Pretty < Default + + # If compact is set to true, then the formatter will attempt to use as + # little space as possible + attr_accessor :compact + # The width of a page. Used for formatting text + attr_accessor :width + + # Create a new pretty printer. + # + # output:: + # An object implementing '<<(String)', to which the output will be written. + # indentation:: + # An integer greater than 0. The indentation of each level will be + # this number of spaces. If this is < 1, the behavior of this object + # is undefined. Defaults to 2. + # ie_hack:: + # If true, the printer will insert whitespace before closing empty + # tags, thereby allowing Internet Explorer's XML parser to + # function. Defaults to false. + def initialize( indentation=2, ie_hack=false ) + @indentation = indentation + @level = 0 + @ie_hack = ie_hack + @width = 80 + @compact = false + end + + protected + def write_element(node, output) + output << ' '*@level + output << "<#{node.expanded_name}" + + node.attributes.each_attribute do |attr| + output << " " + attr.write( output ) + end unless node.attributes.empty? + + if node.children.empty? + if @ie_hack + output << " " + end + output << "/" + else + output << ">" + # If compact and all children are text, and if the formatted output + # is less than the specified width, then try to print everything on + # one line + skip = false + if compact + if node.children.inject(true) {|s,c| s & c.kind_of?(Text)} + string = "" + old_level = @level + @level = 0 + node.children.each { |child| write( child, string ) } + @level = old_level + if string.length < @width + output << string + skip = true + end + end + end + unless skip + output << "\n" + @level += @indentation + node.children.each { |child| + next if child.kind_of?(Text) and child.to_s.strip.length == 0 + write( child, output ) + output << "\n" + } + @level -= @indentation + output << ' '*@level + end + output << "" + end + + def write_text( node, output ) + s = node.to_s() + s.gsub!(/\s/,' ') + s.squeeze!(" ") + s = wrap(s, @width - @level) + s = indent_text(s, @level, " ", true) + output << (' '*@level + s) + end + + def write_comment( node, output) + output << ' ' * @level + super + end + + def write_cdata( node, output) + output << ' ' * @level + super + end + + def write_document( node, output ) + # Ok, this is a bit odd. All XML documents have an XML declaration, + # but it may not write itself if the user didn't specifically add it, + # either through the API or in the input document. If it doesn't write + # itself, then we don't need a carriage return... which makes this + # logic more complex. + node.children.each { |child| + next if child == node.children[-1] and child.instance_of?(Text) + unless child == node.children[0] or child.instance_of?(Text) or + (child == node.children[1] and !node.children[0].writethis) + output << "\n" + end + write( child, output ) + } + end + + private + def indent_text(string, level=1, style="\t", indentfirstline=true) + return string if level < 0 + string.gsub(/\n/, "\n#{style*level}") + end + + def wrap(string, width) + parts = [] + while string.length > width and place = string.rindex(' ', width) + parts << string[0...place] + string = string[place+1..-1] + end + parts << string + parts.join("\n") + end + + end + end +end + diff --git a/gxg/deps/rexml/formatters/transitive.rb b/gxg/deps/rexml/formatters/transitive.rb new file mode 100644 index 0000000..b2b271e --- /dev/null +++ b/gxg/deps/rexml/formatters/transitive.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: false +require 'deps/rexml/formatters/pretty' + +module REXML + module Formatters + # The Transitive formatter writes an XML document that parses to an + # identical document as the source document. This means that no extra + # whitespace nodes are inserted, and whitespace within text nodes is + # preserved. Within these constraints, the document is pretty-printed, + # with whitespace inserted into the metadata to introduce formatting. + # + # Note that this is only useful if the original XML is not already + # formatted. Since this formatter does not alter whitespace nodes, the + # results of formatting already formatted XML will be odd. + class Transitive < Default + def initialize( indentation=2, ie_hack=false ) + @indentation = indentation + @level = 0 + @ie_hack = ie_hack + end + + protected + def write_element( node, output ) + output << "<#{node.expanded_name}" + + node.attributes.each_attribute do |attr| + output << " " + attr.write( output ) + end unless node.attributes.empty? + + output << "\n" + output << ' '*@level + if node.children.empty? + output << " " if @ie_hack + output << "/" + else + output << ">" + # If compact and all children are text, and if the formatted output + # is less than the specified width, then try to print everything on + # one line + @level += @indentation + node.children.each { |child| + write( child, output ) + } + @level -= @indentation + output << "" + end + + def write_text( node, output ) + output << node.to_s() + end + end + end +end diff --git a/gxg/deps/rexml/functions.rb b/gxg/deps/rexml/functions.rb new file mode 100644 index 0000000..cd879fd --- /dev/null +++ b/gxg/deps/rexml/functions.rb @@ -0,0 +1,421 @@ +# frozen_string_literal: false +module REXML + # If you add a method, keep in mind two things: + # (1) the first argument will always be a list of nodes from which to + # filter. In the case of context methods (such as position), the function + # should return an array with a value for each child in the array. + # (2) all method calls from XML will have "-" replaced with "_". + # Therefore, in XML, "local-name()" is identical (and actually becomes) + # "local_name()" + module Functions + @@available_functions = {} + @@context = nil + @@namespace_context = {} + @@variables = {} + + INTERNAL_METHODS = [ + :namespace_context, + :namespace_context=, + :variables, + :variables=, + :context=, + :get_namespace, + :send, + ] + class << self + def singleton_method_added(name) + unless INTERNAL_METHODS.include?(name) + @@available_functions[name] = true + end + end + end + + def Functions::namespace_context=(x) ; @@namespace_context=x ; end + def Functions::variables=(x) ; @@variables=x ; end + def Functions::namespace_context ; @@namespace_context ; end + def Functions::variables ; @@variables ; end + + def Functions::context=(value); @@context = value; end + + def Functions::text( ) + if @@context[:node].node_type == :element + return @@context[:node].find_all{|n| n.node_type == :text}.collect{|n| n.value} + elsif @@context[:node].node_type == :text + return @@context[:node].value + else + return false + end + end + + # Returns the last node of the given list of nodes. + def Functions::last( ) + @@context[:size] + end + + def Functions::position( ) + @@context[:index] + end + + # Returns the size of the given list of nodes. + def Functions::count( node_set ) + node_set.size + end + + # Since REXML is non-validating, this method is not implemented as it + # requires a DTD + def Functions::id( object ) + end + + # UNTESTED + def Functions::local_name( node_set=nil ) + get_namespace( node_set ) do |node| + return node.local_name + end + end + + def Functions::namespace_uri( node_set=nil ) + get_namespace( node_set ) {|node| node.namespace} + end + + def Functions::name( node_set=nil ) + get_namespace( node_set ) do |node| + node.expanded_name + end + end + + # Helper method. + def Functions::get_namespace( node_set = nil ) + if node_set == nil + yield @@context[:node] if defined? @@context[:node].namespace + else + if node_set.respond_to? :each + node_set.each { |node| yield node if defined? node.namespace } + elsif node_set.respond_to? :namespace + yield node_set + end + end + end + + # A node-set is converted to a string by returning the string-value of the + # node in the node-set that is first in document order. If the node-set is + # empty, an empty string is returned. + # + # A number is converted to a string as follows + # + # NaN is converted to the string NaN + # + # positive zero is converted to the string 0 + # + # negative zero is converted to the string 0 + # + # positive infinity is converted to the string Infinity + # + # negative infinity is converted to the string -Infinity + # + # if the number is an integer, the number is represented in decimal form + # as a Number with no decimal point and no leading zeros, preceded by a + # minus sign (-) if the number is negative + # + # otherwise, the number is represented in decimal form as a Number + # including a decimal point with at least one digit before the decimal + # point and at least one digit after the decimal point, preceded by a + # minus sign (-) if the number is negative; there must be no leading zeros + # before the decimal point apart possibly from the one required digit + # immediately before the decimal point; beyond the one required digit + # after the decimal point there must be as many, but only as many, more + # digits as are needed to uniquely distinguish the number from all other + # IEEE 754 numeric values. + # + # The boolean false value is converted to the string false. The boolean + # true value is converted to the string true. + # + # An object of a type other than the four basic types is converted to a + # string in a way that is dependent on that type. + def Functions::string( object=nil ) + #object = @context unless object + if object.instance_of? Array + string( object[0] ) + elsif defined? object.node_type + if object.node_type == :attribute + object.value + elsif object.node_type == :element || object.node_type == :document + string_value(object) + else + object.to_s + end + elsif object.nil? + return "" + else + object.to_s + end + end + + # A node-set is converted to a string by + # returning the concatenation of the string-value + # of each of the children of the node in the + # node-set that is first in document order. + # If the node-set is empty, an empty string is returned. + def Functions::string_value( o ) + rv = "" + o.children.each { |e| + if e.node_type == :text + rv << e.to_s + elsif e.node_type == :element + rv << string_value( e ) + end + } + rv + end + + # UNTESTED + def Functions::concat( *objects ) + objects.join + end + + # Fixed by Mike Stok + def Functions::starts_with( string, test ) + string(string).index(string(test)) == 0 + end + + # Fixed by Mike Stok + def Functions::contains( string, test ) + string(string).include?(string(test)) + end + + # Kouhei fixed this + def Functions::substring_before( string, test ) + ruby_string = string(string) + ruby_index = ruby_string.index(string(test)) + if ruby_index.nil? + "" + else + ruby_string[ 0...ruby_index ] + end + end + + # Kouhei fixed this too + def Functions::substring_after( string, test ) + ruby_string = string(string) + return $1 if ruby_string =~ /#{test}(.*)/ + "" + end + + # Take equal portions of Mike Stok and Sean Russell; mix + # vigorously, and pour into a tall, chilled glass. Serves 10,000. + def Functions::substring( string, start, length=nil ) + ruby_string = string(string) + ruby_length = if length.nil? + ruby_string.length.to_f + else + number(length) + end + ruby_start = number(start) + + # Handle the special cases + return '' if ( + ruby_length.nan? or + ruby_start.nan? or + ruby_start.infinite? + ) + + infinite_length = ruby_length.infinite? == 1 + ruby_length = ruby_string.length if infinite_length + + # Now, get the bounds. The XPath bounds are 1..length; the ruby bounds + # are 0..length. Therefore, we have to offset the bounds by one. + ruby_start = round(ruby_start) - 1 + ruby_length = round(ruby_length) + + if ruby_start < 0 + ruby_length += ruby_start unless infinite_length + ruby_start = 0 + end + return '' if ruby_length <= 0 + ruby_string[ruby_start,ruby_length] + end + + # UNTESTED + def Functions::string_length( string ) + string(string).length + end + + # UNTESTED + def Functions::normalize_space( string=nil ) + string = string(@@context[:node]) if string.nil? + if string.kind_of? Array + string.collect{|x| string.to_s.strip.gsub(/\s+/um, ' ') if string} + else + string.to_s.strip.gsub(/\s+/um, ' ') + end + end + + # This is entirely Mike Stok's beast + def Functions::translate( string, tr1, tr2 ) + from = string(tr1) + to = string(tr2) + + # the map is our translation table. + # + # if a character occurs more than once in the + # from string then we ignore the second & + # subsequent mappings + # + # if a character maps to nil then we delete it + # in the output. This happens if the from + # string is longer than the to string + # + # there's nothing about - or ^ being special in + # http://www.w3.org/TR/xpath#function-translate + # so we don't build ranges or negated classes + + map = Hash.new + 0.upto(from.length - 1) { |pos| + from_char = from[pos] + unless map.has_key? from_char + map[from_char] = + if pos < to.length + to[pos] + else + nil + end + end + } + + if ''.respond_to? :chars + string(string).chars.collect { |c| + if map.has_key? c then map[c] else c end + }.compact.join + else + string(string).unpack('U*').collect { |c| + if map.has_key? c then map[c] else c end + }.compact.pack('U*') + end + end + + # UNTESTED + def Functions::boolean( object=nil ) + if object.kind_of? String + if object =~ /\d+/u + return object.to_f != 0 + else + return object.size > 0 + end + elsif object.kind_of? Array + object = object.find{|x| x and true} + end + return object ? true : false + end + + # UNTESTED + def Functions::not( object ) + not boolean( object ) + end + + # UNTESTED + def Functions::true( ) + true + end + + # UNTESTED + def Functions::false( ) + false + end + + # UNTESTED + def Functions::lang( language ) + lang = false + node = @@context[:node] + attr = nil + until node.nil? + if node.node_type == :element + attr = node.attributes["xml:lang"] + unless attr.nil? + lang = compare_language(string(language), attr) + break + else + end + end + node = node.parent + end + lang + end + + def Functions::compare_language lang1, lang2 + lang2.downcase.index(lang1.downcase) == 0 + end + + # a string that consists of optional whitespace followed by an optional + # minus sign followed by a Number followed by whitespace is converted to + # the IEEE 754 number that is nearest (according to the IEEE 754 + # round-to-nearest rule) to the mathematical value represented by the + # string; any other string is converted to NaN + # + # boolean true is converted to 1; boolean false is converted to 0 + # + # a node-set is first converted to a string as if by a call to the string + # function and then converted in the same way as a string argument + # + # an object of a type other than the four basic types is converted to a + # number in a way that is dependent on that type + def Functions::number( object=nil ) + object = @@context[:node] unless object + case object + when true + Float(1) + when false + Float(0) + when Array + number(string( object )) + when Numeric + object.to_f + else + str = string( object ) + # If XPath ever gets scientific notation... + #if str =~ /^\s*-?(\d*\.?\d+|\d+\.)([Ee]\d*)?\s*$/ + if str =~ /^\s*-?(\d*\.?\d+|\d+\.)\s*$/ + str.to_f + else + (0.0 / 0.0) + end + end + end + + def Functions::sum( nodes ) + nodes = [nodes] unless nodes.kind_of? Array + nodes.inject(0) { |r,n| r + number(string(n)) } + end + + def Functions::floor( number ) + number(number).floor + end + + def Functions::ceiling( number ) + number(number).ceil + end + + def Functions::round( number ) + number = number(number) + begin + neg = number.negative? + number = number.abs.round(half: :up) + neg ? -number : number + rescue FloatDomainError + number + end + end + + def Functions::processing_instruction( node ) + node.node_type == :processing_instruction + end + + def Functions::send(name, *args) + if @@available_functions[name.to_sym] + super + else + # TODO: Maybe, this is not XPath spec behavior. + # This behavior must be reconsidered. + XPath.match(@@context[:node], name.to_s) + end + end + end +end diff --git a/gxg/deps/rexml/instruction.rb b/gxg/deps/rexml/instruction.rb new file mode 100644 index 0000000..f1da46e --- /dev/null +++ b/gxg/deps/rexml/instruction.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: false +require "deps/rexml/child" +require "deps/rexml/source" + +module REXML + # Represents an XML Instruction; IE, + # TODO: Add parent arg (3rd arg) to constructor + class Instruction < Child + START = '<\?' + STOP = '\?>' + + # target is the "name" of the Instruction; IE, the "tag" in + # content is everything else. + attr_accessor :target, :content + + # Constructs a new Instruction + # @param target can be one of a number of things. If String, then + # the target of this instruction is set to this. If an Instruction, + # then the Instruction is shallowly cloned (target and content are + # copied). If a Source, then the source is scanned and parsed for + # an Instruction declaration. + # @param content Must be either a String, or a Parent. Can only + # be a Parent if the target argument is a Source. Otherwise, this + # String is set as the content of this instruction. + def initialize(target, content=nil) + if target.kind_of? String + super() + @target = target + @content = content + elsif target.kind_of? Instruction + super(content) + @target = target.target + @content = target.content + end + @content.strip! if @content + end + + def clone + Instruction.new self + end + + # == DEPRECATED + # See the rexml/formatters package + # + def write writer, indent=-1, transitive=false, ie_hack=false + Kernel.warn( "#{self.class.name}.write is deprecated" ) + indent(writer, indent) + writer << START.sub(/\\/u, '') + writer << @target + writer << ' ' + writer << @content + writer << STOP.sub(/\\/u, '') + end + + # @return true if other is an Instruction, and the content and target + # of the other matches the target and content of this object. + def ==( other ) + other.kind_of? Instruction and + other.target == @target and + other.content == @content + end + + def node_type + :processing_instruction + end + + def inspect + "" + end + end +end diff --git a/gxg/deps/rexml/light/node.rb b/gxg/deps/rexml/light/node.rb new file mode 100644 index 0000000..61f2584 --- /dev/null +++ b/gxg/deps/rexml/light/node.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: false +require 'deps/rexml/xmltokens' + +# [ :element, parent, name, attributes, children* ] + # a = Node.new + # a << "B" # => B + # a.b # => B + # a.b[1] # => B + # a.b[1]["x"] = "y" # => B + # a.b[0].c # => B + # a.b.c << "D" # => BD +module REXML + module Light + # Represents a tagged XML element. Elements are characterized by + # having children, attributes, and names, and can themselves be + # children. + class Node + NAMESPLIT = /^(?:(#{XMLTokens::NCNAME_STR}):)?(#{XMLTokens::NCNAME_STR})/u + PARENTS = [ :element, :document, :doctype ] + # Create a new element. + def initialize node=nil + @node = node + if node.kind_of? String + node = [ :text, node ] + elsif node.nil? + node = [ :document, nil, nil ] + elsif node[0] == :start_element + node[0] = :element + elsif node[0] == :start_doctype + node[0] = :doctype + elsif node[0] == :start_document + node[0] = :document + end + end + + def size + if PARENTS.include? @node[0] + @node[-1].size + else + 0 + end + end + + def each + size.times { |x| yield( at(x+4) ) } + end + + def name + at(2) + end + + def name=( name_str, ns=nil ) + pfx = '' + pfx = "#{prefix(ns)}:" if ns + _old_put(2, "#{pfx}#{name_str}") + end + + def parent=( node ) + _old_put(1,node) + end + + def local_name + namesplit + @name + end + + def local_name=( name_str ) + _old_put( 1, "#@prefix:#{name_str}" ) + end + + def prefix( namespace=nil ) + prefix_of( self, namespace ) + end + + def namespace( prefix=prefix() ) + namespace_of( self, prefix ) + end + + def namespace=( namespace ) + @prefix = prefix( namespace ) + pfx = '' + pfx = "#@prefix:" if @prefix.size > 0 + _old_put(1, "#{pfx}#@name") + end + + def []( reference, ns=nil ) + if reference.kind_of? String + pfx = '' + pfx = "#{prefix(ns)}:" if ns + at(3)["#{pfx}#{reference}"] + elsif reference.kind_of? Range + _old_get( Range.new(4+reference.begin, reference.end, reference.exclude_end?) ) + else + _old_get( 4+reference ) + end + end + + def =~( path ) + XPath.match( self, path ) + end + + # Doesn't handle namespaces yet + def []=( reference, ns, value=nil ) + if reference.kind_of? String + value = ns unless value + at( 3 )[reference] = value + elsif reference.kind_of? Range + _old_put( Range.new(3+reference.begin, reference.end, reference.exclude_end?), ns ) + else + if value + _old_put( 4+reference, ns, value ) + else + _old_put( 4+reference, ns ) + end + end + end + + # Append a child to this element, optionally under a provided namespace. + # The namespace argument is ignored if the element argument is an Element + # object. Otherwise, the element argument is a string, the namespace (if + # provided) is the namespace the element is created in. + def << element + if node_type() == :text + at(-1) << element + else + newnode = Node.new( element ) + newnode.parent = self + self.push( newnode ) + end + at(-1) + end + + def node_type + _old_get(0) + end + + def text=( foo ) + replace = at(4).kind_of?(String)? 1 : 0 + self._old_put(4,replace, normalizefoo) + end + + def root + context = self + context = context.at(1) while context.at(1) + end + + def has_name?( name, namespace = '' ) + at(3) == name and namespace() == namespace + end + + def children + self + end + + def parent + at(1) + end + + def to_s + + end + + private + + def namesplit + return if @name.defined? + at(2) =~ NAMESPLIT + @prefix = '' || $1 + @name = $2 + end + + def namespace_of( node, prefix=nil ) + if not prefix + name = at(2) + name =~ NAMESPLIT + prefix = $1 + end + to_find = 'xmlns' + to_find = "xmlns:#{prefix}" if not prefix.nil? + ns = at(3)[ to_find ] + ns ? ns : namespace_of( @node[0], prefix ) + end + + def prefix_of( node, namespace=nil ) + if not namespace + name = node.name + name =~ NAMESPLIT + $1 + else + ns = at(3).find { |k,v| v == namespace } + ns ? ns : prefix_of( node.parent, namespace ) + end + end + end + end +end diff --git a/gxg/deps/rexml/namespace.rb b/gxg/deps/rexml/namespace.rb new file mode 100644 index 0000000..4ae4099 --- /dev/null +++ b/gxg/deps/rexml/namespace.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: false +require 'deps/rexml/xmltokens' + +module REXML + # Adds named attributes to an object. + module Namespace + # The name of the object, valid if set + attr_reader :name, :expanded_name + # The expanded name of the object, valid if name is set + attr_accessor :prefix + include XMLTokens + NAMESPLIT = /^(?:(#{NCNAME_STR}):)?(#{NCNAME_STR})/u + + # Sets the name and the expanded name + def name=( name ) + @expanded_name = name + name =~ NAMESPLIT + if $1 + @prefix = $1 + else + @prefix = "" + @namespace = "" + end + @name = $2 + end + + # Compares names optionally WITH namespaces + def has_name?( other, ns=nil ) + if ns + return (namespace() == ns and name() == other) + elsif other.include? ":" + return fully_expanded_name == other + else + return name == other + end + end + + alias :local_name :name + + # Fully expand the name, even if the prefix wasn't specified in the + # source file. + def fully_expanded_name + ns = prefix + return "#{ns}:#@name" if ns.size > 0 + return @name + end + end +end diff --git a/gxg/deps/rexml/node.rb b/gxg/deps/rexml/node.rb new file mode 100644 index 0000000..ae394f0 --- /dev/null +++ b/gxg/deps/rexml/node.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: false +require "deps/rexml/parseexception" +require "deps/rexml/formatters/pretty" +require "deps/rexml/formatters/default" + +module REXML + # Represents a node in the tree. Nodes are never encountered except as + # superclasses of other objects. Nodes have siblings. + module Node + # @return the next sibling (nil if unset) + def next_sibling_node + return nil if @parent.nil? + @parent[ @parent.index(self) + 1 ] + end + + # @return the previous sibling (nil if unset) + def previous_sibling_node + return nil if @parent.nil? + ind = @parent.index(self) + return nil if ind == 0 + @parent[ ind - 1 ] + end + + # indent:: + # *DEPRECATED* This parameter is now ignored. See the formatters in the + # REXML::Formatters package for changing the output style. + def to_s indent=nil + unless indent.nil? + Kernel.warn( "#{self.class.name}.to_s(indent) parameter is deprecated" ) + f = REXML::Formatters::Pretty.new( indent ) + f.write( self, rv = "" ) + else + f = REXML::Formatters::Default.new + f.write( self, rv = "" ) + end + return rv + end + + def indent to, ind + if @parent and @parent.context and not @parent.context[:indentstyle].nil? then + indentstyle = @parent.context[:indentstyle] + else + indentstyle = ' ' + end + to << indentstyle*ind unless ind<1 + end + + def parent? + false; + end + + + # Visit all subnodes of +self+ recursively + def each_recursive(&block) # :yields: node + self.elements.each {|node| + block.call(node) + node.each_recursive(&block) + } + end + + # Find (and return) first subnode (recursively) for which the block + # evaluates to true. Returns +nil+ if none was found. + def find_first_recursive(&block) # :yields: node + each_recursive {|node| + return node if block.call(node) + } + return nil + end + + # Returns the position that +self+ holds in its parent's array, indexed + # from 1. + def index_in_parent + parent.index(self)+1 + end + end +end diff --git a/gxg/deps/rexml/output.rb b/gxg/deps/rexml/output.rb new file mode 100644 index 0000000..5acffff --- /dev/null +++ b/gxg/deps/rexml/output.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: false +require 'deps/rexml/encoding' + +module REXML + class Output + include Encoding + + attr_reader :encoding + + def initialize real_IO, encd="iso-8859-1" + @output = real_IO + self.encoding = encd + + @to_utf = encoding != 'UTF-8' + + if encoding == "UTF-16" + @output << "\ufeff".encode("UTF-16BE") + self.encoding = "UTF-16BE" + end + end + + def <<( content ) + @output << (@to_utf ? self.encode(content) : content) + end + + def to_s + "Output[#{encoding}]" + end + end +end diff --git a/gxg/deps/rexml/parent.rb b/gxg/deps/rexml/parent.rb new file mode 100644 index 0000000..500a9c6 --- /dev/null +++ b/gxg/deps/rexml/parent.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: false +require "deps/rexml/child" + +module REXML + # A parent has children, and has methods for accessing them. The Parent + # class is never encountered except as the superclass for some other + # object. + class Parent < Child + include Enumerable + + # Constructor + # @param parent if supplied, will be set as the parent of this object + def initialize parent=nil + super(parent) + @children = [] + end + + def add( object ) + object.parent = self + @children << object + object + end + + alias :push :add + alias :<< :push + + def unshift( object ) + object.parent = self + @children.unshift object + end + + def delete( object ) + found = false + @children.delete_if {|c| c.equal?(object) and found = true } + object.parent = nil if found + found ? object : nil + end + + def each(&block) + @children.each(&block) + end + + def delete_if( &block ) + @children.delete_if(&block) + end + + def delete_at( index ) + @children.delete_at index + end + + def each_index( &block ) + @children.each_index(&block) + end + + # Fetches a child at a given index + # @param index the Integer index of the child to fetch + def []( index ) + @children[index] + end + + alias :each_child :each + + + + # Set an index entry. See Array.[]= + # @param index the index of the element to set + # @param opt either the object to set, or an Integer length + # @param child if opt is an Integer, this is the child to set + # @return the parent (self) + def []=( *args ) + args[-1].parent = self + @children[*args[0..-2]] = args[-1] + end + + # Inserts an child before another child + # @param child1 this is either an xpath or an Element. If an Element, + # child2 will be inserted before child1 in the child list of the parent. + # If an xpath, child2 will be inserted before the first child to match + # the xpath. + # @param child2 the child to insert + # @return the parent (self) + def insert_before( child1, child2 ) + if child1.kind_of? String + child1 = XPath.first( self, child1 ) + child1.parent.insert_before child1, child2 + else + ind = index(child1) + child2.parent.delete(child2) if child2.parent + @children[ind,0] = child2 + child2.parent = self + end + self + end + + # Inserts an child after another child + # @param child1 this is either an xpath or an Element. If an Element, + # child2 will be inserted after child1 in the child list of the parent. + # If an xpath, child2 will be inserted after the first child to match + # the xpath. + # @param child2 the child to insert + # @return the parent (self) + def insert_after( child1, child2 ) + if child1.kind_of? String + child1 = XPath.first( self, child1 ) + child1.parent.insert_after child1, child2 + else + ind = index(child1)+1 + child2.parent.delete(child2) if child2.parent + @children[ind,0] = child2 + child2.parent = self + end + self + end + + def to_a + @children.dup + end + + # Fetches the index of a given child + # @param child the child to get the index of + # @return the index of the child, or nil if the object is not a child + # of this parent. + def index( child ) + count = -1 + @children.find { |i| count += 1 ; i.hash == child.hash } + count + end + + # @return the number of children of this parent + def size + @children.size + end + + alias :length :size + + # Replaces one child with another, making sure the nodelist is correct + # @param to_replace the child to replace (must be a Child) + # @param replacement the child to insert into the nodelist (must be a + # Child) + def replace_child( to_replace, replacement ) + @children.map! {|c| c.equal?( to_replace ) ? replacement : c } + to_replace.parent = nil + replacement.parent = self + end + + # Deeply clones this object. This creates a complete duplicate of this + # Parent, including all descendants. + def deep_clone + cl = clone() + each do |child| + if child.kind_of? Parent + cl << child.deep_clone + else + cl << child.clone + end + end + cl + end + + alias :children :to_a + + def parent? + true + end + end +end diff --git a/gxg/deps/rexml/parseexception.rb b/gxg/deps/rexml/parseexception.rb new file mode 100644 index 0000000..7b16cd1 --- /dev/null +++ b/gxg/deps/rexml/parseexception.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: false +module REXML + class ParseException < RuntimeError + attr_accessor :source, :parser, :continued_exception + + def initialize( message, source=nil, parser=nil, exception=nil ) + super(message) + @source = source + @parser = parser + @continued_exception = exception + end + + def to_s + # Quote the original exception, if there was one + if @continued_exception + err = @continued_exception.inspect + err << "\n" + err << @continued_exception.backtrace.join("\n") + err << "\n...\n" + else + err = "" + end + + # Get the stack trace and error message + err << super + + # Add contextual information + if @source + err << "\nLine: #{line}\n" + err << "Position: #{position}\n" + err << "Last 80 unconsumed characters:\n" + err << @source.buffer[0..80].force_encoding("ASCII-8BIT").gsub(/\n/, ' ') + end + + err + end + + def position + @source.current_line[0] if @source and defined? @source.current_line and + @source.current_line + end + + def line + @source.current_line[2] if @source and defined? @source.current_line and + @source.current_line + end + + def context + @source.current_line + end + end +end diff --git a/gxg/deps/rexml/parsers/baseparser.rb b/gxg/deps/rexml/parsers/baseparser.rb new file mode 100644 index 0000000..1630afd --- /dev/null +++ b/gxg/deps/rexml/parsers/baseparser.rb @@ -0,0 +1,533 @@ +# frozen_string_literal: false +require 'deps/rexml/parseexception' +require 'deps/rexml/undefinednamespaceexception' +require 'deps/rexml/source' +require 'set' + +module REXML + module Parsers + # = Using the Pull Parser + # This API is experimental, and subject to change. + # parser = PullParser.new( "texttxet" ) + # while parser.has_next? + # res = parser.next + # puts res[1]['att'] if res.start_tag? and res[0] == 'b' + # end + # See the PullEvent class for information on the content of the results. + # The data is identical to the arguments passed for the various events to + # the StreamListener API. + # + # Notice that: + # parser = PullParser.new( "BAD DOCUMENT" ) + # while parser.has_next? + # res = parser.next + # raise res[1] if res.error? + # end + # + # Nat Price gave me some good ideas for the API. + class BaseParser + LETTER = '[:alpha:]' + DIGIT = '[:digit:]' + + COMBININGCHAR = '' # TODO + EXTENDER = '' # TODO + + NCNAME_STR= "[#{LETTER}_:][-[:alnum:]._:#{COMBININGCHAR}#{EXTENDER}]*" + NAME_STR= "(?:(#{NCNAME_STR}):)?(#{NCNAME_STR})" + UNAME_STR= "(?:#{NCNAME_STR}:)?#{NCNAME_STR}" + + NAMECHAR = '[\-\w\.:]' + NAME = "([\\w:]#{NAMECHAR}*)" + NMTOKEN = "(?:#{NAMECHAR})+" + NMTOKENS = "#{NMTOKEN}(\\s+#{NMTOKEN})*" + REFERENCE = "&(?:#{NAME};|#\\d+;|#x[0-9a-fA-F]+;)" + REFERENCE_RE = /#{REFERENCE}/ + + DOCTYPE_START = /\A\s*/um + DOCTYPE_PATTERN = /\s*)/um + ATTRIBUTE_PATTERN = /\s*(#{NAME_STR})\s*=\s*(["'])(.*?)\4/um + COMMENT_START = /\A/um + CDATA_START = /\A/um + CDATA_PATTERN = //um + XMLDECL_START = /\A<\?xml\s/u; + XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>/um + INSTRUCTION_START = /\A<\?/u + INSTRUCTION_PATTERN = /<\?(.*?)(\s+.*?)?\?>/um + TAG_MATCH = /^<((?>#{NAME_STR}))\s*((?>\s+#{UNAME_STR}\s*=\s*(["']).*?\5)*)\s*(\/)?>/um + CLOSE_MATCH = /^\s*<\/(#{NAME_STR})\s*>/um + + VERSION = /\bversion\s*=\s*["'](.*?)['"]/um + ENCODING = /\bencoding\s*=\s*["'](.*?)['"]/um + STANDALONE = /\bstandalone\s*=\s*["'](.*?)['"]/um + + ENTITY_START = /\A\s*/um + SYSTEMENTITY = /\A\s*(%.*?;)\s*$/um + ENUMERATION = "\\(\\s*#{NMTOKEN}(?:\\s*\\|\\s*#{NMTOKEN})*\\s*\\)" + NOTATIONTYPE = "NOTATION\\s+\\(\\s*#{NAME}(?:\\s*\\|\\s*#{NAME})*\\s*\\)" + ENUMERATEDTYPE = "(?:(?:#{NOTATIONTYPE})|(?:#{ENUMERATION}))" + ATTTYPE = "(CDATA|ID|IDREF|IDREFS|ENTITY|ENTITIES|NMTOKEN|NMTOKENS|#{ENUMERATEDTYPE})" + ATTVALUE = "(?:\"((?:[^<&\"]|#{REFERENCE})*)\")|(?:'((?:[^<&']|#{REFERENCE})*)')" + DEFAULTDECL = "(#REQUIRED|#IMPLIED|(?:(#FIXED\\s+)?#{ATTVALUE}))" + ATTDEF = "\\s+#{NAME}\\s+#{ATTTYPE}\\s+#{DEFAULTDECL}" + ATTDEF_RE = /#{ATTDEF}/ + ATTLISTDECL_START = /\A\s*/um + NOTATIONDECL_START = /\A\s*/um + SYSTEM = /\A\s*/um + + TEXT_PATTERN = /\A([^<]*)/um + + # Entity constants + PUBIDCHAR = "\x20\x0D\x0Aa-zA-Z0-9\\-()+,./:=?;!*@$_%#" + SYSTEMLITERAL = %Q{((?:"[^"]*")|(?:'[^']*'))} + PUBIDLITERAL = %Q{("[#{PUBIDCHAR}']*"|'[#{PUBIDCHAR}]*')} + EXTERNALID = "(?:(?:(SYSTEM)\\s+#{SYSTEMLITERAL})|(?:(PUBLIC)\\s+#{PUBIDLITERAL}\\s+#{SYSTEMLITERAL}))" + NDATADECL = "\\s+NDATA\\s+#{NAME}" + PEREFERENCE = "%#{NAME};" + ENTITYVALUE = %Q{((?:"(?:[^%&"]|#{PEREFERENCE}|#{REFERENCE})*")|(?:'([^%&']|#{PEREFERENCE}|#{REFERENCE})*'))} + PEDEF = "(?:#{ENTITYVALUE}|#{EXTERNALID})" + ENTITYDEF = "(?:#{ENTITYVALUE}|(?:#{EXTERNALID}(#{NDATADECL})?))" + PEDECL = "" + GEDECL = "" + ENTITYDECL = /\s*(?:#{GEDECL})|(?:#{PEDECL})/um + + EREFERENCE = /&(?!#{NAME};)/ + + DEFAULT_ENTITIES = { + 'gt' => [/>/, '>', '>', />/], + 'lt' => [/</, '<', '<', / [/"/, '"', '"', /"/], + "apos" => [/'/, "'", "'", /'/] + } + + + ###################################################################### + # These are patterns to identify common markup errors, to make the + # error messages more informative. + ###################################################################### + MISSING_ATTRIBUTE_QUOTES = /^<#{NAME_STR}\s+#{NAME_STR}\s*=\s*[^"']/um + + def initialize( source ) + self.stream = source + @listeners = [] + end + + def add_listener( listener ) + @listeners << listener + end + + attr_reader :source + + def stream=( source ) + @source = SourceFactory.create_from( source ) + @closed = nil + @document_status = nil + @tags = [] + @stack = [] + @entities = [] + @nsstack = [] + end + + def position + if @source.respond_to? :position + @source.position + else + # FIXME + 0 + end + end + + # Returns true if there are no more events + def empty? + return (@source.empty? and @stack.empty?) + end + + # Returns true if there are more events. Synonymous with !empty? + def has_next? + return !(@source.empty? and @stack.empty?) + end + + # Push an event back on the head of the stream. This method + # has (theoretically) infinite depth. + def unshift token + @stack.unshift(token) + end + + # Peek at the +depth+ event in the stack. The first element on the stack + # is at depth 0. If +depth+ is -1, will parse to the end of the input + # stream and return the last event, which is always :end_document. + # Be aware that this causes the stream to be parsed up to the +depth+ + # event, so you can effectively pre-parse the entire document (pull the + # entire thing into memory) using this method. + def peek depth=0 + raise %Q[Illegal argument "#{depth}"] if depth < -1 + temp = [] + if depth == -1 + temp.push(pull()) until empty? + else + while @stack.size+temp.size < depth+1 + temp.push(pull()) + end + end + @stack += temp if temp.size > 0 + @stack[depth] + end + + # Returns the next event. This is a +PullEvent+ object. + def pull + pull_event.tap do |event| + @listeners.each do |listener| + listener.receive event + end + end + end + + def pull_event + if @closed + x, @closed = @closed, nil + return [ :end_element, x ] + end + return [ :end_document ] if empty? + return @stack.shift if @stack.size > 0 + #STDERR.puts @source.encoding + @source.read if @source.buffer.size<2 + #STDERR.puts "BUFFER = #{@source.buffer.inspect}" + if @document_status == nil + #@source.consume( /^\s*/um ) + word = @source.match( /^((?:\s+)|(?:<[^>]*>))/um ) + word = word[1] unless word.nil? + #STDERR.puts "WORD = #{word.inspect}" + case word + when COMMENT_START + return [ :comment, @source.match( COMMENT_PATTERN, true )[1] ] + when XMLDECL_START + #STDERR.puts "XMLDECL" + results = @source.match( XMLDECL_PATTERN, true )[1] + version = VERSION.match( results ) + version = version[1] unless version.nil? + encoding = ENCODING.match(results) + encoding = encoding[1] unless encoding.nil? + if need_source_encoding_update?(encoding) + @source.encoding = encoding + end + if encoding.nil? and /\AUTF-16(?:BE|LE)\z/i =~ @source.encoding + encoding = "UTF-16" + end + standalone = STANDALONE.match(results) + standalone = standalone[1] unless standalone.nil? + return [ :xmldecl, version, encoding, standalone ] + when INSTRUCTION_START + return [ :processing_instruction, *@source.match(INSTRUCTION_PATTERN, true)[1,2] ] + when DOCTYPE_START + md = @source.match( DOCTYPE_PATTERN, true ) + @nsstack.unshift(curr_ns=Set.new) + identity = md[1] + close = md[2] + identity =~ IDENTITY + name = $1 + raise REXML::ParseException.new("DOCTYPE is missing a name") if name.nil? + pub_sys = $2.nil? ? nil : $2.strip + long_name = $4.nil? ? nil : $4.strip + uri = $6.nil? ? nil : $6.strip + args = [ :start_doctype, name, pub_sys, long_name, uri ] + if close == ">" + @document_status = :after_doctype + @source.read if @source.buffer.size<2 + md = @source.match(/^\s*/um, true) + @stack << [ :end_doctype ] + else + @document_status = :in_doctype + end + return args + when /^\s+/ + else + @document_status = :after_doctype + @source.read if @source.buffer.size<2 + md = @source.match(/\s*/um, true) + if @source.encoding == "UTF-8" + @source.buffer.force_encoding(::Encoding::UTF_8) + end + end + end + if @document_status == :in_doctype + md = @source.match(/\s*(.*?>)/um) + case md[1] + when SYSTEMENTITY + match = @source.match( SYSTEMENTITY, true )[1] + return [ :externalentity, match ] + + when ELEMENTDECL_START + return [ :elementdecl, @source.match( ELEMENTDECL_PATTERN, true )[1] ] + + when ENTITY_START + match = @source.match( ENTITYDECL, true ).to_a.compact + match[0] = :entitydecl + ref = false + if match[1] == '%' + ref = true + match.delete_at 1 + end + # Now we have to sort out what kind of entity reference this is + if match[2] == 'SYSTEM' + # External reference + match[3] = match[3][1..-2] # PUBID + match.delete_at(4) if match.size > 4 # Chop out NDATA decl + # match is [ :entity, name, SYSTEM, pubid(, ndata)? ] + elsif match[2] == 'PUBLIC' + # External reference + match[3] = match[3][1..-2] # PUBID + match[4] = match[4][1..-2] # HREF + match.delete_at(5) if match.size > 5 # Chop out NDATA decl + # match is [ :entity, name, PUBLIC, pubid, href(, ndata)? ] + else + match[2] = match[2][1..-2] + match.pop if match.size == 4 + # match is [ :entity, name, value ] + end + match << '%' if ref + return match + when ATTLISTDECL_START + md = @source.match( ATTLISTDECL_PATTERN, true ) + raise REXML::ParseException.new( "Bad ATTLIST declaration!", @source ) if md.nil? + element = md[1] + contents = md[0] + + pairs = {} + values = md[0].scan( ATTDEF_RE ) + values.each do |attdef| + unless attdef[3] == "#IMPLIED" + attdef.compact! + val = attdef[3] + val = attdef[4] if val == "#FIXED " + pairs[attdef[0]] = val + if attdef[0] =~ /^xmlns:(.*)/ + @nsstack[0] << $1 + end + end + end + return [ :attlistdecl, element, pairs, contents ] + when NOTATIONDECL_START + md = nil + if @source.match( PUBLIC ) + md = @source.match( PUBLIC, true ) + vals = [md[1],md[2],md[4],md[6]] + elsif @source.match( SYSTEM ) + md = @source.match( SYSTEM, true ) + vals = [md[1],md[2],nil,md[4]] + else + raise REXML::ParseException.new( "error parsing notation: no matching pattern", @source ) + end + return [ :notationdecl, *vals ] + when DOCTYPE_END + @document_status = :after_doctype + @source.match( DOCTYPE_END, true ) + return [ :end_doctype ] + end + end + begin + if @source.buffer[0] == ?< + if @source.buffer[1] == ?/ + @nsstack.shift + last_tag = @tags.pop + #md = @source.match_to_consume( '>', CLOSE_MATCH) + md = @source.match( CLOSE_MATCH, true ) + raise REXML::ParseException.new( "Missing end tag for "+ + "'#{last_tag}' (got \"#{md[1]}\")", + @source) unless last_tag == md[1] + return [ :end_element, last_tag ] + elsif @source.buffer[1] == ?! + md = @source.match(/\A(\s*[^>]*>)/um) + #STDERR.puts "SOURCE BUFFER = #{source.buffer}, #{source.buffer.size}" + raise REXML::ParseException.new("Malformed node", @source) unless md + if md[0][2] == ?- + md = @source.match( COMMENT_PATTERN, true ) + + case md[1] + when /--/, /-\z/ + raise REXML::ParseException.new("Malformed comment", @source) + end + + return [ :comment, md[1] ] if md + else + md = @source.match( CDATA_PATTERN, true ) + return [ :cdata, md[1] ] if md + end + raise REXML::ParseException.new( "Declarations can only occur "+ + "in the doctype declaration.", @source) + elsif @source.buffer[1] == ?? + md = @source.match( INSTRUCTION_PATTERN, true ) + return [ :processing_instruction, md[1], md[2] ] if md + raise REXML::ParseException.new( "Bad instruction declaration", + @source) + else + # Get the next tag + md = @source.match(TAG_MATCH, true) + unless md + # Check for missing attribute quotes + raise REXML::ParseException.new("missing attribute quote", @source) if @source.match(MISSING_ATTRIBUTE_QUOTES ) + raise REXML::ParseException.new("malformed XML: missing tag start", @source) + end + attributes = {} + prefixes = Set.new + prefixes << md[2] if md[2] + @nsstack.unshift(curr_ns=Set.new) + if md[4].size > 0 + attrs = md[4].scan( ATTRIBUTE_PATTERN ) + raise REXML::ParseException.new( "error parsing attributes: [#{attrs.join ', '}], excess = \"#$'\"", @source) if $' and $'.strip.size > 0 + attrs.each do |attr_name, prefix, local_part, quote, value| + if prefix == "xmlns" + if local_part == "xml" + if value != "http://www.w3.org/XML/1998/namespace" + msg = "The 'xml' prefix must not be bound to any other namespace "+ + "(http://www.w3.org/TR/REC-xml-names/#ns-decl)" + raise REXML::ParseException.new( msg, @source, self ) + end + elsif local_part == "xmlns" + msg = "The 'xmlns' prefix must not be declared "+ + "(http://www.w3.org/TR/REC-xml-names/#ns-decl)" + raise REXML::ParseException.new( msg, @source, self) + end + curr_ns << local_part + elsif prefix + prefixes << prefix unless prefix == "xml" + end + + if attributes.has_key?(attr_name) + msg = "Duplicate attribute #{attr_name.inspect}" + raise REXML::ParseException.new(msg, @source, self) + end + + attributes[attr_name] = value + end + end + + # Verify that all of the prefixes have been defined + for prefix in prefixes + unless @nsstack.find{|k| k.member?(prefix)} + raise UndefinedNamespaceException.new(prefix,@source,self) + end + end + + if md[6] + @closed = md[1] + @nsstack.shift + else + @tags.push( md[1] ) + end + return [ :start_element, md[1], attributes ] + end + else + md = @source.match( TEXT_PATTERN, true ) + if md[0].length == 0 + @source.match( /(\s+)/, true ) + end + #STDERR.puts "GOT #{md[1].inspect}" unless md[0].length == 0 + #return [ :text, "" ] if md[0].length == 0 + # unnormalized = Text::unnormalize( md[1], self ) + # return PullEvent.new( :text, md[1], unnormalized ) + return [ :text, md[1] ] + end + rescue REXML::UndefinedNamespaceException + raise + rescue REXML::ParseException + raise + rescue Exception, NameError => error + raise REXML::ParseException.new( "Exception parsing", + @source, self, (error ? error : $!) ) + end + return [ :dummy ] + end + private :pull_event + + def entity( reference, entities ) + value = nil + value = entities[ reference ] if entities + if not value + value = DEFAULT_ENTITIES[ reference ] + value = value[2] if value + end + unnormalize( value, entities ) if value + end + + # Escapes all possible entities + def normalize( input, entities=nil, entity_filter=nil ) + copy = input.clone + # Doing it like this rather than in a loop improves the speed + copy.gsub!( EREFERENCE, '&' ) + entities.each do |key, value| + copy.gsub!( value, "&#{key};" ) unless entity_filter and + entity_filter.include?(entity) + end if entities + copy.gsub!( EREFERENCE, '&' ) + DEFAULT_ENTITIES.each do |key, value| + copy.gsub!( value[3], value[1] ) + end + copy + end + + # Unescapes all possible entities + def unnormalize( string, entities=nil, filter=nil ) + rv = string.clone + rv.gsub!( /\r\n?/, "\n" ) + matches = rv.scan( REFERENCE_RE ) + return rv if matches.size == 0 + rv.gsub!( /�*((?:\d+)|(?:x[a-fA-F0-9]+));/ ) { + m=$1 + m = "0#{m}" if m[0] == ?x + [Integer(m)].pack('U*') + } + matches.collect!{|x|x[0]}.compact! + if matches.size > 0 + matches.each do |entity_reference| + unless filter and filter.include?(entity_reference) + entity_value = entity( entity_reference, entities ) + if entity_value + re = /&#{entity_reference};/ + rv.gsub!( re, entity_value ) + else + er = DEFAULT_ENTITIES[entity_reference] + rv.gsub!( er[0], er[2] ) if er + end + end + end + rv.gsub!( /&/, '&' ) + end + rv + end + + private + def need_source_encoding_update?(xml_declaration_encoding) + return false if xml_declaration_encoding.nil? + return false if /\AUTF-16\z/i =~ xml_declaration_encoding + true + end + end + end +end + +=begin + case event[0] + when :start_element + when :text + when :end_element + when :processing_instruction + when :cdata + when :comment + when :xmldecl + when :start_doctype + when :end_doctype + when :externalentity + when :elementdecl + when :entity + when :attlistdecl + when :notationdecl + when :end_doctype + end +=end diff --git a/gxg/deps/rexml/parsers/lightparser.rb b/gxg/deps/rexml/parsers/lightparser.rb new file mode 100644 index 0000000..76ad67f --- /dev/null +++ b/gxg/deps/rexml/parsers/lightparser.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: false +require 'deps/rexml/parsers/streamparser' +require 'deps/rexml/parsers/baseparser' +require 'deps/rexml/light/node' + +module REXML + module Parsers + class LightParser + def initialize stream + @stream = stream + @parser = REXML::Parsers::BaseParser.new( stream ) + end + + def add_listener( listener ) + @parser.add_listener( listener ) + end + + def rewind + @stream.rewind + @parser.stream = @stream + end + + def parse + root = context = [ :document ] + while true + event = @parser.pull + case event[0] + when :end_document + break + when :start_element, :start_doctype + new_node = event + context << new_node + new_node[1,0] = [context] + context = new_node + when :end_element, :end_doctype + context = context[1] + else + new_node = event + context << new_node + new_node[1,0] = [context] + end + end + root + end + end + + # An element is an array. The array contains: + # 0 The parent element + # 1 The tag name + # 2 A hash of attributes + # 3..-1 The child elements + # An element is an array of size > 3 + # Text is a String + # PIs are [ :processing_instruction, target, data ] + # Comments are [ :comment, data ] + # DocTypes are DocType structs + # The root is an array with XMLDecls, Text, DocType, Array, Text + end +end diff --git a/gxg/deps/rexml/parsers/pullparser.rb b/gxg/deps/rexml/parsers/pullparser.rb new file mode 100644 index 0000000..d578c63 --- /dev/null +++ b/gxg/deps/rexml/parsers/pullparser.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: false +require 'forwardable' + +require 'deps/rexml/parseexception' +require 'deps/rexml/parsers/baseparser' +require 'deps/rexml/xmltokens' + +module REXML + module Parsers + # = Using the Pull Parser + # This API is experimental, and subject to change. + # parser = PullParser.new( "texttxet" ) + # while parser.has_next? + # res = parser.next + # puts res[1]['att'] if res.start_tag? and res[0] == 'b' + # end + # See the PullEvent class for information on the content of the results. + # The data is identical to the arguments passed for the various events to + # the StreamListener API. + # + # Notice that: + # parser = PullParser.new( "BAD DOCUMENT" ) + # while parser.has_next? + # res = parser.next + # raise res[1] if res.error? + # end + # + # Nat Price gave me some good ideas for the API. + class PullParser + include XMLTokens + extend Forwardable + + def_delegators( :@parser, :has_next? ) + def_delegators( :@parser, :entity ) + def_delegators( :@parser, :empty? ) + def_delegators( :@parser, :source ) + + def initialize stream + @entities = {} + @listeners = nil + @parser = BaseParser.new( stream ) + @my_stack = [] + end + + def add_listener( listener ) + @listeners = [] unless @listeners + @listeners << listener + end + + def each + while has_next? + yield self.pull + end + end + + def peek depth=0 + if @my_stack.length <= depth + (depth - @my_stack.length + 1).times { + e = PullEvent.new(@parser.pull) + @my_stack.push(e) + } + end + @my_stack[depth] + end + + def pull + return @my_stack.shift if @my_stack.length > 0 + + event = @parser.pull + case event[0] + when :entitydecl + @entities[ event[1] ] = + event[2] unless event[2] =~ /PUBLIC|SYSTEM/ + when :text + unnormalized = @parser.unnormalize( event[1], @entities ) + event << unnormalized + end + PullEvent.new( event ) + end + + def unshift token + @my_stack.unshift token + end + end + + # A parsing event. The contents of the event are accessed as an +Array?, + # and the type is given either by the ...? methods, or by accessing the + # +type+ accessor. The contents of this object vary from event to event, + # but are identical to the arguments passed to +StreamListener+s for each + # event. + class PullEvent + # The type of this event. Will be one of :tag_start, :tag_end, :text, + # :processing_instruction, :comment, :doctype, :attlistdecl, :entitydecl, + # :notationdecl, :entity, :cdata, :xmldecl, or :error. + def initialize(arg) + @contents = arg + end + + def []( start, endd=nil) + if start.kind_of? Range + @contents.slice( start.begin+1 .. start.end ) + elsif start.kind_of? Numeric + if endd.nil? + @contents.slice( start+1 ) + else + @contents.slice( start+1, endd ) + end + else + raise "Illegal argument #{start.inspect} (#{start.class})" + end + end + + def event_type + @contents[0] + end + + # Content: [ String tag_name, Hash attributes ] + def start_element? + @contents[0] == :start_element + end + + # Content: [ String tag_name ] + def end_element? + @contents[0] == :end_element + end + + # Content: [ String raw_text, String unnormalized_text ] + def text? + @contents[0] == :text + end + + # Content: [ String text ] + def instruction? + @contents[0] == :processing_instruction + end + + # Content: [ String text ] + def comment? + @contents[0] == :comment + end + + # Content: [ String name, String pub_sys, String long_name, String uri ] + def doctype? + @contents[0] == :start_doctype + end + + # Content: [ String text ] + def attlistdecl? + @contents[0] == :attlistdecl + end + + # Content: [ String text ] + def elementdecl? + @contents[0] == :elementdecl + end + + # Due to the wonders of DTDs, an entity declaration can be just about + # anything. There's no way to normalize it; you'll have to interpret the + # content yourself. However, the following is true: + # + # * If the entity declaration is an internal entity: + # [ String name, String value ] + # Content: [ String text ] + def entitydecl? + @contents[0] == :entitydecl + end + + # Content: [ String text ] + def notationdecl? + @contents[0] == :notationdecl + end + + # Content: [ String text ] + def entity? + @contents[0] == :entity + end + + # Content: [ String text ] + def cdata? + @contents[0] == :cdata + end + + # Content: [ String version, String encoding, String standalone ] + def xmldecl? + @contents[0] == :xmldecl + end + + def error? + @contents[0] == :error + end + + def inspect + @contents[0].to_s + ": " + @contents[1..-1].inspect + end + end + end +end diff --git a/gxg/deps/rexml/parsers/sax2parser.rb b/gxg/deps/rexml/parsers/sax2parser.rb new file mode 100644 index 0000000..f69784c --- /dev/null +++ b/gxg/deps/rexml/parsers/sax2parser.rb @@ -0,0 +1,273 @@ +# frozen_string_literal: false +require 'deps/rexml/parsers/baseparser' +require 'deps/rexml/parseexception' +require 'deps/rexml/namespace' +require 'deps/rexml/text' + +module REXML + module Parsers + # SAX2Parser + class SAX2Parser + def initialize source + @parser = BaseParser.new(source) + @listeners = [] + @procs = [] + @namespace_stack = [] + @has_listeners = false + @tag_stack = [] + @entities = {} + end + + def source + @parser.source + end + + def add_listener( listener ) + @parser.add_listener( listener ) + end + + # Listen arguments: + # + # Symbol, Array, Block + # Listen to Symbol events on Array elements + # Symbol, Block + # Listen to Symbol events + # Array, Listener + # Listen to all events on Array elements + # Array, Block + # Listen to :start_element events on Array elements + # Listener + # Listen to All events + # + # Symbol can be one of: :start_element, :end_element, + # :start_prefix_mapping, :end_prefix_mapping, :characters, + # :processing_instruction, :doctype, :attlistdecl, :elementdecl, + # :entitydecl, :notationdecl, :cdata, :xmldecl, :comment + # + # There is an additional symbol that can be listened for: :progress. + # This will be called for every event generated, passing in the current + # stream position. + # + # Array contains regular expressions or strings which will be matched + # against fully qualified element names. + # + # Listener must implement the methods in SAX2Listener + # + # Block will be passed the same arguments as a SAX2Listener method would + # be, where the method name is the same as the matched Symbol. + # See the SAX2Listener for more information. + def listen( *args, &blok ) + if args[0].kind_of? Symbol + if args.size == 2 + args[1].each { |match| @procs << [args[0], match, blok] } + else + add( [args[0], nil, blok] ) + end + elsif args[0].kind_of? Array + if args.size == 2 + args[0].each { |match| add( [nil, match, args[1]] ) } + else + args[0].each { |match| add( [ :start_element, match, blok ] ) } + end + else + add([nil, nil, args[0]]) + end + end + + def deafen( listener=nil, &blok ) + if listener + @listeners.delete_if {|item| item[-1] == listener } + @has_listeners = false if @listeners.size == 0 + else + @procs.delete_if {|item| item[-1] == blok } + end + end + + def parse + @procs.each { |sym,match,block| block.call if sym == :start_document } + @listeners.each { |sym,match,block| + block.start_document if sym == :start_document or sym.nil? + } + context = [] + while true + event = @parser.pull + case event[0] + when :end_document + handle( :end_document ) + break + when :start_doctype + handle( :doctype, *event[1..-1]) + when :end_doctype + context = context[1] + when :start_element + @tag_stack.push(event[1]) + # find the observers for namespaces + procs = get_procs( :start_prefix_mapping, event[1] ) + listeners = get_listeners( :start_prefix_mapping, event[1] ) + if procs or listeners + # break out the namespace declarations + # The attributes live in event[2] + event[2].each {|n, v| event[2][n] = @parser.normalize(v)} + nsdecl = event[2].find_all { |n, value| n =~ /^xmlns(:|$)/ } + nsdecl.collect! { |n, value| [ n[6..-1], value ] } + @namespace_stack.push({}) + nsdecl.each do |n,v| + @namespace_stack[-1][n] = v + # notify observers of namespaces + procs.each { |ob| ob.call( n, v ) } if procs + listeners.each { |ob| ob.start_prefix_mapping(n, v) } if listeners + end + end + event[1] =~ Namespace::NAMESPLIT + prefix = $1 + local = $2 + uri = get_namespace(prefix) + # find the observers for start_element + procs = get_procs( :start_element, event[1] ) + listeners = get_listeners( :start_element, event[1] ) + # notify observers + procs.each { |ob| ob.call( uri, local, event[1], event[2] ) } if procs + listeners.each { |ob| + ob.start_element( uri, local, event[1], event[2] ) + } if listeners + when :end_element + @tag_stack.pop + event[1] =~ Namespace::NAMESPLIT + prefix = $1 + local = $2 + uri = get_namespace(prefix) + # find the observers for start_element + procs = get_procs( :end_element, event[1] ) + listeners = get_listeners( :end_element, event[1] ) + # notify observers + procs.each { |ob| ob.call( uri, local, event[1] ) } if procs + listeners.each { |ob| + ob.end_element( uri, local, event[1] ) + } if listeners + + namespace_mapping = @namespace_stack.pop + # find the observers for namespaces + procs = get_procs( :end_prefix_mapping, event[1] ) + listeners = get_listeners( :end_prefix_mapping, event[1] ) + if procs or listeners + namespace_mapping.each do |ns_prefix, ns_uri| + # notify observers of namespaces + procs.each { |ob| ob.call( ns_prefix ) } if procs + listeners.each { |ob| ob.end_prefix_mapping(ns_prefix) } if listeners + end + end + when :text + #normalized = @parser.normalize( event[1] ) + #handle( :characters, normalized ) + copy = event[1].clone + + esub = proc { |match| + if @entities.has_key?($1) + @entities[$1].gsub(Text::REFERENCE, &esub) + else + match + end + } + + copy.gsub!( Text::REFERENCE, &esub ) + copy.gsub!( Text::NUMERICENTITY ) {|m| + m=$1 + m = "0#{m}" if m[0] == ?x + [Integer(m)].pack('U*') + } + handle( :characters, copy ) + when :entitydecl + handle_entitydecl( event ) + when :processing_instruction, :comment, :attlistdecl, + :elementdecl, :cdata, :notationdecl, :xmldecl + handle( *event ) + end + handle( :progress, @parser.position ) + end + end + + private + def handle( symbol, *arguments ) + tag = @tag_stack[-1] + procs = get_procs( symbol, tag ) + listeners = get_listeners( symbol, tag ) + # notify observers + procs.each { |ob| ob.call( *arguments ) } if procs + listeners.each { |l| + l.send( symbol.to_s, *arguments ) + } if listeners + end + + def handle_entitydecl( event ) + @entities[ event[1] ] = event[2] if event.size == 3 + parameter_reference_p = false + case event[2] + when "SYSTEM" + if event.size == 5 + if event.last == "%" + parameter_reference_p = true + else + event[4, 0] = "NDATA" + end + end + when "PUBLIC" + if event.size == 6 + if event.last == "%" + parameter_reference_p = true + else + event[5, 0] = "NDATA" + end + end + else + parameter_reference_p = (event.size == 4) + end + event[1, 0] = event.pop if parameter_reference_p + handle( event[0], event[1..-1] ) + end + + # The following methods are duplicates, but it is faster than using + # a helper + def get_procs( symbol, name ) + return nil if @procs.size == 0 + @procs.find_all do |sym, match, block| + ( + (sym.nil? or symbol == sym) and + ((name.nil? and match.nil?) or match.nil? or ( + (name == match) or + (match.kind_of? Regexp and name =~ match) + ) + ) + ) + end.collect{|x| x[-1]} + end + def get_listeners( symbol, name ) + return nil if @listeners.size == 0 + @listeners.find_all do |sym, match, block| + ( + (sym.nil? or symbol == sym) and + ((name.nil? and match.nil?) or match.nil? or ( + (name == match) or + (match.kind_of? Regexp and name =~ match) + ) + ) + ) + end.collect{|x| x[-1]} + end + + def add( pair ) + if pair[-1].respond_to? :call + @procs << pair unless @procs.include? pair + else + @listeners << pair unless @listeners.include? pair + @has_listeners = true + end + end + + def get_namespace( prefix ) + uris = (@namespace_stack.find_all { |ns| not ns[prefix].nil? }) || + (@namespace_stack.find { |ns| not ns[nil].nil? }) + uris[-1][prefix] unless uris.nil? or 0 == uris.size + end + end + end +end diff --git a/gxg/deps/rexml/parsers/streamparser.rb b/gxg/deps/rexml/parsers/streamparser.rb new file mode 100644 index 0000000..6b6a072 --- /dev/null +++ b/gxg/deps/rexml/parsers/streamparser.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: false +require "deps/rexml/parsers/baseparser" + +module REXML + module Parsers + class StreamParser + def initialize source, listener + @listener = listener + @parser = BaseParser.new( source ) + @tag_stack = [] + end + + def add_listener( listener ) + @parser.add_listener( listener ) + end + + def parse + # entity string + while true + event = @parser.pull + case event[0] + when :end_document + unless @tag_stack.empty? + tag_path = "/" + @tag_stack.join("/") + raise ParseException.new("Missing end tag for '#{tag_path}'", + @parser.source) + end + return + when :start_element + @tag_stack << event[1] + attrs = event[2].each do |n, v| + event[2][n] = @parser.unnormalize( v ) + end + @listener.tag_start( event[1], attrs ) + when :end_element + @listener.tag_end( event[1] ) + @tag_stack.pop + when :text + normalized = @parser.unnormalize( event[1] ) + @listener.text( normalized ) + when :processing_instruction + @listener.instruction( *event[1,2] ) + when :start_doctype + @listener.doctype( *event[1..-1] ) + when :end_doctype + # FIXME: remove this condition for milestone:3.2 + @listener.doctype_end if @listener.respond_to? :doctype_end + when :comment, :attlistdecl, :cdata, :xmldecl, :elementdecl + @listener.send( event[0].to_s, *event[1..-1] ) + when :entitydecl, :notationdecl + @listener.send( event[0].to_s, event[1..-1] ) + when :externalentity + entity_reference = event[1] + content = entity_reference.gsub(/\A%|;\z/, "") + @listener.entity(content) + end + end + end + end + end +end diff --git a/gxg/deps/rexml/parsers/treeparser.rb b/gxg/deps/rexml/parsers/treeparser.rb new file mode 100644 index 0000000..63d5c7e --- /dev/null +++ b/gxg/deps/rexml/parsers/treeparser.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: false +require 'deps/rexml/validation/validationexception' +require 'deps/rexml/undefinednamespaceexception' + +module REXML + module Parsers + class TreeParser + def initialize( source, build_context = Document.new ) + @build_context = build_context + @parser = Parsers::BaseParser.new( source ) + end + + def add_listener( listener ) + @parser.add_listener( listener ) + end + + def parse + tag_stack = [] + in_doctype = false + entities = nil + begin + while true + event = @parser.pull + #STDERR.puts "TREEPARSER GOT #{event.inspect}" + case event[0] + when :end_document + unless tag_stack.empty? + raise ParseException.new("No close tag for #{@build_context.xpath}", + @parser.source, @parser) + end + return + when :start_element + tag_stack.push(event[1]) + el = @build_context = @build_context.add_element( event[1] ) + event[2].each do |key, value| + el.attributes[key]=Attribute.new(key,value,self) + end + when :end_element + tag_stack.pop + @build_context = @build_context.parent + when :text + if not in_doctype + if @build_context[-1].instance_of? Text + @build_context[-1] << event[1] + else + @build_context.add( + Text.new(event[1], @build_context.whitespace, nil, true) + ) unless ( + @build_context.ignore_whitespace_nodes and + event[1].strip.size==0 + ) + end + end + when :comment + c = Comment.new( event[1] ) + @build_context.add( c ) + when :cdata + c = CData.new( event[1] ) + @build_context.add( c ) + when :processing_instruction + @build_context.add( Instruction.new( event[1], event[2] ) ) + when :end_doctype + in_doctype = false + entities.each { |k,v| entities[k] = @build_context.entities[k].value } + @build_context = @build_context.parent + when :start_doctype + doctype = DocType.new( event[1..-1], @build_context ) + @build_context = doctype + entities = {} + in_doctype = true + when :attlistdecl + n = AttlistDecl.new( event[1..-1] ) + @build_context.add( n ) + when :externalentity + n = ExternalEntity.new( event[1] ) + @build_context.add( n ) + when :elementdecl + n = ElementDecl.new( event[1] ) + @build_context.add(n) + when :entitydecl + entities[ event[1] ] = event[2] unless event[2] =~ /PUBLIC|SYSTEM/ + @build_context.add(Entity.new(event)) + when :notationdecl + n = NotationDecl.new( *event[1..-1] ) + @build_context.add( n ) + when :xmldecl + x = XMLDecl.new( event[1], event[2], event[3] ) + @build_context.add( x ) + end + end + rescue REXML::Validation::ValidationException + raise + rescue REXML::ParseException + raise + rescue + raise ParseException.new( $!.message, @parser.source, @parser, $! ) + end + end + end + end +end diff --git a/gxg/deps/rexml/parsers/ultralightparser.rb b/gxg/deps/rexml/parsers/ultralightparser.rb new file mode 100644 index 0000000..3c7a089 --- /dev/null +++ b/gxg/deps/rexml/parsers/ultralightparser.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: false +require 'deps/rexml/parsers/streamparser' +require 'deps/rexml/parsers/baseparser' + +module REXML + module Parsers + class UltraLightParser + def initialize stream + @stream = stream + @parser = REXML::Parsers::BaseParser.new( stream ) + end + + def add_listener( listener ) + @parser.add_listener( listener ) + end + + def rewind + @stream.rewind + @parser.stream = @stream + end + + def parse + root = context = [] + while true + event = @parser.pull + case event[0] + when :end_document + break + when :end_doctype + context = context[1] + when :start_element, :start_doctype + context << event + event[1,0] = [context] + context = event + when :end_element + context = context[1] + else + context << event + end + end + root + end + end + + # An element is an array. The array contains: + # 0 The parent element + # 1 The tag name + # 2 A hash of attributes + # 3..-1 The child elements + # An element is an array of size > 3 + # Text is a String + # PIs are [ :processing_instruction, target, data ] + # Comments are [ :comment, data ] + # DocTypes are DocType structs + # The root is an array with XMLDecls, Text, DocType, Array, Text + end +end diff --git a/gxg/deps/rexml/parsers/xpathparser.rb b/gxg/deps/rexml/parsers/xpathparser.rb new file mode 100644 index 0000000..5567ade --- /dev/null +++ b/gxg/deps/rexml/parsers/xpathparser.rb @@ -0,0 +1,657 @@ +# frozen_string_literal: false +require 'deps/rexml/namespace' +require 'deps/rexml/xmltokens' + +module REXML + module Parsers + # You don't want to use this class. Really. Use XPath, which is a wrapper + # for this class. Believe me. You don't want to poke around in here. + # There is strange, dark magic at work in this code. Beware. Go back! Go + # back while you still can! + class XPathParser + include XMLTokens + LITERAL = /^'([^']*)'|^"([^"]*)"/u + + def namespaces=( namespaces ) + Functions::namespace_context = namespaces + @namespaces = namespaces + end + + def parse path + path = path.dup + path.gsub!(/([\(\[])\s+/, '\1') # Strip ignorable spaces + path.gsub!( /\s+([\]\)])/, '\1') + parsed = [] + OrExpr(path, parsed) + parsed + end + + def predicate path + parsed = [] + Predicate( "[#{path}]", parsed ) + parsed + end + + def abbreviate( path ) + path = path.kind_of?(String) ? parse( path ) : path + string = "" + document = false + while path.size > 0 + op = path.shift + case op + when :node + when :attribute + string << "/" if string.size > 0 + string << "@" + when :child + string << "/" if string.size > 0 + when :descendant_or_self + string << "/" + when :self + string << "." + when :parent + string << ".." + when :any + string << "*" + when :text + string << "text()" + when :following, :following_sibling, + :ancestor, :ancestor_or_self, :descendant, + :namespace, :preceding, :preceding_sibling + string << "/" unless string.size == 0 + string << op.to_s.tr("_", "-") + string << "::" + when :qname + prefix = path.shift + name = path.shift + string << prefix+":" if prefix.size > 0 + string << name + when :predicate + string << '[' + string << predicate_to_string( path.shift ) {|x| abbreviate( x ) } + string << ']' + when :document + document = true + when :function + string << path.shift + string << "( " + string << predicate_to_string( path.shift[0] ) {|x| abbreviate( x )} + string << " )" + when :literal + string << %Q{ "#{path.shift}" } + else + string << "/" unless string.size == 0 + string << "UNKNOWN(" + string << op.inspect + string << ")" + end + end + string = "/"+string if document + return string + end + + def expand( path ) + path = path.kind_of?(String) ? parse( path ) : path + string = "" + document = false + while path.size > 0 + op = path.shift + case op + when :node + string << "node()" + when :attribute, :child, :following, :following_sibling, + :ancestor, :ancestor_or_self, :descendant, :descendant_or_self, + :namespace, :preceding, :preceding_sibling, :self, :parent + string << "/" unless string.size == 0 + string << op.to_s.tr("_", "-") + string << "::" + when :any + string << "*" + when :qname + prefix = path.shift + name = path.shift + string << prefix+":" if prefix.size > 0 + string << name + when :predicate + string << '[' + string << predicate_to_string( path.shift ) { |x| expand(x) } + string << ']' + when :document + document = true + else + string << "/" unless string.size == 0 + string << "UNKNOWN(" + string << op.inspect + string << ")" + end + end + string = "/"+string if document + return string + end + + def predicate_to_string( path, &block ) + string = "" + case path[0] + when :and, :or, :mult, :plus, :minus, :neq, :eq, :lt, :gt, :lteq, :gteq, :div, :mod, :union + op = path.shift + case op + when :eq + op = "=" + when :lt + op = "<" + when :gt + op = ">" + when :lteq + op = "<=" + when :gteq + op = ">=" + when :neq + op = "!=" + when :union + op = "|" + end + left = predicate_to_string( path.shift, &block ) + right = predicate_to_string( path.shift, &block ) + string << " " + string << left + string << " " + string << op.to_s + string << " " + string << right + string << " " + when :function + path.shift + name = path.shift + string << name + string << "( " + string << predicate_to_string( path.shift, &block ) + string << " )" + when :literal + path.shift + string << " " + string << path.shift.inspect + string << " " + else + string << " " + string << yield( path ) + string << " " + end + return string.squeeze(" ") + end + + private + #LocationPath + # | RelativeLocationPath + # | '/' RelativeLocationPath? + # | '//' RelativeLocationPath + def LocationPath path, parsed + path = path.strip + if path[0] == ?/ + parsed << :document + if path[1] == ?/ + parsed << :descendant_or_self + parsed << :node + path = path[2..-1] + else + path = path[1..-1] + end + end + return RelativeLocationPath( path, parsed ) if path.size > 0 + end + + #RelativeLocationPath + # | Step + # | (AXIS_NAME '::' | '@' | '') AxisSpecifier + # NodeTest + # Predicate + # | '.' | '..' AbbreviatedStep + # | RelativeLocationPath '/' Step + # | RelativeLocationPath '//' Step + AXIS = /^(ancestor|ancestor-or-self|attribute|child|descendant|descendant-or-self|following|following-sibling|namespace|parent|preceding|preceding-sibling|self)::/ + def RelativeLocationPath path, parsed + while path.size > 0 + # (axis or @ or ) nodetest predicate > + # OR > / Step + # (. or ..) > + if path[0] == ?. + if path[1] == ?. + parsed << :parent + parsed << :node + path = path[2..-1] + else + parsed << :self + parsed << :node + path = path[1..-1] + end + else + if path[0] == ?@ + parsed << :attribute + path = path[1..-1] + # Goto Nodetest + elsif path =~ AXIS + parsed << $1.tr('-','_').intern + path = $' + # Goto Nodetest + else + parsed << :child + end + + n = [] + path = NodeTest( path, n) + + if path[0] == ?[ + path = Predicate( path, n ) + end + + parsed.concat(n) + end + + if path.size > 0 + if path[0] == ?/ + if path[1] == ?/ + parsed << :descendant_or_self + parsed << :node + path = path[2..-1] + else + path = path[1..-1] + end + else + return path + end + end + end + return path + end + + # Returns a 1-1 map of the nodeset + # The contents of the resulting array are either: + # true/false, if a positive match + # String, if a name match + #NodeTest + # | ('*' | NCNAME ':' '*' | QNAME) NameTest + # | NODE_TYPE '(' ')' NodeType + # | PI '(' LITERAL ')' PI + # | '[' expr ']' Predicate + NCNAMETEST= /^(#{NCNAME_STR}):\*/u + QNAME = Namespace::NAMESPLIT + NODE_TYPE = /^(comment|text|node)\(\s*\)/m + PI = /^processing-instruction\(/ + def NodeTest path, parsed + case path + when /^\*/ + path = $' + parsed << :any + when NODE_TYPE + type = $1 + path = $' + parsed << type.tr('-', '_').intern + when PI + path = $' + literal = nil + if path !~ /^\s*\)/ + path =~ LITERAL + literal = $1 + path = $' + raise ParseException.new("Missing ')' after processing instruction") if path[0] != ?) + path = path[1..-1] + end + parsed << :processing_instruction + parsed << (literal || '') + when NCNAMETEST + prefix = $1 + path = $' + parsed << :namespace + parsed << prefix + when QNAME + prefix = $1 + name = $2 + path = $' + prefix = "" unless prefix + parsed << :qname + parsed << prefix + parsed << name + end + return path + end + + # Filters the supplied nodeset on the predicate(s) + def Predicate path, parsed + return nil unless path[0] == ?[ + predicates = [] + while path[0] == ?[ + path, expr = get_group(path) + predicates << expr[1..-2] if expr + end + predicates.each{ |pred| + preds = [] + parsed << :predicate + parsed << preds + OrExpr(pred, preds) + } + path + end + + # The following return arrays of true/false, a 1-1 mapping of the + # supplied nodeset, except for axe(), which returns a filtered + # nodeset + + #| OrExpr S 'or' S AndExpr + #| AndExpr + def OrExpr path, parsed + n = [] + rest = AndExpr( path, n ) + if rest != path + while rest =~ /^\s*( or )/ + n = [ :or, n, [] ] + rest = AndExpr( $', n[-1] ) + end + end + if parsed.size == 0 and n.size != 0 + parsed.replace(n) + elsif n.size > 0 + parsed << n + end + rest + end + + #| AndExpr S 'and' S EqualityExpr + #| EqualityExpr + def AndExpr path, parsed + n = [] + rest = EqualityExpr( path, n ) + if rest != path + while rest =~ /^\s*( and )/ + n = [ :and, n, [] ] + rest = EqualityExpr( $', n[-1] ) + end + end + if parsed.size == 0 and n.size != 0 + parsed.replace(n) + elsif n.size > 0 + parsed << n + end + rest + end + + #| EqualityExpr ('=' | '!=') RelationalExpr + #| RelationalExpr + def EqualityExpr path, parsed + n = [] + rest = RelationalExpr( path, n ) + if rest != path + while rest =~ /^\s*(!?=)\s*/ + if $1[0] == ?! + n = [ :neq, n, [] ] + else + n = [ :eq, n, [] ] + end + rest = RelationalExpr( $', n[-1] ) + end + end + if parsed.size == 0 and n.size != 0 + parsed.replace(n) + elsif n.size > 0 + parsed << n + end + rest + end + + #| RelationalExpr ('<' | '>' | '<=' | '>=') AdditiveExpr + #| AdditiveExpr + def RelationalExpr path, parsed + n = [] + rest = AdditiveExpr( path, n ) + if rest != path + while rest =~ /^\s*([<>]=?)\s*/ + if $1[0] == ?< + sym = "lt" + else + sym = "gt" + end + sym << "eq" if $1[-1] == ?= + n = [ sym.intern, n, [] ] + rest = AdditiveExpr( $', n[-1] ) + end + end + if parsed.size == 0 and n.size != 0 + parsed.replace(n) + elsif n.size > 0 + parsed << n + end + rest + end + + #| AdditiveExpr ('+' | S '-') MultiplicativeExpr + #| MultiplicativeExpr + def AdditiveExpr path, parsed + n = [] + rest = MultiplicativeExpr( path, n ) + if rest != path + while rest =~ /^\s*(\+| -)\s*/ + if $1[0] == ?+ + n = [ :plus, n, [] ] + else + n = [ :minus, n, [] ] + end + rest = MultiplicativeExpr( $', n[-1] ) + end + end + if parsed.size == 0 and n.size != 0 + parsed.replace(n) + elsif n.size > 0 + parsed << n + end + rest + end + + #| MultiplicativeExpr ('*' | S ('div' | 'mod') S) UnaryExpr + #| UnaryExpr + def MultiplicativeExpr path, parsed + n = [] + rest = UnaryExpr( path, n ) + if rest != path + while rest =~ /^\s*(\*| div | mod )\s*/ + if $1[0] == ?* + n = [ :mult, n, [] ] + elsif $1.include?( "div" ) + n = [ :div, n, [] ] + else + n = [ :mod, n, [] ] + end + rest = UnaryExpr( $', n[-1] ) + end + end + if parsed.size == 0 and n.size != 0 + parsed.replace(n) + elsif n.size > 0 + parsed << n + end + rest + end + + #| '-' UnaryExpr + #| UnionExpr + def UnaryExpr path, parsed + path =~ /^(\-*)/ + path = $' + if $1 and (($1.size % 2) != 0) + mult = -1 + else + mult = 1 + end + parsed << :neg if mult < 0 + + n = [] + path = UnionExpr( path, n ) + parsed.concat( n ) + path + end + + #| UnionExpr '|' PathExpr + #| PathExpr + def UnionExpr path, parsed + n = [] + rest = PathExpr( path, n ) + if rest != path + while rest =~ /^\s*(\|)\s*/ + n = [ :union, n, [] ] + rest = PathExpr( $', n[-1] ) + end + end + if parsed.size == 0 and n.size != 0 + parsed.replace( n ) + elsif n.size > 0 + parsed << n + end + rest + end + + #| LocationPath + #| FilterExpr ('/' | '//') RelativeLocationPath + def PathExpr path, parsed + path =~ /^\s*/ + path = $' + n = [] + rest = FilterExpr( path, n ) + if rest != path + if rest and rest[0] == ?/ + return RelativeLocationPath(rest, n) + end + end + rest = LocationPath(rest, n) if rest =~ /\A[\/\.\@\[\w*]/ + parsed.concat(n) + return rest + end + + #| FilterExpr Predicate + #| PrimaryExpr + def FilterExpr path, parsed + n = [] + path = PrimaryExpr( path, n ) + path = Predicate(path, n) if path and path[0] == ?[ + parsed.concat(n) + path + end + + #| VARIABLE_REFERENCE + #| '(' expr ')' + #| LITERAL + #| NUMBER + #| FunctionCall + VARIABLE_REFERENCE = /^\$(#{NAME_STR})/u + NUMBER = /^(\d*\.?\d+)/ + NT = /^comment|text|processing-instruction|node$/ + def PrimaryExpr path, parsed + case path + when VARIABLE_REFERENCE + varname = $1 + path = $' + parsed << :variable + parsed << varname + #arry << @variables[ varname ] + when /^(\w[-\w]*)(?:\()/ + fname = $1 + tmp = $' + return path if fname =~ NT + path = tmp + parsed << :function + parsed << fname + path = FunctionCall(path, parsed) + when NUMBER + varname = $1.nil? ? $2 : $1 + path = $' + parsed << :literal + parsed << (varname.include?('.') ? varname.to_f : varname.to_i) + when LITERAL + varname = $1.nil? ? $2 : $1 + path = $' + parsed << :literal + parsed << varname + when /^\(/ #/ + path, contents = get_group(path) + contents = contents[1..-2] + n = [] + OrExpr( contents, n ) + parsed.concat(n) + end + path + end + + #| FUNCTION_NAME '(' ( expr ( ',' expr )* )? ')' + def FunctionCall rest, parsed + path, arguments = parse_args(rest) + argset = [] + for argument in arguments + args = [] + OrExpr( argument, args ) + argset << args + end + parsed << argset + path + end + + # get_group( '[foo]bar' ) -> ['bar', '[foo]'] + def get_group string + ind = 0 + depth = 0 + st = string[0,1] + en = (st == "(" ? ")" : "]") + begin + case string[ind,1] + when st + depth += 1 + when en + depth -= 1 + end + ind += 1 + end while depth > 0 and ind < string.length + return nil unless depth==0 + [string[ind..-1], string[0..ind-1]] + end + + def parse_args( string ) + arguments = [] + ind = 0 + inquot = false + inapos = false + depth = 1 + begin + case string[ind] + when ?" + inquot = !inquot unless inapos + when ?' + inapos = !inapos unless inquot + else + unless inquot or inapos + case string[ind] + when ?( + depth += 1 + if depth == 1 + string = string[1..-1] + ind -= 1 + end + when ?) + depth -= 1 + if depth == 0 + s = string[0,ind].strip + arguments << s unless s == "" + string = string[ind+1..-1] + end + when ?, + if depth == 1 + s = string[0,ind].strip + arguments << s unless s == "" + string = string[ind+1..-1] + ind = -1 + end + end + end + end + ind += 1 + end while depth > 0 and ind < string.length + return nil unless depth==0 + [string,arguments] + end + end + end +end diff --git a/gxg/deps/rexml/quickpath.rb b/gxg/deps/rexml/quickpath.rb new file mode 100644 index 0000000..b727f24 --- /dev/null +++ b/gxg/deps/rexml/quickpath.rb @@ -0,0 +1,266 @@ +# frozen_string_literal: false +require 'deps/rexml/functions' +require 'deps/rexml/xmltokens' + +module REXML + class QuickPath + include Functions + include XMLTokens + + # A base Hash object to be used when initializing a + # default empty namespaces set. + EMPTY_HASH = {} + + def QuickPath::first element, path, namespaces=EMPTY_HASH + match(element, path, namespaces)[0] + end + + def QuickPath::each element, path, namespaces=EMPTY_HASH, &block + path = "*" unless path + match(element, path, namespaces).each( &block ) + end + + def QuickPath::match element, path, namespaces=EMPTY_HASH + raise "nil is not a valid xpath" unless path + results = nil + Functions::namespace_context = namespaces + case path + when /^\/([^\/]|$)/u + # match on root + path = path[1..-1] + return [element.root.parent] if path == '' + results = filter([element.root], path) + when /^[-\w]*::/u + results = filter([element], path) + when /^\*/u + results = filter(element.to_a, path) + when /^[\[!\w:]/u + # match on child + children = element.to_a + results = filter(children, path) + else + results = filter([element], path) + end + return results + end + + # Given an array of nodes it filters the array based on the path. The + # result is that when this method returns, the array will contain elements + # which match the path + def QuickPath::filter elements, path + return elements if path.nil? or path == '' or elements.size == 0 + case path + when /^\/\//u # Descendant + return axe( elements, "descendant-or-self", $' ) + when /^\/?\b(\w[-\w]*)\b::/u # Axe + return axe( elements, $1, $' ) + when /^\/(?=\b([:!\w][-\.\w]*:)?[-!\*\.\w]*\b([^:(]|$)|\*)/u # Child + rest = $' + results = [] + elements.each do |element| + results |= filter( element.to_a, rest ) + end + return results + when /^\/?(\w[-\w]*)\(/u # / Function + return function( elements, $1, $' ) + when Namespace::NAMESPLIT # Element name + name = $2 + ns = $1 + rest = $' + elements.delete_if do |element| + !(element.kind_of? Element and + (element.expanded_name == name or + (element.name == name and + element.namespace == Functions.namespace_context[ns]))) + end + return filter( elements, rest ) + when /^\/\[/u + matches = [] + elements.each do |element| + matches |= predicate( element.to_a, path[1..-1] ) if element.kind_of? Element + end + return matches + when /^\[/u # Predicate + return predicate( elements, path ) + when /^\/?\.\.\./u # Ancestor + return axe( elements, "ancestor", $' ) + when /^\/?\.\./u # Parent + return filter( elements.collect{|e|e.parent}, $' ) + when /^\/?\./u # Self + return filter( elements, $' ) + when /^\*/u # Any + results = [] + elements.each do |element| + results |= filter( [element], $' ) if element.kind_of? Element + #if element.kind_of? Element + # children = element.to_a + # children.delete_if { |child| !child.kind_of?(Element) } + # results |= filter( children, $' ) + #end + end + return results + end + return [] + end + + def QuickPath::axe( elements, axe_name, rest ) + matches = [] + matches = filter( elements.dup, rest ) if axe_name =~ /-or-self$/u + case axe_name + when /^descendant/u + elements.each do |element| + matches |= filter( element.to_a, "descendant-or-self::#{rest}" ) if element.kind_of? Element + end + when /^ancestor/u + elements.each do |element| + while element.parent + matches << element.parent + element = element.parent + end + end + matches = filter( matches, rest ) + when "self" + matches = filter( elements, rest ) + when "child" + elements.each do |element| + matches |= filter( element.to_a, rest ) if element.kind_of? Element + end + when "attribute" + elements.each do |element| + matches << element.attributes[ rest ] if element.kind_of? Element + end + when "parent" + matches = filter(elements.collect{|element| element.parent}.uniq, rest) + when "following-sibling" + matches = filter(elements.collect{|element| element.next_sibling}.uniq, + rest) + when "previous-sibling" + matches = filter(elements.collect{|element| + element.previous_sibling}.uniq, rest ) + end + return matches.uniq + end + + OPERAND_ = '((?=(?:(?!and|or).)*[^\s<>=])[^\s<>=]+)' + # A predicate filters a node-set with respect to an axis to produce a + # new node-set. For each node in the node-set to be filtered, the + # PredicateExpr is evaluated with that node as the context node, with + # the number of nodes in the node-set as the context size, and with the + # proximity position of the node in the node-set with respect to the + # axis as the context position; if PredicateExpr evaluates to true for + # that node, the node is included in the new node-set; otherwise, it is + # not included. + # + # A PredicateExpr is evaluated by evaluating the Expr and converting + # the result to a boolean. If the result is a number, the result will + # be converted to true if the number is equal to the context position + # and will be converted to false otherwise; if the result is not a + # number, then the result will be converted as if by a call to the + # boolean function. Thus a location path para[3] is equivalent to + # para[position()=3]. + def QuickPath::predicate( elements, path ) + ind = 1 + bcount = 1 + while bcount > 0 + bcount += 1 if path[ind] == ?[ + bcount -= 1 if path[ind] == ?] + ind += 1 + end + ind -= 1 + predicate = path[1..ind-1] + rest = path[ind+1..-1] + + # have to change 'a [=<>] b [=<>] c' into 'a [=<>] b and b [=<>] c' + # + predicate.gsub!( + /#{OPERAND_}\s*([<>=])\s*#{OPERAND_}\s*([<>=])\s*#{OPERAND_}/u, + '\1 \2 \3 and \3 \4 \5' ) + # Let's do some Ruby trickery to avoid some work: + predicate.gsub!( /&/u, "&&" ) + predicate.gsub!( /=/u, "==" ) + predicate.gsub!( /@(\w[-\w.]*)/u, 'attribute("\1")' ) + predicate.gsub!( /\bmod\b/u, "%" ) + predicate.gsub!( /\b(\w[-\w.]*\()/u ) { + fname = $1 + fname.gsub( /-/u, "_" ) + } + + Functions.pair = [ 0, elements.size ] + results = [] + elements.each do |element| + Functions.pair[0] += 1 + Functions.node = element + res = eval( predicate ) + case res + when true + results << element + when Integer + results << element if Functions.pair[0] == res + when String + results << element + end + end + return filter( results, rest ) + end + + def QuickPath::attribute( name ) + return Functions.node.attributes[name] if Functions.node.kind_of? Element + end + + def QuickPath::name() + return Functions.node.name if Functions.node.kind_of? Element + end + + def QuickPath::method_missing( id, *args ) + begin + Functions.send( id.id2name, *args ) + rescue Exception + raise "METHOD: #{id.id2name}(#{args.join ', '})\n#{$!.message}" + end + end + + def QuickPath::function( elements, fname, rest ) + args = parse_args( elements, rest ) + Functions.pair = [0, elements.size] + results = [] + elements.each do |element| + Functions.pair[0] += 1 + Functions.node = element + res = Functions.send( fname, *args ) + case res + when true + results << element + when Integer + results << element if Functions.pair[0] == res + end + end + return results + end + + def QuickPath::parse_args( element, string ) + # /.*?(?:\)|,)/ + arguments = [] + buffer = "" + while string and string != "" + c = string[0] + string.sub!(/^./u, "") + case c + when ?, + # if depth = 1, then we start a new argument + arguments << evaluate( buffer ) + #arguments << evaluate( string[0..count] ) + when ?( + # start a new method call + function( element, buffer, string ) + buffer = "" + when ?) + # close the method call and return arguments + return arguments + else + buffer << c + end + end + "" + end + end +end diff --git a/gxg/deps/rexml/rexml.rb b/gxg/deps/rexml/rexml.rb new file mode 100644 index 0000000..bc78f70 --- /dev/null +++ b/gxg/deps/rexml/rexml.rb @@ -0,0 +1,32 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +# REXML is an XML toolkit for Ruby[http://www.ruby-lang.org], in Ruby. +# +# REXML is a _pure_ Ruby, XML 1.0 conforming, +# non-validating[http://www.w3.org/TR/2004/REC-xml-20040204/#sec-conformance] +# toolkit with an intuitive API. REXML passes 100% of the non-validating Oasis +# tests[http://www.oasis-open.org/committees/xml-conformance/xml-test-suite.shtml], +# and provides tree, stream, SAX2, pull, and lightweight APIs. REXML also +# includes a full XPath[http://www.w3c.org/tr/xpath] 1.0 implementation. Since +# Ruby 1.8, REXML is included in the standard Ruby distribution. +# +# Main page:: http://www.germane-software.com/software/rexml +# Author:: Sean Russell +# Date:: 2008/019 +# Version:: 3.1.7.3 +# +# This API documentation can be downloaded from the REXML home page, or can +# be accessed online[http://www.germane-software.com/software/rexml_doc] +# +# A tutorial is available in the REXML distribution in docs/tutorial.html, +# or can be accessed +# online[http://www.germane-software.com/software/rexml/docs/tutorial.html] +module REXML + COPYRIGHT = "Copyright © 2001-2008 Sean Russell " + DATE = "2008/019" + VERSION = "3.1.7.3" + REVISION = %w$Revision: 53141 $[1] || '' + + Copyright = COPYRIGHT + Version = VERSION +end diff --git a/gxg/deps/rexml/sax2listener.rb b/gxg/deps/rexml/sax2listener.rb new file mode 100644 index 0000000..5afdc80 --- /dev/null +++ b/gxg/deps/rexml/sax2listener.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: false +module REXML + # A template for stream parser listeners. + # Note that the declarations (attlistdecl, elementdecl, etc) are trivially + # processed; REXML doesn't yet handle doctype entity declarations, so you + # have to parse them out yourself. + # === Missing methods from SAX2 + # ignorable_whitespace + # === Methods extending SAX2 + # +WARNING+ + # These methods are certainly going to change, until DTDs are fully + # supported. Be aware of this. + # start_document + # end_document + # doctype + # elementdecl + # attlistdecl + # entitydecl + # notationdecl + # cdata + # xmldecl + # comment + module SAX2Listener + def start_document + end + def end_document + end + def start_prefix_mapping prefix, uri + end + def end_prefix_mapping prefix + end + def start_element uri, localname, qname, attributes + end + def end_element uri, localname, qname + end + def characters text + end + def processing_instruction target, data + end + # Handles a doctype declaration. Any attributes of the doctype which are + # not supplied will be nil. # EG, + # @p name the name of the doctype; EG, "me" + # @p pub_sys "PUBLIC", "SYSTEM", or nil. EG, "PUBLIC" + # @p long_name the supplied long name, or nil. EG, "foo" + # @p uri the uri of the doctype, or nil. EG, "bar" + def doctype name, pub_sys, long_name, uri + end + # If a doctype includes an ATTLIST declaration, it will cause this + # method to be called. The content is the declaration itself, unparsed. + # EG, will come to this method as "el + # attr CDATA #REQUIRED". This is the same for all of the .*decl + # methods. + def attlistdecl(element, pairs, contents) + end + # + def elementdecl content + end + # + # The argument passed to this method is an array of the entity + # declaration. It can be in a number of formats, but in general it + # returns (example, result): + # + # ["%", "YN", "\"Yes\""] + # + # ["%", "YN", "Yes"] + # + # ["WhatHeSaid", "He said %YN;"] + # + # ["open-hatch", "SYSTEM", "http://www.textuality.com/boilerplate/OpenHatch.xml"] + # + # ["open-hatch", "PUBLIC", "-//Textuality//TEXT Standard open-hatch boilerplate//EN", "http://www.textuality.com/boilerplate/OpenHatch.xml"] + # + # ["hatch-pic", "SYSTEM", "../grafix/OpenHatch.gif", "NDATA", "gif"] + def entitydecl declaration + end + # + def notationdecl name, public_or_system, public_id, system_id + end + # Called when is encountered in a document. + # @p content "..." + def cdata content + end + # Called when an XML PI is encountered in the document. + # EG: + # @p version the version attribute value. EG, "1.0" + # @p encoding the encoding attribute value, or nil. EG, "utf" + # @p standalone the standalone attribute value, or nil. EG, nil + # @p spaced the declaration is followed by a line break + def xmldecl version, encoding, standalone + end + # Called when a comment is encountered. + # @p comment The content of the comment + def comment comment + end + def progress position + end + end +end diff --git a/gxg/deps/rexml/security.rb b/gxg/deps/rexml/security.rb new file mode 100644 index 0000000..99b7460 --- /dev/null +++ b/gxg/deps/rexml/security.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: false +module REXML + module Security + @@entity_expansion_limit = 10_000 + + # Set the entity expansion limit. By default the limit is set to 10000. + def self.entity_expansion_limit=( val ) + @@entity_expansion_limit = val + end + + # Get the entity expansion limit. By default the limit is set to 10000. + def self.entity_expansion_limit + return @@entity_expansion_limit + end + + @@entity_expansion_text_limit = 10_240 + + # Set the entity expansion limit. By default the limit is set to 10240. + def self.entity_expansion_text_limit=( val ) + @@entity_expansion_text_limit = val + end + + # Get the entity expansion limit. By default the limit is set to 10240. + def self.entity_expansion_text_limit + return @@entity_expansion_text_limit + end + end +end diff --git a/gxg/deps/rexml/source.rb b/gxg/deps/rexml/source.rb new file mode 100644 index 0000000..680db4e --- /dev/null +++ b/gxg/deps/rexml/source.rb @@ -0,0 +1,297 @@ +# coding: US-ASCII +# frozen_string_literal: false +require 'deps/rexml/encoding' + +module REXML + # Generates Source-s. USE THIS CLASS. + class SourceFactory + # Generates a Source object + # @param arg Either a String, or an IO + # @return a Source, or nil if a bad argument was given + def SourceFactory::create_from(arg) + if arg.respond_to? :read and + arg.respond_to? :readline and + arg.respond_to? :nil? and + arg.respond_to? :eof? + IOSource.new(arg) + elsif arg.respond_to? :to_str + require 'stringio' + IOSource.new(StringIO.new(arg)) + elsif arg.kind_of? Source + arg + else + raise "#{arg.class} is not a valid input stream. It must walk \n"+ + "like either a String, an IO, or a Source." + end + end + end + + # A Source can be searched for patterns, and wraps buffers and other + # objects and provides consumption of text + class Source + include Encoding + # The current buffer (what we're going to read next) + attr_reader :buffer + # The line number of the last consumed text + attr_reader :line + attr_reader :encoding + + # Constructor + # @param arg must be a String, and should be a valid XML document + # @param encoding if non-null, sets the encoding of the source to this + # value, overriding all encoding detection + def initialize(arg, encoding=nil) + @orig = @buffer = arg + if encoding + self.encoding = encoding + else + detect_encoding + end + @line = 0 + end + + + # Inherited from Encoding + # Overridden to support optimized en/decoding + def encoding=(enc) + return unless super + encoding_updated + end + + # Scans the source for a given pattern. Note, that this is not your + # usual scan() method. For one thing, the pattern argument has some + # requirements; for another, the source can be consumed. You can easily + # confuse this method. Originally, the patterns were easier + # to construct and this method more robust, because this method + # generated search regexps on the fly; however, this was + # computationally expensive and slowed down the entire REXML package + # considerably, since this is by far the most commonly called method. + # @param pattern must be a Regexp, and must be in the form of + # /^\s*(#{your pattern, with no groups})(.*)/. The first group + # will be returned; the second group is used if the consume flag is + # set. + # @param consume if true, the pattern returned will be consumed, leaving + # everything after it in the Source. + # @return the pattern, if found, or nil if the Source is empty or the + # pattern is not found. + def scan(pattern, cons=false) + return nil if @buffer.nil? + rv = @buffer.scan(pattern) + @buffer = $' if cons and rv.size>0 + rv + end + + def read + end + + def consume( pattern ) + @buffer = $' if pattern.match( @buffer ) + end + + def match_to( char, pattern ) + return pattern.match(@buffer) + end + + def match_to_consume( char, pattern ) + md = pattern.match(@buffer) + @buffer = $' + return md + end + + def match(pattern, cons=false) + md = pattern.match(@buffer) + @buffer = $' if cons and md + return md + end + + # @return true if the Source is exhausted + def empty? + @buffer == "" + end + + def position + @orig.index( @buffer ) + end + + # @return the current line in the source + def current_line + lines = @orig.split + res = lines.grep @buffer[0..30] + res = res[-1] if res.kind_of? Array + lines.index( res ) if res + end + + private + def detect_encoding + buffer_encoding = @buffer.encoding + detected_encoding = "UTF-8" + begin + @buffer.force_encoding("ASCII-8BIT") + if @buffer[0, 2] == (254.chr + 255.chr) + @buffer[0, 2] = "" + detected_encoding = "UTF-16BE" + elsif @buffer[0, 2] == (255.chr + 254.chr) + @buffer[0, 2] = "" + detected_encoding = "UTF-16LE" + elsif @buffer[0, 3] == "\xef\xbb\xbf" + @buffer[0, 3] = "" + detected_encoding = "UTF-8" + end + ensure + @buffer.force_encoding(buffer_encoding) + end + self.encoding = detected_encoding + end + + def encoding_updated + if @encoding != 'UTF-8' + @buffer = decode(@buffer) + @to_utf = true + else + @to_utf = false + @buffer.force_encoding ::Encoding::UTF_8 + end + end + end + + # A Source that wraps an IO. See the Source class for method + # documentation + class IOSource < Source + #attr_reader :block_size + + # block_size has been deprecated + def initialize(arg, block_size=500, encoding=nil) + @er_source = @source = arg + @to_utf = false + @pending_buffer = nil + + if encoding + super("", encoding) + else + super(@source.read(3) || "") + end + + if !@to_utf and + @buffer.respond_to?(:force_encoding) and + @source.respond_to?(:external_encoding) and + @source.external_encoding != ::Encoding::UTF_8 + @force_utf8 = true + else + @force_utf8 = false + end + end + + def scan(pattern, cons=false) + rv = super + # You'll notice that this next section is very similar to the same + # section in match(), but just a liiittle different. This is + # because it is a touch faster to do it this way with scan() + # than the way match() does it; enough faster to warrant duplicating + # some code + if rv.size == 0 + until @buffer =~ pattern or @source.nil? + begin + @buffer << readline + rescue Iconv::IllegalSequence + raise + rescue + @source = nil + end + end + rv = super + end + rv.taint + rv + end + + def read + begin + @buffer << readline + rescue Exception, NameError + @source = nil + end + end + + def consume( pattern ) + match( pattern, true ) + end + + def match( pattern, cons=false ) + rv = pattern.match(@buffer) + @buffer = $' if cons and rv + while !rv and @source + begin + @buffer << readline + rv = pattern.match(@buffer) + @buffer = $' if cons and rv + rescue + @source = nil + end + end + rv.taint + rv + end + + def empty? + super and ( @source.nil? || @source.eof? ) + end + + def position + @er_source.pos rescue 0 + end + + # @return the current line in the source + def current_line + begin + pos = @er_source.pos # The byte position in the source + lineno = @er_source.lineno # The XML < position in the source + @er_source.rewind + line = 0 # The \r\n position in the source + begin + while @er_source.pos < pos + @er_source.readline + line += 1 + end + rescue + end + rescue IOError + pos = -1 + line = -1 + end + [pos, lineno, line] + end + + private + def readline + str = @source.readline(@line_break) + if @pending_buffer + if str.nil? + str = @pending_buffer + else + str = @pending_buffer + str + end + @pending_buffer = nil + end + return nil if str.nil? + + if @to_utf + decode(str) + else + str.force_encoding(::Encoding::UTF_8) if @force_utf8 + str + end + end + + def encoding_updated + case @encoding + when "UTF-16BE", "UTF-16LE" + @source.binmode + @source.set_encoding(@encoding, @encoding) + end + @line_break = encode(">") + @pending_buffer, @buffer = @buffer, "" + @pending_buffer.force_encoding(@encoding) + super + end + end +end diff --git a/gxg/deps/rexml/streamlistener.rb b/gxg/deps/rexml/streamlistener.rb new file mode 100644 index 0000000..30c8945 --- /dev/null +++ b/gxg/deps/rexml/streamlistener.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: false +module REXML + # A template for stream parser listeners. + # Note that the declarations (attlistdecl, elementdecl, etc) are trivially + # processed; REXML doesn't yet handle doctype entity declarations, so you + # have to parse them out yourself. + module StreamListener + # Called when a tag is encountered. + # @p name the tag name + # @p attrs an array of arrays of attribute/value pairs, suitable for + # use with assoc or rassoc. IE, + # will result in + # tag_start( "tag", # [["attr1","value1"],["attr2","value2"]]) + def tag_start name, attrs + end + # Called when the end tag is reached. In the case of , tag_end + # will be called immediately after tag_start + # @p the name of the tag + def tag_end name + end + # Called when text is encountered in the document + # @p text the text content. + def text text + end + # Called when an instruction is encountered. EG: + # @p name the instruction name; in the example, "xsl" + # @p instruction the rest of the instruction. In the example, + # "sheet='foo'" + def instruction name, instruction + end + # Called when a comment is encountered. + # @p comment The content of the comment + def comment comment + end + # Handles a doctype declaration. Any attributes of the doctype which are + # not supplied will be nil. # EG, + # @p name the name of the doctype; EG, "me" + # @p pub_sys "PUBLIC", "SYSTEM", or nil. EG, "PUBLIC" + # @p long_name the supplied long name, or nil. EG, "foo" + # @p uri the uri of the doctype, or nil. EG, "bar" + def doctype name, pub_sys, long_name, uri + end + # Called when the doctype is done + def doctype_end + end + # If a doctype includes an ATTLIST declaration, it will cause this + # method to be called. The content is the declaration itself, unparsed. + # EG, will come to this method as "el + # attr CDATA #REQUIRED". This is the same for all of the .*decl + # methods. + def attlistdecl element_name, attributes, raw_content + end + # + def elementdecl content + end + # + # The argument passed to this method is an array of the entity + # declaration. It can be in a number of formats, but in general it + # returns (example, result): + # + # ["YN", "\"Yes\"", "%"] + # + # ["YN", "Yes", "%"] + # + # ["WhatHeSaid", "He said %YN;"] + # + # ["open-hatch", "SYSTEM", "http://www.textuality.com/boilerplate/OpenHatch.xml"] + # + # ["open-hatch", "PUBLIC", "-//Textuality//TEXT Standard open-hatch boilerplate//EN", "http://www.textuality.com/boilerplate/OpenHatch.xml"] + # + # ["hatch-pic", "SYSTEM", "../grafix/OpenHatch.gif", "gif"] + def entitydecl content + end + # + def notationdecl content + end + # Called when %foo; is encountered in a doctype declaration. + # @p content "foo" + def entity content + end + # Called when is encountered in a document. + # @p content "..." + def cdata content + end + # Called when an XML PI is encountered in the document. + # EG: + # @p version the version attribute value. EG, "1.0" + # @p encoding the encoding attribute value, or nil. EG, "utf" + # @p standalone the standalone attribute value, or nil. EG, nil + def xmldecl version, encoding, standalone + end + end +end diff --git a/gxg/deps/rexml/syncenumerator.rb b/gxg/deps/rexml/syncenumerator.rb new file mode 100644 index 0000000..a9d2ad7 --- /dev/null +++ b/gxg/deps/rexml/syncenumerator.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: false +module REXML + class SyncEnumerator + include Enumerable + + # Creates a new SyncEnumerator which enumerates rows of given + # Enumerable objects. + def initialize(*enums) + @gens = enums + @length = @gens.collect {|x| x.size }.max + end + + # Returns the number of enumerated Enumerable objects, i.e. the size + # of each row. + def size + @gens.size + end + + # Returns the number of enumerated Enumerable objects, i.e. the size + # of each row. + def length + @gens.length + end + + # Enumerates rows of the Enumerable objects. + def each + @length.times {|i| + yield @gens.collect {|x| x[i]} + } + self + end + end +end diff --git a/gxg/deps/rexml/text.rb b/gxg/deps/rexml/text.rb new file mode 100644 index 0000000..24c7abc --- /dev/null +++ b/gxg/deps/rexml/text.rb @@ -0,0 +1,430 @@ +# frozen_string_literal: false +require 'deps/rexml/security' +require 'deps/rexml/entity' +require 'deps/rexml/doctype' +require 'deps/rexml/child' +require 'deps/rexml/doctype' +require 'deps/rexml/parseexception' + +module REXML + # Represents text nodes in an XML document + class Text < Child + include Comparable + # The order in which the substitutions occur + SPECIALS = [ /&(?!#?[\w-]+;)/u, //u, /"/u, /'/u, /\r/u ] + SUBSTITUTES = ['&', '<', '>', '"', ''', ' '] + # Characters which are substituted in written strings + SLAICEPS = [ '<', '>', '"', "'", '&' ] + SETUTITSBUS = [ /</u, />/u, /"/u, /'/u, /&/u ] + + # If +raw+ is true, then REXML leaves the value alone + attr_accessor :raw + + NEEDS_A_SECOND_CHECK = /(<|&((#{Entity::NAME});|(#0*((?:\d+)|(?:x[a-fA-F0-9]+)));)?)/um + NUMERICENTITY = /�*((?:\d+)|(?:x[a-fA-F0-9]+));/ + VALID_CHAR = [ + 0x9, 0xA, 0xD, + (0x20..0xD7FF), + (0xE000..0xFFFD), + (0x10000..0x10FFFF) + ] + + if String.method_defined? :encode + VALID_XML_CHARS = /^/ +# VALID_XML_CHARS = Regexp.new('^['+ +# VALID_CHAR.map { |item| +# case item +# when Integer +# [item].pack('U').force_encoding('utf-8') +# when Range +# [item.first, '-'.ord, item.last].pack('UUU').force_encoding('utf-8') +# end +# }.join + +# ']*$') + else + VALID_XML_CHARS = /^/ +# VALID_XML_CHARS = /^([\x09\x0A\x0D\x20-\x7E]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE][\x80-\xBF]{2}|\xEF[\x80-\xBE]{2}|\xEF\xBF[\x80-\xBD]|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})/ +# VALID_XML_CHARS = /^( +# [\x09\x0A\x0D\x20-\x7E] # ASCII +# | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte +# | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs +# | [\xE1-\xEC\xEE][\x80-\xBF]{2} # straight 3-byte +# | \xEF[\x80-\xBE]{2} # +# | \xEF\xBF[\x80-\xBD] # excluding U+fffe and U+ffff +# | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates +# | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 +# | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 +# | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 +# )*$/nx; + end + + # Constructor + # +arg+ if a String, the content is set to the String. If a Text, + # the object is shallowly cloned. + # + # +respect_whitespace+ (boolean, false) if true, whitespace is + # respected + # + # +parent+ (nil) if this is a Parent object, the parent + # will be set to this. + # + # +raw+ (nil) This argument can be given three values. + # If true, then the value of used to construct this object is expected to + # contain no unescaped XML markup, and REXML will not change the text. If + # this value is false, the string may contain any characters, and REXML will + # escape any and all defined entities whose values are contained in the + # text. If this value is nil (the default), then the raw value of the + # parent will be used as the raw value for this node. If there is no raw + # value for the parent, and no value is supplied, the default is false. + # Use this field if you have entities defined for some text, and you don't + # want REXML to escape that text in output. + # Text.new( "<&", false, nil, false ) #-> "<&" + # Text.new( "<&", false, nil, false ) #-> "&lt;&amp;" + # Text.new( "<&", false, nil, true ) #-> Parse exception + # Text.new( "<&", false, nil, true ) #-> "<&" + # # Assume that the entity "s" is defined to be "sean" + # # and that the entity "r" is defined to be "russell" + # Text.new( "sean russell" ) #-> "&s; &r;" + # Text.new( "sean russell", false, nil, true ) #-> "sean russell" + # + # +entity_filter+ (nil) This can be an array of entities to match in the + # supplied text. This argument is only useful if +raw+ is set to false. + # Text.new( "sean russell", false, nil, false, ["s"] ) #-> "&s; russell" + # Text.new( "sean russell", false, nil, true, ["s"] ) #-> "sean russell" + # In the last example, the +entity_filter+ argument is ignored. + # + # +illegal+ INTERNAL USE ONLY + def initialize(arg, respect_whitespace=false, parent=nil, raw=nil, + entity_filter=nil, illegal=NEEDS_A_SECOND_CHECK ) + + @raw = false + @parent = nil + + if parent + super( parent ) + @raw = parent.raw + end + + @raw = raw unless raw.nil? + @entity_filter = entity_filter + clear_cache + + if arg.kind_of? String + @string = arg.dup + @string.squeeze!(" \n\t") unless respect_whitespace + elsif arg.kind_of? Text + @string = arg.to_s + @raw = arg.raw + elsif + raise "Illegal argument of type #{arg.type} for Text constructor (#{arg})" + end + + @string.gsub!( /\r\n?/, "\n" ) + + Text.check(@string, illegal, doctype) if @raw + end + + def parent= parent + super(parent) + Text.check(@string, NEEDS_A_SECOND_CHECK, doctype) if @raw and @parent + end + + # check for illegal characters + def Text.check string, pattern, doctype + + # illegal anywhere + if string !~ VALID_XML_CHARS + if String.method_defined? :encode + string.chars.each do |c| + case c.ord + when *VALID_CHAR + else + raise "Illegal character #{c.inspect} in raw string \"#{string}\"" + end + end + else +# string.scan(/[\x00-\x7F]|[\x80-\xBF][\xC0-\xF0]*|[\xC0-\xF0]/n) do |c| + string.chars.each do |c| + case c.unpack('U') + when *VALID_CHAR + else + raise "Illegal character #{c.inspect} in raw string \"#{string}\"" + end + end + end + end + + # context sensitive + string.scan(pattern) do + if $1[-1] != ?; + raise "Illegal character '#{$1}' in raw string \"#{string}\"" + elsif $1[0] == ?& + if $5 and $5[0] == ?# + case ($5[1] == ?x ? $5[2..-1].to_i(16) : $5[1..-1].to_i) + when *VALID_CHAR + else + raise "Illegal character '#{$1}' in raw string \"#{string}\"" + end + # FIXME: below can't work but this needs API change. + # elsif @parent and $3 and !SUBSTITUTES.include?($1) + # if !doctype or !doctype.entities.has_key?($3) + # raise "Undeclared entity '#{$1}' in raw string \"#{string}\"" + # end + end + end + end + end + + def node_type + :text + end + + def empty? + @string.size==0 + end + + + def clone + return Text.new(self) + end + + + # Appends text to this text node. The text is appended in the +raw+ mode + # of this text node. + # + # +returns+ the text itself to enable method chain like + # 'text << "XXX" << "YYY"'. + def <<( to_append ) + @string << to_append.gsub( /\r\n?/, "\n" ) + clear_cache + self + end + + + # +other+ a String or a Text + # +returns+ the result of (to_s <=> arg.to_s) + def <=>( other ) + to_s() <=> other.to_s + end + + def doctype + if @parent + doc = @parent.document + doc.doctype if doc + end + end + + REFERENCE = /#{Entity::REFERENCE}/ + # Returns the string value of this text node. This string is always + # escaped, meaning that it is a valid XML text node string, and all + # entities that can be escaped, have been inserted. This method respects + # the entity filter set in the constructor. + # + # # Assume that the entity "s" is defined to be "sean", and that the + # # entity "r" is defined to be "russell" + # t = Text.new( "< & sean russell", false, nil, false, ['s'] ) + # t.to_s #-> "< & &s; russell" + # t = Text.new( "< & &s; russell", false, nil, false ) + # t.to_s #-> "< & &s; russell" + # u = Text.new( "sean russell", false, nil, true ) + # u.to_s #-> "sean russell" + def to_s + return @string if @raw + return @normalized if @normalized + + @normalized = Text::normalize( @string, doctype, @entity_filter ) + end + + def inspect + @string.inspect + end + + # Returns the string value of this text. This is the text without + # entities, as it might be used programmatically, or printed to the + # console. This ignores the 'raw' attribute setting, and any + # entity_filter. + # + # # Assume that the entity "s" is defined to be "sean", and that the + # # entity "r" is defined to be "russell" + # t = Text.new( "< & sean russell", false, nil, false, ['s'] ) + # t.value #-> "< & sean russell" + # t = Text.new( "< & &s; russell", false, nil, false ) + # t.value #-> "< & sean russell" + # u = Text.new( "sean russell", false, nil, true ) + # u.value #-> "sean russell" + def value + return @unnormalized if @unnormalized + @unnormalized = Text::unnormalize( @string, doctype ) + end + + # Sets the contents of this text node. This expects the text to be + # unnormalized. It returns self. + # + # e = Element.new( "a" ) + # e.add_text( "foo" ) # foo + # e[0].value = "bar" # bar + # e[0].value = "" # <a> + def value=( val ) + @string = val.gsub( /\r\n?/, "\n" ) + clear_cache + @raw = false + end + + def wrap(string, width, addnewline=false) + # Recursively wrap string at width. + return string if string.length <= width + place = string.rindex(' ', width) # Position in string with last ' ' before cutoff + if addnewline then + return "\n" + string[0,place] + "\n" + wrap(string[place+1..-1], width) + else + return string[0,place] + "\n" + wrap(string[place+1..-1], width) + end + end + + def indent_text(string, level=1, style="\t", indentfirstline=true) + return string if level < 0 + new_string = '' + string.each_line { |line| + indent_string = style * level + new_line = (indent_string + line).sub(/[\s]+$/,'') + new_string << new_line + } + new_string.strip! unless indentfirstline + return new_string + end + + # == DEPRECATED + # See REXML::Formatters + # + def write( writer, indent=-1, transitive=false, ie_hack=false ) + Kernel.warn("#{self.class.name}.write is deprecated. See REXML::Formatters") + formatter = if indent > -1 + REXML::Formatters::Pretty.new( indent ) + else + REXML::Formatters::Default.new + end + formatter.write( self, writer ) + end + + # FIXME + # This probably won't work properly + def xpath + path = @parent.xpath + path += "/text()" + return path + end + + # Writes out text, substituting special characters beforehand. + # +out+ A String, IO, or any other object supporting <<( String ) + # +input+ the text to substitute and the write out + # + # z=utf8.unpack("U*") + # ascOut="" + # z.each{|r| + # if r < 0x100 + # ascOut.concat(r.chr) + # else + # ascOut.concat(sprintf("&#x%x;", r)) + # end + # } + # puts ascOut + def write_with_substitution out, input + copy = input.clone + # Doing it like this rather than in a loop improves the speed + copy.gsub!( SPECIALS[0], SUBSTITUTES[0] ) + copy.gsub!( SPECIALS[1], SUBSTITUTES[1] ) + copy.gsub!( SPECIALS[2], SUBSTITUTES[2] ) + copy.gsub!( SPECIALS[3], SUBSTITUTES[3] ) + copy.gsub!( SPECIALS[4], SUBSTITUTES[4] ) + copy.gsub!( SPECIALS[5], SUBSTITUTES[5] ) + out << copy + end + + private + def clear_cache + @normalized = nil + @unnormalized = nil + end + + # Reads text, substituting entities + def Text::read_with_substitution( input, illegal=nil ) + copy = input.clone + + if copy =~ illegal + raise ParseException.new( "malformed text: Illegal character #$& in \"#{copy}\"" ) + end if illegal + + copy.gsub!( /\r\n?/, "\n" ) + if copy.include? ?& + copy.gsub!( SETUTITSBUS[0], SLAICEPS[0] ) + copy.gsub!( SETUTITSBUS[1], SLAICEPS[1] ) + copy.gsub!( SETUTITSBUS[2], SLAICEPS[2] ) + copy.gsub!( SETUTITSBUS[3], SLAICEPS[3] ) + copy.gsub!( SETUTITSBUS[4], SLAICEPS[4] ) + copy.gsub!( /�*((?:\d+)|(?:x[a-f0-9]+));/ ) { + m=$1 + #m='0' if m=='' + m = "0#{m}" if m[0] == ?x + [Integer(m)].pack('U*') + } + end + copy + end + + EREFERENCE = /&(?!#{Entity::NAME};)/ + # Escapes all possible entities + def Text::normalize( input, doctype=nil, entity_filter=nil ) + copy = input.to_s + # Doing it like this rather than in a loop improves the speed + #copy = copy.gsub( EREFERENCE, '&' ) + copy = copy.gsub( "&", "&" ) + if doctype + # Replace all ampersands that aren't part of an entity + doctype.entities.each_value do |entity| + copy = copy.gsub( entity.value, + "&#{entity.name};" ) if entity.value and + not( entity_filter and entity_filter.include?(entity.name) ) + end + else + # Replace all ampersands that aren't part of an entity + DocType::DEFAULT_ENTITIES.each_value do |entity| + copy = copy.gsub(entity.value, "&#{entity.name};" ) + end + end + copy + end + + # Unescapes all possible entities + def Text::unnormalize( string, doctype=nil, filter=nil, illegal=nil ) + sum = 0 + string.gsub( /\r\n?/, "\n" ).gsub( REFERENCE ) { + s = Text.expand($&, doctype, filter) + if sum + s.bytesize > Security.entity_expansion_text_limit + raise "entity expansion has grown too large" + else + sum += s.bytesize + end + s + } + end + + def Text.expand(ref, doctype, filter) + if ref[1] == ?# + if ref[2] == ?x + [ref[3...-1].to_i(16)].pack('U*') + else + [ref[2...-1].to_i].pack('U*') + end + elsif ref == '&' + '&' + elsif filter and filter.include?( ref[1...-1] ) + ref + elsif doctype + doctype.entity( ref[1...-1] ) or ref + else + entity_value = DocType::DEFAULT_ENTITIES[ ref[1...-1] ] + entity_value ? entity_value.value : ref + end + end + end +end diff --git a/gxg/deps/rexml/undefinednamespaceexception.rb b/gxg/deps/rexml/undefinednamespaceexception.rb new file mode 100644 index 0000000..a99fc60 --- /dev/null +++ b/gxg/deps/rexml/undefinednamespaceexception.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: false +require 'deps/rexml/parseexception' +module REXML + class UndefinedNamespaceException < ParseException + def initialize( prefix, source, parser ) + super( "Undefined prefix #{prefix} found" ) + end + end +end diff --git a/gxg/deps/rexml/validation/relaxng.rb b/gxg/deps/rexml/validation/relaxng.rb new file mode 100644 index 0000000..593e2f5 --- /dev/null +++ b/gxg/deps/rexml/validation/relaxng.rb @@ -0,0 +1,539 @@ +# frozen_string_literal: false +require "deps/rexml/validation/validation" +require "deps/rexml/parsers/baseparser" + +module REXML + module Validation + # Implemented: + # * empty + # * element + # * attribute + # * text + # * optional + # * choice + # * oneOrMore + # * zeroOrMore + # * group + # * value + # * interleave + # * mixed + # * ref + # * grammar + # * start + # * define + # + # Not implemented: + # * data + # * param + # * include + # * externalRef + # * notAllowed + # * anyName + # * nsName + # * except + # * name + class RelaxNG + include Validator + + INFINITY = 1.0 / 0.0 + EMPTY = Event.new( nil ) + TEXT = [:start_element, "text"] + attr_accessor :current + attr_accessor :count + attr_reader :references + + # FIXME: Namespaces + def initialize source + parser = REXML::Parsers::BaseParser.new( source ) + + @count = 0 + @references = {} + @root = @current = Sequence.new(self) + @root.previous = true + states = [ @current ] + begin + event = parser.pull + case event[0] + when :start_element + case event[1] + when "empty" + when "element", "attribute", "text", "value" + states[-1] << event + when "optional" + states << Optional.new( self ) + states[-2] << states[-1] + when "choice" + states << Choice.new( self ) + states[-2] << states[-1] + when "oneOrMore" + states << OneOrMore.new( self ) + states[-2] << states[-1] + when "zeroOrMore" + states << ZeroOrMore.new( self ) + states[-2] << states[-1] + when "group" + states << Sequence.new( self ) + states[-2] << states[-1] + when "interleave" + states << Interleave.new( self ) + states[-2] << states[-1] + when "mixed" + states << Interleave.new( self ) + states[-2] << states[-1] + states[-1] << TEXT + when "define" + states << [ event[2]["name"] ] + when "ref" + states[-1] << Ref.new( event[2]["name"] ) + when "anyName" + states << AnyName.new( self ) + states[-2] << states[-1] + when "nsName" + when "except" + when "name" + when "data" + when "param" + when "include" + when "grammar" + when "start" + when "externalRef" + when "notAllowed" + end + when :end_element + case event[1] + when "element", "attribute" + states[-1] << event + when "zeroOrMore", "oneOrMore", "choice", "optional", + "interleave", "group", "mixed" + states.pop + when "define" + ref = states.pop + @references[ ref.shift ] = ref + #when "empty" + end + when :end_document + states[-1] << event + when :text + states[-1] << event + end + end while event[0] != :end_document + end + + def receive event + validate( event ) + end + end + + class State + def initialize( context ) + @previous = [] + @events = [] + @current = 0 + @count = context.count += 1 + @references = context.references + @value = false + end + + def reset + return if @current == 0 + @current = 0 + @events.each {|s| s.reset if s.kind_of? State } + end + + def previous=( previous ) + @previous << previous + end + + def next( event ) + #print "In next with #{event.inspect}. " + #p @previous + return @previous.pop.next( event ) if @events[@current].nil? + expand_ref_in( @events, @current ) if @events[@current].class == Ref + if ( @events[@current].kind_of? State ) + @current += 1 + @events[@current-1].previous = self + return @events[@current-1].next( event ) + end + if ( @events[@current].matches?(event) ) + @current += 1 + if @events[@current].nil? + return @previous.pop + elsif @events[@current].kind_of? State + @current += 1 + @events[@current-1].previous = self + return @events[@current-1] + else + return self + end + else + return nil + end + end + + def to_s + # Abbreviated: + self.class.name =~ /(?:::)(\w)\w+$/ + # Full: + #self.class.name =~ /(?:::)(\w+)$/ + "#$1.#@count" + end + + def inspect + "< #{to_s} #{@events.collect{|e| + pre = e == @events[@current] ? '#' : '' + pre + e.inspect unless self == e + }.join(', ')} >" + end + + def expected + return [@events[@current]] + end + + def <<( event ) + add_event_to_arry( @events, event ) + end + + + protected + def expand_ref_in( arry, ind ) + new_events = [] + @references[ arry[ind].to_s ].each{ |evt| + add_event_to_arry(new_events,evt) + } + arry[ind,1] = new_events + end + + def add_event_to_arry( arry, evt ) + evt = generate_event( evt ) + if evt.kind_of? String + arry[-1].event_arg = evt if arry[-1].kind_of? Event and @value + @value = false + else + arry << evt + end + end + + def generate_event( event ) + return event if event.kind_of? State or event.class == Ref + evt = nil + arg = nil + case event[0] + when :start_element + case event[1] + when "element" + evt = :start_element + arg = event[2]["name"] + when "attribute" + evt = :start_attribute + arg = event[2]["name"] + when "text" + evt = :text + when "value" + evt = :text + @value = true + end + when :text + return event[1] + when :end_document + return Event.new( event[0] ) + else # then :end_element + case event[1] + when "element" + evt = :end_element + when "attribute" + evt = :end_attribute + end + end + return Event.new( evt, arg ) + end + end + + + class Sequence < State + def matches?(event) + @events[@current].matches?( event ) + end + end + + + class Optional < State + def next( event ) + if @current == 0 + rv = super + return rv if rv + @prior = @previous.pop + return @prior.next( event ) + end + super + end + + def matches?(event) + @events[@current].matches?(event) || + (@current == 0 and @previous[-1].matches?(event)) + end + + def expected + return [ @prior.expected, @events[0] ].flatten if @current == 0 + return [@events[@current]] + end + end + + + class ZeroOrMore < Optional + def next( event ) + expand_ref_in( @events, @current ) if @events[@current].class == Ref + if ( @events[@current].matches?(event) ) + @current += 1 + if @events[@current].nil? + @current = 0 + return self + elsif @events[@current].kind_of? State + @current += 1 + @events[@current-1].previous = self + return @events[@current-1] + else + return self + end + else + @prior = @previous.pop + return @prior.next( event ) if @current == 0 + return nil + end + end + + def expected + return [ @prior.expected, @events[0] ].flatten if @current == 0 + return [@events[@current]] + end + end + + + class OneOrMore < State + def initialize context + super + @ord = 0 + end + + def reset + super + @ord = 0 + end + + def next( event ) + expand_ref_in( @events, @current ) if @events[@current].class == Ref + if ( @events[@current].matches?(event) ) + @current += 1 + @ord += 1 + if @events[@current].nil? + @current = 0 + return self + elsif @events[@current].kind_of? State + @current += 1 + @events[@current-1].previous = self + return @events[@current-1] + else + return self + end + else + return @previous.pop.next( event ) if @current == 0 and @ord > 0 + return nil + end + end + + def matches?( event ) + @events[@current].matches?(event) || + (@current == 0 and @ord > 0 and @previous[-1].matches?(event)) + end + + def expected + if @current == 0 and @ord > 0 + return [@previous[-1].expected, @events[0]].flatten + else + return [@events[@current]] + end + end + end + + + class Choice < State + def initialize context + super + @choices = [] + end + + def reset + super + @events = [] + @choices.each { |c| c.each { |s| s.reset if s.kind_of? State } } + end + + def <<( event ) + add_event_to_arry( @choices, event ) + end + + def next( event ) + # Make the choice if we haven't + if @events.size == 0 + c = 0 ; max = @choices.size + while c < max + if @choices[c][0].class == Ref + expand_ref_in( @choices[c], 0 ) + @choices += @choices[c] + @choices.delete( @choices[c] ) + max -= 1 + else + c += 1 + end + end + @events = @choices.find { |evt| evt[0].matches? event } + # Remove the references + # Find the events + end + unless @events + @events = [] + return nil + end + super + end + + def matches?( event ) + return @events[@current].matches?( event ) if @events.size > 0 + !@choices.find{|evt| evt[0].matches?(event)}.nil? + end + + def expected + return [@events[@current]] if @events.size > 0 + return @choices.collect do |x| + if x[0].kind_of? State + x[0].expected + else + x[0] + end + end.flatten + end + + def inspect + "< #{to_s} #{@choices.collect{|e| e.collect{|f|f.to_s}.join(', ')}.join(' or ')} >" + end + + protected + def add_event_to_arry( arry, evt ) + if evt.kind_of? State or evt.class == Ref + arry << [evt] + elsif evt[0] == :text + if arry[-1] and + arry[-1][-1].kind_of?( Event ) and + arry[-1][-1].event_type == :text and @value + + arry[-1][-1].event_arg = evt[1] + @value = false + end + else + arry << [] if evt[0] == :start_element + arry[-1] << generate_event( evt ) + end + end + end + + + class Interleave < Choice + def initialize context + super + @choice = 0 + end + + def reset + @choice = 0 + end + + def next_current( event ) + # Expand references + c = 0 ; max = @choices.size + while c < max + if @choices[c][0].class == Ref + expand_ref_in( @choices[c], 0 ) + @choices += @choices[c] + @choices.delete( @choices[c] ) + max -= 1 + else + c += 1 + end + end + @events = @choices[@choice..-1].find { |evt| evt[0].matches? event } + @current = 0 + if @events + # reorder the choices + old = @choices[@choice] + idx = @choices.index( @events ) + @choices[@choice] = @events + @choices[idx] = old + @choice += 1 + end + + @events = [] unless @events + end + + + def next( event ) + # Find the next series + next_current(event) unless @events[@current] + return nil unless @events[@current] + + expand_ref_in( @events, @current ) if @events[@current].class == Ref + if ( @events[@current].kind_of? State ) + @current += 1 + @events[@current-1].previous = self + return @events[@current-1].next( event ) + end + return @previous.pop.next( event ) if @events[@current].nil? + if ( @events[@current].matches?(event) ) + @current += 1 + if @events[@current].nil? + return self unless @choices[@choice].nil? + return @previous.pop + elsif @events[@current].kind_of? State + @current += 1 + @events[@current-1].previous = self + return @events[@current-1] + else + return self + end + else + return nil + end + end + + def matches?( event ) + return @events[@current].matches?( event ) if @events[@current] + !@choices[@choice..-1].find{|evt| evt[0].matches?(event)}.nil? + end + + def expected + return [@events[@current]] if @events[@current] + return @choices[@choice..-1].collect do |x| + if x[0].kind_of? State + x[0].expected + else + x[0] + end + end.flatten + end + + def inspect + "< #{to_s} #{@choices.collect{|e| e.collect{|f|f.to_s}.join(', ')}.join(' and ')} >" + end + end + + class Ref + def initialize value + @value = value + end + def to_s + @value + end + def inspect + "{#{to_s}}" + end + end + end +end diff --git a/gxg/deps/rexml/validation/validation.rb b/gxg/deps/rexml/validation/validation.rb new file mode 100644 index 0000000..a9a5a76 --- /dev/null +++ b/gxg/deps/rexml/validation/validation.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: false +require 'deps/rexml/validation/validationexception' + +module REXML + module Validation + module Validator + NILEVENT = [ nil ] + def reset + @current = @root + @root.reset + @root.previous = true + @attr_stack = [] + self + end + def dump + puts @root.inspect + end + def validate( event ) + @attr_stack = [] unless defined? @attr_stack + match = @current.next(event) + raise ValidationException.new( "Validation error. Expected: "+ + @current.expected.join( " or " )+" from #{@current.inspect} "+ + " but got #{Event.new( event[0], event[1] ).inspect}" ) unless match + @current = match + + # Check for attributes + case event[0] + when :start_element + @attr_stack << event[2] + begin + sattr = [:start_attribute, nil] + eattr = [:end_attribute] + text = [:text, nil] + k, = event[2].find { |key,value| + sattr[1] = key + m = @current.next( sattr ) + if m + # If the state has text children... + if m.matches?( eattr ) + @current = m + else + text[1] = value + m = m.next( text ) + text[1] = nil + return false unless m + @current = m if m + end + m = @current.next( eattr ) + if m + @current = m + true + else + false + end + else + false + end + } + event[2].delete(k) if k + end while k + when :end_element + attrs = @attr_stack.pop + raise ValidationException.new( "Validation error. Illegal "+ + " attributes: #{attrs.inspect}") if attrs.length > 0 + end + end + end + + class Event + def initialize(event_type, event_arg=nil ) + @event_type = event_type + @event_arg = event_arg + end + + attr_reader :event_type + attr_accessor :event_arg + + def done? + @done + end + + def single? + return (@event_type != :start_element and @event_type != :start_attribute) + end + + def matches?( event ) + return false unless event[0] == @event_type + case event[0] + when nil + return true + when :start_element + return true if event[1] == @event_arg + when :end_element + return true + when :start_attribute + return true if event[1] == @event_arg + when :end_attribute + return true + when :end_document + return true + when :text + return (@event_arg.nil? or @event_arg == event[1]) +=begin + when :processing_instruction + false + when :xmldecl + false + when :start_doctype + false + when :end_doctype + false + when :externalentity + false + when :elementdecl + false + when :entity + false + when :attlistdecl + false + when :notationdecl + false + when :end_doctype + false +=end + else + false + end + end + + def ==( other ) + return false unless other.kind_of? Event + @event_type == other.event_type and @event_arg == other.event_arg + end + + def to_s + inspect + end + + def inspect + "#{@event_type.inspect}( #@event_arg )" + end + end + end +end diff --git a/gxg/deps/rexml/validation/validationexception.rb b/gxg/deps/rexml/validation/validationexception.rb new file mode 100644 index 0000000..78cd63f --- /dev/null +++ b/gxg/deps/rexml/validation/validationexception.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: false +module REXML + module Validation + class ValidationException < RuntimeError + def initialize msg + super + end + end + end +end diff --git a/gxg/deps/rexml/xmldecl.rb b/gxg/deps/rexml/xmldecl.rb new file mode 100644 index 0000000..74dacbb --- /dev/null +++ b/gxg/deps/rexml/xmldecl.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: false +require 'deps/rexml/encoding' +require 'deps/rexml/source' + +module REXML + # NEEDS DOCUMENTATION + class XMLDecl < Child + include Encoding + + DEFAULT_VERSION = "1.0"; + DEFAULT_ENCODING = "UTF-8"; + DEFAULT_STANDALONE = "no"; + START = '<\?xml'; + STOP = '\?>'; + + attr_accessor :version, :standalone + attr_reader :writeencoding, :writethis + + def initialize(version=DEFAULT_VERSION, encoding=nil, standalone=nil) + @writethis = true + @writeencoding = !encoding.nil? + if version.kind_of? XMLDecl + super() + @version = version.version + self.encoding = version.encoding + @writeencoding = version.writeencoding + @standalone = version.standalone + else + super() + @version = version + self.encoding = encoding + @standalone = standalone + end + @version = DEFAULT_VERSION if @version.nil? + end + + def clone + XMLDecl.new(self) + end + + # indent:: + # Ignored. There must be no whitespace before an XML declaration + # transitive:: + # Ignored + # ie_hack:: + # Ignored + def write(writer, indent=-1, transitive=false, ie_hack=false) + return nil unless @writethis or writer.kind_of? Output + writer << START.sub(/\\/u, '') + writer << " #{content encoding}" + writer << STOP.sub(/\\/u, '') + end + + def ==( other ) + other.kind_of?(XMLDecl) and + other.version == @version and + other.encoding == self.encoding and + other.standalone == @standalone + end + + def xmldecl version, encoding, standalone + @version = version + self.encoding = encoding + @standalone = standalone + end + + def node_type + :xmldecl + end + + alias :stand_alone? :standalone + alias :old_enc= :encoding= + + def encoding=( enc ) + if enc.nil? + self.old_enc = "UTF-8" + @writeencoding = false + else + self.old_enc = enc + @writeencoding = true + end + self.dowrite + end + + # Only use this if you do not want the XML declaration to be written; + # this object is ignored by the XML writer. Otherwise, instantiate your + # own XMLDecl and add it to the document. + # + # Note that XML 1.1 documents *must* include an XML declaration + def XMLDecl.default + rv = XMLDecl.new( "1.0" ) + rv.nowrite + rv + end + + def nowrite + @writethis = false + end + + def dowrite + @writethis = true + end + + def inspect + START.sub(/\\/u, '') + " ... " + STOP.sub(/\\/u, '') + end + + private + def content(enc) + rv = "version='#@version'" + rv << " encoding='#{enc}'" if @writeencoding || enc !~ /\Autf-8\z/i + rv << " standalone='#@standalone'" if @standalone + rv + end + end +end diff --git a/gxg/deps/rexml/xmltokens.rb b/gxg/deps/rexml/xmltokens.rb new file mode 100644 index 0000000..392b47b --- /dev/null +++ b/gxg/deps/rexml/xmltokens.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: false +module REXML + # Defines a number of tokens used for parsing XML. Not for general + # consumption. + module XMLTokens + # From http://www.w3.org/TR/REC-xml/#sec-common-syn + # + # [4] NameStartChar ::= + # ":" | + # [A-Z] | + # "_" | + # [a-z] | + # [#xC0-#xD6] | + # [#xD8-#xF6] | + # [#xF8-#x2FF] | + # [#x370-#x37D] | + # [#x37F-#x1FFF] | + # [#x200C-#x200D] | + # [#x2070-#x218F] | + # [#x2C00-#x2FEF] | + # [#x3001-#xD7FF] | + # [#xF900-#xFDCF] | + # [#xFDF0-#xFFFD] | + # [#x10000-#xEFFFF] + name_start_chars = [ + ":", + "A-Z", + "_", + "a-z", + "\\u00C0-\\u00D6", + "\\u00D8-\\u00F6", + "\\u00F8-\\u02FF", + "\\u0370-\\u037D", + "\\u037F-\\u1FFF", + "\\u200C-\\u200D", + "\\u2070-\\u218F", + "\\u2C00-\\u2FEF", + "\\u3001-\\uD7FF", + "\\uF900-\\uFDCF", + "\\uFDF0-\\uFFFD", + "\\u{10000}-\\u{EFFFF}", + ] + # From http://www.w3.org/TR/REC-xml/#sec-common-syn + # + # [4a] NameChar ::= + # NameStartChar | + # "-" | + # "." | + # [0-9] | + # #xB7 | + # [#x0300-#x036F] | + # [#x203F-#x2040] + name_chars = name_start_chars + [ + "\\-", + "\\.", + "0-9", + "\\u00B7", + "\\u0300-\\u036F", + "\\u203F-\\u2040", + ] + NAME_START_CHAR = "[#{name_start_chars.join('')}]" + NAME_CHAR = "[#{name_chars.join('')}]" + NAMECHAR = NAME_CHAR # deprecated. Use NAME_CHAR instead. + + # From http://www.w3.org/TR/xml-names11/#NT-NCName + # + # [6] NCNameStartChar ::= NameStartChar - ':' + ncname_start_chars = name_start_chars - [":"] + # From http://www.w3.org/TR/xml-names11/#NT-NCName + # + # [5] NCNameChar ::= NameChar - ':' + ncname_chars = name_chars - [":"] + NCNAME_STR = "[#{ncname_start_chars.join('')}][#{ncname_chars.join('')}]*" + NAME_STR = "(?:#{NCNAME_STR}:)?#{NCNAME_STR}" + + NAME = "(#{NAME_START_CHAR}#{NAME_CHAR}*)" + NMTOKEN = "(?:#{NAME_CHAR})+" + NMTOKENS = "#{NMTOKEN}(\\s+#{NMTOKEN})*" + REFERENCE = "(?:&#{NAME};|&#\\d+;|&#x[0-9a-fA-F]+;)" + + #REFERENCE = "(?:#{ENTITYREF}|#{CHARREF})" + #ENTITYREF = "&#{NAME};" + #CHARREF = "&#\\d+;|&#x[0-9a-fA-F]+;" + end +end diff --git a/gxg/deps/rexml/xpath.rb b/gxg/deps/rexml/xpath.rb new file mode 100644 index 0000000..d7417e9 --- /dev/null +++ b/gxg/deps/rexml/xpath.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: false +require 'deps/rexml/functions' +require 'deps/rexml/xpath_parser' + +module REXML + # Wrapper class. Use this class to access the XPath functions. + class XPath + include Functions + # A base Hash object, supposing to be used when initializing a + # default empty namespaces set, but is currently unused. + # TODO: either set the namespaces=EMPTY_HASH, or deprecate this. + EMPTY_HASH = {} + + # Finds and returns the first node that matches the supplied xpath. + # element:: + # The context element + # path:: + # The xpath to search for. If not supplied or nil, returns the first + # node matching '*'. + # namespaces:: + # If supplied, a Hash which defines a namespace mapping. + # variables:: + # If supplied, a Hash which maps $variables in the query + # to values. This can be used to avoid XPath injection attacks + # or to automatically handle escaping string values. + # + # XPath.first( node ) + # XPath.first( doc, "//b"} ) + # XPath.first( node, "a/x:b", { "x"=>"http://doofus" } ) + # XPath.first( node, '/book/publisher/text()=$publisher', {}, {"publisher"=>"O'Reilly"}) + def XPath::first element, path=nil, namespaces=nil, variables={} + raise "The namespaces argument, if supplied, must be a hash object." unless namespaces.nil? or namespaces.kind_of?(Hash) + raise "The variables argument, if supplied, must be a hash object." unless variables.kind_of?(Hash) + parser = XPathParser.new + parser.namespaces = namespaces + parser.variables = variables + path = "*" unless path + element = [element] unless element.kind_of? Array + parser.parse(path, element).flatten[0] + end + + # Iterates over nodes that match the given path, calling the supplied + # block with the match. + # element:: + # The context element + # path:: + # The xpath to search for. If not supplied or nil, defaults to '*' + # namespaces:: + # If supplied, a Hash which defines a namespace mapping + # variables:: + # If supplied, a Hash which maps $variables in the query + # to values. This can be used to avoid XPath injection attacks + # or to automatically handle escaping string values. + # + # XPath.each( node ) { |el| ... } + # XPath.each( node, '/*[@attr='v']' ) { |el| ... } + # XPath.each( node, 'ancestor::x' ) { |el| ... } + # XPath.each( node, '/book/publisher/text()=$publisher', {}, {"publisher"=>"O'Reilly"}) \ + # {|el| ... } + def XPath::each element, path=nil, namespaces=nil, variables={}, &block + raise "The namespaces argument, if supplied, must be a hash object." unless namespaces.nil? or namespaces.kind_of?(Hash) + raise "The variables argument, if supplied, must be a hash object." unless variables.kind_of?(Hash) + parser = XPathParser.new + parser.namespaces = namespaces + parser.variables = variables + path = "*" unless path + element = [element] unless element.kind_of? Array + parser.parse(path, element).each( &block ) + end + + # Returns an array of nodes matching a given XPath. + def XPath::match element, path=nil, namespaces=nil, variables={} + parser = XPathParser.new + parser.namespaces = namespaces + parser.variables = variables + path = "*" unless path + element = [element] unless element.kind_of? Array + parser.parse(path,element) + end + end +end diff --git a/gxg/deps/rexml/xpath_parser.rb b/gxg/deps/rexml/xpath_parser.rb new file mode 100644 index 0000000..4375f77 --- /dev/null +++ b/gxg/deps/rexml/xpath_parser.rb @@ -0,0 +1,704 @@ +# frozen_string_literal: false +require 'deps/rexml/namespace' +require 'deps/rexml/xmltokens' +require 'deps/rexml/attribute' +require 'deps/rexml/syncenumerator' +require 'deps/rexml/parsers/xpathparser' + +class Object + # provides a unified +clone+ operation, for REXML::XPathParser + # to use across multiple Object types + def dclone + clone + end +end +class Symbol + # provides a unified +clone+ operation, for REXML::XPathParser + # to use across multiple Object types + def dclone ; self ; end +end +class Integer + # provides a unified +clone+ operation, for REXML::XPathParser + # to use across multiple Object types + def dclone ; self ; end +end +class Float + # provides a unified +clone+ operation, for REXML::XPathParser + # to use across multiple Object types + def dclone ; self ; end +end +class Array + # provides a unified +clone+ operation, for REXML::XPathParser + # to use across multiple Object+ types + def dclone + klone = self.clone + klone.clear + self.each{|v| klone << v.dclone} + klone + end +end + +module REXML + # You don't want to use this class. Really. Use XPath, which is a wrapper + # for this class. Believe me. You don't want to poke around in here. + # There is strange, dark magic at work in this code. Beware. Go back! Go + # back while you still can! + class XPathParser + include XMLTokens + LITERAL = /^'([^']*)'|^"([^"]*)"/u + + def initialize( ) + @parser = REXML::Parsers::XPathParser.new + @namespaces = nil + @variables = {} + end + + def namespaces=( namespaces={} ) + Functions::namespace_context = namespaces + @namespaces = namespaces + end + + def variables=( vars={} ) + Functions::variables = vars + @variables = vars + end + + def parse path, nodeset + path_stack = @parser.parse( path ) + match( path_stack, nodeset ) + end + + def get_first path, nodeset + path_stack = @parser.parse( path ) + first( path_stack, nodeset ) + end + + def predicate path, nodeset + path_stack = @parser.parse( path ) + expr( path_stack, nodeset ) + end + + def []=( variable_name, value ) + @variables[ variable_name ] = value + end + + + # Performs a depth-first (document order) XPath search, and returns the + # first match. This is the fastest, lightest way to return a single result. + # + # FIXME: This method is incomplete! + def first( path_stack, node ) + return nil if path.size == 0 + + case path[0] + when :document + # do nothing + return first( path[1..-1], node ) + when :child + for c in node.children + r = first( path[1..-1], c ) + return r if r + end + when :qname + name = path[2] + if node.name == name + return node if path.size == 3 + return first( path[3..-1], node ) + else + return nil + end + when :descendant_or_self + r = first( path[1..-1], node ) + return r if r + for c in node.children + r = first( path, c ) + return r if r + end + when :node + return first( path[1..-1], node ) + when :any + return first( path[1..-1], node ) + end + return nil + end + + + def match( path_stack, nodeset ) + r = expr( path_stack, nodeset ) + r + end + + private + + + # Returns a String namespace for a node, given a prefix + # The rules are: + # + # 1. Use the supplied namespace mapping first. + # 2. If no mapping was supplied, use the context node to look up the namespace + def get_namespace( node, prefix ) + if @namespaces + return @namespaces[prefix] || '' + else + return node.namespace( prefix ) if node.node_type == :element + return '' + end + end + + + # Expr takes a stack of path elements and a set of nodes (either a Parent + # or an Array and returns an Array of matching nodes + ALL = [ :attribute, :element, :text, :processing_instruction, :comment ] + ELEMENTS = [ :element ] + def expr( path_stack, nodeset, context=nil ) + node_types = ELEMENTS + return nodeset if path_stack.length == 0 || nodeset.length == 0 + while path_stack.length > 0 + if nodeset.length == 0 + path_stack.clear + return [] + end + case (op = path_stack.shift) + when :document + nodeset = [ nodeset[0].root_node ] + + when :qname + prefix = path_stack.shift + name = path_stack.shift + nodeset.delete_if do |node| + # FIXME: This DOUBLES the time XPath searches take + ns = get_namespace( node, prefix ) + if node.node_type == :element + if node.name == name + end + end + !(node.node_type == :element and + node.name == name and + node.namespace == ns ) + end + node_types = ELEMENTS + + when :any + nodeset.delete_if { |node| !node_types.include?(node.node_type) } + + when :self + # This space left intentionally blank + + when :processing_instruction + target = path_stack.shift + nodeset.delete_if do |node| + (node.node_type != :processing_instruction) or + ( target!='' and ( node.target != target ) ) + end + + when :text + nodeset.delete_if { |node| node.node_type != :text } + + when :comment + nodeset.delete_if { |node| node.node_type != :comment } + + when :node + # This space left intentionally blank + node_types = ALL + + when :child + new_nodeset = [] + nt = nil + nodeset.each do |node| + nt = node.node_type + new_nodeset += node.children if nt == :element or nt == :document + end + nodeset = new_nodeset + node_types = ELEMENTS + + when :literal + return path_stack.shift + + when :attribute + new_nodeset = [] + case path_stack.shift + when :qname + prefix = path_stack.shift + name = path_stack.shift + for element in nodeset + if element.node_type == :element + attrib = element.attribute( name, get_namespace(element, prefix) ) + new_nodeset << attrib if attrib + end + end + when :any + for element in nodeset + if element.node_type == :element + new_nodeset += element.attributes.to_a + end + end + end + nodeset = new_nodeset + + when :parent + nodeset = nodeset.collect{|n| n.parent}.compact + #nodeset = expr(path_stack.dclone, nodeset.collect{|n| n.parent}.compact) + node_types = ELEMENTS + + when :ancestor + new_nodeset = [] + nodeset.each do |node| + while node.parent + node = node.parent + new_nodeset << node unless new_nodeset.include? node + end + end + nodeset = new_nodeset + node_types = ELEMENTS + + when :ancestor_or_self + new_nodeset = [] + nodeset.each do |node| + if node.node_type == :element + new_nodeset << node + while ( node.parent ) + node = node.parent + new_nodeset << node unless new_nodeset.include? node + end + end + end + nodeset = new_nodeset + node_types = ELEMENTS + + when :predicate + new_nodeset = [] + subcontext = { :size => nodeset.size } + pred = path_stack.shift + nodeset.each_with_index { |node, index| + subcontext[ :node ] = node + subcontext[ :index ] = index+1 + pc = pred.dclone + result = expr( pc, [node], subcontext ) + result = result[0] if result.kind_of? Array and result.length == 1 + if result.kind_of? Numeric + new_nodeset << node if result == (index+1) + elsif result.instance_of? Array + if result.size > 0 and result.inject(false) {|k,s| s or k} + new_nodeset << node if result.size > 0 + end + else + new_nodeset << node if result + end + } + nodeset = new_nodeset +=begin + predicate = path_stack.shift + ns = nodeset.clone + result = expr( predicate, ns ) + if result.kind_of? Array + nodeset = result.zip(ns).collect{|m,n| n if m}.compact + else + nodeset = result ? nodeset : [] + end +=end + + when :descendant_or_self + rv = descendant_or_self( path_stack, nodeset ) + path_stack.clear + nodeset = rv + node_types = ELEMENTS + + when :descendant + results = [] + nt = nil + nodeset.each do |node| + nt = node.node_type + results += expr( path_stack.dclone.unshift( :descendant_or_self ), + node.children ) if nt == :element or nt == :document + end + nodeset = results + node_types = ELEMENTS + + when :following_sibling + results = [] + nodeset.each do |node| + next if node.parent.nil? + all_siblings = node.parent.children + current_index = all_siblings.index( node ) + following_siblings = all_siblings[ current_index+1 .. -1 ] + results += expr( path_stack.dclone, following_siblings ) + end + nodeset = results + + when :preceding_sibling + results = [] + nodeset.each do |node| + next if node.parent.nil? + all_siblings = node.parent.children + current_index = all_siblings.index( node ) + preceding_siblings = all_siblings[ 0, current_index ].reverse + results += preceding_siblings + end + nodeset = results + node_types = ELEMENTS + + when :preceding + new_nodeset = [] + nodeset.each do |node| + new_nodeset += preceding( node ) + end + nodeset = new_nodeset + node_types = ELEMENTS + + when :following + new_nodeset = [] + nodeset.each do |node| + new_nodeset += following( node ) + end + nodeset = new_nodeset + node_types = ELEMENTS + + when :namespace + new_nodeset = [] + prefix = path_stack.shift + nodeset.each do |node| + if (node.node_type == :element or node.node_type == :attribute) + if @namespaces + namespaces = @namespaces + elsif (node.node_type == :element) + namespaces = node.namespaces + else + namespaces = node.element.namesapces + end + if (node.namespace == namespaces[prefix]) + new_nodeset << node + end + end + end + nodeset = new_nodeset + + when :variable + var_name = path_stack.shift + return @variables[ var_name ] + + # :and, :or, :eq, :neq, :lt, :lteq, :gt, :gteq + # TODO: Special case for :or and :and -- not evaluate the right + # operand if the left alone determines result (i.e. is true for + # :or and false for :and). + when :eq, :neq, :lt, :lteq, :gt, :gteq, :or + left = expr( path_stack.shift, nodeset.dup, context ) + right = expr( path_stack.shift, nodeset.dup, context ) + res = equality_relational_compare( left, op, right ) + return res + + when :and + left = expr( path_stack.shift, nodeset.dup, context ) + return [] unless left + if left.respond_to?(:inject) and !left.inject(false) {|a,b| a | b} + return [] + end + right = expr( path_stack.shift, nodeset.dup, context ) + res = equality_relational_compare( left, op, right ) + return res + + when :div + left = Functions::number(expr(path_stack.shift, nodeset, context)).to_f + right = Functions::number(expr(path_stack.shift, nodeset, context)).to_f + return (left / right) + + when :mod + left = Functions::number(expr(path_stack.shift, nodeset, context )).to_f + right = Functions::number(expr(path_stack.shift, nodeset, context )).to_f + return (left % right) + + when :mult + left = Functions::number(expr(path_stack.shift, nodeset, context )).to_f + right = Functions::number(expr(path_stack.shift, nodeset, context )).to_f + return (left * right) + + when :plus + left = Functions::number(expr(path_stack.shift, nodeset, context )).to_f + right = Functions::number(expr(path_stack.shift, nodeset, context )).to_f + return (left + right) + + when :minus + left = Functions::number(expr(path_stack.shift, nodeset, context )).to_f + right = Functions::number(expr(path_stack.shift, nodeset, context )).to_f + return (left - right) + + when :union + left = expr( path_stack.shift, nodeset, context ) + right = expr( path_stack.shift, nodeset, context ) + return (left | right) + + when :neg + res = expr( path_stack, nodeset, context ) + return -(res.to_f) + + when :not + when :function + func_name = path_stack.shift.tr('-','_') + arguments = path_stack.shift + subcontext = context ? nil : { :size => nodeset.size } + + res = [] + cont = context + nodeset.each_with_index { |n, i| + if subcontext + subcontext[:node] = n + subcontext[:index] = i + cont = subcontext + end + arg_clone = arguments.dclone + args = arg_clone.collect { |arg| + expr( arg, [n], cont ) + } + Functions.context = cont + res << Functions.send( func_name, *args ) + } + return res + + end + end # while + return nodeset + end + + + ########################################################## + # FIXME + # The next two methods are BAD MOJO! + # This is my achilles heel. If anybody thinks of a better + # way of doing this, be my guest. This really sucks, but + # it is a wonder it works at all. + # ######################################################## + + def descendant_or_self( path_stack, nodeset ) + rs = [] + d_o_s( path_stack, nodeset, rs ) + document_order(rs.flatten.compact) + #rs.flatten.compact + end + + def d_o_s( p, ns, r ) + nt = nil + ns.each_index do |i| + n = ns[i] + x = expr( p.dclone, [ n ] ) + nt = n.node_type + d_o_s( p, n.children, x ) if nt == :element or nt == :document and n.children.size > 0 + r.concat(x) if x.size > 0 + end + end + + + # Reorders an array of nodes so that they are in document order + # It tries to do this efficiently. + # + # FIXME: I need to get rid of this, but the issue is that most of the XPath + # interpreter functions as a filter, which means that we lose context going + # in and out of function calls. If I knew what the index of the nodes was, + # I wouldn't have to do this. Maybe add a document IDX for each node? + # Problems with mutable documents. Or, rewrite everything. + def document_order( array_of_nodes ) + new_arry = [] + array_of_nodes.each { |node| + node_idx = [] + np = node.node_type == :attribute ? node.element : node + while np.parent and np.parent.node_type == :element + node_idx << np.parent.index( np ) + np = np.parent + end + new_arry << [ node_idx.reverse, node ] + } + new_arry.sort{ |s1, s2| s1[0] <=> s2[0] }.collect{ |s| s[1] } + end + + + def recurse( nodeset, &block ) + for node in nodeset + yield node + recurse( node, &block ) if node.node_type == :element + end + end + + + + # Builds a nodeset of all of the preceding nodes of the supplied node, + # in reverse document order + # preceding:: includes every element in the document that precedes this node, + # except for ancestors + def preceding( node ) + ancestors = [] + p = node.parent + while p + ancestors << p + p = p.parent + end + + acc = [] + p = preceding_node_of( node ) + while p + if ancestors.include? p + ancestors.delete(p) + else + acc << p + end + p = preceding_node_of( p ) + end + acc + end + + def preceding_node_of( node ) + psn = node.previous_sibling_node + if psn.nil? + if node.parent.nil? or node.parent.class == Document + return nil + end + return node.parent + #psn = preceding_node_of( node.parent ) + end + while psn and psn.kind_of? Element and psn.children.size > 0 + psn = psn.children[-1] + end + psn + end + + def following( node ) + acc = [] + p = next_sibling_node( node ) + while p + acc << p + p = following_node_of( p ) + end + acc + end + + def following_node_of( node ) + if node.kind_of? Element and node.children.size > 0 + return node.children[0] + end + return next_sibling_node(node) + end + + def next_sibling_node(node) + psn = node.next_sibling_node + while psn.nil? + if node.parent.nil? or node.parent.class == Document + return nil + end + node = node.parent + psn = node.next_sibling_node + end + return psn + end + + def norm b + case b + when true, false + return b + when 'true', 'false' + return Functions::boolean( b ) + when /^\d+(\.\d+)?$/ + return Functions::number( b ) + else + return Functions::string( b ) + end + end + + def equality_relational_compare( set1, op, set2 ) + if set1.kind_of? Array and set2.kind_of? Array + if set1.size == 1 and set2.size == 1 + set1 = set1[0] + set2 = set2[0] + elsif set1.size == 0 or set2.size == 0 + nd = set1.size==0 ? set2 : set1 + rv = nd.collect { |il| compare( il, op, nil ) } + return rv + else + res = [] + SyncEnumerator.new( set1, set2 ).each { |i1, i2| + i1 = norm( i1 ) + i2 = norm( i2 ) + res << compare( i1, op, i2 ) + } + return res + end + end + # If one is nodeset and other is number, compare number to each item + # in nodeset s.t. number op number(string(item)) + # If one is nodeset and other is string, compare string to each item + # in nodeset s.t. string op string(item) + # If one is nodeset and other is boolean, compare boolean to each item + # in nodeset s.t. boolean op boolean(item) + if set1.kind_of? Array or set2.kind_of? Array + if set1.kind_of? Array + a = set1 + b = set2 + else + a = set2 + b = set1 + end + + case b + when true, false + return a.collect {|v| compare( Functions::boolean(v), op, b ) } + when Numeric + return a.collect {|v| compare( Functions::number(v), op, b )} + when /^\d+(\.\d+)?$/ + b = Functions::number( b ) + return a.collect {|v| compare( Functions::number(v), op, b )} + else + b = Functions::string( b ) + return a.collect { |v| compare( Functions::string(v), op, b ) } + end + else + # If neither is nodeset, + # If op is = or != + # If either boolean, convert to boolean + # If either number, convert to number + # Else, convert to string + # Else + # Convert both to numbers and compare + s1 = set1.to_s + s2 = set2.to_s + if s1 == 'true' or s1 == 'false' or s2 == 'true' or s2 == 'false' + set1 = Functions::boolean( set1 ) + set2 = Functions::boolean( set2 ) + else + if op == :eq or op == :neq + if s1 =~ /^\d+(\.\d+)?$/ or s2 =~ /^\d+(\.\d+)?$/ + set1 = Functions::number( s1 ) + set2 = Functions::number( s2 ) + else + set1 = Functions::string( set1 ) + set2 = Functions::string( set2 ) + end + else + set1 = Functions::number( set1 ) + set2 = Functions::number( set2 ) + end + end + return compare( set1, op, set2 ) + end + return false + end + + def compare a, op, b + case op + when :eq + a == b + when :neq + a != b + when :lt + a < b + when :lteq + a <= b + when :gt + a > b + when :gteq + a >= b + when :and + a and b + when :or + a or b + else + false + end + end + end +end diff --git a/gxg/deps/xmlsimple.rb b/gxg/deps/xmlsimple.rb new file mode 100644 index 0000000..b9c13be --- /dev/null +++ b/gxg/deps/xmlsimple.rb @@ -0,0 +1,1040 @@ +# = XmlSimple +# +# Author:: Maik Schmidt +# Copyright:: Copyright (c) 2003-2014 Maik Schmidt +# License:: Distributes under the same terms as Ruby. +# +require 'deps/rexml/document' +require 'stringio' + +# Easy API to maintain XML (especially configuration files). +class XmlSimple + include REXML + + @@VERSION = '1.1.5' + + # A simple cache for XML documents that were already transformed + # by xml_in. + class Cache + # Creates and initializes a new Cache object. + def initialize + @mem_share_cache = {} + @mem_copy_cache = {} + end + + # Saves a data structure into a file. + # + # data:: + # Data structure to be saved. + # filename:: + # Name of the file belonging to the data structure. + def save_storable(data, filename) + cache_file = get_cache_filename(filename) + File.open(cache_file, "w+") { |f| Marshal.dump(data, f) } + end + + # Restores a data structure from a file. If restoring the data + # structure failed for any reason, nil will be returned. + # + # filename:: + # Name of the file belonging to the data structure. + def restore_storable(filename) + cache_file = get_cache_filename(filename) + return nil unless File::exist?(cache_file) + return nil unless File::mtime(cache_file).to_i > File::mtime(filename).to_i + data = nil + File.open(cache_file) { |f| data = Marshal.load(f) } + data + end + + # Saves a data structure in a shared memory cache. + # + # data:: + # Data structure to be saved. + # filename:: + # Name of the file belonging to the data structure. + def save_mem_share(data, filename) + @mem_share_cache[filename] = [Time::now.to_i, data] + end + + # Restores a data structure from a shared memory cache. You + # should consider these elements as "read only". If restoring + # the data structure failed for any reason, nil will be + # returned. + # + # filename:: + # Name of the file belonging to the data structure. + def restore_mem_share(filename) + get_from_memory_cache(filename, @mem_share_cache) + end + + # Copies a data structure to a memory cache. + # + # data:: + # Data structure to be copied. + # filename:: + # Name of the file belonging to the data structure. + def save_mem_copy(data, filename) + @mem_share_cache[filename] = [Time::now.to_i, Marshal.dump(data)] + end + + # Restores a data structure from a memory cache. If restoring + # the data structure failed for any reason, nil will be + # returned. + # + # filename:: + # Name of the file belonging to the data structure. + def restore_mem_copy(filename) + data = get_from_memory_cache(filename, @mem_share_cache) + data = Marshal.load(data) unless data.nil? + data + end + + private + + # Returns the "cache filename" belonging to a filename, i.e. + # the extension '.xml' in the original filename will be replaced + # by '.stor'. If filename does not have this extension, '.stor' + # will be appended. + # + # filename:: + # Filename to get "cache filename" for. + def get_cache_filename(filename) + filename.sub(/(\.xml)?$/, '.stor') + end + + # Returns a cache entry from a memory cache belonging to a + # certain filename. If no entry could be found for any reason, + # nil will be returned. + # + # filename:: + # Name of the file the cache entry belongs to. + # cache:: + # Memory cache to get entry from. + def get_from_memory_cache(filename, cache) + return nil unless cache[filename] + return nil unless cache[filename][0] > File::mtime(filename).to_i + return cache[filename][1] + end + end + + # Create a "global" cache. + @@cache = Cache.new + + # Creates and initializes a new XmlSimple object. + # + # defaults:: + # Default values for options. + def initialize(defaults = nil) + unless defaults.nil? || defaults.is_a?(Hash) + raise ArgumentError, "Options have to be a Hash." + end + @default_options = normalize_option_names(defaults, (KNOWN_OPTIONS['in'] + KNOWN_OPTIONS['out']).uniq) + @options = Hash.new + @_var_values = nil + end + + # Converts an XML document in the same way as the Perl module XML::Simple. + # + # string:: + # XML source. Could be one of the following: + # + # - nil: Tries to load and parse '.xml'. + # - filename: Tries to load and parse filename. + # - IO object: Reads from object until EOF is detected and parses result. + # - XML string: Parses string. + # + # options:: + # Options to be used. + def xml_in(string = nil, options = nil) + handle_options('in', options) + + # If no XML string or filename was supplied look for scriptname.xml. + if string.nil? + string = File::basename($0).dup + string.sub!(/\.[^.]+$/, '') + string += '.xml' + + directory = File::dirname($0) + @options['searchpath'].unshift(directory) unless directory.nil? + end + + if string.is_a?(String) + if string =~ /<.*?>/m + @doc = parse(string) + elsif string == '-' + @doc = parse($stdin.read) + else + filename = find_xml_file(string, @options['searchpath']) + + if @options.has_key?('cache') + @options['cache'].each { |scheme| + case(scheme) + when 'storable' + content = @@cache.restore_storable(filename) + when 'mem_share' + content = @@cache.restore_mem_share(filename) + when 'mem_copy' + content = @@cache.restore_mem_copy(filename) + else + raise ArgumentError, "Unsupported caching scheme: <#{scheme}>." + end + return content if content + } + end + + @doc = load_xml_file(filename) + end + elsif string.respond_to?(:read) + @doc = parse(string.read) + else + raise ArgumentError, "Could not parse object of type: <#{string.class}>." + end + + result = collapse(@doc.root) + result = @options['keeproot'] ? merge({}, @doc.root.name, result) : result + put_into_cache(result, filename) + result + end + + # This is the functional version of the instance method xml_in. + def XmlSimple.xml_in(string = nil, options = nil) + xml_simple = XmlSimple.new + xml_simple.xml_in(string, options) + end + + # Converts a data structure into an XML document. + # + # ref:: + # Reference to data structure to be converted into XML. + # options:: + # Options to be used. + def xml_out(ref, options = nil) + handle_options('out', options) + if ref.is_a?(Array) + ref = { @options['anonymoustag'] => ref } + end + + if @options['keeproot'] + keys = ref.keys + if keys.size == 1 + ref = ref[keys[0]] + @options['rootname'] = keys[0] + end + elsif @options['rootname'] == '' + if ref.is_a?(Hash) + refsave = ref + ref = {} + refsave.each { |key, value| + if !scalar(value) + ref[key] = value + else + ref[key] = [ value.to_s ] + end + } + end + end + + @ancestors = [] + xml = value_to_xml(ref, @options['rootname'], '') + @ancestors = nil + + if @options['xmldeclaration'] + xml = @options['xmldeclaration'] + "\n" + xml + end + + if @options.has_key?('outputfile') + if @options['outputfile'].kind_of?(IO) + return @options['outputfile'].write(xml) + else + File.open(@options['outputfile'], "w") { |file| file.write(xml) } + end + end + xml + end + + # This is the functional version of the instance method xml_out. + def XmlSimple.xml_out(hash, options = nil) + xml_simple = XmlSimple.new + xml_simple.xml_out(hash, options) + end + + private + + # Declare options that are valid for xml_in and xml_out. + KNOWN_OPTIONS = { + 'in' => %w( + keyattr keeproot forcecontent contentkey noattr searchpath + forcearray suppressempty anonymoustag cache grouptags + normalisespace normalizespace variables varattr keytosymbol + attrtosymbol attrprefix conversions + ), + 'out' => %w( + keyattr keeproot contentkey noattr rootname + xmldeclaration outputfile noescape suppressempty + anonymoustag indent grouptags noindent attrprefix + ) + } + + # Define some reasonable defaults. + DEF_KEY_ATTRIBUTES = [] + DEF_ROOT_NAME = 'opt' + DEF_CONTENT_KEY = 'content' + DEF_XML_DECLARATION = "" + DEF_ANONYMOUS_TAG = 'anon' + DEF_FORCE_ARRAY = true + DEF_INDENTATION = ' ' + DEF_KEY_TO_SYMBOL = false + DEF_ATTR_TO_SYMBOL = false + + # Normalizes option names in a hash, i.e., turns all + # characters to lower case and removes all underscores. + # Additionally, this method checks if an unknown option + # was used, and raises an according exception. + # + # options:: + # Hash to be normalized. + # known_options:: + # List of known options. + def normalize_option_names(options, known_options) + return nil if options.nil? + result = Hash.new + options.each { |key, value| + lkey = key.to_s.downcase.gsub(/_/, '') + if !known_options.member?(lkey) + raise ArgumentError, "Unrecognized option: #{lkey}." + end + result[lkey] = value + } + result + end + + # Merges a set of options with the default options. + # + # direction:: + # 'in': If options should be handled for xml_in. + # 'out': If options should be handled for xml_out. + # options:: + # Options to be merged with the default options. + def handle_options(direction, options) + @options = options || Hash.new + + raise ArgumentError, "Options must be a Hash!" unless @options.is_a?(Hash) + + unless KNOWN_OPTIONS.has_key?(direction) + raise ArgumentError, "Unknown direction: <#{direction}>." + end + + known_options = KNOWN_OPTIONS[direction] + @options = normalize_option_names(@options, known_options) + + unless @default_options.nil? + known_options.each { |option| + unless @options.has_key?(option) + if @default_options.has_key?(option) + @options[option] = @default_options[option] + end + end + } + end + + unless @options.has_key?('noattr') + @options['noattr'] = false + end + + if @options.has_key?('rootname') + @options['rootname'] = '' if @options['rootname'].nil? + else + @options['rootname'] = DEF_ROOT_NAME + end + + if @options.has_key?('xmldeclaration') && @options['xmldeclaration'] == true + @options['xmldeclaration'] = DEF_XML_DECLARATION + end + + @options['keytosymbol'] = DEF_KEY_TO_SYMBOL unless @options.has_key?('keytosymbol') + + @options['attrtosymbol'] = DEF_ATTR_TO_SYMBOL unless @options.has_key?('attrtosymbol') + + if @options.has_key?('contentkey') + if @options['contentkey'] =~ /^-(.*)$/ + @options['contentkey'] = $1 + @options['collapseagain'] = true + end + else + @options['contentkey'] = DEF_CONTENT_KEY + end + + unless @options.has_key?('normalisespace') + @options['normalisespace'] = @options['normalizespace'] + end + @options['normalisespace'] = 0 if @options['normalisespace'].nil? + + if @options.has_key?('searchpath') + unless @options['searchpath'].is_a?(Array) + @options['searchpath'] = [ @options['searchpath'] ] + end + else + @options['searchpath'] = [] + end + + if @options.has_key?('cache') && scalar(@options['cache']) + @options['cache'] = [ @options['cache'] ] + end + + @options['anonymoustag'] = DEF_ANONYMOUS_TAG unless @options.has_key?('anonymoustag') + + if !@options.has_key?('indent') || @options['indent'].nil? + @options['indent'] = DEF_INDENTATION + end + + @options['indent'] = '' if @options.has_key?('noindent') + + # Special cleanup for 'keyattr' which could be an array or + # a hash or left to default to array. + if @options.has_key?('keyattr') + if !scalar(@options['keyattr']) + # Convert keyattr => { elem => '+attr' } + # to keyattr => { elem => ['attr', '+'] } + if @options['keyattr'].is_a?(Hash) + @options['keyattr'].each { |key, value| + if value =~ /^([-+])?(.*)$/ + @options['keyattr'][key] = [$2, $1 ? $1 : ''] + end + } + elsif !@options['keyattr'].is_a?(Array) + raise ArgumentError, "'keyattr' must be String, Hash, or Array!" + end + else + @options['keyattr'] = [ @options['keyattr'] ] + end + else + @options['keyattr'] = DEF_KEY_ATTRIBUTES + end + + if @options.has_key?('forcearray') + if @options['forcearray'].is_a?(Regexp) + @options['forcearray'] = [ @options['forcearray'] ] + end + + if @options['forcearray'].is_a?(Array) + force_list = @options['forcearray'] + unless force_list.empty? + @options['forcearray'] = {} + force_list.each { |tag| + if tag.is_a?(Regexp) + unless @options['forcearray']['_regex'].is_a?(Array) + @options['forcearray']['_regex'] = [] + end + @options['forcearray']['_regex'] << tag + else + @options['forcearray'][tag] = true + end + } + else + @options['forcearray'] = false + end + else + @options['forcearray'] = @options['forcearray'] ? true : false + end + else + @options['forcearray'] = DEF_FORCE_ARRAY + end + + if @options.has_key?('grouptags') && !@options['grouptags'].is_a?(Hash) + raise ArgumentError, "Illegal value for 'GroupTags' option - expected a Hash." + end + + if @options.has_key?('variables') && !@options['variables'].is_a?(Hash) + raise ArgumentError, "Illegal value for 'Variables' option - expected a Hash." + end + + if @options.has_key?('variables') + @_var_values = @options['variables'] + elsif @options.has_key?('varattr') + @_var_values = {} + end + end + + # Actually converts an XML document element into a data structure. + # + # element:: + # The document element to be collapsed. + def collapse(element) + result = @options['noattr'] ? {} : get_attributes(element) + + if @options['normalisespace'] == 2 + result.each { |k, v| result[k] = normalise_space(v) } + end + + if element.has_elements? + element.each_element { |child| + value = collapse(child) + if empty(value) && (element.attributes.empty? || @options['noattr']) + next if @options.has_key?('suppressempty') && @options['suppressempty'] == true + end + result = merge(result, child.name, value) + } + if has_mixed_content?(element) + # normalisespace? + content = element.texts.map { |x| x.to_s } + content = content[0] if content.size == 1 + result[@options['contentkey']] = content + end + elsif element.has_text? # i.e. it has only text. + return collapse_text_node(result, element) + end + + # Turn Arrays into Hashes if key fields present. + count = fold_arrays(result) + + # Disintermediate grouped tags. + if @options.has_key?('grouptags') + result.each { |key, value| + next unless (value.is_a?(Hash) && (value.size == 1)) + child_key, child_value = value.to_a[0] + if @options['grouptags'][key] == child_key + result[key] = child_value + end + } + end + + # Fold Hashes containing a single anonymous Array up into just the Array. + if count == 1 + anonymoustag = @options['anonymoustag'] + if result.has_key?(anonymoustag) && result[anonymoustag].is_a?(Array) + return result[anonymoustag] + end + end + + if result.empty? && @options.has_key?('suppressempty') + return @options['suppressempty'] == '' ? '' : nil + end + + result + end + + # Collapses a text node and merges it with an existing Hash, if + # possible. + # Thanks to Curtis Schofield for reporting a subtle bug. + # + # hash:: + # Hash to merge text node value with, if possible. + # element:: + # Text node to be collapsed. + def collapse_text_node(hash, element) + value = node_to_text(element) + if empty(value) && !element.has_attributes? + return {} + end + + if element.has_attributes? && !@options['noattr'] + return merge(hash, @options['contentkey'], value) + else + if @options['forcecontent'] + return merge(hash, @options['contentkey'], value) + else + return value + end + end + end + + # Folds all arrays in a Hash. + # + # hash:: + # Hash to be folded. + def fold_arrays(hash) + fold_amount = 0 + keyattr = @options['keyattr'] + if (keyattr.is_a?(Array) || keyattr.is_a?(Hash)) + hash.each { |key, value| + if value.is_a?(Array) + if keyattr.is_a?(Array) + hash[key] = fold_array(value) + else + hash[key] = fold_array_by_name(key, value) + end + fold_amount += 1 + end + } + end + fold_amount + end + + # Folds an Array to a Hash, if possible. Folding happens + # according to the content of keyattr, which has to be + # an array. + # + # array:: + # Array to be folded. + def fold_array(array) + hash = Hash.new + array.each { |x| + return array unless x.is_a?(Hash) + key_matched = false + @options['keyattr'].each { |key| + if x.has_key?(key) + key_matched = true + value = x[key] + return array if value.is_a?(Hash) || value.is_a?(Array) + value = normalise_space(value) if @options['normalisespace'] == 1 + x.delete(key) + hash[value] = x + break + end + } + return array unless key_matched + } + hash = collapse_content(hash) if @options['collapseagain'] + hash + end + + # Folds an Array to a Hash, if possible. Folding happens + # according to the content of keyattr, which has to be + # a Hash. + # + # name:: + # Name of the attribute to be folded upon. + # array:: + # Array to be folded. + def fold_array_by_name(name, array) + return array unless @options['keyattr'].has_key?(name) + key, flag = @options['keyattr'][name] + + hash = Hash.new + array.each { |x| + if x.is_a?(Hash) && x.has_key?(key) + value = x[key] + return array if value.is_a?(Hash) || value.is_a?(Array) + value = normalise_space(value) if @options['normalisespace'] == 1 + hash[value] = x + hash[value]["-#{key}"] = hash[value][key] if flag == '-' + hash[value].delete(key) unless flag == '+' + else + $stderr.puts("Warning: <#{name}> element has no '#{key}' attribute.") + return array + end + } + hash = collapse_content(hash) if @options['collapseagain'] + hash + end + + # Tries to collapse a Hash even more ;-) + # + # hash:: + # Hash to be collapsed again. + def collapse_content(hash) + content_key = @options['contentkey'] + hash.each_value { |value| + return hash unless value.is_a?(Hash) && value.size == 1 && value.has_key?(content_key) + hash.each_key { |key| hash[key] = hash[key][content_key] } + } + hash + end + + # Adds a new key/value pair to an existing Hash. If the key to be added + # does already exist and the existing value associated with key is not + # an Array, it will be converted into an Array. Then the new value is + # appended to that Array. + # + # hash:: + # Hash to add key/value pair to. + # key:: + # Key to be added. + # value:: + # Value to be associated with key. + def merge(hash, key, value) + if value.is_a?(String) + value = normalise_space(value) if @options['normalisespace'] == 2 + + if conv = @options['conversions'] and conv = conv.find {|c,_| c.match(key)} and conv = conv.at(1) + value = conv.call(value) + end + + # do variable substitutions + unless @_var_values.nil? || @_var_values.empty? + value = value.gsub(/\$\{(\w+)\}/) { |x| get_var($1) } + end + + # look for variable definitions + if @options.has_key?('varattr') + varattr = @options['varattr'] + if hash.has_key?(varattr) + set_var(hash[varattr], value) + end + end + end + + #patch for converting keys to symbols + if @options.has_key?('keytosymbol') + if @options['keytosymbol'] == true + key = key.to_s.downcase.to_sym + end + end + + if hash.has_key?(key) + if hash[key].is_a?(Array) + hash[key] << value + else + hash[key] = [ hash[key], value ] + end + elsif value.is_a?(Array) # Handle anonymous arrays. + hash[key] = [ value ] + else + if force_array?(key) + hash[key] = [ value ] + else + hash[key] = value + end + end + hash + end + + # Checks, if the 'forcearray' option has to be used for + # a certain key. + def force_array?(key) + return false if key == @options['contentkey'] + return true if @options['forcearray'] == true + forcearray = @options['forcearray'] + if forcearray.is_a?(Hash) + return true if forcearray.has_key?(key) + return false unless forcearray.has_key?('_regex') + forcearray['_regex'].each { |x| return true if key =~ x } + end + return false + end + + # Converts the attributes array of a document node into a Hash. + # Returns an empty Hash, if node has no attributes. + # + # node:: + # Document node to extract attributes from. + def get_attributes(node) + attributes = {} + if @options['attrprefix'] + node.attributes.each { |n,v| attributes["@" + n] = v } + elsif @options.has_key?('attrtosymbol') and @options['attrtosymbol'] == true + #patch for converting attribute names to symbols + node.attributes.each { |n,v| attributes[n.to_sym] = v } + else + node.attributes.each { |n,v| attributes[n] = v } + end + + attributes + end + + # Determines, if a document element has mixed content. + # + # element:: + # Document element to be checked. + def has_mixed_content?(element) + element.has_text? && + element.has_elements? && + !element.texts.join('').strip.empty? + end + + # Called when a variable definition is encountered in the XML. + # A variable definition looks like + # value + # where attrname matches the varattr setting. + def set_var(name, value) + @_var_values[name] = value + end + + # Called during variable substitution to get the value for the + # named variable. + def get_var(name) + if @_var_values.has_key?(name) + return @_var_values[name] + else + return "${#{name}}" + end + end + + # Recurses through a data structure building up and returning an + # XML representation of that structure as a string. + # + # ref:: + # Reference to the data structure to be encoded. + # name:: + # The XML tag name to be used for this item. + # indent:: + # A string of spaces for use as the current indent level. + def value_to_xml(ref, name, indent) + named = !name.nil? && name != '' + nl = @options.has_key?('noindent') ? '' : "\n" + + if !scalar(ref) + if @ancestors.member?(ref) + raise ArgumentError, "Circular data structures not supported!" + end + @ancestors << ref + else + if named + return [indent, '<', name, '>', @options['noescape'] ? ref.to_s : escape_value(ref.to_s), '', nl].join('') + else + return ref.to_s + nl + end + end + + # Unfold hash to array if possible. + if ref.is_a?(Hash) && !ref.empty? && !@options['keyattr'].empty? && indent != '' + ref = hash_to_array(name, ref) + end + + result = [] + if ref.is_a?(Hash) + # Reintermediate grouped values if applicable. + if @options.has_key?('grouptags') + ref.each { |key, value| + if @options['grouptags'].has_key?(key) + ref[key] = { @options['grouptags'][key] => value } + end + } + end + + nested = [] + text_content = nil + if named + result << (indent + '<' + name) + end + + if !ref.empty? + ref.each { |key, value| + next if !key.nil? && key.to_s[0, 1] == '-' + if value.nil? + unless @options.has_key?('suppressempty') && @options['suppressempty'].nil? + raise ArgumentError, "Use of uninitialized value!" + end + value = {} + end + + # Check for the '@' attribute prefix to allow separation of attributes and elements + + if (@options['noattr'] || + (@options['attrprefix'] && !(key =~ /^@(.*)/)) || + !scalar(value) + ) && + key != @options['contentkey'] + nested << value_to_xml(value, key, indent + @options['indent']) + else + value = value.to_s + value = escape_value(value) unless @options['noescape'] + if key == @options['contentkey'] + text_content = value + else + result << (' ' + ($1||key) + '="' + value + '"') + end + end + } + else + text_content = '' + end + + if !nested.empty? || !text_content.nil? + if named + result << '>' + if !text_content.nil? + result << text_content + nested[0] = nested[0].sub(/^\s+/, '') if !nested.empty? + else + result << nl + end + if !nested.empty? + result << nested << indent + end + result << ('' + nl) + else + result << nested + end + else + result << (' />' + nl) + end + elsif ref.is_a?(Array) + ref.each { |value| + if scalar(value) + result << (indent + '<' + name + '>') + result << (@options['noescape'] ? value.to_s : escape_value(value.to_s)) + result << ('' + nl) + elsif value.is_a?(Hash) + result << value_to_xml(value, name, indent) + else + result << (indent + '<' + name + '>' + nl) + result << value_to_xml(value, @options['anonymoustag'], indent + @options['indent']) + result << (indent + '' + nl) + end + } + else + # Probably, this is obsolete. + raise ArgumentError, "Can't encode a value of type: #{ref.type}." + end + @ancestors.pop if !scalar(ref) + result.join('') + end + + # Checks, if a certain value is a "scalar" value. Whatever + # that will be in Ruby ... ;-) + # + # value:: + # Value to be checked. + def scalar(value) + return false if value.is_a?(Hash) || value.is_a?(Array) + return true + end + + # Attempts to unfold a hash of hashes into an array of hashes. Returns + # a reference to th array on success or the original hash, if unfolding + # is not possible. + # + # parent:: + # + # hashref:: + # Reference to the hash to be unfolded. + def hash_to_array(parent, hashref) + arrayref = [] + hashref.each { |key, value| + return hashref unless value.is_a?(Hash) + + if @options['keyattr'].is_a?(Hash) + return hashref unless @options['keyattr'].has_key?(parent) + arrayref << { @options['keyattr'][parent][0] => key }.update(value) + else + arrayref << { @options['keyattr'][0] => key }.update(value) + end + } + arrayref + end + + # Replaces XML markup characters by their external entities. + # + # data:: + # The string to be escaped. + def escape_value(data) + Text::normalize(data) + end + + # Removes leading and trailing whitespace and sequences of + # whitespaces from a string. + # + # text:: + # String to be normalised. + def normalise_space(text) + text.strip.gsub(/\s\s+/, ' ') + end + + # Checks, if an object is nil, an empty String or an empty Hash. + # Thanks to Norbert Gawor for a bugfix. + # + # value:: + # Value to be checked for emptyness. + def empty(value) + case value + when Hash + return value.empty? + when String + return value !~ /\S/m + else + return value.nil? + end + end + + # Converts a document node into a String. + # If the node could not be converted into a String + # for any reason, default will be returned. + # + # node:: + # Document node to be converted. + # default:: + # Value to be returned, if node could not be converted. + def node_to_text(node, default = nil) + if node.is_a?(REXML::Element) + node.texts.map { |t| t.value }.join('') + elsif node.is_a?(REXML::Attribute) + node.value.nil? ? default : node.value.strip + elsif node.is_a?(REXML::Text) + node.value.strip + else + default + end + end + + # Parses an XML string and returns the according document. + # + # xml_string:: + # XML string to be parsed. + # + # The following exception may be raised: + # + # REXML::ParseException:: + # If the specified file is not wellformed. + def parse(xml_string) + Document.new(xml_string) + end + + # Searches in a list of paths for a certain file. Returns + # the full path to the file, if it could be found. Otherwise, + # an exception will be raised. + # + # filename:: + # Name of the file to search for. + # searchpath:: + # List of paths to search in. + def find_xml_file(file, searchpath) + filename = File::basename(file) + + if filename != file + return file if File::file?(file) + else + searchpath.each { |path| + full_path = File::join(path, filename) + return full_path if File::file?(full_path) + } + end + + if searchpath.empty? + return file if File::file?(file) + raise ArgumentError, "File does not exist: #{file}." + end + raise ArgumentError, "Could not find <#{filename}> in <#{searchpath.join(':')}>" + end + + # Loads and parses an XML configuration file. + # + # filename:: + # Name of the configuration file to be loaded. + # + # The following exceptions may be raised: + # + # Errno::ENOENT:: + # If the specified file does not exist. + # REXML::ParseException:: + # If the specified file is not wellformed. + def load_xml_file(filename) + parse(IO::read(filename)) + end + + # Caches the data belonging to a certain file. + # + # data:: + # Data to be cached. + # filename:: + # Name of file the data was read from. + def put_into_cache(data, filename) + if @options.has_key?('cache') + @options['cache'].each { |scheme| + case(scheme) + when 'storable' + @@cache.save_storable(data, filename) + when 'mem_share' + @@cache.save_mem_share(data, filename) + when 'mem_copy' + @@cache.save_mem_copy(data, filename) + else + raise ArgumentError, "Unsupported caching scheme: <#{scheme}>." + end + } + end + end +end + +# vim:sw=2 diff --git a/gxg/standard/English.rb b/gxg/standard/English.rb new file mode 100644 index 0000000..a4f5bb6 --- /dev/null +++ b/gxg/standard/English.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true +# Include the English library file in a Ruby script, and you can +# reference the global variables such as \VAR{\$\_} using less +# cryptic names, listed in the following table.% \vref{tab:english}. +# +# Without 'English': +# +# $\ = ' -- ' +# "waterbuffalo" =~ /buff/ +# print $', $$, "\n" +# +# With English: +# +# require "English" +# +# $OUTPUT_FIELD_SEPARATOR = ' -- ' +# "waterbuffalo" =~ /buff/ +# print $POSTMATCH, $PID, "\n" +# +# Below is a full list of descriptive aliases and their associated global +# variable: +# +# $ERROR_INFO:: $! +# $ERROR_POSITION:: $@ +# $FS:: $; +# $FIELD_SEPARATOR:: $; +# $OFS:: $, +# $OUTPUT_FIELD_SEPARATOR:: $, +# $RS:: $/ +# $INPUT_RECORD_SEPARATOR:: $/ +# $ORS:: $\ +# $OUTPUT_RECORD_SEPARATOR:: $\ +# $INPUT_LINE_NUMBER:: $. +# $NR:: $. +# $LAST_READ_LINE:: $_ +# $DEFAULT_OUTPUT:: $> +# $DEFAULT_INPUT:: $< +# $PID:: $$ +# $PROCESS_ID:: $$ +# $CHILD_STATUS:: $? +# $LAST_MATCH_INFO:: $~ +# $IGNORECASE:: $= +# $ARGV:: $* +# $MATCH:: $& +# $PREMATCH:: $` +# $POSTMATCH:: $' +# $LAST_PAREN_MATCH:: $+ +# +module English end if false + +# The exception object passed to +raise+. +alias $ERROR_INFO $! + +# The stack backtrace generated by the last +# exception. See Kernel.caller for details. Thread local. +alias $ERROR_POSITION $@ + +# The default separator pattern used by String.split. May be +# set from the command line using the -F flag. +alias $FS $; + +# The default separator pattern used by String.split. May be +# set from the command line using the -F flag. +alias $FIELD_SEPARATOR $; + +# The separator string output between the parameters to methods such +# as Kernel.print and Array.join. Defaults to +nil+, +# which adds no text. +alias $OFS $, + +# The separator string output between the parameters to methods such +# as Kernel.print and Array.join. Defaults to +nil+, +# which adds no text. +alias $OUTPUT_FIELD_SEPARATOR $, + +# The input record separator (newline by default). This is the value +# that routines such as Kernel.gets use to determine record +# boundaries. If set to +nil+, +gets+ will read the entire file. +alias $RS $/ + +# The input record separator (newline by default). This is the value +# that routines such as Kernel.gets use to determine record +# boundaries. If set to +nil+, +gets+ will read the entire file. +alias $INPUT_RECORD_SEPARATOR $/ + +# The string appended to the output of every call to methods such as +# Kernel.print and IO.write. The default value is +# +nil+. +alias $ORS $\ + +# The string appended to the output of every call to methods such as +# Kernel.print and IO.write. The default value is +# +nil+. +alias $OUTPUT_RECORD_SEPARATOR $\ + +# The number of the last line read from the current input file. +alias $INPUT_LINE_NUMBER $. + +# The number of the last line read from the current input file. +alias $NR $. + +# The last line read by Kernel.gets or +# Kernel.readline. Many string-related functions in the +# +Kernel+ module operate on $_ by default. The variable is +# local to the current scope. Thread local. +alias $LAST_READ_LINE $_ + +# The destination of output for Kernel.print +# and Kernel.printf. The default value is +# $stdout. +alias $DEFAULT_OUTPUT $> + +# An object that provides access to the concatenation +# of the contents of all the files +# given as command-line arguments, or $stdin +# (in the case where there are no +# arguments). $< supports methods similar to a +# +File+ object: +# +inmode+, +close+, +# closed?, +each+, +# each_byte, each_line, +# +eof+, eof?, +file+, +# +filename+, +fileno+, +# +getc+, +gets+, +lineno+, +# lineno=, +path+, +# +pos+, pos=, +# +read+, +readchar+, +# +readline+, +readlines+, +# +rewind+, +seek+, +skip+, +# +tell+, to_a, to_i, +# to_io, to_s, along with the +# methods in +Enumerable+. The method +file+ +# returns a +File+ object for the file currently +# being read. This may change as $< reads +# through the files on the command line. Read only. +alias $DEFAULT_INPUT $< + +# The process number of the program being executed. Read only. +alias $PID $$ + +# The process number of the program being executed. Read only. +alias $PROCESS_ID $$ + +# The exit status of the last child process to terminate. Read +# only. Thread local. +alias $CHILD_STATUS $? + +# A +MatchData+ object that encapsulates the results of a successful +# pattern match. The variables $&, $`, $', +# and $1 to $9 are all derived from +# $~. Assigning to $~ changes the values of these +# derived variables. This variable is local to the current +# scope. +alias $LAST_MATCH_INFO $~ + +# This variable is no longer effective. Deprecated. +alias $IGNORECASE $= + +# An array of strings containing the command-line +# options from the invocation of the program. Options +# used by the Ruby interpreter will have been +# removed. Read only. Also known simply as +ARGV+. +alias $ARGV $* + +# The string matched by the last successful pattern +# match. This variable is local to the current +# scope. Read only. +alias $MATCH $& + +# The string preceding the match in the last +# successful pattern match. This variable is local to +# the current scope. Read only. +alias $PREMATCH $` + +# The string following the match in the last +# successful pattern match. This variable is local to +# the current scope. Read only. +alias $POSTMATCH $' + +# The contents of the highest-numbered group matched in the last +# successful pattern match. Thus, in "cat" =~ /(c|a)(t|z)/, +# $+ will be set to "t". This variable is local to the +# current scope. Read only. +alias $LAST_PAREN_MATCH $+ diff --git a/gxg/standard/abbrev.rb b/gxg/standard/abbrev.rb new file mode 100644 index 0000000..4dee76a --- /dev/null +++ b/gxg/standard/abbrev.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true +#-- +# Copyright (c) 2001,2003 Akinori MUSHA +# +# All rights reserved. You can redistribute and/or modify it under +# the same terms as Ruby. +# +# $Idaemons: /home/cvs/rb/abbrev.rb,v 1.2 2001/05/30 09:37:45 knu Exp $ +# $RoughId: abbrev.rb,v 1.4 2003/10/14 19:45:42 knu Exp $ +# $Id: abbrev.rb 57585 2017-02-09 16:12:00Z kazu $ +#++ + +## +# Calculates the set of unambiguous abbreviations for a given set of strings. +# +# require 'abbrev' +# require 'pp' +# +# pp Abbrev.abbrev(['ruby']) +# #=> {"ruby"=>"ruby", "rub"=>"ruby", "ru"=>"ruby", "r"=>"ruby"} +# +# pp Abbrev.abbrev(%w{ ruby rules }) +# +# _Generates:_ +# { "ruby" => "ruby", +# "rub" => "ruby", +# "rules" => "rules", +# "rule" => "rules", +# "rul" => "rules" } +# +# It also provides an array core extension, Array#abbrev. +# +# pp %w{ summer winter }.abbrev +# +# _Generates:_ +# { "summer" => "summer", +# "summe" => "summer", +# "summ" => "summer", +# "sum" => "summer", +# "su" => "summer", +# "s" => "summer", +# "winter" => "winter", +# "winte" => "winter", +# "wint" => "winter", +# "win" => "winter", +# "wi" => "winter", +# "w" => "winter" } + +module Abbrev + + # Given a set of strings, calculate the set of unambiguous abbreviations for + # those strings, and return a hash where the keys are all the possible + # abbreviations and the values are the full strings. + # + # Thus, given +words+ is "car" and "cone", the keys pointing to "car" would + # be "ca" and "car", while those pointing to "cone" would be "co", "con", and + # "cone". + # + # require 'abbrev' + # + # Abbrev.abbrev(%w{ car cone }) + # #=> {"ca"=>"car", "con"=>"cone", "co"=>"cone", "car"=>"car", "cone"=>"cone"} + # + # The optional +pattern+ parameter is a pattern or a string. Only input + # strings that match the pattern or start with the string are included in the + # output hash. + # + # Abbrev.abbrev(%w{car box cone crab}, /b/) + # #=> {"box"=>"box", "bo"=>"box", "b"=>"box", "crab" => "crab"} + # + # Abbrev.abbrev(%w{car box cone}, 'ca') + # #=> {"car"=>"car", "ca"=>"car"} + def abbrev(words, pattern = nil) + table = {} + seen = Hash.new(0) + + if pattern.is_a?(String) + pattern = /\A#{Regexp.quote(pattern)}/ # regard as a prefix + end + + words.each do |word| + next if word.empty? + word.size.downto(1) { |len| + abbrev = word[0...len] + + next if pattern && pattern !~ abbrev + + case seen[abbrev] += 1 + when 1 + table[abbrev] = word + when 2 + table.delete(abbrev) + else + break + end + } + end + + words.each do |word| + next if pattern && pattern !~ word + + table[word] = word + end + + table + end + + module_function :abbrev +end + +class Array + # Calculates the set of unambiguous abbreviations for the strings in +self+. + # + # require 'abbrev' + # %w{ car cone }.abbrev + # #=> {"car"=>"car", "ca"=>"car", "cone"=>"cone", "con"=>"cone", "co"=>"cone"} + # + # The optional +pattern+ parameter is a pattern or a string. Only input + # strings that match the pattern or start with the string are included in the + # output hash. + # + # %w{ fast boat day }.abbrev(/^.a/) + # #=> {"fast"=>"fast", "fas"=>"fast", "fa"=>"fast", "day"=>"day", "da"=>"day"} + # + # Abbrev.abbrev(%w{car box cone}, "ca") + # #=> {"car"=>"car", "ca"=>"car"} + # + # See also Abbrev.abbrev + def abbrev(pattern = nil) + Abbrev::abbrev(self, pattern) + end +end diff --git a/gxg/standard/cmath.rb b/gxg/standard/cmath.rb new file mode 100644 index 0000000..7dbd65e --- /dev/null +++ b/gxg/standard/cmath.rb @@ -0,0 +1,435 @@ +# frozen_string_literal: true +## +# = Trigonometric and transcendental functions for complex numbers. +# +# CMath is a library that provides trigonometric and transcendental +# functions for complex numbers. The functions in this module accept +# integers, floating-point numbers or complex numbers as arguments. +# +# Note that the selection of functions is similar, but not identical, +# to that in module math. The reason for having two modules is that +# some users aren't interested in complex numbers, and perhaps don't +# even know what they are. They would rather have Math.sqrt(-1) raise +# an exception than return a complex number. +# +# For more information you can see Complex class. +# +# == Usage +# +# To start using this library, simply require cmath library: +# +# require "cmath" + +module CMath + + include Math + + # Backup of Math is needed because mathn.rb replaces Math with CMath. + RealMath = Math # :nodoc: + private_constant :RealMath + + %w[ + exp + log + log2 + log10 + sqrt + cbrt + sin + cos + tan + sinh + cosh + tanh + asin + acos + atan + atan2 + asinh + acosh + atanh + ].each do |meth| + define_method(meth + '!') do |*args, &block| + warn("CMath##{meth}! is deprecated; use CMath##{meth} or Math##{meth}", uplevel: 1) if $VERBOSE + RealMath.send(meth, *args, &block) + end + end + + ## + # Math::E raised to the +z+ power + # + # CMath.exp(1.i * Math::PI) #=> (-1.0+1.2246467991473532e-16i) + def exp(z) + begin + if z.real? + RealMath.exp(z) + else + ere = RealMath.exp(z.real) + Complex(ere * RealMath.cos(z.imag), + ere * RealMath.sin(z.imag)) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the natural logarithm of Complex. If a second argument is given, + # it will be the base of logarithm. + # + # CMath.log(1 + 4i) #=> (1.416606672028108+1.3258176636680326i) + # CMath.log(1 + 4i, 10) #=> (0.6152244606891369+0.5757952953408879i) + def log(z, b=::Math::E) + begin + if z.real? && z >= 0 && b >= 0 + RealMath.log(z, b) + else + Complex(RealMath.log(z.abs), z.arg) / log(b) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the base 2 logarithm of +z+ + # + # CMath.log2(-1) => (0.0+4.532360141827194i) + def log2(z) + begin + if z.real? and z >= 0 + RealMath.log2(z) + else + log(z) / RealMath.log(2) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the base 10 logarithm of +z+ + # + # CMath.log10(-1) #=> (0.0+1.3643763538418412i) + def log10(z) + begin + if z.real? and z >= 0 + RealMath.log10(z) + else + log(z) / RealMath.log(10) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the non-negative square root of Complex. + # + # CMath.sqrt(-1 + 0i) #=> 0.0+1.0i + def sqrt(z) + begin + if z.real? + if z < 0 + Complex(0, RealMath.sqrt(-z)) + else + RealMath.sqrt(z) + end + else + if z.imag < 0 || + (z.imag == 0 && z.imag.to_s[0] == '-') + sqrt(z.conjugate).conjugate + else + r = z.abs + x = z.real + Complex(RealMath.sqrt((r + x) / 2.0), RealMath.sqrt((r - x) / 2.0)) + end + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the principal value of the cube root of +z+ + # + # CMath.cbrt(1 + 4i) #=> (1.449461632813119+0.6858152562177092i) + def cbrt(z) + z ** (1.0/3) + end + + ## + # Returns the sine of +z+, where +z+ is given in radians + # + # CMath.sin(1 + 1i) #=> (1.2984575814159773+0.6349639147847361i) + def sin(z) + begin + if z.real? + RealMath.sin(z) + else + Complex(RealMath.sin(z.real) * RealMath.cosh(z.imag), + RealMath.cos(z.real) * RealMath.sinh(z.imag)) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the cosine of +z+, where +z+ is given in radians + # + # CMath.cos(1 + 1i) #=> (0.8337300251311491-0.9888977057628651i) + def cos(z) + begin + if z.real? + RealMath.cos(z) + else + Complex(RealMath.cos(z.real) * RealMath.cosh(z.imag), + -RealMath.sin(z.real) * RealMath.sinh(z.imag)) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the tangent of +z+, where +z+ is given in radians + # + # CMath.tan(1 + 1i) #=> (0.27175258531951174+1.0839233273386943i) + def tan(z) + begin + if z.real? + RealMath.tan(z) + else + sin(z) / cos(z) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the hyperbolic sine of +z+, where +z+ is given in radians + # + # CMath.sinh(1 + 1i) #=> (0.6349639147847361+1.2984575814159773i) + def sinh(z) + begin + if z.real? + RealMath.sinh(z) + else + Complex(RealMath.sinh(z.real) * RealMath.cos(z.imag), + RealMath.cosh(z.real) * RealMath.sin(z.imag)) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the hyperbolic cosine of +z+, where +z+ is given in radians + # + # CMath.cosh(1 + 1i) #=> (0.8337300251311491+0.9888977057628651i) + def cosh(z) + begin + if z.real? + RealMath.cosh(z) + else + Complex(RealMath.cosh(z.real) * RealMath.cos(z.imag), + RealMath.sinh(z.real) * RealMath.sin(z.imag)) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the hyperbolic tangent of +z+, where +z+ is given in radians + # + # CMath.tanh(1 + 1i) #=> (1.0839233273386943+0.27175258531951174i) + def tanh(z) + begin + if z.real? + RealMath.tanh(z) + else + sinh(z) / cosh(z) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the arc sine of +z+ + # + # CMath.asin(1 + 1i) #=> (0.6662394324925153+1.0612750619050355i) + def asin(z) + begin + if z.real? and z >= -1 and z <= 1 + RealMath.asin(z) + else + (-1.0).i * log(1.0.i * z + sqrt(1.0 - z * z)) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the arc cosine of +z+ + # + # CMath.acos(1 + 1i) #=> (0.9045568943023813-1.0612750619050357i) + def acos(z) + begin + if z.real? and z >= -1 and z <= 1 + RealMath.acos(z) + else + (-1.0).i * log(z + 1.0.i * sqrt(1.0 - z * z)) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # Returns the arc tangent of +z+ + # + # CMath.atan(1 + 1i) #=> (1.0172219678978514+0.4023594781085251i) + def atan(z) + begin + if z.real? + RealMath.atan(z) + else + 1.0.i * log((1.0.i + z) / (1.0.i - z)) / 2.0 + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # returns the arc tangent of +y+ divided by +x+ using the signs of +y+ and + # +x+ to determine the quadrant + # + # CMath.atan2(1 + 1i, 0) #=> (1.5707963267948966+0.0i) + def atan2(y,x) + begin + if y.real? and x.real? + RealMath.atan2(y,x) + else + (-1.0).i * log((x + 1.0.i * y) / sqrt(x * x + y * y)) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # returns the inverse hyperbolic sine of +z+ + # + # CMath.asinh(1 + 1i) #=> (1.0612750619050357+0.6662394324925153i) + def asinh(z) + begin + if z.real? + RealMath.asinh(z) + else + log(z + sqrt(1.0 + z * z)) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # returns the inverse hyperbolic cosine of +z+ + # + # CMath.acosh(1 + 1i) #=> (1.0612750619050357+0.9045568943023813i) + def acosh(z) + begin + if z.real? and z >= 1 + RealMath.acosh(z) + else + log(z + sqrt(z * z - 1.0)) + end + rescue NoMethodError + handle_no_method_error + end + end + + ## + # returns the inverse hyperbolic tangent of +z+ + # + # CMath.atanh(1 + 1i) #=> (0.4023594781085251+1.0172219678978514i) + def atanh(z) + begin + if z.real? and z >= -1 and z <= 1 + RealMath.atanh(z) + else + log((1.0 + z) / (1.0 - z)) / 2.0 + end + rescue NoMethodError + handle_no_method_error + end + end + + module_function :exp! + module_function :exp + module_function :log! + module_function :log + module_function :log2! + module_function :log2 + module_function :log10! + module_function :log10 + module_function :sqrt! + module_function :sqrt + module_function :cbrt! + module_function :cbrt + + module_function :sin! + module_function :sin + module_function :cos! + module_function :cos + module_function :tan! + module_function :tan + + module_function :sinh! + module_function :sinh + module_function :cosh! + module_function :cosh + module_function :tanh! + module_function :tanh + + module_function :asin! + module_function :asin + module_function :acos! + module_function :acos + module_function :atan! + module_function :atan + module_function :atan2! + module_function :atan2 + + module_function :asinh! + module_function :asinh + module_function :acosh! + module_function :acosh + module_function :atanh! + module_function :atanh + + module_function :frexp + module_function :ldexp + module_function :hypot + module_function :erf + module_function :erfc + module_function :gamma + module_function :lgamma + + private + def handle_no_method_error # :nodoc: + if $!.name == :real? + raise TypeError, "Numeric Number required" + else + raise + end + end + module_function :handle_no_method_error + +end diff --git a/gxg/standard/csv.rb b/gxg/standard/csv.rb new file mode 100644 index 0000000..802db1e --- /dev/null +++ b/gxg/standard/csv.rb @@ -0,0 +1,2355 @@ +# encoding: US-ASCII +# frozen_string_literal: true +# = csv.rb -- CSV Reading and Writing +# +# Created by James Edward Gray II on 2005-10-31. +# Copyright 2005 James Edward Gray II. You can redistribute or modify this code +# under the terms of Ruby's license. +# +# See CSV for documentation. +# +# == Description +# +# Welcome to the new and improved CSV. +# +# This version of the CSV library began its life as FasterCSV. FasterCSV was +# intended as a replacement to Ruby's then standard CSV library. It was +# designed to address concerns users of that library had and it had three +# primary goals: +# +# 1. Be significantly faster than CSV while remaining a pure Ruby library. +# 2. Use a smaller and easier to maintain code base. (FasterCSV eventually +# grew larger, was also but considerably richer in features. The parsing +# core remains quite small.) +# 3. Improve on the CSV interface. +# +# Obviously, the last one is subjective. I did try to defer to the original +# interface whenever I didn't have a compelling reason to change it though, so +# hopefully this won't be too radically different. +# +# We must have met our goals because FasterCSV was renamed to CSV and replaced +# the original library as of Ruby 1.9. If you are migrating code from 1.8 or +# earlier, you may have to change your code to comply with the new interface. +# +# == What's Different From the Old CSV? +# +# I'm sure I'll miss something, but I'll try to mention most of the major +# differences I am aware of, to help others quickly get up to speed: +# +# === CSV Parsing +# +# * This parser is m17n aware. See CSV for full details. +# * This library has a stricter parser and will throw MalformedCSVErrors on +# problematic data. +# * This library has a less liberal idea of a line ending than CSV. What you +# set as the :row_sep is law. It can auto-detect your line endings +# though. +# * The old library returned empty lines as [nil]. This library calls +# them []. +# * This library has a much faster parser. +# +# === Interface +# +# * CSV now uses Hash-style parameters to set options. +# * CSV no longer has generate_row() or parse_row(). +# * The old CSV's Reader and Writer classes have been dropped. +# * CSV::open() is now more like Ruby's open(). +# * CSV objects now support most standard IO methods. +# * CSV now has a new() method used to wrap objects like String and IO for +# reading and writing. +# * CSV::generate() is different from the old method. +# * CSV no longer supports partial reads. It works line-by-line. +# * CSV no longer allows the instance methods to override the separators for +# performance reasons. They must be set in the constructor. +# +# If you use this library and find yourself missing any functionality I have +# trimmed, please {let me know}[mailto:james@grayproductions.net]. +# +# == Documentation +# +# See CSV for documentation. +# +# == What is CSV, really? +# +# CSV maintains a pretty strict definition of CSV taken directly from +# {the RFC}[http://www.ietf.org/rfc/rfc4180.txt]. I relax the rules in only one +# place and that is to make using this library easier. CSV will parse all valid +# CSV. +# +# What you don't want to do is feed CSV invalid data. Because of the way the +# CSV format works, it's common for a parser to need to read until the end of +# the file to be sure a field is invalid. This eats a lot of time and memory. +# +# Luckily, when working with invalid CSV, Ruby's built-in methods will almost +# always be superior in every way. For example, parsing non-quoted fields is as +# easy as: +# +# data.split(",") +# +# == Questions and/or Comments +# +# Feel free to email {James Edward Gray II}[mailto:james@grayproductions.net] +# with any questions. + +require "forwardable" +require "standard/English" +require "date" +require "stringio" + +# +# This class provides a complete interface to CSV files and data. It offers +# tools to enable you to read and write to and from Strings or IO objects, as +# needed. +# +# == Reading +# +# === From a File +# +# ==== A Line at a Time +# +# CSV.foreach("path/to/file.csv") do |row| +# # use row here... +# end +# +# ==== All at Once +# +# arr_of_arrs = CSV.read("path/to/file.csv") +# +# === From a String +# +# ==== A Line at a Time +# +# CSV.parse("CSV,data,String") do |row| +# # use row here... +# end +# +# ==== All at Once +# +# arr_of_arrs = CSV.parse("CSV,data,String") +# +# == Writing +# +# === To a File +# +# CSV.open("path/to/file.csv", "wb") do |csv| +# csv << ["row", "of", "CSV", "data"] +# csv << ["another", "row"] +# # ... +# end +# +# === To a String +# +# csv_string = CSV.generate do |csv| +# csv << ["row", "of", "CSV", "data"] +# csv << ["another", "row"] +# # ... +# end +# +# == Convert a Single Line +# +# csv_string = ["CSV", "data"].to_csv # to CSV +# csv_array = "CSV,String".parse_csv # from CSV +# +# == Shortcut Interface +# +# CSV { |csv_out| csv_out << %w{my data here} } # to $stdout +# CSV(csv = "") { |csv_str| csv_str << %w{my data here} } # to a String +# CSV($stderr) { |csv_err| csv_err << %w{my data here} } # to $stderr +# CSV($stdin) { |csv_in| csv_in.each { |row| p row } } # from $stdin +# +# == Advanced Usage +# +# === Wrap an IO Object +# +# csv = CSV.new(io, options) +# # ... read (with gets() or each()) from and write (with <<) to csv here ... +# +# == CSV and Character Encodings (M17n or Multilingualization) +# +# This new CSV parser is m17n savvy. The parser works in the Encoding of the IO +# or String object being read from or written to. Your data is never transcoded +# (unless you ask Ruby to transcode it for you) and will literally be parsed in +# the Encoding it is in. Thus CSV will return Arrays or Rows of Strings in the +# Encoding of your data. This is accomplished by transcoding the parser itself +# into your Encoding. +# +# Some transcoding must take place, of course, to accomplish this multiencoding +# support. For example, :col_sep, :row_sep, and +# :quote_char must be transcoded to match your data. Hopefully this +# makes the entire process feel transparent, since CSV's defaults should just +# magically work for your data. However, you can set these values manually in +# the target Encoding to avoid the translation. +# +# It's also important to note that while all of CSV's core parser is now +# Encoding agnostic, some features are not. For example, the built-in +# converters will try to transcode data to UTF-8 before making conversions. +# Again, you can provide custom converters that are aware of your Encodings to +# avoid this translation. It's just too hard for me to support native +# conversions in all of Ruby's Encodings. +# +# Anyway, the practical side of this is simple: make sure IO and String objects +# passed into CSV have the proper Encoding set and everything should just work. +# CSV methods that allow you to open IO objects (CSV::foreach(), CSV::open(), +# CSV::read(), and CSV::readlines()) do allow you to specify the Encoding. +# +# One minor exception comes when generating CSV into a String with an Encoding +# that is not ASCII compatible. There's no existing data for CSV to use to +# prepare itself and thus you will probably need to manually specify the desired +# Encoding for most of those cases. It will try to guess using the fields in a +# row of output though, when using CSV::generate_line() or Array#to_csv(). +# +# I try to point out any other Encoding issues in the documentation of methods +# as they come up. +# +# This has been tested to the best of my ability with all non-"dummy" Encodings +# Ruby ships with. However, it is brave new code and may have some bugs. +# Please feel free to {report}[mailto:james@grayproductions.net] any issues you +# find with it. +# +class CSV + # The version of the installed library. + VERSION = "2.4.8" + + # + # A CSV::Row is part Array and part Hash. It retains an order for the fields + # and allows duplicates just as an Array would, but also allows you to access + # fields by name just as you could if they were in a Hash. + # + # All rows returned by CSV will be constructed from this class, if header row + # processing is activated. + # + class Row + # + # Construct a new CSV::Row from +headers+ and +fields+, which are expected + # to be Arrays. If one Array is shorter than the other, it will be padded + # with +nil+ objects. + # + # The optional +header_row+ parameter can be set to +true+ to indicate, via + # CSV::Row.header_row?() and CSV::Row.field_row?(), that this is a header + # row. Otherwise, the row is assumes to be a field row. + # + # A CSV::Row object supports the following Array methods through delegation: + # + # * empty?() + # * length() + # * size() + # + def initialize(headers, fields, header_row = false) + @header_row = header_row + headers.each { |h| h.freeze if h.is_a? String } + + # handle extra headers or fields + @row = if headers.size >= fields.size + headers.zip(fields) + else + fields.zip(headers).each(&:reverse!) + end + end + + # Internal data format used to compare equality. + attr_reader :row + protected :row + + ### Array Delegation ### + + extend Forwardable + def_delegators :@row, :empty?, :length, :size + + # Returns +true+ if this is a header row. + def header_row? + @header_row + end + + # Returns +true+ if this is a field row. + def field_row? + not header_row? + end + + # Returns the headers of this row. + def headers + @row.map(&:first) + end + + # + # :call-seq: + # field( header ) + # field( header, offset ) + # field( index ) + # + # This method will return the field value by +header+ or +index+. If a field + # is not found, +nil+ is returned. + # + # When provided, +offset+ ensures that a header match occurs on or later + # than the +offset+ index. You can use this to find duplicate headers, + # without resorting to hard-coding exact indices. + # + def field(header_or_index, minimum_index = 0) + # locate the pair + finder = (header_or_index.is_a?(Integer) || header_or_index.is_a?(Range)) ? :[] : :assoc + pair = @row[minimum_index..-1].send(finder, header_or_index) + + # return the field if we have a pair + if pair.nil? + nil + else + header_or_index.is_a?(Range) ? pair.map(&:last) : pair.last + end + end + alias_method :[], :field + + # + # :call-seq: + # fetch( header ) + # fetch( header ) { |row| ... } + # fetch( header, default ) + # + # This method will fetch the field value by +header+. It has the same + # behavior as Hash#fetch: if there is a field with the given +header+, its + # value is returned. Otherwise, if a block is given, it is yielded the + # +header+ and its result is returned; if a +default+ is given as the + # second argument, it is returned; otherwise a KeyError is raised. + # + def fetch(header, *varargs) + raise ArgumentError, "Too many arguments" if varargs.length > 1 + pair = @row.assoc(header) + if pair + pair.last + else + if block_given? + yield header + elsif varargs.empty? + raise KeyError, "key not found: #{header}" + else + varargs.first + end + end + end + + # Returns +true+ if there is a field with the given +header+. + def has_key?(header) + !!@row.assoc(header) + end + alias_method :include?, :has_key? + alias_method :key?, :has_key? + alias_method :member?, :has_key? + + # + # :call-seq: + # []=( header, value ) + # []=( header, offset, value ) + # []=( index, value ) + # + # Looks up the field by the semantics described in CSV::Row.field() and + # assigns the +value+. + # + # Assigning past the end of the row with an index will set all pairs between + # to [nil, nil]. Assigning to an unused header appends the new + # pair. + # + def []=(*args) + value = args.pop + + if args.first.is_a? Integer + if @row[args.first].nil? # extending past the end with index + @row[args.first] = [nil, value] + @row.map! { |pair| pair.nil? ? [nil, nil] : pair } + else # normal index assignment + @row[args.first][1] = value + end + else + index = index(*args) + if index.nil? # appending a field + self << [args.first, value] + else # normal header assignment + @row[index][1] = value + end + end + end + + # + # :call-seq: + # <<( field ) + # <<( header_and_field_array ) + # <<( header_and_field_hash ) + # + # If a two-element Array is provided, it is assumed to be a header and field + # and the pair is appended. A Hash works the same way with the key being + # the header and the value being the field. Anything else is assumed to be + # a lone field which is appended with a +nil+ header. + # + # This method returns the row for chaining. + # + def <<(arg) + if arg.is_a?(Array) and arg.size == 2 # appending a header and name + @row << arg + elsif arg.is_a?(Hash) # append header and name pairs + arg.each { |pair| @row << pair } + else # append field value + @row << [nil, arg] + end + + self # for chaining + end + + # + # A shortcut for appending multiple fields. Equivalent to: + # + # args.each { |arg| csv_row << arg } + # + # This method returns the row for chaining. + # + def push(*args) + args.each { |arg| self << arg } + + self # for chaining + end + + # + # :call-seq: + # delete( header ) + # delete( header, offset ) + # delete( index ) + # + # Used to remove a pair from the row by +header+ or +index+. The pair is + # located as described in CSV::Row.field(). The deleted pair is returned, + # or +nil+ if a pair could not be found. + # + def delete(header_or_index, minimum_index = 0) + if header_or_index.is_a? Integer # by index + @row.delete_at(header_or_index) + elsif i = index(header_or_index, minimum_index) # by header + @row.delete_at(i) + else + [ ] + end + end + + # + # The provided +block+ is passed a header and field for each pair in the row + # and expected to return +true+ or +false+, depending on whether the pair + # should be deleted. + # + # This method returns the row for chaining. + # + # If no block is given, an Enumerator is returned. + # + def delete_if(&block) + block or return enum_for(__method__) { size } + + @row.delete_if(&block) + + self # for chaining + end + + # + # This method accepts any number of arguments which can be headers, indices, + # Ranges of either, or two-element Arrays containing a header and offset. + # Each argument will be replaced with a field lookup as described in + # CSV::Row.field(). + # + # If called with no arguments, all fields are returned. + # + def fields(*headers_and_or_indices) + if headers_and_or_indices.empty? # return all fields--no arguments + @row.map(&:last) + else # or work like values_at() + all = [] + headers_and_or_indices.each do |h_or_i| + if h_or_i.is_a? Range + index_begin = h_or_i.begin.is_a?(Integer) ? h_or_i.begin : + index(h_or_i.begin) + index_end = h_or_i.end.is_a?(Integer) ? h_or_i.end : + index(h_or_i.end) + new_range = h_or_i.exclude_end? ? (index_begin...index_end) : + (index_begin..index_end) + all.concat(fields.values_at(new_range)) + else + all << field(*Array(h_or_i)) + end + end + return all + end + end + alias_method :values_at, :fields + + # + # :call-seq: + # index( header ) + # index( header, offset ) + # + # This method will return the index of a field with the provided +header+. + # The +offset+ can be used to locate duplicate header names, as described in + # CSV::Row.field(). + # + def index(header, minimum_index = 0) + # find the pair + index = headers[minimum_index..-1].index(header) + # return the index at the right offset, if we found one + index.nil? ? nil : index + minimum_index + end + + # Returns +true+ if +name+ is a header for this row, and +false+ otherwise. + def header?(name) + headers.include? name + end + alias_method :include?, :header? + + # + # Returns +true+ if +data+ matches a field in this row, and +false+ + # otherwise. + # + def field?(data) + fields.include? data + end + + include Enumerable + + # + # Yields each pair of the row as header and field tuples (much like + # iterating over a Hash). This method returns the row for chaining. + # + # If no block is given, an Enumerator is returned. + # + # Support for Enumerable. + # + def each(&block) + block or return enum_for(__method__) { size } + + @row.each(&block) + + self # for chaining + end + + # + # Returns +true+ if this row contains the same headers and fields in the + # same order as +other+. + # + def ==(other) + return @row == other.row if other.is_a? CSV::Row + @row == other + end + + # + # Collapses the row into a simple Hash. Be warned that this discards field + # order and clobbers duplicate fields. + # + def to_hash + @row.to_h + end + + # + # Returns the row as a CSV String. Headers are not used. Equivalent to: + # + # csv_row.fields.to_csv( options ) + # + def to_csv(**options) + fields.to_csv(options) + end + alias_method :to_s, :to_csv + + # A summary of fields, by header, in an ASCII compatible String. + def inspect + str = ["#<", self.class.to_s] + each do |header, field| + str << " " << (header.is_a?(Symbol) ? header.to_s : header.inspect) << + ":" << field.inspect + end + str << ">" + begin + str.join('') + rescue # any encoding error + str.map do |s| + e = Encoding::Converter.asciicompat_encoding(s.encoding) + e ? s.encode(e) : s.force_encoding("ASCII-8BIT") + end.join('') + end + end + end + + # + # A CSV::Table is a two-dimensional data structure for representing CSV + # documents. Tables allow you to work with the data by row or column, + # manipulate the data, and even convert the results back to CSV, if needed. + # + # All tables returned by CSV will be constructed from this class, if header + # row processing is activated. + # + class Table + # + # Construct a new CSV::Table from +array_of_rows+, which are expected + # to be CSV::Row objects. All rows are assumed to have the same headers. + # + # A CSV::Table object supports the following Array methods through + # delegation: + # + # * empty?() + # * length() + # * size() + # + def initialize(array_of_rows) + @table = array_of_rows + @mode = :col_or_row + end + + # The current access mode for indexing and iteration. + attr_reader :mode + + # Internal data format used to compare equality. + attr_reader :table + protected :table + + ### Array Delegation ### + + extend Forwardable + def_delegators :@table, :empty?, :length, :size + + # + # Returns a duplicate table object, in column mode. This is handy for + # chaining in a single call without changing the table mode, but be aware + # that this method can consume a fair amount of memory for bigger data sets. + # + # This method returns the duplicate table for chaining. Don't chain + # destructive methods (like []=()) this way though, since you are working + # with a duplicate. + # + def by_col + self.class.new(@table.dup).by_col! + end + + # + # Switches the mode of this table to column mode. All calls to indexing and + # iteration methods will work with columns until the mode is changed again. + # + # This method returns the table and is safe to chain. + # + def by_col! + @mode = :col + + self + end + + # + # Returns a duplicate table object, in mixed mode. This is handy for + # chaining in a single call without changing the table mode, but be aware + # that this method can consume a fair amount of memory for bigger data sets. + # + # This method returns the duplicate table for chaining. Don't chain + # destructive methods (like []=()) this way though, since you are working + # with a duplicate. + # + def by_col_or_row + self.class.new(@table.dup).by_col_or_row! + end + + # + # Switches the mode of this table to mixed mode. All calls to indexing and + # iteration methods will use the default intelligent indexing system until + # the mode is changed again. In mixed mode an index is assumed to be a row + # reference while anything else is assumed to be column access by headers. + # + # This method returns the table and is safe to chain. + # + def by_col_or_row! + @mode = :col_or_row + + self + end + + # + # Returns a duplicate table object, in row mode. This is handy for chaining + # in a single call without changing the table mode, but be aware that this + # method can consume a fair amount of memory for bigger data sets. + # + # This method returns the duplicate table for chaining. Don't chain + # destructive methods (like []=()) this way though, since you are working + # with a duplicate. + # + def by_row + self.class.new(@table.dup).by_row! + end + + # + # Switches the mode of this table to row mode. All calls to indexing and + # iteration methods will work with rows until the mode is changed again. + # + # This method returns the table and is safe to chain. + # + def by_row! + @mode = :row + + self + end + + # + # Returns the headers for the first row of this table (assumed to match all + # other rows). An empty Array is returned for empty tables. + # + def headers + if @table.empty? + Array.new + else + @table.first.headers + end + end + + # + # In the default mixed mode, this method returns rows for index access and + # columns for header access. You can force the index association by first + # calling by_col!() or by_row!(). + # + # Columns are returned as an Array of values. Altering that Array has no + # effect on the table. + # + def [](index_or_header) + if @mode == :row or # by index + (@mode == :col_or_row and (index_or_header.is_a?(Integer) or index_or_header.is_a?(Range))) + @table[index_or_header] + else # by header + @table.map { |row| row[index_or_header] } + end + end + + # + # In the default mixed mode, this method assigns rows for index access and + # columns for header access. You can force the index association by first + # calling by_col!() or by_row!(). + # + # Rows may be set to an Array of values (which will inherit the table's + # headers()) or a CSV::Row. + # + # Columns may be set to a single value, which is copied to each row of the + # column, or an Array of values. Arrays of values are assigned to rows top + # to bottom in row major order. Excess values are ignored and if the Array + # does not have a value for each row the extra rows will receive a +nil+. + # + # Assigning to an existing column or row clobbers the data. Assigning to + # new columns creates them at the right end of the table. + # + def []=(index_or_header, value) + if @mode == :row or # by index + (@mode == :col_or_row and index_or_header.is_a? Integer) + if value.is_a? Array + @table[index_or_header] = Row.new(headers, value) + else + @table[index_or_header] = value + end + else # set column + if value.is_a? Array # multiple values + @table.each_with_index do |row, i| + if row.header_row? + row[index_or_header] = index_or_header + else + row[index_or_header] = value[i] + end + end + else # repeated value + @table.each do |row| + if row.header_row? + row[index_or_header] = index_or_header + else + row[index_or_header] = value + end + end + end + end + end + + # + # The mixed mode default is to treat a list of indices as row access, + # returning the rows indicated. Anything else is considered columnar + # access. For columnar access, the return set has an Array for each row + # with the values indicated by the headers in each Array. You can force + # column or row mode using by_col!() or by_row!(). + # + # You cannot mix column and row access. + # + def values_at(*indices_or_headers) + if @mode == :row or # by indices + ( @mode == :col_or_row and indices_or_headers.all? do |index| + index.is_a?(Integer) or + ( index.is_a?(Range) and + index.first.is_a?(Integer) and + index.last.is_a?(Integer) ) + end ) + @table.values_at(*indices_or_headers) + else # by headers + @table.map { |row| row.values_at(*indices_or_headers) } + end + end + + # + # Adds a new row to the bottom end of this table. You can provide an Array, + # which will be converted to a CSV::Row (inheriting the table's headers()), + # or a CSV::Row. + # + # This method returns the table for chaining. + # + def <<(row_or_array) + if row_or_array.is_a? Array # append Array + @table << Row.new(headers, row_or_array) + else # append Row + @table << row_or_array + end + + self # for chaining + end + + # + # A shortcut for appending multiple rows. Equivalent to: + # + # rows.each { |row| self << row } + # + # This method returns the table for chaining. + # + def push(*rows) + rows.each { |row| self << row } + + self # for chaining + end + + # + # Removes and returns the indicated column or row. In the default mixed + # mode indices refer to rows and everything else is assumed to be a column + # header. Use by_col!() or by_row!() to force the lookup. + # + def delete(index_or_header) + if @mode == :row or # by index + (@mode == :col_or_row and index_or_header.is_a? Integer) + @table.delete_at(index_or_header) + else # by header + @table.map { |row| row.delete(index_or_header).last } + end + end + + # + # Removes any column or row for which the block returns +true+. In the + # default mixed mode or row mode, iteration is the standard row major + # walking of rows. In column mode, iteration will +yield+ two element + # tuples containing the column name and an Array of values for that column. + # + # This method returns the table for chaining. + # + # If no block is given, an Enumerator is returned. + # + def delete_if(&block) + block or return enum_for(__method__) { @mode == :row or @mode == :col_or_row ? size : headers.size } + + if @mode == :row or @mode == :col_or_row # by index + @table.delete_if(&block) + else # by header + deleted = [] + headers.each do |header| + deleted << delete(header) if block[[header, self[header]]] + end + end + + self # for chaining + end + + include Enumerable + + # + # In the default mixed mode or row mode, iteration is the standard row major + # walking of rows. In column mode, iteration will +yield+ two element + # tuples containing the column name and an Array of values for that column. + # + # This method returns the table for chaining. + # + # If no block is given, an Enumerator is returned. + # + def each(&block) + block or return enum_for(__method__) { @mode == :col ? headers.size : size } + + if @mode == :col + headers.each { |header| block[[header, self[header]]] } + else + @table.each(&block) + end + + self # for chaining + end + + # Returns +true+ if all rows of this table ==() +other+'s rows. + def ==(other) + return @table == other.table if other.is_a? CSV::Table + @table == other + end + + # + # Returns the table as an Array of Arrays. Headers will be the first row, + # then all of the field rows will follow. + # + def to_a + array = [headers] + @table.each do |row| + array.push(row.fields) unless row.header_row? + end + return array + end + + # + # Returns the table as a complete CSV String. Headers will be listed first, + # then all of the field rows. + # + # This method assumes you want the Table.headers(), unless you explicitly + # pass :write_headers => false. + # + def to_csv(write_headers: true, **options) + array = write_headers ? [headers.to_csv(options)] : [] + @table.each do |row| + array.push(row.fields.to_csv(options)) unless row.header_row? + end + return array.join('') + end + alias_method :to_s, :to_csv + + # Shows the mode and size of this table in a US-ASCII String. + def inspect + "#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>".encode("US-ASCII") + end + end + + # The error thrown when the parser encounters illegal CSV formatting. + class MalformedCSVError < RuntimeError; end + + # + # A FieldInfo Struct contains details about a field's position in the data + # source it was read from. CSV will pass this Struct to some blocks that make + # decisions based on field structure. See CSV.convert_fields() for an + # example. + # + # index:: The zero-based index of the field in its row. + # line:: The line of the data source this row is from. + # header:: The header for the column, when available. + # + FieldInfo = Struct.new(:index, :line, :header) + + # A Regexp used to find and convert some common Date formats. + DateMatcher = / \A(?: (\w+,?\s+)?\w+\s+\d{1,2},?\s+\d{2,4} | + \d{4}-\d{2}-\d{2} )\z /x + # A Regexp used to find and convert some common DateTime formats. + DateTimeMatcher = + / \A(?: (\w+,?\s+)?\w+\s+\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2},?\s+\d{2,4} | + \d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2} )\z /x + + # The encoding used by all converters. + ConverterEncoding = Encoding.find("UTF-8") + + # + # This Hash holds the built-in converters of CSV that can be accessed by name. + # You can select Converters with CSV.convert() or through the +options+ Hash + # passed to CSV::new(). + # + # :integer:: Converts any field Integer() accepts. + # :float:: Converts any field Float() accepts. + # :numeric:: A combination of :integer + # and :float. + # :date:: Converts any field Date::parse() accepts. + # :date_time:: Converts any field DateTime::parse() accepts. + # :all:: All built-in converters. A combination of + # :date_time and :numeric. + # + # All built-in converters transcode field data to UTF-8 before attempting a + # conversion. If your data cannot be transcoded to UTF-8 the conversion will + # fail and the field will remain unchanged. + # + # This Hash is intentionally left unfrozen and users should feel free to add + # values to it that can be accessed by all CSV objects. + # + # To add a combo field, the value should be an Array of names. Combo fields + # can be nested with other combo fields. + # + Converters = { + integer: lambda { |f| + Integer(f.encode(ConverterEncoding)) rescue f + }, + float: lambda { |f| + Float(f.encode(ConverterEncoding)) rescue f + }, + numeric: [:integer, :float], + date: lambda { |f| + begin + e = f.encode(ConverterEncoding) + e =~ DateMatcher ? Date.parse(e) : f + rescue # encoding conversion or date parse errors + f + end + }, + date_time: lambda { |f| + begin + e = f.encode(ConverterEncoding) + e =~ DateTimeMatcher ? DateTime.parse(e) : f + rescue # encoding conversion or date parse errors + f + end + }, + all: [:date_time, :numeric], + } + + # + # This Hash holds the built-in header converters of CSV that can be accessed + # by name. You can select HeaderConverters with CSV.header_convert() or + # through the +options+ Hash passed to CSV::new(). + # + # :downcase:: Calls downcase() on the header String. + # :symbol:: Leading/trailing spaces are dropped, string is + # downcased, remaining spaces are replaced with + # underscores, non-word characters are dropped, + # and finally to_sym() is called. + # + # All built-in header converters transcode header data to UTF-8 before + # attempting a conversion. If your data cannot be transcoded to UTF-8 the + # conversion will fail and the header will remain unchanged. + # + # This Hash is intentionally left unfrozen and users should feel free to add + # values to it that can be accessed by all CSV objects. + # + # To add a combo field, the value should be an Array of names. Combo fields + # can be nested with other combo fields. + # + HeaderConverters = { + downcase: lambda { |h| h.encode(ConverterEncoding).downcase }, + symbol: lambda { |h| + h.encode(ConverterEncoding).downcase.gsub(/[^\s\w]+/, "").strip. + gsub(/\s+/, "_").to_sym + } + } + + # + # The options used when no overrides are given by calling code. They are: + # + # :col_sep:: "," + # :row_sep:: :auto + # :quote_char:: '"' + # :field_size_limit:: +nil+ + # :converters:: +nil+ + # :unconverted_fields:: +nil+ + # :headers:: +false+ + # :return_headers:: +false+ + # :header_converters:: +nil+ + # :skip_blanks:: +false+ + # :force_quotes:: +false+ + # :skip_lines:: +nil+ + # :liberal_parsing:: +false+ + # + DEFAULT_OPTIONS = { + col_sep: ",", + row_sep: :auto, + quote_char: '"', + field_size_limit: nil, + converters: nil, + unconverted_fields: nil, + headers: false, + return_headers: false, + header_converters: nil, + skip_blanks: false, + force_quotes: false, + skip_lines: nil, + liberal_parsing: false, + }.freeze + + # + # This method will return a CSV instance, just like CSV::new(), but the + # instance will be cached and returned for all future calls to this method for + # the same +data+ object (tested by Object#object_id()) with the same + # +options+. + # + # If a block is given, the instance is passed to the block and the return + # value becomes the return value of the block. + # + def self.instance(data = $stdout, **options) + # create a _signature_ for this method call, data object and options + sig = [data.object_id] + + options.values_at(*DEFAULT_OPTIONS.keys.sort_by { |sym| sym.to_s }) + + # fetch or create the instance for this signature + @@instances ||= Hash.new + instance = (@@instances[sig] ||= new(data, options)) + + if block_given? + yield instance # run block, if given, returning result + else + instance # or return the instance + end + end + + # + # :call-seq: + # filter( **options ) { |row| ... } + # filter( input, **options ) { |row| ... } + # filter( input, output, **options ) { |row| ... } + # + # This method is a convenience for building Unix-like filters for CSV data. + # Each row is yielded to the provided block which can alter it as needed. + # After the block returns, the row is appended to +output+ altered or not. + # + # The +input+ and +output+ arguments can be anything CSV::new() accepts + # (generally String or IO objects). If not given, they default to + # ARGF and $stdout. + # + # The +options+ parameter is also filtered down to CSV::new() after some + # clever key parsing. Any key beginning with :in_ or + # :input_ will have that leading identifier stripped and will only + # be used in the +options+ Hash for the +input+ object. Keys starting with + # :out_ or :output_ affect only +output+. All other keys + # are assigned to both objects. + # + # The :output_row_sep +option+ defaults to + # $INPUT_RECORD_SEPARATOR ($/). + # + def self.filter(input=nil, output=nil, **options) + # parse options for input, output, or both + in_options, out_options = Hash.new, {row_sep: $INPUT_RECORD_SEPARATOR} + options.each do |key, value| + case key.to_s + when /\Ain(?:put)?_(.+)\Z/ + in_options[$1.to_sym] = value + when /\Aout(?:put)?_(.+)\Z/ + out_options[$1.to_sym] = value + else + in_options[key] = value + out_options[key] = value + end + end + # build input and output wrappers + input = new(input || ARGF, in_options) + output = new(output || $stdout, out_options) + + # read, yield, write + input.each do |row| + yield row + output << row + end + end + + # + # This method is intended as the primary interface for reading CSV files. You + # pass a +path+ and any +options+ you wish to set for the read. Each row of + # file will be passed to the provided +block+ in turn. + # + # The +options+ parameter can be anything CSV::new() understands. This method + # also understands an additional :encoding parameter that you can use + # to specify the Encoding of the data in the file to be read. You must provide + # this unless your data is in Encoding::default_external(). CSV will use this + # to determine how to parse the data. You may provide a second Encoding to + # have the data transcoded as it is read. For example, + # encoding: "UTF-32BE:UTF-8" would read UTF-32BE data from the file + # but transcode it to UTF-8 before CSV parses it. + # + def self.foreach(path, **options, &block) + return to_enum(__method__, path, options) unless block + open(path, options) do |csv| + csv.each(&block) + end + end + + # + # :call-seq: + # generate( str, **options ) { |csv| ... } + # generate( **options ) { |csv| ... } + # + # This method wraps a String you provide, or an empty default String, in a + # CSV object which is passed to the provided block. You can use the block to + # append CSV rows to the String and when the block exits, the final String + # will be returned. + # + # Note that a passed String *is* modified by this method. Call dup() before + # passing if you need a new String. + # + # The +options+ parameter can be anything CSV::new() understands. This method + # understands an additional :encoding parameter when not passed a + # String to set the base Encoding for the output. CSV needs this hint if you + # plan to output non-ASCII compatible data. + # + def self.generate(str=nil, **options) + # add a default empty String, if none was given + if str + io = StringIO.new(str) + io.seek(0, IO::SEEK_END) + else + encoding = options[:encoding] + str = String.new + str.force_encoding(encoding) if encoding + end + csv = new(str, options) # wrap + yield csv # yield for appending + csv.string # return final String + end + + # + # This method is a shortcut for converting a single row (Array) into a CSV + # String. + # + # The +options+ parameter can be anything CSV::new() understands. This method + # understands an additional :encoding parameter to set the base + # Encoding for the output. This method will try to guess your Encoding from + # the first non-+nil+ field in +row+, if possible, but you may need to use + # this parameter as a backup plan. + # + # The :row_sep +option+ defaults to $INPUT_RECORD_SEPARATOR + # ($/) when calling this method. + # + def self.generate_line(row, **options) + options = {row_sep: $INPUT_RECORD_SEPARATOR}.merge(options) + str = String.new + if options[:encoding] + str.force_encoding(options[:encoding]) + elsif field = row.find { |f| not f.nil? } + str.force_encoding(String(field).encoding) + end + (new(str, options) << row).string + end + + # + # :call-seq: + # open( filename, mode = "rb", **options ) { |faster_csv| ... } + # open( filename, **options ) { |faster_csv| ... } + # open( filename, mode = "rb", **options ) + # open( filename, **options ) + # + # This method opens an IO object, and wraps that with CSV. This is intended + # as the primary interface for writing a CSV file. + # + # You must pass a +filename+ and may optionally add a +mode+ for Ruby's + # open(). You may also pass an optional Hash containing any +options+ + # CSV::new() understands as the final argument. + # + # This method works like Ruby's open() call, in that it will pass a CSV object + # to a provided block and close it when the block terminates, or it will + # return the CSV object when no block is provided. (*Note*: This is different + # from the Ruby 1.8 CSV library which passed rows to the block. Use + # CSV::foreach() for that behavior.) + # + # You must provide a +mode+ with an embedded Encoding designator unless your + # data is in Encoding::default_external(). CSV will check the Encoding of the + # underlying IO object (set by the +mode+ you pass) to determine how to parse + # the data. You may provide a second Encoding to have the data transcoded as + # it is read just as you can with a normal call to IO::open(). For example, + # "rb:UTF-32BE:UTF-8" would read UTF-32BE data from the file but + # transcode it to UTF-8 before CSV parses it. + # + # An opened CSV object will delegate to many IO methods for convenience. You + # may call: + # + # * binmode() + # * binmode?() + # * close() + # * close_read() + # * close_write() + # * closed?() + # * eof() + # * eof?() + # * external_encoding() + # * fcntl() + # * fileno() + # * flock() + # * flush() + # * fsync() + # * internal_encoding() + # * ioctl() + # * isatty() + # * path() + # * pid() + # * pos() + # * pos=() + # * reopen() + # * seek() + # * stat() + # * sync() + # * sync=() + # * tell() + # * to_i() + # * to_io() + # * truncate() + # * tty?() + # + def self.open(filename, mode="r", **options) + # wrap a File opened with the remaining +args+ with no newline + # decorator + file_opts = {universal_newline: false}.merge(options) + + begin + f = File.open(filename, mode, file_opts) + rescue ArgumentError => e + raise unless /needs binmode/ =~ e.message and mode == "r" + mode = "rb" + file_opts = {encoding: Encoding.default_external}.merge(file_opts) + retry + end + begin + csv = new(f, options) + rescue Exception + f.close + raise + end + + # handle blocks like Ruby's open(), not like the CSV library + if block_given? + begin + yield csv + ensure + csv.close + end + else + csv + end + end + + # + # :call-seq: + # parse( str, **options ) { |row| ... } + # parse( str, **options ) + # + # This method can be used to easily parse CSV out of a String. You may either + # provide a +block+ which will be called with each row of the String in turn, + # or just use the returned Array of Arrays (when no +block+ is given). + # + # You pass your +str+ to read from, and an optional +options+ containing + # anything CSV::new() understands. + # + def self.parse(*args, &block) + csv = new(*args) + if block.nil? # slurp contents, if no block is given + begin + csv.read + ensure + csv.close + end + else # or pass each row to a provided block + csv.each(&block) + end + end + + # + # This method is a shortcut for converting a single line of a CSV String into + # an Array. Note that if +line+ contains multiple rows, anything beyond the + # first row is ignored. + # + # The +options+ parameter can be anything CSV::new() understands. + # + def self.parse_line(line, **options) + new(line, options).shift + end + + # + # Use to slurp a CSV file into an Array of Arrays. Pass the +path+ to the + # file and any +options+ CSV::new() understands. This method also understands + # an additional :encoding parameter that you can use to specify the + # Encoding of the data in the file to be read. You must provide this unless + # your data is in Encoding::default_external(). CSV will use this to determine + # how to parse the data. You may provide a second Encoding to have the data + # transcoded as it is read. For example, + # encoding: "UTF-32BE:UTF-8" would read UTF-32BE data from the file + # but transcode it to UTF-8 before CSV parses it. + # + def self.read(path, *options) + open(path, *options) { |csv| csv.read } + end + + # Alias for CSV::read(). + def self.readlines(*args) + read(*args) + end + + # + # A shortcut for: + # + # CSV.read( path, { headers: true, + # converters: :numeric, + # header_converters: :symbol }.merge(options) ) + # + def self.table(path, **options) + read( path, { headers: true, + converters: :numeric, + header_converters: :symbol }.merge(options) ) + end + + # + # This constructor will wrap either a String or IO object passed in +data+ for + # reading and/or writing. In addition to the CSV instance methods, several IO + # methods are delegated. (See CSV::open() for a complete list.) If you pass + # a String for +data+, you can later retrieve it (after writing to it, for + # example) with CSV.string(). + # + # Note that a wrapped String will be positioned at the beginning (for + # reading). If you want it at the end (for writing), use CSV::generate(). + # If you want any other positioning, pass a preset StringIO object instead. + # + # You may set any reading and/or writing preferences in the +options+ Hash. + # Available options are: + # + # :col_sep:: The String placed between each field. + # This String will be transcoded into + # the data's Encoding before parsing. + # :row_sep:: The String appended to the end of each + # row. This can be set to the special + # :auto setting, which requests + # that CSV automatically discover this + # from the data. Auto-discovery reads + # ahead in the data looking for the next + # "\r\n", "\n", or + # "\r" sequence. A sequence + # will be selected even if it occurs in + # a quoted field, assuming that you + # would have the same line endings + # there. If none of those sequences is + # found, +data+ is ARGF, + # STDIN, STDOUT, or + # STDERR, or the stream is only + # available for output, the default + # $INPUT_RECORD_SEPARATOR + # ($/) is used. Obviously, + # discovery takes a little time. Set + # manually if speed is important. Also + # note that IO objects should be opened + # in binary mode on Windows if this + # feature will be used as the + # line-ending translation can cause + # problems with resetting the document + # position to where it was before the + # read ahead. This String will be + # transcoded into the data's Encoding + # before parsing. + # :quote_char:: The character used to quote fields. + # This has to be a single character + # String. This is useful for + # application that incorrectly use + # ' as the quote character + # instead of the correct ". + # CSV will always consider a double + # sequence of this character to be an + # escaped quote. This String will be + # transcoded into the data's Encoding + # before parsing. + # :field_size_limit:: This is a maximum size CSV will read + # ahead looking for the closing quote + # for a field. (In truth, it reads to + # the first line ending beyond this + # size.) If a quote cannot be found + # within the limit CSV will raise a + # MalformedCSVError, assuming the data + # is faulty. You can use this limit to + # prevent what are effectively DoS + # attacks on the parser. However, this + # limit can cause a legitimate parse to + # fail and thus is set to +nil+, or off, + # by default. + # :converters:: An Array of names from the Converters + # Hash and/or lambdas that handle custom + # conversion. A single converter + # doesn't have to be in an Array. All + # built-in converters try to transcode + # fields to UTF-8 before converting. + # The conversion will fail if the data + # cannot be transcoded, leaving the + # field unchanged. + # :unconverted_fields:: If set to +true+, an + # unconverted_fields() method will be + # added to all returned rows (Array or + # CSV::Row) that will return the fields + # as they were before conversion. Note + # that :headers supplied by + # Array or String were not fields of the + # document and thus will have an empty + # Array attached. + # :headers:: If set to :first_row or + # +true+, the initial row of the CSV + # file will be treated as a row of + # headers. If set to an Array, the + # contents will be used as the headers. + # If set to a String, the String is run + # through a call of CSV::parse_line() + # with the same :col_sep, + # :row_sep, and + # :quote_char as this instance + # to produce an Array of headers. This + # setting causes CSV#shift() to return + # rows as CSV::Row objects instead of + # Arrays and CSV#read() to return + # CSV::Table objects instead of an Array + # of Arrays. + # :return_headers:: When +false+, header rows are silently + # swallowed. If set to +true+, header + # rows are returned in a CSV::Row object + # with identical headers and + # fields (save that the fields do not go + # through the converters). + # :write_headers:: When +true+ and :headers is + # set, a header row will be added to the + # output. + # :header_converters:: Identical in functionality to + # :converters save that the + # conversions are only made to header + # rows. All built-in converters try to + # transcode headers to UTF-8 before + # converting. The conversion will fail + # if the data cannot be transcoded, + # leaving the header unchanged. + # :skip_blanks:: When set to a +true+ value, CSV will + # skip over any empty rows. Note that + # this setting will not skip rows that + # contain column separators, even if + # the rows contain no actual data. If + # you want to skip rows that contain + # separators but no content, consider + # using :skip_lines, or + # inspecting fields.compact.empty? on + # each row. + # :force_quotes:: When set to a +true+ value, CSV will + # quote all CSV fields it creates. + # :skip_lines:: When set to an object responding to + # match, every line matching + # it is considered a comment and ignored + # during parsing. When set to a String, + # it is first converted to a Regexp. + # When set to +nil+ no line is considered + # a comment. If the passed object does + # not respond to match, + # ArgumentError is thrown. + # :liberal_parsing:: When set to a +true+ value, CSV will + # attempt to parse input not conformant + # with RFC 4180, such as double quotes + # in unquoted fields. + # + # See CSV::DEFAULT_OPTIONS for the default settings. + # + # Options cannot be overridden in the instance methods for performance reasons, + # so be sure to set what you want here. + # + def initialize(data, col_sep: ",", row_sep: :auto, quote_char: '"', field_size_limit: nil, + converters: nil, unconverted_fields: nil, headers: false, return_headers: false, + write_headers: nil, header_converters: nil, skip_blanks: false, force_quotes: false, + skip_lines: nil, liberal_parsing: false, internal_encoding: nil, external_encoding: nil, encoding: nil) + raise ArgumentError.new("Cannot parse nil as CSV") if data.nil? + + # create the IO object we will read from + @io = data.is_a?(String) ? StringIO.new(data) : data + # honor the IO encoding if we can, otherwise default to ASCII-8BIT + internal_encoding = Encoding.find(internal_encoding) if internal_encoding + external_encoding = Encoding.find(external_encoding) if external_encoding + if encoding + encoding, = encoding.split(":", 2) if encoding.is_a?(String) + encoding = Encoding.find(encoding) + end + @encoding = raw_encoding(nil) || internal_encoding || encoding || + Encoding.default_internal || Encoding.default_external + # + # prepare for building safe regular expressions in the target encoding, + # if we can transcode the needed characters + # + @re_esc = "\\".encode(@encoding).freeze rescue "" + @re_chars = /#{%"[-\\]\\[\\.^$?*+{}()|# \r\n\t\f\v]".encode(@encoding)}/ + @unconverted_fields = unconverted_fields + + # Stores header row settings and loads header converters, if needed. + @use_headers = headers + @return_headers = return_headers + @write_headers = write_headers + + # headers must be delayed until shift(), in case they need a row of content + @headers = nil + + init_separators(col_sep, row_sep, quote_char, force_quotes) + init_parsers(skip_blanks, field_size_limit, liberal_parsing) + init_converters(converters, :@converters, :convert) + init_converters(header_converters, :@header_converters, :header_convert) + init_comments(skip_lines) + + @force_encoding = !!encoding + + # track our own lineno since IO gets confused about line-ends is CSV fields + @lineno = 0 + + # make sure headers have been assigned + if header_row? and [Array, String].include? @use_headers.class and @write_headers + parse_headers # won't read data for Array or String + self << @headers + end + end + + # + # The encoded :col_sep used in parsing and writing. See CSV::new + # for details. + # + attr_reader :col_sep + # + # The encoded :row_sep used in parsing and writing. See CSV::new + # for details. + # + attr_reader :row_sep + # + # The encoded :quote_char used in parsing and writing. See CSV::new + # for details. + # + attr_reader :quote_char + # The limit for field size, if any. See CSV::new for details. + attr_reader :field_size_limit + + # The regex marking a line as a comment. See CSV::new for details + attr_reader :skip_lines + + # + # Returns the current list of converters in effect. See CSV::new for details. + # Built-in converters will be returned by name, while others will be returned + # as is. + # + def converters + @converters.map do |converter| + name = Converters.rassoc(converter) + name ? name.first : converter + end + end + # + # Returns +true+ if unconverted_fields() to parsed results. See CSV::new + # for details. + # + def unconverted_fields?() @unconverted_fields end + # + # Returns +nil+ if headers will not be used, +true+ if they will but have not + # yet been read, or the actual headers after they have been read. See + # CSV::new for details. + # + def headers + @headers || true if @use_headers + end + # + # Returns +true+ if headers will be returned as a row of results. + # See CSV::new for details. + # + def return_headers?() @return_headers end + # Returns +true+ if headers are written in output. See CSV::new for details. + def write_headers?() @write_headers end + # + # Returns the current list of converters in effect for headers. See CSV::new + # for details. Built-in converters will be returned by name, while others + # will be returned as is. + # + def header_converters + @header_converters.map do |converter| + name = HeaderConverters.rassoc(converter) + name ? name.first : converter + end + end + # + # Returns +true+ blank lines are skipped by the parser. See CSV::new + # for details. + # + def skip_blanks?() @skip_blanks end + # Returns +true+ if all output fields are quoted. See CSV::new for details. + def force_quotes?() @force_quotes end + # Returns +true+ if illegal input is handled. See CSV::new for details. + def liberal_parsing?() @liberal_parsing end + + # + # The Encoding CSV is parsing or writing in. This will be the Encoding you + # receive parsed data in and/or the Encoding data will be written in. + # + attr_reader :encoding + + # + # The line number of the last row read from this file. Fields with nested + # line-end characters will not affect this count. + # + attr_reader :lineno, :line + + ### IO and StringIO Delegation ### + + extend Forwardable + def_delegators :@io, :binmode, :binmode?, :close, :close_read, :close_write, + :closed?, :eof, :eof?, :external_encoding, :fcntl, + :fileno, :flock, :flush, :fsync, :internal_encoding, + :ioctl, :isatty, :path, :pid, :pos, :pos=, :reopen, + :seek, :stat, :string, :sync, :sync=, :tell, :to_i, + :to_io, :truncate, :tty? + + # Rewinds the underlying IO object and resets CSV's lineno() counter. + def rewind + @headers = nil + @lineno = 0 + + @io.rewind + end + + ### End Delegation ### + + # + # The primary write method for wrapped Strings and IOs, +row+ (an Array or + # CSV::Row) is converted to CSV and appended to the data source. When a + # CSV::Row is passed, only the row's fields() are appended to the output. + # + # The data source must be open for writing. + # + def <<(row) + # make sure headers have been assigned + if header_row? and [Array, String].include? @use_headers.class and !@write_headers + parse_headers # won't read data for Array or String + end + + # handle CSV::Row objects and Hashes + row = case row + when self.class::Row then row.fields + when Hash then @headers.map { |header| row[header] } + else row + end + + @headers = row if header_row? + @lineno += 1 + + output = row.map(&@quote).join(@col_sep) + @row_sep # quote and separate + if @io.is_a?(StringIO) and + output.encoding != (encoding = raw_encoding) + if @force_encoding + output = output.encode(encoding) + elsif (compatible_encoding = Encoding.compatible?(@io.string, output)) + @io.set_encoding(compatible_encoding) + @io.seek(0, IO::SEEK_END) + end + end + @io << output + + self # for chaining + end + alias_method :add_row, :<< + alias_method :puts, :<< + + # + # :call-seq: + # convert( name ) + # convert { |field| ... } + # convert { |field, field_info| ... } + # + # You can use this method to install a CSV::Converters built-in, or provide a + # block that handles a custom conversion. + # + # If you provide a block that takes one argument, it will be passed the field + # and is expected to return the converted value or the field itself. If your + # block takes two arguments, it will also be passed a CSV::FieldInfo Struct, + # containing details about the field. Again, the block should return a + # converted field or the field itself. + # + def convert(name = nil, &converter) + add_converter(:@converters, self.class::Converters, name, &converter) + end + + # + # :call-seq: + # header_convert( name ) + # header_convert { |field| ... } + # header_convert { |field, field_info| ... } + # + # Identical to CSV#convert(), but for header rows. + # + # Note that this method must be called before header rows are read to have any + # effect. + # + def header_convert(name = nil, &converter) + add_converter( :@header_converters, + self.class::HeaderConverters, + name, + &converter ) + end + + include Enumerable + + # + # Yields each row of the data source in turn. + # + # Support for Enumerable. + # + # The data source must be open for reading. + # + def each + if block_given? + while row = shift + yield row + end + else + to_enum + end + end + + # + # Slurps the remaining rows and returns an Array of Arrays. + # + # The data source must be open for reading. + # + def read + rows = to_a + if @use_headers + Table.new(rows) + else + rows + end + end + alias_method :readlines, :read + + # Returns +true+ if the next row read will be a header row. + def header_row? + @use_headers and @headers.nil? + end + + # + # The primary read method for wrapped Strings and IOs, a single row is pulled + # from the data source, parsed and returned as an Array of fields (if header + # rows are not used) or a CSV::Row (when header rows are used). + # + # The data source must be open for reading. + # + def shift + ######################################################################### + ### This method is purposefully kept a bit long as simple conditional ### + ### checks are faster than numerous (expensive) method calls. ### + ######################################################################### + + # handle headers not based on document content + if header_row? and @return_headers and + [Array, String].include? @use_headers.class + if @unconverted_fields + return add_unconverted_fields(parse_headers, Array.new) + else + return parse_headers + end + end + + # + # it can take multiple calls to @io.gets() to get a full line, + # because of \r and/or \n characters embedded in quoted fields + # + in_extended_col = false + csv = Array.new + + loop do + # add another read to the line + unless parse = @io.gets(@row_sep) + return nil + end + + if in_extended_col + @line.concat(parse) + else + @line = parse.clone + end + + parse.sub!(@parsers[:line_end], "") + + if csv.empty? + # + # I believe a blank line should be an Array.new, not Ruby 1.8 + # CSV's [nil] + # + if parse.empty? + @lineno += 1 + if @skip_blanks + next + elsif @unconverted_fields + return add_unconverted_fields(Array.new, Array.new) + elsif @use_headers + return self.class::Row.new(Array.new, Array.new) + else + return Array.new + end + end + end + + next if @skip_lines and @skip_lines.match parse + + parts = parse.split(@col_sep, -1) + if parts.empty? + if in_extended_col + csv[-1] << @col_sep # will be replaced with a @row_sep after the parts.each loop + else + csv << nil + end + end + + # This loop is the hot path of csv parsing. Some things may be non-dry + # for a reason. Make sure to benchmark when refactoring. + parts.each do |part| + if in_extended_col + # If we are continuing a previous column + if part.end_with?(@quote_char) && part.count(@quote_char) % 2 != 0 + # extended column ends + csv.last << part[0..-2] + if csv.last =~ @parsers[:stray_quote] + raise MalformedCSVError, + "Missing or stray quote in line #{lineno + 1}" + end + csv.last.gsub!(@double_quote_char, @quote_char) + in_extended_col = false + else + csv.last << part << @col_sep + end + elsif part.start_with?(@quote_char) + # If we are starting a new quoted column + if part.count(@quote_char) % 2 != 0 + # start an extended column + csv << (part[1..-1] << @col_sep) + in_extended_col = true + elsif part.end_with?(@quote_char) + # regular quoted column + csv << part[1..-2] + if csv.last =~ @parsers[:stray_quote] + raise MalformedCSVError, + "Missing or stray quote in line #{lineno + 1}" + end + csv.last.gsub!(@double_quote_char, @quote_char) + elsif @liberal_parsing + csv << part + else + raise MalformedCSVError, + "Missing or stray quote in line #{lineno + 1}" + end + elsif part =~ @parsers[:quote_or_nl] + # Unquoted field with bad characters. + if part =~ @parsers[:nl_or_lf] + raise MalformedCSVError, "Unquoted fields do not allow " + + "\\r or \\n (line #{lineno + 1})." + else + if @liberal_parsing + csv << part + else + raise MalformedCSVError, "Illegal quoting in line #{lineno + 1}." + end + end + else + # Regular ole unquoted field. + csv << (part.empty? ? nil : part) + end + end + + # Replace tacked on @col_sep with @row_sep if we are still in an extended + # column. + csv[-1][-1] = @row_sep if in_extended_col + + if in_extended_col + # if we're at eof?(), a quoted field wasn't closed... + if @io.eof? + raise MalformedCSVError, + "Unclosed quoted field on line #{lineno + 1}." + elsif @field_size_limit and csv.last.size >= @field_size_limit + raise MalformedCSVError, "Field size exceeded on line #{lineno + 1}." + end + # otherwise, we need to loop and pull some more data to complete the row + else + @lineno += 1 + + # save fields unconverted fields, if needed... + unconverted = csv.dup if @unconverted_fields + + # convert fields, if needed... + csv = convert_fields(csv) unless @use_headers or @converters.empty? + # parse out header rows and handle CSV::Row conversions... + csv = parse_headers(csv) if @use_headers + + # inject unconverted fields and accessor, if requested... + if @unconverted_fields and not csv.respond_to? :unconverted_fields + add_unconverted_fields(csv, unconverted) + end + + # return the results + break csv + end + end + end + alias_method :gets, :shift + alias_method :readline, :shift + + # + # Returns a simplified description of the key CSV attributes in an + # ASCII compatible String. + # + def inspect + str = ["<#", self.class.to_s, " io_type:"] + # show type of wrapped IO + if @io == $stdout then str << "$stdout" + elsif @io == $stdin then str << "$stdin" + elsif @io == $stderr then str << "$stderr" + else str << @io.class.to_s + end + # show IO.path(), if available + if @io.respond_to?(:path) and (p = @io.path) + str << " io_path:" << p.inspect + end + # show encoding + str << " encoding:" << @encoding.name + # show other attributes + %w[ lineno col_sep row_sep + quote_char skip_blanks liberal_parsing ].each do |attr_name| + if a = instance_variable_get("@#{attr_name}") + str << " " << attr_name << ":" << a.inspect + end + end + if @use_headers + str << " headers:" << headers.inspect + end + str << ">" + begin + str.join('') + rescue # any encoding error + str.map do |s| + e = Encoding::Converter.asciicompat_encoding(s.encoding) + e ? s.encode(e) : s.force_encoding("ASCII-8BIT") + end.join('') + end + end + + private + + # + # Stores the indicated separators for later use. + # + # If auto-discovery was requested for @row_sep, this method will read + # ahead in the @io and try to find one. +ARGF+, +STDIN+, +STDOUT+, + # +STDERR+ and any stream open for output only with a default + # @row_sep of $INPUT_RECORD_SEPARATOR ($/). + # + # This method also establishes the quoting rules used for CSV output. + # + def init_separators(col_sep, row_sep, quote_char, force_quotes) + # store the selected separators + @col_sep = col_sep.to_s.encode(@encoding) + @row_sep = row_sep # encode after resolving :auto + @quote_char = quote_char.to_s.encode(@encoding) + @double_quote_char = @quote_char * 2 + + if @quote_char.length != 1 + raise ArgumentError, ":quote_char has to be a single character String" + end + + # + # automatically discover row separator when requested + # (not fully encoding safe) + # + if @row_sep == :auto + if [ARGF, STDIN, STDOUT, STDERR].include?(@io) or + (defined?(Zlib) and @io.class == Zlib::GzipWriter) + @row_sep = $INPUT_RECORD_SEPARATOR + else + begin + # + # remember where we were (pos() will raise an exception if @io is pipe + # or not opened for reading) + # + saved_pos = @io.pos + while @row_sep == :auto + # + # if we run out of data, it's probably a single line + # (ensure will set default value) + # + break unless sample = @io.gets(nil, 1024) + # extend sample if we're unsure of the line ending + if sample.end_with? encode_str("\r") + sample << (@io.gets(nil, 1) || "") + end + + # try to find a standard separator + if sample =~ encode_re("\r\n?|\n") + @row_sep = $& + break + end + end + + # tricky seek() clone to work around GzipReader's lack of seek() + @io.rewind + # reset back to the remembered position + while saved_pos > 1024 # avoid loading a lot of data into memory + @io.read(1024) + saved_pos -= 1024 + end + @io.read(saved_pos) if saved_pos.nonzero? + rescue IOError # not opened for reading + # do nothing: ensure will set default + rescue NoMethodError # Zlib::GzipWriter doesn't have some IO methods + # do nothing: ensure will set default + rescue SystemCallError # pipe + # do nothing: ensure will set default + ensure + # + # set default if we failed to detect + # (stream not opened for reading, a pipe, or a single line of data) + # + @row_sep = $INPUT_RECORD_SEPARATOR if @row_sep == :auto + end + end + end + @row_sep = @row_sep.to_s.encode(@encoding) + + # establish quoting rules + @force_quotes = force_quotes + do_quote = lambda do |field| + field = String(field) + encoded_quote = @quote_char.encode(field.encoding) + encoded_quote + field.gsub(encoded_quote, encoded_quote * 2) + encoded_quote + end + quotable_chars = encode_str("\r\n", @col_sep, @quote_char) + @quote = if @force_quotes + do_quote + else + lambda do |field| + if field.nil? # represent +nil+ fields as empty unquoted fields + "" + else + field = String(field) # Stringify fields + # represent empty fields as empty quoted fields + if field.empty? or + field.count(quotable_chars).nonzero? + do_quote.call(field) + else + field # unquoted field + end + end + end + end + end + + # Pre-compiles parsers and stores them by name for access during reads. + def init_parsers(skip_blanks, field_size_limit, liberal_parsing) + # store the parser behaviors + @skip_blanks = skip_blanks + @field_size_limit = field_size_limit + @liberal_parsing = liberal_parsing + + # prebuild Regexps for faster parsing + esc_row_sep = escape_re(@row_sep) + esc_quote = escape_re(@quote_char) + @parsers = { + # for detecting parse errors + quote_or_nl: encode_re("[", esc_quote, "\r\n]"), + nl_or_lf: encode_re("[\r\n]"), + stray_quote: encode_re( "[^", esc_quote, "]", esc_quote, + "[^", esc_quote, "]" ), + # safer than chomp!() + line_end: encode_re(esc_row_sep, "\\z"), + # illegal unquoted characters + return_newline: encode_str("\r\n") + } + end + + # + # Loads any converters requested during construction. + # + # If +field_name+ is set :converters (the default) field converters + # are set. When +field_name+ is :header_converters header converters + # are added instead. + # + # The :unconverted_fields option is also activated for + # :converters calls, if requested. + # + def init_converters(converters, ivar_name, convert_method) + converters = case converters + when nil then [] + when Array then converters + else [converters] + end + instance_variable_set(ivar_name, []) + convert = method(convert_method) + + # load converters + converters.each do |converter| + if converter.is_a? Proc # custom code block + convert.call(&converter) + else # by name + convert.call(converter) + end + end + end + + # Stores the pattern of comments to skip from the provided options. + # + # The pattern must respond to +.match+, else ArgumentError is raised. + # Strings are converted to a Regexp. + # + # See also CSV.new + def init_comments(skip_lines) + @skip_lines = skip_lines + @skip_lines = Regexp.new(Regexp.escape(@skip_lines)) if @skip_lines.is_a? String + if @skip_lines and not @skip_lines.respond_to?(:match) + raise ArgumentError, ":skip_lines has to respond to matches" + end + end + # + # The actual work method for adding converters, used by both CSV.convert() and + # CSV.header_convert(). + # + # This method requires the +var_name+ of the instance variable to place the + # converters in, the +const+ Hash to lookup named converters in, and the + # normal parameters of the CSV.convert() and CSV.header_convert() methods. + # + def add_converter(var_name, const, name = nil, &converter) + if name.nil? # custom converter + instance_variable_get(var_name) << converter + else # named converter + combo = const[name] + case combo + when Array # combo converter + combo.each do |converter_name| + add_converter(var_name, const, converter_name) + end + else # individual named converter + instance_variable_get(var_name) << combo + end + end + end + + # + # Processes +fields+ with @converters, or @header_converters + # if +headers+ is passed as +true+, returning the converted field set. Any + # converter that changes the field into something other than a String halts + # the pipeline of conversion for that field. This is primarily an efficiency + # shortcut. + # + def convert_fields(fields, headers = false) + # see if we are converting headers or fields + converters = headers ? @header_converters : @converters + + fields.map.with_index do |field, index| + converters.each do |converter| + break if headers && field.nil? + field = if converter.arity == 1 # straight field converter + converter[field] + else # FieldInfo converter + header = @use_headers && !headers ? @headers[index] : nil + converter[field, FieldInfo.new(index, lineno, header)] + end + break unless field.is_a? String # short-circuit pipeline for speed + end + field # final state of each field, converted or original + end + end + + # + # This method is used to turn a finished +row+ into a CSV::Row. Header rows + # are also dealt with here, either by returning a CSV::Row with identical + # headers and fields (save that the fields do not go through the converters) + # or by reading past them to return a field row. Headers are also saved in + # @headers for use in future rows. + # + # When +nil+, +row+ is assumed to be a header row not based on an actual row + # of the stream. + # + def parse_headers(row = nil) + if @headers.nil? # header row + @headers = case @use_headers # save headers + # Array of headers + when Array then @use_headers + # CSV header String + when String + self.class.parse_line( @use_headers, + col_sep: @col_sep, + row_sep: @row_sep, + quote_char: @quote_char ) + # first row is headers + else row + end + + # prepare converted and unconverted copies + row = @headers if row.nil? + @headers = convert_fields(@headers, true) + @headers.each { |h| h.freeze if h.is_a? String } + + if @return_headers # return headers + return self.class::Row.new(@headers, row, true) + elsif not [Array, String].include? @use_headers.class # skip to field row + return shift + end + end + + self.class::Row.new(@headers, convert_fields(row)) # field row + end + + # + # This method injects an instance variable unconverted_fields into + # +row+ and an accessor method for +row+ called unconverted_fields(). The + # variable is set to the contents of +fields+. + # + def add_unconverted_fields(row, fields) + class << row + attr_reader :unconverted_fields + end + row.instance_variable_set(:@unconverted_fields, fields) + row + end + + # + # This method is an encoding safe version of Regexp::escape(). It will escape + # any characters that would change the meaning of a regular expression in the + # encoding of +str+. Regular expression characters that cannot be transcoded + # to the target encoding will be skipped and no escaping will be performed if + # a backslash cannot be transcoded. + # + def escape_re(str) + str.gsub(@re_chars) {|c| @re_esc + c} + end + + # + # Builds a regular expression in @encoding. All +chunks+ will be + # transcoded to that encoding. + # + def encode_re(*chunks) + Regexp.new(encode_str(*chunks)) + end + + # + # Builds a String in @encoding. All +chunks+ will be transcoded to + # that encoding. + # + def encode_str(*chunks) + chunks.map { |chunk| chunk.encode(@encoding.name) }.join('') + end + + # + # Returns the encoding of the internal IO object or the +default+ if the + # encoding cannot be determined. + # + def raw_encoding(default = Encoding::ASCII_8BIT) + if @io.respond_to? :internal_encoding + @io.internal_encoding || @io.external_encoding + elsif @io.is_a? StringIO + @io.string.encoding + elsif @io.respond_to? :encoding + @io.encoding + else + default + end + end +end + +# Passes +args+ to CSV::instance. +# +# CSV("CSV,data").read +# #=> [["CSV", "data"]] +# +# If a block is given, the instance is passed the block and the return value +# becomes the return value of the block. +# +# CSV("CSV,data") { |c| +# c.read.any? { |a| a.include?("data") } +# } #=> true +# +# CSV("CSV,data") { |c| +# c.read.any? { |a| a.include?("zombies") } +# } #=> false +# +def CSV(*args, &block) + CSV.instance(*args, &block) +end + +class Array # :nodoc: + # Equivalent to CSV::generate_line(self, options) + # + # ["CSV", "data"].to_csv + # #=> "CSV,data\n" + def to_csv(**options) + CSV.generate_line(self, options) + end +end + +class String # :nodoc: + # Equivalent to CSV::parse_line(self, options) + # + # "CSV,data".parse_csv + # #=> ["CSV", "data"] + def parse_csv(**options) + CSV.parse_line(self, options) + end +end diff --git a/gxg/standard/expect.rb b/gxg/standard/expect.rb new file mode 100644 index 0000000..5dbfa09 --- /dev/null +++ b/gxg/standard/expect.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true +$expect_verbose = false + +# Expect library adds the IO instance method #expect, which does similar act to +# tcl's expect extension. +# +# In order to use this method, you must require expect: +# +# require 'expect' +# +# Please see #expect for usage. +class IO + # call-seq: + # IO#expect(pattern,timeout=9999999) -> Array + # IO#expect(pattern,timeout=9999999) { |result| ... } -> nil + # + # Reads from the IO until the given +pattern+ matches or the +timeout+ is over. + # + # It returns an array with the read buffer, followed by the matches. + # If a block is given, the result is yielded to the block and returns nil. + # + # When called without a block, it waits until the input that matches the + # given +pattern+ is obtained from the IO or the time specified as the + # timeout passes. An array is returned when the pattern is obtained from the + # IO. The first element of the array is the entire string obtained from the + # IO until the pattern matches, followed by elements indicating which the + # pattern which matched to the anchor in the regular expression. + # + # The optional timeout parameter defines, in seconds, the total time to wait + # for the pattern. If the timeout expires or eof is found, nil is returned + # or yielded. However, the buffer in a timeout session is kept for the next + # expect call. The default timeout is 9999999 seconds. + def expect(pat,timeout=9999999) + buf = ''.dup + case pat + when String + e_pat = Regexp.new(Regexp.quote(pat)) + when Regexp + e_pat = pat + else + raise TypeError, "unsupported pattern class: #{pat.class}" + end + @unusedBuf ||= '' + while true + if not @unusedBuf.empty? + c = @unusedBuf.slice!(0) + elsif !IO.select([self],nil,nil,timeout) or eof? then + result = nil + @unusedBuf = buf + break + else + c = getc + end + buf << c + if $expect_verbose + STDOUT.print c + STDOUT.flush + end + if mat=e_pat.match(buf) then + result = [buf,*mat.captures] + break + end + end + if block_given? then + yield result + else + return result + end + nil + end +end diff --git a/gxg/standard/find.rb b/gxg/standard/find.rb new file mode 100644 index 0000000..f97cc1b --- /dev/null +++ b/gxg/standard/find.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true +# +# find.rb: the Find module for processing all files under a given directory. +# + +# +# The +Find+ module supports the top-down traversal of a set of file paths. +# +# For example, to total the size of all files under your home directory, +# ignoring anything in a "dot" directory (e.g. $HOME/.ssh): +# +# require 'find' +# +# total_size = 0 +# +# Find.find(ENV["HOME"]) do |path| +# if FileTest.directory?(path) +# if File.basename(path)[0] == ?. +# Find.prune # Don't look any further into this directory. +# else +# next +# end +# else +# total_size += FileTest.size(path) +# end +# end +# +module Find + + # + # Calls the associated block with the name of every file and directory listed + # as arguments, then recursively on their subdirectories, and so on. + # + # Returns an enumerator if no block is given. + # + # See the +Find+ module documentation for an example. + # + def find(*paths, ignore_error: true) # :yield: path + block_given? or return enum_for(__method__, *paths, ignore_error: ignore_error) + + fs_encoding = Encoding.find("filesystem") + + paths.collect!{|d| raise Errno::ENOENT, d unless File.exist?(d); d.dup}.each do |path| + path = path.to_path if path.respond_to? :to_path + enc = path.encoding == Encoding::US_ASCII ? fs_encoding : path.encoding + ps = [path] + while file = ps.shift + catch(:prune) do + yield file.dup.taint + begin + s = File.lstat(file) + rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG + raise unless ignore_error + next + end + if s.directory? then + begin + fs = Dir.children(file, encoding: enc) + rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG + raise unless ignore_error + next + end + fs.sort! + fs.reverse_each {|f| + f = File.join(file, f) + ps.unshift f.untaint + } + end + end + end + end + nil + end + + # + # Skips the current file or directory, restarting the loop with the next + # entry. If the current file is a directory, that directory will not be + # recursively entered. Meaningful only within the block associated with + # Find::find. + # + # See the +Find+ module documentation for an example. + # + def prune + throw :prune + end + + module_function :find, :prune +end diff --git a/gxg/standard/getoptlong.rb b/gxg/standard/getoptlong.rb new file mode 100644 index 0000000..ff7674f --- /dev/null +++ b/gxg/standard/getoptlong.rb @@ -0,0 +1,613 @@ +# frozen_string_literal: true +# +# GetoptLong for Ruby +# +# Copyright (C) 1998, 1999, 2000 Motoyuki Kasahara. +# +# You may redistribute and/or modify this library under the same license +# terms as Ruby. +# +# See GetoptLong for documentation. +# +# Additional documents and the latest version of `getoptlong.rb' can be +# found at http://www.sra.co.jp/people/m-kasahr/ruby/getoptlong/ + +# The GetoptLong class allows you to parse command line options similarly to +# the GNU getopt_long() C library call. Note, however, that GetoptLong is a +# pure Ruby implementation. +# +# GetoptLong allows for POSIX-style options like --file as well +# as single letter options like -f +# +# The empty option -- (two minus symbols) is used to end option +# processing. This can be particularly important if options have optional +# arguments. +# +# Here is a simple example of usage: +# +# require 'getoptlong' +# +# opts = GetoptLong.new( +# [ '--help', '-h', GetoptLong::NO_ARGUMENT ], +# [ '--repeat', '-n', GetoptLong::REQUIRED_ARGUMENT ], +# [ '--name', GetoptLong::OPTIONAL_ARGUMENT ] +# ) +# +# dir = nil +# name = nil +# repetitions = 1 +# opts.each do |opt, arg| +# case opt +# when '--help' +# puts <<-EOF +# hello [OPTION] ... DIR +# +# -h, --help: +# show help +# +# --repeat x, -n x: +# repeat x times +# +# --name [name]: +# greet user by name, if name not supplied default is John +# +# DIR: The directory in which to issue the greeting. +# EOF +# when '--repeat' +# repetitions = arg.to_i +# when '--name' +# if arg == '' +# name = 'John' +# else +# name = arg +# end +# end +# end +# +# if ARGV.length != 1 +# puts "Missing dir argument (try --help)" +# exit 0 +# end +# +# dir = ARGV.shift +# +# Dir.chdir(dir) +# for i in (1..repetitions) +# print "Hello" +# if name +# print ", #{name}" +# end +# puts +# end +# +# Example command line: +# +# hello -n 6 --name -- /tmp +# +class GetoptLong + # + # Orderings. + # + ORDERINGS = [REQUIRE_ORDER = 0, PERMUTE = 1, RETURN_IN_ORDER = 2] + + # + # Argument flags. + # + ARGUMENT_FLAGS = [NO_ARGUMENT = 0, REQUIRED_ARGUMENT = 1, + OPTIONAL_ARGUMENT = 2] + + # + # Status codes. + # + STATUS_YET, STATUS_STARTED, STATUS_TERMINATED = 0, 1, 2 + + # + # Error types. + # + class Error < StandardError; end + class AmbiguousOption < Error; end + class NeedlessArgument < Error; end + class MissingArgument < Error; end + class InvalidOption < Error; end + + # + # Set up option processing. + # + # The options to support are passed to new() as an array of arrays. + # Each sub-array contains any number of String option names which carry + # the same meaning, and one of the following flags: + # + # GetoptLong::NO_ARGUMENT :: Option does not take an argument. + # + # GetoptLong::REQUIRED_ARGUMENT :: Option always takes an argument. + # + # GetoptLong::OPTIONAL_ARGUMENT :: Option may or may not take an argument. + # + # The first option name is considered to be the preferred (canonical) name. + # Other than that, the elements of each sub-array can be in any order. + # + def initialize(*arguments) + # + # Current ordering. + # + if ENV.include?('POSIXLY_CORRECT') + @ordering = REQUIRE_ORDER + else + @ordering = PERMUTE + end + + # + # Hash table of option names. + # Keys of the table are option names, and their values are canonical + # names of the options. + # + @canonical_names = Hash.new + + # + # Hash table of argument flags. + # Keys of the table are option names, and their values are argument + # flags of the options. + # + @argument_flags = Hash.new + + # + # Whether error messages are output to $stderr. + # + @quiet = false + + # + # Status code. + # + @status = STATUS_YET + + # + # Error code. + # + @error = nil + + # + # Error message. + # + @error_message = nil + + # + # Rest of catenated short options. + # + @rest_singles = '' + + # + # List of non-option-arguments. + # Append them to ARGV when option processing is terminated. + # + @non_option_arguments = Array.new + + if 0 < arguments.length + set_options(*arguments) + end + end + + # + # Set the handling of the ordering of options and arguments. + # A RuntimeError is raised if option processing has already started. + # + # The supplied value must be a member of GetoptLong::ORDERINGS. It alters + # the processing of options as follows: + # + # REQUIRE_ORDER : + # + # Options are required to occur before non-options. + # + # Processing of options ends as soon as a word is encountered that has not + # been preceded by an appropriate option flag. + # + # For example, if -a and -b are options which do not take arguments, + # parsing command line arguments of '-a one -b two' would result in + # 'one', '-b', 'two' being left in ARGV, and only ('-a', '') being + # processed as an option/arg pair. + # + # This is the default ordering, if the environment variable + # POSIXLY_CORRECT is set. (This is for compatibility with GNU getopt_long.) + # + # PERMUTE : + # + # Options can occur anywhere in the command line parsed. This is the + # default behavior. + # + # Every sequence of words which can be interpreted as an option (with or + # without argument) is treated as an option; non-option words are skipped. + # + # For example, if -a does not require an argument and -b optionally takes + # an argument, parsing '-a one -b two three' would result in ('-a','') and + # ('-b', 'two') being processed as option/arg pairs, and 'one','three' + # being left in ARGV. + # + # If the ordering is set to PERMUTE but the environment variable + # POSIXLY_CORRECT is set, REQUIRE_ORDER is used instead. This is for + # compatibility with GNU getopt_long. + # + # RETURN_IN_ORDER : + # + # All words on the command line are processed as options. Words not + # preceded by a short or long option flag are passed as arguments + # with an option of '' (empty string). + # + # For example, if -a requires an argument but -b does not, a command line + # of '-a one -b two three' would result in option/arg pairs of ('-a', 'one') + # ('-b', ''), ('', 'two'), ('', 'three') being processed. + # + def ordering=(ordering) + # + # The method is failed if option processing has already started. + # + if @status != STATUS_YET + set_error(ArgumentError, "argument error") + raise RuntimeError, + "invoke ordering=, but option processing has already started" + end + + # + # Check ordering. + # + if !ORDERINGS.include?(ordering) + raise ArgumentError, "invalid ordering `#{ordering}'" + end + if ordering == PERMUTE && ENV.include?('POSIXLY_CORRECT') + @ordering = REQUIRE_ORDER + else + @ordering = ordering + end + end + + # + # Return ordering. + # + attr_reader :ordering + + # + # Set options. Takes the same argument as GetoptLong.new. + # + # Raises a RuntimeError if option processing has already started. + # + def set_options(*arguments) + # + # The method is failed if option processing has already started. + # + if @status != STATUS_YET + raise RuntimeError, + "invoke set_options, but option processing has already started" + end + + # + # Clear tables of option names and argument flags. + # + @canonical_names.clear + @argument_flags.clear + + arguments.each do |arg| + if !arg.is_a?(Array) + raise ArgumentError, "the option list contains non-Array argument" + end + + # + # Find an argument flag and it set to `argument_flag'. + # + argument_flag = nil + arg.each do |i| + if ARGUMENT_FLAGS.include?(i) + if argument_flag != nil + raise ArgumentError, "too many argument-flags" + end + argument_flag = i + end + end + + raise ArgumentError, "no argument-flag" if argument_flag == nil + + canonical_name = nil + arg.each do |i| + # + # Check an option name. + # + next if i == argument_flag + begin + if !i.is_a?(String) || i !~ /\A-([^-]|-.+)\z/ + raise ArgumentError, "an invalid option `#{i}'" + end + if (@canonical_names.include?(i)) + raise ArgumentError, "option redefined `#{i}'" + end + rescue + @canonical_names.clear + @argument_flags.clear + raise + end + + # + # Register the option (`i') to the `@canonical_names' and + # `@canonical_names' Hashes. + # + if canonical_name == nil + canonical_name = i + end + @canonical_names[i] = canonical_name + @argument_flags[i] = argument_flag + end + raise ArgumentError, "no option name" if canonical_name == nil + end + return self + end + + # + # Set/Unset `quiet' mode. + # + attr_writer :quiet + + # + # Return the flag of `quiet' mode. + # + attr_reader :quiet + + # + # `quiet?' is an alias of `quiet'. + # + alias quiet? quiet + + # + # Explicitly terminate option processing. + # + def terminate + return nil if @status == STATUS_TERMINATED + raise RuntimeError, "an error has occurred" if @error != nil + + @status = STATUS_TERMINATED + @non_option_arguments.reverse_each do |argument| + ARGV.unshift(argument) + end + + @canonical_names = nil + @argument_flags = nil + @rest_singles = nil + @non_option_arguments = nil + + return self + end + + # + # Returns true if option processing has terminated, false otherwise. + # + def terminated? + return @status == STATUS_TERMINATED + end + + # + # Set an error (a protected method). + # + def set_error(type, message) + $stderr.print("#{$0}: #{message}\n") if !@quiet + + @error = type + @error_message = message + @canonical_names = nil + @argument_flags = nil + @rest_singles = nil + @non_option_arguments = nil + + raise type, message + end + protected :set_error + + # + # Examine whether an option processing is failed. + # + attr_reader :error + + # + # `error?' is an alias of `error'. + # + alias error? error + + # Return the appropriate error message in POSIX-defined format. + # If no error has occurred, returns nil. + # + def error_message + return @error_message + end + + # + # Get next option name and its argument, as an Array of two elements. + # + # The option name is always converted to the first (preferred) + # name given in the original options to GetoptLong.new. + # + # Example: ['--option', 'value'] + # + # Returns nil if the processing is complete (as determined by + # STATUS_TERMINATED). + # + def get + option_name, option_argument = nil, '' + + # + # Check status. + # + return nil if @error != nil + case @status + when STATUS_YET + @status = STATUS_STARTED + when STATUS_TERMINATED + return nil + end + + # + # Get next option argument. + # + if 0 < @rest_singles.length + argument = '-' + @rest_singles + elsif (ARGV.length == 0) + terminate + return nil + elsif @ordering == PERMUTE + while 0 < ARGV.length && ARGV[0] !~ /\A-./ + @non_option_arguments.push(ARGV.shift) + end + if ARGV.length == 0 + terminate + return nil + end + argument = ARGV.shift + elsif @ordering == REQUIRE_ORDER + if (ARGV[0] !~ /\A-./) + terminate + return nil + end + argument = ARGV.shift + else + argument = ARGV.shift + end + + # + # Check the special argument `--'. + # `--' indicates the end of the option list. + # + if argument == '--' && @rest_singles.length == 0 + terminate + return nil + end + + # + # Check for long and short options. + # + if argument =~ /\A(--[^=]+)/ && @rest_singles.length == 0 + # + # This is a long style option, which start with `--'. + # + pattern = $1 + if @canonical_names.include?(pattern) + option_name = pattern + else + # + # The option `option_name' is not registered in `@canonical_names'. + # It may be an abbreviated. + # + matches = [] + @canonical_names.each_key do |key| + if key.index(pattern) == 0 + option_name = key + matches << key + end + end + if 2 <= matches.length + set_error(AmbiguousOption, "option `#{argument}' is ambiguous between #{matches.join(', ')}") + elsif matches.length == 0 + set_error(InvalidOption, "unrecognized option `#{argument}'") + end + end + + # + # Check an argument to the option. + # + if @argument_flags[option_name] == REQUIRED_ARGUMENT + if argument =~ /=(.*)/m + option_argument = $1 + elsif 0 < ARGV.length + option_argument = ARGV.shift + else + set_error(MissingArgument, + "option `#{argument}' requires an argument") + end + elsif @argument_flags[option_name] == OPTIONAL_ARGUMENT + if argument =~ /=(.*)/m + option_argument = $1 + elsif 0 < ARGV.length && ARGV[0] !~ /\A-./ + option_argument = ARGV.shift + else + option_argument = '' + end + elsif argument =~ /=(.*)/m + set_error(NeedlessArgument, + "option `#{option_name}' doesn't allow an argument") + end + + elsif argument =~ /\A(-(.))(.*)/m + # + # This is a short style option, which start with `-' (not `--'). + # Short options may be catenated (e.g. `-l -g' is equivalent to + # `-lg'). + # + option_name, ch, @rest_singles = $1, $2, $3 + + if @canonical_names.include?(option_name) + # + # The option `option_name' is found in `@canonical_names'. + # Check its argument. + # + if @argument_flags[option_name] == REQUIRED_ARGUMENT + if 0 < @rest_singles.length + option_argument = @rest_singles + @rest_singles = '' + elsif 0 < ARGV.length + option_argument = ARGV.shift + else + # 1003.2 specifies the format of this message. + set_error(MissingArgument, "option requires an argument -- #{ch}") + end + elsif @argument_flags[option_name] == OPTIONAL_ARGUMENT + if 0 < @rest_singles.length + option_argument = @rest_singles + @rest_singles = '' + elsif 0 < ARGV.length && ARGV[0] !~ /\A-./ + option_argument = ARGV.shift + else + option_argument = '' + end + end + else + # + # This is an invalid option. + # 1003.2 specifies the format of this message. + # + if ENV.include?('POSIXLY_CORRECT') + set_error(InvalidOption, "invalid option -- #{ch}") + else + set_error(InvalidOption, "invalid option -- #{ch}") + end + end + else + # + # This is a non-option argument. + # Only RETURN_IN_ORDER fell into here. + # + return '', argument + end + + return @canonical_names[option_name], option_argument + end + + # + # `get_option' is an alias of `get'. + # + alias get_option get + + # Iterator version of `get'. + # + # The block is called repeatedly with two arguments: + # The first is the option name. + # The second is the argument which followed it (if any). + # Example: ('--opt', 'value') + # + # The option name is always converted to the first (preferred) + # name given in the original options to GetoptLong.new. + # + def each + loop do + option_name, option_argument = get_option + break if option_name == nil + yield option_name, option_argument + end + end + + # + # `each_option' is an alias of `each'. + # + alias each_option each +end diff --git a/gxg/standard/io/console/size.rb b/gxg/standard/io/console/size.rb new file mode 100644 index 0000000..14b9a74 --- /dev/null +++ b/gxg/standard/io/console/size.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: false +# fallback to console window size +def IO.default_console_size + [ + ENV["LINES"].to_i.nonzero? || 25, + ENV["COLUMNS"].to_i.nonzero? || 80, + ] +end + +begin + require 'io/console' +rescue LoadError + class << IO + alias console_size default_console_size + end +else + # returns console window size + def IO.console_size + console.winsize + rescue NoMethodError + default_console_size + end +end diff --git a/gxg/standard/monitor.rb b/gxg/standard/monitor.rb new file mode 100644 index 0000000..288ed75 --- /dev/null +++ b/gxg/standard/monitor.rb @@ -0,0 +1,315 @@ +# frozen_string_literal: false +# = monitor.rb +# +# Copyright (C) 2001 Shugo Maeda +# +# This library is distributed under the terms of the Ruby license. +# You can freely distribute/modify this library. +# + +# +# In concurrent programming, a monitor is an object or module intended to be +# used safely by more than one thread. The defining characteristic of a +# monitor is that its methods are executed with mutual exclusion. That is, at +# each point in time, at most one thread may be executing any of its methods. +# This mutual exclusion greatly simplifies reasoning about the implementation +# of monitors compared to reasoning about parallel code that updates a data +# structure. +# +# You can read more about the general principles on the Wikipedia page for +# Monitors[http://en.wikipedia.org/wiki/Monitor_%28synchronization%29] +# +# == Examples +# +# === Simple object.extend +# +# require 'monitor.rb' +# +# buf = [] +# buf.extend(MonitorMixin) +# empty_cond = buf.new_cond +# +# # consumer +# Thread.start do +# loop do +# buf.synchronize do +# empty_cond.wait_while { buf.empty? } +# print buf.shift +# end +# end +# end +# +# # producer +# while line = ARGF.gets +# buf.synchronize do +# buf.push(line) +# empty_cond.signal +# end +# end +# +# The consumer thread waits for the producer thread to push a line to buf +# while buf.empty?. The producer thread (main thread) reads a +# line from ARGF and pushes it into buf then calls empty_cond.signal +# to notify the consumer thread of new data. +# +# === Simple Class include +# +# require 'monitor' +# +# class SynchronizedArray < Array +# +# include MonitorMixin +# +# def initialize(*args) +# super(*args) +# end +# +# alias :old_shift :shift +# alias :old_unshift :unshift +# +# def shift(n=1) +# self.synchronize do +# self.old_shift(n) +# end +# end +# +# def unshift(item) +# self.synchronize do +# self.old_unshift(item) +# end +# end +# +# # other methods ... +# end +# +# +SynchronizedArray+ implements an Array with synchronized access to items. +# This Class is implemented as subclass of Array which includes the +# MonitorMixin module. +# +module MonitorMixin + # + # FIXME: This isn't documented in Nutshell. + # + # Since MonitorMixin.new_cond returns a ConditionVariable, and the example + # above calls while_wait and signal, this class should be documented. + # + class ConditionVariable + class Timeout < Exception; end + + # + # Releases the lock held in the associated monitor and waits; reacquires the lock on wakeup. + # + # If +timeout+ is given, this method returns after +timeout+ seconds passed, + # even if no other thread doesn't signal. + # + def wait(timeout = nil) + @monitor.__send__(:mon_check_owner) + count = @monitor.__send__(:mon_exit_for_cond) + begin + @cond.wait(@monitor.instance_variable_get(:@mon_mutex), timeout) + return true + ensure + @monitor.__send__(:mon_enter_for_cond, count) + end + end + + # + # Calls wait repeatedly while the given block yields a truthy value. + # + def wait_while + while yield + wait + end + end + + # + # Calls wait repeatedly until the given block yields a truthy value. + # + def wait_until + until yield + wait + end + end + + # + # Wakes up the first thread in line waiting for this lock. + # + def signal + @monitor.__send__(:mon_check_owner) + @cond.signal + end + + # + # Wakes up all threads waiting for this lock. + # + def broadcast + @monitor.__send__(:mon_check_owner) + @cond.broadcast + end + + private + + def initialize(monitor) + @monitor = monitor + @cond = Thread::ConditionVariable.new + end + end + + def self.extend_object(obj) + super(obj) + obj.__send__(:mon_initialize) + end + + # + # Attempts to enter exclusive section. Returns +false+ if lock fails. + # + def mon_try_enter + if @mon_owner != Thread.current + unless @mon_mutex.try_lock + return false + end + @mon_owner = Thread.current + @mon_count = 0 + end + @mon_count += 1 + return true + end + # For backward compatibility + alias try_mon_enter mon_try_enter + + # + # Enters exclusive section. + # + def mon_enter + if @mon_owner != Thread.current + @mon_mutex.lock + @mon_owner = Thread.current + @mon_count = 0 + end + @mon_count += 1 + end + + # + # Leaves exclusive section. + # + def mon_exit + mon_check_owner + @mon_count -=1 + if @mon_count == 0 + @mon_owner = nil + @mon_mutex.unlock + end + end + + # + # Returns true if this monitor is locked by any thread + # + def mon_locked? + @mon_mutex.locked? + end + + # + # Returns true if this monitor is locked by current thread. + # + def mon_owned? + @mon_mutex.locked? && @mon_owner == Thread.current + end + + # + # Enters exclusive section and executes the block. Leaves the exclusive + # section automatically when the block exits. See example under + # +MonitorMixin+. + # + def mon_synchronize + mon_enter + begin + yield + ensure + mon_exit + end + end + alias synchronize mon_synchronize + + # + # Creates a new MonitorMixin::ConditionVariable associated with the + # receiver. + # + def new_cond + return ConditionVariable.new(self) + end + + private + + # Use extend MonitorMixin or include MonitorMixin instead + # of this constructor. Have look at the examples above to understand how to + # use this module. + def initialize(*args) + super + mon_initialize + end + + # Initializes the MonitorMixin after being included in a class or when an + # object has been extended with the MonitorMixin + def mon_initialize + @mon_owner = nil + @mon_count = 0 + @mon_mutex = Thread::Mutex.new + end + + def mon_check_owner + if @mon_owner != Thread.current + raise ThreadError, "current thread not owner" + end + end + + def mon_enter_for_cond(count) + @mon_owner = Thread.current + @mon_count = count + end + + def mon_exit_for_cond + count = @mon_count + @mon_owner = nil + @mon_count = 0 + return count + end +end + +# Use the Monitor class when you want to have a lock object for blocks with +# mutual exclusion. +# +# require 'monitor' +# +# lock = Monitor.new +# lock.synchronize do +# # exclusive access +# end +# +class Monitor + include MonitorMixin + alias try_enter try_mon_enter + alias enter mon_enter + alias exit mon_exit +end + + +# Documentation comments: +# - All documentation comes from Nutshell. +# - MonitorMixin.new_cond appears in the example, but is not documented in +# Nutshell. +# - All the internals (internal modules Accessible and Initializable, class +# ConditionVariable) appear in RDoc. It might be good to hide them, by +# making them private, or marking them :nodoc:, etc. +# - RDoc doesn't recognise aliases, so we have mon_synchronize documented, but +# not synchronize. +# - mon_owner is in Nutshell, but appears as an accessor in a separate module +# here, so is hard/impossible to RDoc. Some other useful accessors +# (mon_count and some queue stuff) are also in this module, and don't appear +# directly in the RDoc output. +# - in short, it may be worth changing the code layout in this file to make the +# documentation easier + +# Local variables: +# mode: Ruby +# tab-width: 8 +# End: diff --git a/gxg/standard/mutex_m.rb b/gxg/standard/mutex_m.rb new file mode 100644 index 0000000..592e384 --- /dev/null +++ b/gxg/standard/mutex_m.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: false +# +# mutex_m.rb - +# $Release Version: 3.0$ +# $Revision: 1.7 $ +# Original from mutex.rb +# by Keiju ISHITSUKA(keiju@ishitsuka.com) +# modified by matz +# patched by akira yamada +# +# -- + +# = mutex_m.rb +# +# When 'mutex_m' is required, any object that extends or includes Mutex_m will +# be treated like a Mutex. +# +# Start by requiring the standard library Mutex_m: +# +# require "mutex_m.rb" +# +# From here you can extend an object with Mutex instance methods: +# +# obj = Object.new +# obj.extend Mutex_m +# +# Or mixin Mutex_m into your module to your class inherit Mutex instance +# methods. +# +# class Foo +# include Mutex_m +# # ... +# end +# obj = Foo.new +# # this obj can be handled like Mutex +# +module Mutex_m + def Mutex_m.define_aliases(cl) # :nodoc: + cl.module_eval %q{ + alias locked? mu_locked? + alias lock mu_lock + alias unlock mu_unlock + alias try_lock mu_try_lock + alias synchronize mu_synchronize + } + end + + def Mutex_m.append_features(cl) # :nodoc: + super + define_aliases(cl) unless cl.instance_of?(Module) + end + + def Mutex_m.extend_object(obj) # :nodoc: + super + obj.mu_extended + end + + def mu_extended # :nodoc: + unless (defined? locked? and + defined? lock and + defined? unlock and + defined? try_lock and + defined? synchronize) + Mutex_m.define_aliases(singleton_class) + end + mu_initialize + end + + # See Mutex#synchronize + def mu_synchronize(&block) + @_mutex.synchronize(&block) + end + + # See Mutex#locked? + def mu_locked? + @_mutex.locked? + end + + # See Mutex#try_lock + def mu_try_lock + @_mutex.try_lock + end + + # See Mutex#lock + def mu_lock + @_mutex.lock + end + + # See Mutex#unlock + def mu_unlock + @_mutex.unlock + end + + # See Mutex#sleep + def sleep(timeout = nil) + @_mutex.sleep(timeout) + end + + private + + def mu_initialize # :nodoc: + @_mutex = Thread::Mutex.new + end + + def initialize(*args) # :nodoc: + mu_initialize + super + end +end diff --git a/gxg/standard/net/ftp.rb b/gxg/standard/net/ftp.rb new file mode 100644 index 0000000..9902f9d --- /dev/null +++ b/gxg/standard/net/ftp.rb @@ -0,0 +1,1493 @@ +# frozen_string_literal: true +# +# = net/ftp.rb - FTP Client Library +# +# Written by Shugo Maeda . +# +# Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas) +# and "Ruby In a Nutshell" (Matsumoto), used with permission. +# +# This library is distributed under the terms of the Ruby license. +# You can freely distribute/modify this library. +# +# It is included in the Ruby standard library. +# +# See the Net::FTP class for an overview. +# + +require "socket" +require "monitor" +require "net/protocol" +require "time" +begin + require "openssl" +rescue LoadError +end + +module Net + + # :stopdoc: + class FTPError < StandardError; end + class FTPReplyError < FTPError; end + class FTPTempError < FTPError; end + class FTPPermError < FTPError; end + class FTPProtoError < FTPError; end + class FTPConnectionError < FTPError; end + # :startdoc: + + # + # This class implements the File Transfer Protocol. If you have used a + # command-line FTP program, and are familiar with the commands, you will be + # able to use this class easily. Some extra features are included to take + # advantage of Ruby's style and strengths. + # + # == Example + # + # require 'net/ftp' + # + # === Example 1 + # + # ftp = Net::FTP.new('example.com') + # ftp.login + # files = ftp.chdir('pub/lang/ruby/contrib') + # files = ftp.list('n*') + # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) + # ftp.close + # + # === Example 2 + # + # Net::FTP.open('example.com') do |ftp| + # ftp.login + # files = ftp.chdir('pub/lang/ruby/contrib') + # files = ftp.list('n*') + # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) + # end + # + # == Major Methods + # + # The following are the methods most likely to be useful to users: + # - FTP.open + # - #getbinaryfile + # - #gettextfile + # - #putbinaryfile + # - #puttextfile + # - #chdir + # - #nlst + # - #size + # - #rename + # - #delete + # + class FTP < Protocol + include MonitorMixin + if defined?(OpenSSL::SSL) + include OpenSSL + include SSL + end + + # :stopdoc: + FTP_PORT = 21 + CRLF = "\r\n" + DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE + @@default_passive = true + # :startdoc: + + # When +true+, transfers are performed in binary mode. Default: +true+. + attr_reader :binary + + # When +true+, the connection is in passive mode. Default: +true+. + attr_accessor :passive + + # When +true+, all traffic to and from the server is written + # to +$stdout+. Default: +false+. + attr_accessor :debug_mode + + # Sets or retrieves the +resume+ status, which decides whether incomplete + # transfers are resumed or restarted. Default: +false+. + attr_accessor :resume + + # Number of seconds to wait for the connection to open. Any number + # may be used, including Floats for fractional seconds. If the FTP + # object cannot open a connection in this many seconds, it raises a + # Net::OpenTimeout exception. The default value is +nil+. + attr_accessor :open_timeout + + # Number of seconds to wait for the TLS handshake. Any number + # may be used, including Floats for fractional seconds. If the FTP + # object cannot complete the TLS handshake in this many seconds, it + # raises a Net::OpenTimeout exception. The default value is +nil+. + # If +ssl_handshake_timeout+ is +nil+, +open_timeout+ is used instead. + attr_accessor :ssl_handshake_timeout + + # Number of seconds to wait for one block to be read (via one read(2) + # call). Any number may be used, including Floats for fractional + # seconds. If the FTP object cannot read data in this many seconds, + # it raises a Timeout::Error exception. The default value is 60 seconds. + attr_reader :read_timeout + + # Setter for the read_timeout attribute. + def read_timeout=(sec) + @sock.read_timeout = sec + @read_timeout = sec + end + + # The server's welcome message. + attr_reader :welcome + + # The server's last response code. + attr_reader :last_response_code + alias lastresp last_response_code + + # The server's last response. + attr_reader :last_response + + # When +true+, connections are in passive mode per default. + # Default: +true+. + def self.default_passive=(value) + @@default_passive = value + end + + # When +true+, connections are in passive mode per default. + # Default: +true+. + def self.default_passive + @@default_passive + end + + # + # A synonym for FTP.new, but with a mandatory host parameter. + # + # If a block is given, it is passed the +FTP+ object, which will be closed + # when the block finishes, or when an exception is raised. + # + def FTP.open(host, *args) + if block_given? + ftp = new(host, *args) + begin + yield ftp + ensure + ftp.close + end + else + new(host, *args) + end + end + + # :call-seq: + # Net::FTP.new(host = nil, options = {}) + # + # Creates and returns a new +FTP+ object. If a +host+ is given, a connection + # is made. + # + # +options+ is an option hash, each key of which is a symbol. + # + # The available options are: + # + # port:: Port number (default value is 21) + # ssl:: If options[:ssl] is true, then an attempt will be made + # to use SSL (now TLS) to connect to the server. For this + # to work OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] + # extensions need to be installed. If options[:ssl] is a + # hash, it's passed to OpenSSL::SSL::SSLContext#set_params + # as parameters. + # private_data_connection:: If true, TLS is used for data connections. + # Default: +true+ when options[:ssl] is true. + # username:: Username for login. If options[:username] is the string + # "anonymous" and the options[:password] is +nil+, + # "anonymous@" is used as a password. + # password:: Password for login. + # account:: Account information for ACCT. + # passive:: When +true+, the connection is in passive mode. Default: + # +true+. + # open_timeout:: Number of seconds to wait for the connection to open. + # See Net::FTP#open_timeout for details. Default: +nil+. + # read_timeout:: Number of seconds to wait for one block to be read. + # See Net::FTP#read_timeout for details. Default: +60+. + # ssl_handshake_timeout:: Number of seconds to wait for the TLS + # handshake. + # See Net::FTP#ssl_handshake_timeout for + # details. Default: +nil+. + # debug_mode:: When +true+, all traffic to and from the server is + # written to +$stdout+. Default: +false+. + # + def initialize(host = nil, user_or_options = {}, passwd = nil, acct = nil) + super() + begin + options = user_or_options.to_hash + rescue NoMethodError + # for backward compatibility + options = {} + options[:username] = user_or_options + options[:password] = passwd + options[:account] = acct + end + @host = nil + if options[:ssl] + unless defined?(OpenSSL::SSL) + raise "SSL extension not installed" + end + ssl_params = options[:ssl] == true ? {} : options[:ssl] + @ssl_context = SSLContext.new + @ssl_context.set_params(ssl_params) + if defined?(VerifyCallbackProc) + @ssl_context.verify_callback = VerifyCallbackProc + end + @ssl_context.session_cache_mode = + OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | + OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE + @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess } + @ssl_session = nil + if options[:private_data_connection].nil? + @private_data_connection = true + else + @private_data_connection = options[:private_data_connection] + end + else + @ssl_context = nil + if options[:private_data_connection] + raise ArgumentError, + "private_data_connection can be set to true only when ssl is enabled" + end + @private_data_connection = false + end + @binary = true + if options[:passive].nil? + @passive = @@default_passive + else + @passive = options[:passive] + end + if options[:debug_mode].nil? + @debug_mode = false + else + @debug_mode = options[:debug_mode] + end + @resume = false + @bare_sock = @sock = NullSocket.new + @logged_in = false + @open_timeout = options[:open_timeout] + @ssl_handshake_timeout = options[:ssl_handshake_timeout] + @read_timeout = options[:read_timeout] || 60 + if host + connect(host, options[:port] || FTP_PORT) + if options[:username] + login(options[:username], options[:password], options[:account]) + end + end + end + + # A setter to toggle transfers in binary mode. + # +newmode+ is either +true+ or +false+ + def binary=(newmode) + if newmode != @binary + @binary = newmode + send_type_command if @logged_in + end + end + + # Sends a command to destination host, with the current binary sendmode + # type. + # + # If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE + # A" (ascii) is sent. + def send_type_command # :nodoc: + if @binary + voidcmd("TYPE I") + else + voidcmd("TYPE A") + end + end + private :send_type_command + + # Toggles transfers in binary mode and yields to a block. + # This preserves your current binary send mode, but allows a temporary + # transaction with binary sendmode of +newmode+. + # + # +newmode+ is either +true+ or +false+ + def with_binary(newmode) # :nodoc: + oldmode = binary + self.binary = newmode + begin + yield + ensure + self.binary = oldmode + end + end + private :with_binary + + # Obsolete + def return_code # :nodoc: + warn("Net::FTP#return_code is obsolete and do nothing", uplevel: 1) + return "\n" + end + + # Obsolete + def return_code=(s) # :nodoc: + warn("Net::FTP#return_code= is obsolete and do nothing", uplevel: 1) + end + + # Constructs a socket with +host+ and +port+. + # + # If SOCKSSocket is defined and the environment (ENV) defines + # SOCKS_SERVER, then a SOCKSSocket is returned, else a Socket is + # returned. + def open_socket(host, port) # :nodoc: + return Timeout.timeout(@open_timeout, OpenTimeout) { + if defined? SOCKSSocket and ENV["SOCKS_SERVER"] + @passive = true + SOCKSSocket.open(host, port) + else + Socket.tcp(host, port) + end + } + end + private :open_socket + + def start_tls_session(sock) + ssl_sock = SSLSocket.new(sock, @ssl_context) + ssl_sock.sync_close = true + ssl_sock.hostname = @host if ssl_sock.respond_to? :hostname= + if @ssl_session && + Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout + # ProFTPD returns 425 for data connections if session is not reused. + ssl_sock.session = @ssl_session + end + ssl_socket_connect(ssl_sock, @ssl_handshake_timeout || @open_timeout) + if @ssl_context.verify_mode != VERIFY_NONE + ssl_sock.post_connection_check(@host) + end + return ssl_sock + end + private :start_tls_session + + # + # Establishes an FTP connection to host, optionally overriding the default + # port. If the environment variable +SOCKS_SERVER+ is set, sets up the + # connection through a SOCKS proxy. Raises an exception (typically + # Errno::ECONNREFUSED) if the connection cannot be established. + # + def connect(host, port = FTP_PORT) + if @debug_mode + print "connect: ", host, ", ", port, "\n" + end + synchronize do + @host = host + @bare_sock = open_socket(host, port) + @sock = BufferedSocket.new(@bare_sock, read_timeout: @read_timeout) + voidresp + if @ssl_context + begin + voidcmd("AUTH TLS") + ssl_sock = start_tls_session(@bare_sock) + @sock = BufferedSSLSocket.new(ssl_sock, read_timeout: @read_timeout) + if @private_data_connection + voidcmd("PBSZ 0") + voidcmd("PROT P") + end + rescue OpenSSL::SSL::SSLError, OpenTimeout + @sock.close + raise + end + end + end + end + + # + # Set the socket used to connect to the FTP server. + # + # May raise FTPReplyError if +get_greeting+ is false. + def set_socket(sock, get_greeting = true) + synchronize do + @sock = sock + if get_greeting + voidresp + end + end + end + + # If string +s+ includes the PASS command (password), then the contents of + # the password are cleaned from the string using "*" + def sanitize(s) # :nodoc: + if s =~ /^PASS /i + return s[0, 5] + "*" * (s.length - 5) + else + return s + end + end + private :sanitize + + # Ensures that +line+ has a control return / line feed (CRLF) and writes + # it to the socket. + def putline(line) # :nodoc: + if @debug_mode + print "put: ", sanitize(line), "\n" + end + if /[\r\n]/ =~ line + raise ArgumentError, "A line must not contain CR or LF" + end + line = line + CRLF + @sock.write(line) + end + private :putline + + # Reads a line from the sock. If EOF, then it will raise EOFError + def getline # :nodoc: + line = @sock.readline # if get EOF, raise EOFError + line.sub!(/(\r\n|\n|\r)\z/n, "") + if @debug_mode + print "get: ", sanitize(line), "\n" + end + return line + end + private :getline + + # Receive a section of lines until the response code's match. + def getmultiline # :nodoc: + lines = [] + lines << getline + code = lines.last.slice(/\A([0-9a-zA-Z]{3})-/, 1) + if code + delimiter = code + " " + begin + lines << getline + end until lines.last.start_with?(delimiter) + end + return lines.join("\n") + "\n" + end + private :getmultiline + + # Receives a response from the destination host. + # + # Returns the response code or raises FTPTempError, FTPPermError, or + # FTPProtoError + def getresp # :nodoc: + @last_response = getmultiline + @last_response_code = @last_response[0, 3] + case @last_response_code + when /\A[123]/ + return @last_response + when /\A4/ + raise FTPTempError, @last_response + when /\A5/ + raise FTPPermError, @last_response + else + raise FTPProtoError, @last_response + end + end + private :getresp + + # Receives a response. + # + # Raises FTPReplyError if the first position of the response code is not + # equal 2. + def voidresp # :nodoc: + resp = getresp + if !resp.start_with?("2") + raise FTPReplyError, resp + end + end + private :voidresp + + # + # Sends a command and returns the response. + # + def sendcmd(cmd) + synchronize do + putline(cmd) + return getresp + end + end + + # + # Sends a command and expect a response beginning with '2'. + # + def voidcmd(cmd) + synchronize do + putline(cmd) + voidresp + end + end + + # Constructs and send the appropriate PORT (or EPRT) command + def sendport(host, port) # :nodoc: + remote_address = @bare_sock.remote_address + if remote_address.ipv4? + cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",") + elsif remote_address.ipv6? + cmd = sprintf("EPRT |2|%s|%d|", host, port) + else + raise FTPProtoError, host + end + voidcmd(cmd) + end + private :sendport + + # Constructs a TCPServer socket + def makeport # :nodoc: + Addrinfo.tcp(@bare_sock.local_address.ip_address, 0).listen + end + private :makeport + + # sends the appropriate command to enable a passive connection + def makepasv # :nodoc: + if @bare_sock.remote_address.ipv4? + host, port = parse227(sendcmd("PASV")) + else + host, port = parse229(sendcmd("EPSV")) + # host, port = parse228(sendcmd("LPSV")) + end + return host, port + end + private :makepasv + + # Constructs a connection for transferring data + def transfercmd(cmd, rest_offset = nil) # :nodoc: + if @passive + host, port = makepasv + conn = open_socket(host, port) + if @resume and rest_offset + resp = sendcmd("REST " + rest_offset.to_s) + if !resp.start_with?("3") + raise FTPReplyError, resp + end + end + resp = sendcmd(cmd) + # skip 2XX for some ftp servers + resp = getresp if resp.start_with?("2") + if !resp.start_with?("1") + raise FTPReplyError, resp + end + else + sock = makeport + begin + addr = sock.local_address + sendport(addr.ip_address, addr.ip_port) + if @resume and rest_offset + resp = sendcmd("REST " + rest_offset.to_s) + if !resp.start_with?("3") + raise FTPReplyError, resp + end + end + resp = sendcmd(cmd) + # skip 2XX for some ftp servers + resp = getresp if resp.start_with?("2") + if !resp.start_with?("1") + raise FTPReplyError, resp + end + conn, = sock.accept + sock.shutdown(Socket::SHUT_WR) rescue nil + sock.read rescue nil + ensure + sock.close + end + end + if @private_data_connection + return BufferedSSLSocket.new(start_tls_session(conn), + read_timeout: @read_timeout) + else + return BufferedSocket.new(conn, read_timeout: @read_timeout) + end + end + private :transfercmd + + # + # Logs in to the remote host. The session must have been + # previously connected. If +user+ is the string "anonymous" and + # the +password+ is +nil+, "anonymous@" is used as a password. If + # the +acct+ parameter is not +nil+, an FTP ACCT command is sent + # following the successful login. Raises an exception on error + # (typically Net::FTPPermError). + # + def login(user = "anonymous", passwd = nil, acct = nil) + if user == "anonymous" and passwd == nil + passwd = "anonymous@" + end + + resp = "" + synchronize do + resp = sendcmd('USER ' + user) + if resp.start_with?("3") + raise FTPReplyError, resp if passwd.nil? + resp = sendcmd('PASS ' + passwd) + end + if resp.start_with?("3") + raise FTPReplyError, resp if acct.nil? + resp = sendcmd('ACCT ' + acct) + end + end + if !resp.start_with?("2") + raise FTPReplyError, resp + end + @welcome = resp + send_type_command + @logged_in = true + end + + # + # Puts the connection into binary (image) mode, issues the given command, + # and fetches the data returned, passing it to the associated block in + # chunks of +blocksize+ characters. Note that +cmd+ is a server command + # (such as "RETR myfile"). + # + def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data + synchronize do + with_binary(true) do + begin + conn = transfercmd(cmd, rest_offset) + loop do + data = conn.read(blocksize) + break if data == nil + yield(data) + end + conn.shutdown(Socket::SHUT_WR) + conn.read_timeout = 1 + conn.read + ensure + conn.close if conn + end + voidresp + end + end + end + + # + # Puts the connection into ASCII (text) mode, issues the given command, and + # passes the resulting data, one line at a time, to the associated block. If + # no block is given, prints the lines. Note that +cmd+ is a server command + # (such as "RETR myfile"). + # + def retrlines(cmd) # :yield: line + synchronize do + with_binary(false) do + begin + conn = transfercmd(cmd) + loop do + line = conn.gets + break if line == nil + yield(line.sub(/\r?\n\z/, ""), !line.match(/\n\z/).nil?) + end + conn.shutdown(Socket::SHUT_WR) + conn.read_timeout = 1 + conn.read + ensure + conn.close if conn + end + voidresp + end + end + end + + # + # Puts the connection into binary (image) mode, issues the given server-side + # command (such as "STOR myfile"), and sends the contents of the file named + # +file+ to the server. If the optional block is given, it also passes it + # the data, in chunks of +blocksize+ characters. + # + def storbinary(cmd, file, blocksize, rest_offset = nil) # :yield: data + if rest_offset + file.seek(rest_offset, IO::SEEK_SET) + end + synchronize do + with_binary(true) do + conn = transfercmd(cmd) + loop do + buf = file.read(blocksize) + break if buf == nil + conn.write(buf) + yield(buf) if block_given? + end + conn.close + voidresp + end + end + rescue Errno::EPIPE + # EPIPE, in this case, means that the data connection was unexpectedly + # terminated. Rather than just raising EPIPE to the caller, check the + # response on the control connection. If getresp doesn't raise a more + # appropriate exception, re-raise the original exception. + getresp + raise + end + + # + # Puts the connection into ASCII (text) mode, issues the given server-side + # command (such as "STOR myfile"), and sends the contents of the file + # named +file+ to the server, one line at a time. If the optional block is + # given, it also passes it the lines. + # + def storlines(cmd, file) # :yield: line + synchronize do + with_binary(false) do + conn = transfercmd(cmd) + loop do + buf = file.gets + break if buf == nil + if buf[-2, 2] != CRLF + buf = buf.chomp + CRLF + end + conn.write(buf) + yield(buf) if block_given? + end + conn.close + voidresp + end + end + rescue Errno::EPIPE + # EPIPE, in this case, means that the data connection was unexpectedly + # terminated. Rather than just raising EPIPE to the caller, check the + # response on the control connection. If getresp doesn't raise a more + # appropriate exception, re-raise the original exception. + getresp + raise + end + + # + # Retrieves +remotefile+ in binary mode, storing the result in +localfile+. + # If +localfile+ is nil, returns retrieved data. + # If a block is supplied, it is passed the retrieved data in +blocksize+ + # chunks. + # + def getbinaryfile(remotefile, localfile = File.basename(remotefile), + blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data + f = nil + result = nil + if localfile + if @resume + rest_offset = File.size?(localfile) + f = File.open(localfile, "a") + else + rest_offset = nil + f = File.open(localfile, "w") + end + elsif !block_given? + result = String.new + end + begin + f&.binmode + retrbinary("RETR #{remotefile}", blocksize, rest_offset) do |data| + f&.write(data) + block&.(data) + result&.concat(data) + end + return result + ensure + f&.close + end + end + + # + # Retrieves +remotefile+ in ASCII (text) mode, storing the result in + # +localfile+. + # If +localfile+ is nil, returns retrieved data. + # If a block is supplied, it is passed the retrieved data one + # line at a time. + # + def gettextfile(remotefile, localfile = File.basename(remotefile), + &block) # :yield: line + f = nil + result = nil + if localfile + f = File.open(localfile, "w") + elsif !block_given? + result = String.new + end + begin + retrlines("RETR #{remotefile}") do |line, newline| + l = newline ? line + "\n" : line + f&.print(l) + block&.(line, newline) + result&.concat(l) + end + return result + ensure + f&.close + end + end + + # + # Retrieves +remotefile+ in whatever mode the session is set (text or + # binary). See #gettextfile and #getbinaryfile. + # + def get(remotefile, localfile = File.basename(remotefile), + blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data + if @binary + getbinaryfile(remotefile, localfile, blocksize, &block) + else + gettextfile(remotefile, localfile, &block) + end + end + + # + # Transfers +localfile+ to the server in binary mode, storing the result in + # +remotefile+. If a block is supplied, calls it, passing in the transmitted + # data in +blocksize+ chunks. + # + def putbinaryfile(localfile, remotefile = File.basename(localfile), + blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data + if @resume + begin + rest_offset = size(remotefile) + rescue Net::FTPPermError + rest_offset = nil + end + else + rest_offset = nil + end + f = File.open(localfile) + begin + f.binmode + if rest_offset + storbinary("APPE #{remotefile}", f, blocksize, rest_offset, &block) + else + storbinary("STOR #{remotefile}", f, blocksize, rest_offset, &block) + end + ensure + f.close + end + end + + # + # Transfers +localfile+ to the server in ASCII (text) mode, storing the result + # in +remotefile+. If callback or an associated block is supplied, calls it, + # passing in the transmitted data one line at a time. + # + def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line + f = File.open(localfile) + begin + storlines("STOR #{remotefile}", f, &block) + ensure + f.close + end + end + + # + # Transfers +localfile+ to the server in whatever mode the session is set + # (text or binary). See #puttextfile and #putbinaryfile. + # + def put(localfile, remotefile = File.basename(localfile), + blocksize = DEFAULT_BLOCKSIZE, &block) + if @binary + putbinaryfile(localfile, remotefile, blocksize, &block) + else + puttextfile(localfile, remotefile, &block) + end + end + + # + # Sends the ACCT command. + # + # This is a less common FTP command, to send account + # information if the destination host requires it. + # + def acct(account) + cmd = "ACCT " + account + voidcmd(cmd) + end + + # + # Returns an array of filenames in the remote directory. + # + def nlst(dir = nil) + cmd = "NLST" + if dir + cmd = "#{cmd} #{dir}" + end + files = [] + retrlines(cmd) do |line| + files.push(line) + end + return files + end + + # + # Returns an array of file information in the directory (the output is like + # `ls -l`). If a block is given, it iterates through the listing. + # + def list(*args, &block) # :yield: line + cmd = "LIST" + args.each do |arg| + cmd = "#{cmd} #{arg}" + end + lines = [] + retrlines(cmd) do |line| + lines << line + end + if block + lines.each(&block) + end + return lines + end + alias ls list + alias dir list + + # + # MLSxEntry represents an entry in responses of MLST/MLSD. + # Each entry has the facts (e.g., size, last modification time, etc.) + # and the pathname. + # + class MLSxEntry + attr_reader :facts, :pathname + + def initialize(facts, pathname) + @facts = facts + @pathname = pathname + end + + standard_facts = %w(size modify create type unique perm + lang media-type charset) + standard_facts.each do |factname| + define_method factname.gsub(/-/, "_") do + facts[factname] + end + end + + # + # Returns +true+ if the entry is a file (i.e., the value of the type + # fact is file). + # + def file? + return facts["type"] == "file" + end + + # + # Returns +true+ if the entry is a directory (i.e., the value of the + # type fact is dir, cdir, or pdir). + # + def directory? + if /\A[cp]?dir\z/.match(facts["type"]) + return true + else + return false + end + end + + # + # Returns +true+ if the APPE command may be applied to the file. + # + def appendable? + return facts["perm"].include?(?a) + end + + # + # Returns +true+ if files may be created in the directory by STOU, + # STOR, APPE, and RNTO. + # + def creatable? + return facts["perm"].include?(?c) + end + + # + # Returns +true+ if the file or directory may be deleted by DELE/RMD. + # + def deletable? + return facts["perm"].include?(?d) + end + + # + # Returns +true+ if the directory may be entered by CWD/CDUP. + # + def enterable? + return facts["perm"].include?(?e) + end + + # + # Returns +true+ if the file or directory may be renamed by RNFR. + # + def renamable? + return facts["perm"].include?(?f) + end + + # + # Returns +true+ if the listing commands, LIST, NLST, and MLSD are + # applied to the directory. + # + def listable? + return facts["perm"].include?(?l) + end + + # + # Returns +true+ if the MKD command may be used to create a new + # directory within the directory. + # + def directory_makable? + return facts["perm"].include?(?m) + end + + # + # Returns +true+ if the objects in the directory may be deleted, or + # the directory may be purged. + # + def purgeable? + return facts["perm"].include?(?p) + end + + # + # Returns +true+ if the RETR command may be applied to the file. + # + def readable? + return facts["perm"].include?(?r) + end + + # + # Returns +true+ if the STOR command may be applied to the file. + # + def writable? + return facts["perm"].include?(?w) + end + end + + CASE_DEPENDENT_PARSER = ->(value) { value } + CASE_INDEPENDENT_PARSER = ->(value) { value.downcase } + DECIMAL_PARSER = ->(value) { value.to_i } + OCTAL_PARSER = ->(value) { value.to_i(8) } + TIME_PARSER = ->(value, local = false) { + unless /\A(?\d{4})(?\d{2})(?\d{2}) + (?\d{2})(?\d{2})(?\d{2}) + (\.(?\d+))?/x =~ value + raise FTPProtoError, "invalid time-val: #{value}" + end + usec = fractions.to_i * 10 ** (6 - fractions.to_s.size) + Time.send(local ? :local : :utc, year, month, day, hour, min, sec, usec) + } + FACT_PARSERS = Hash.new(CASE_DEPENDENT_PARSER) + FACT_PARSERS["size"] = DECIMAL_PARSER + FACT_PARSERS["modify"] = TIME_PARSER + FACT_PARSERS["create"] = TIME_PARSER + FACT_PARSERS["type"] = CASE_INDEPENDENT_PARSER + FACT_PARSERS["unique"] = CASE_DEPENDENT_PARSER + FACT_PARSERS["perm"] = CASE_INDEPENDENT_PARSER + FACT_PARSERS["lang"] = CASE_INDEPENDENT_PARSER + FACT_PARSERS["media-type"] = CASE_INDEPENDENT_PARSER + FACT_PARSERS["charset"] = CASE_INDEPENDENT_PARSER + FACT_PARSERS["unix.mode"] = OCTAL_PARSER + FACT_PARSERS["unix.owner"] = DECIMAL_PARSER + FACT_PARSERS["unix.group"] = DECIMAL_PARSER + FACT_PARSERS["unix.ctime"] = TIME_PARSER + FACT_PARSERS["unix.atime"] = TIME_PARSER + + def parse_mlsx_entry(entry) + facts, pathname = entry.chomp.split(/ /, 2) + unless pathname + raise FTPProtoError, entry + end + return MLSxEntry.new( + facts.scan(/(.*?)=(.*?);/).each_with_object({}) { + |(factname, value), h| + name = factname.downcase + h[name] = FACT_PARSERS[name].(value) + }, + pathname) + end + private :parse_mlsx_entry + + # + # Returns data (e.g., size, last modification time, entry type, etc.) + # about the file or directory specified by +pathname+. + # If +pathname+ is omitted, the current directory is assumed. + # + def mlst(pathname = nil) + cmd = pathname ? "MLST #{pathname}" : "MLST" + resp = sendcmd(cmd) + if !resp.start_with?("250") + raise FTPReplyError, resp + end + line = resp.lines[1] + unless line + raise FTPProtoError, resp + end + entry = line.sub(/\A(250-| *)/, "") + return parse_mlsx_entry(entry) + end + + # + # Returns an array of the entries of the directory specified by + # +pathname+. + # Each entry has the facts (e.g., size, last modification time, etc.) + # and the pathname. + # If a block is given, it iterates through the listing. + # If +pathname+ is omitted, the current directory is assumed. + # + def mlsd(pathname = nil, &block) # :yield: entry + cmd = pathname ? "MLSD #{pathname}" : "MLSD" + entries = [] + retrlines(cmd) do |line| + entries << parse_mlsx_entry(line) + end + if block + entries.each(&block) + end + return entries + end + + # + # Renames a file on the server. + # + def rename(fromname, toname) + resp = sendcmd("RNFR #{fromname}") + if !resp.start_with?("3") + raise FTPReplyError, resp + end + voidcmd("RNTO #{toname}") + end + + # + # Deletes a file on the server. + # + def delete(filename) + resp = sendcmd("DELE #{filename}") + if resp.start_with?("250") + return + elsif resp.start_with?("5") + raise FTPPermError, resp + else + raise FTPReplyError, resp + end + end + + # + # Changes the (remote) directory. + # + def chdir(dirname) + if dirname == ".." + begin + voidcmd("CDUP") + return + rescue FTPPermError => e + if e.message[0, 3] != "500" + raise e + end + end + end + cmd = "CWD #{dirname}" + voidcmd(cmd) + end + + def get_body(resp) # :nodoc: + resp.slice(/\A[0-9a-zA-Z]{3} (.*)$/, 1) + end + private :get_body + + # + # Returns the size of the given (remote) filename. + # + def size(filename) + with_binary(true) do + resp = sendcmd("SIZE #{filename}") + if !resp.start_with?("213") + raise FTPReplyError, resp + end + return get_body(resp).to_i + end + end + + # + # Returns the last modification time of the (remote) file. If +local+ is + # +true+, it is returned as a local time, otherwise it's a UTC time. + # + def mtime(filename, local = false) + return TIME_PARSER.(mdtm(filename), local) + end + + # + # Creates a remote directory. + # + def mkdir(dirname) + resp = sendcmd("MKD #{dirname}") + return parse257(resp) + end + + # + # Removes a remote directory. + # + def rmdir(dirname) + voidcmd("RMD #{dirname}") + end + + # + # Returns the current remote directory. + # + def pwd + resp = sendcmd("PWD") + return parse257(resp) + end + alias getdir pwd + + # + # Returns system information. + # + def system + resp = sendcmd("SYST") + if !resp.start_with?("215") + raise FTPReplyError, resp + end + return get_body(resp) + end + + # + # Aborts the previous command (ABOR command). + # + def abort + line = "ABOR" + CRLF + print "put: ABOR\n" if @debug_mode + @sock.send(line, Socket::MSG_OOB) + resp = getmultiline + unless ["426", "226", "225"].include?(resp[0, 3]) + raise FTPProtoError, resp + end + return resp + end + + # + # Returns the status (STAT command). + # pathname - when stat is invoked with pathname as a parameter it acts like + # list but alot faster and over the same tcp session. + # + def status(pathname = nil) + line = pathname ? "STAT #{pathname}" : "STAT" + if /[\r\n]/ =~ line + raise ArgumentError, "A line must not contain CR or LF" + end + print "put: #{line}\n" if @debug_mode + @sock.send(line + CRLF, Socket::MSG_OOB) + return getresp + end + + # + # Returns the raw last modification time of the (remote) file in the format + # "YYYYMMDDhhmmss" (MDTM command). + # + # Use +mtime+ if you want a parsed Time instance. + # + def mdtm(filename) + resp = sendcmd("MDTM #{filename}") + if resp.start_with?("213") + return get_body(resp) + end + end + + # + # Issues the HELP command. + # + def help(arg = nil) + cmd = "HELP" + if arg + cmd = cmd + " " + arg + end + sendcmd(cmd) + end + + # + # Exits the FTP session. + # + def quit + voidcmd("QUIT") + end + + # + # Issues a NOOP command. + # + # Does nothing except return a response. + # + def noop + voidcmd("NOOP") + end + + # + # Issues a SITE command. + # + def site(arg) + cmd = "SITE " + arg + voidcmd(cmd) + end + + # + # Closes the connection. Further operations are impossible until you open + # a new connection with #connect. + # + def close + if @sock and not @sock.closed? + begin + @sock.shutdown(Socket::SHUT_WR) rescue nil + orig, self.read_timeout = self.read_timeout, 3 + @sock.read rescue nil + ensure + @sock.close + self.read_timeout = orig + end + end + end + + # + # Returns +true+ iff the connection is closed. + # + def closed? + @sock == nil or @sock.closed? + end + + # handler for response code 227 + # (Entering Passive Mode (h1,h2,h3,h4,p1,p2)) + # + # Returns host and port. + def parse227(resp) # :nodoc: + if !resp.start_with?("227") + raise FTPReplyError, resp + end + if m = /\((?\d+(,\d+){3}),(?\d+,\d+)\)/.match(resp) + return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"]) + else + raise FTPProtoError, resp + end + end + private :parse227 + + # handler for response code 228 + # (Entering Long Passive Mode) + # + # Returns host and port. + def parse228(resp) # :nodoc: + if !resp.start_with?("228") + raise FTPReplyError, resp + end + if m = /\(4,4,(?\d+(,\d+){3}),2,(?\d+,\d+)\)/.match(resp) + return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"]) + elsif m = /\(6,16,(?\d+(,(\d+)){15}),2,(?\d+,\d+)\)/.match(resp) + return parse_pasv_ipv6_host(m["host"]), parse_pasv_port(m["port"]) + else + raise FTPProtoError, resp + end + end + private :parse228 + + def parse_pasv_ipv4_host(s) + return s.tr(",", ".") + end + private :parse_pasv_ipv4_host + + def parse_pasv_ipv6_host(s) + return s.split(/,/).map { |i| + "%02x" % i.to_i + }.each_slice(2).map(&:join).join(":") + end + private :parse_pasv_ipv6_host + + def parse_pasv_port(s) + return s.split(/,/).map(&:to_i).inject { |x, y| + (x << 8) + y + } + end + private :parse_pasv_port + + # handler for response code 229 + # (Extended Passive Mode Entered) + # + # Returns host and port. + def parse229(resp) # :nodoc: + if !resp.start_with?("229") + raise FTPReplyError, resp + end + if m = /\((?[!-~])\k\k(?\d+)\k\)/.match(resp) + return @bare_sock.remote_address.ip_address, m["port"].to_i + else + raise FTPProtoError, resp + end + end + private :parse229 + + # handler for response code 257 + # ("PATHNAME" created) + # + # Returns host and port. + def parse257(resp) # :nodoc: + if !resp.start_with?("257") + raise FTPReplyError, resp + end + return resp.slice(/"(([^"]|"")*)"/, 1).to_s.gsub(/""/, '"') + end + private :parse257 + + # :stopdoc: + class NullSocket + def read_timeout=(sec) + end + + def closed? + true + end + + def close + end + + def method_missing(mid, *args) + raise FTPConnectionError, "not connected" + end + end + + class BufferedSocket < BufferedIO + [:local_address, :remote_address, :addr, :peeraddr, :send, :shutdown].each do |method| + define_method(method) { |*args| + @io.__send__(method, *args) + } + end + + def read(len = nil) + if len + s = super(len, String.new, true) + return s.empty? ? nil : s + else + result = String.new + while s = super(DEFAULT_BLOCKSIZE, String.new, true) + break if s.empty? + result << s + end + return result + end + end + + def gets + line = readuntil("\n", true) + return line.empty? ? nil : line + end + + def readline + line = gets + if line.nil? + raise EOFError, "end of file reached" + end + return line + end + end + + if defined?(OpenSSL::SSL::SSLSocket) + class BufferedSSLSocket < BufferedSocket + def initialize(*args) + super + @is_shutdown = false + end + + def shutdown(*args) + # SSL_shutdown() will be called from SSLSocket#close, and + # SSL_shutdown() will send the "close notify" alert to the peer, + # so shutdown(2) should not be called. + @is_shutdown = true + end + + def send(mesg, flags, dest = nil) + # Ignore flags and dest. + @io.write(mesg) + end + + private + + def rbuf_fill + if @is_shutdown + raise EOFError, "shutdown has been called" + else + super + end + end + end + end + # :startdoc: + end +end + + +# Documentation comments: +# - sourced from pickaxe and nutshell, with improvements (hopefully) diff --git a/gxg/standard/net/http.rb b/gxg/standard/net/http.rb new file mode 100644 index 0000000..53b6c21 --- /dev/null +++ b/gxg/standard/net/http.rb @@ -0,0 +1,1646 @@ +# frozen_string_literal: false +# +# = net/http.rb +# +# Copyright (c) 1999-2007 Yukihiro Matsumoto +# Copyright (c) 1999-2007 Minero Aoki +# Copyright (c) 2001 GOTOU Yuuzou +# +# Written and maintained by Minero Aoki . +# HTTPS support added by GOTOU Yuuzou . +# +# This file is derived from "http-access.rb". +# +# Documented by Minero Aoki; converted to RDoc by William Webber. +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms of ruby itself --- +# Ruby Distribution License or GNU General Public License. +# +# See Net::HTTP for an overview and examples. +# + +require_relative 'protocol' +require 'uri' + +module Net #:nodoc: + autoload :OpenSSL, 'openssl' + + # :stopdoc: + class HTTPBadResponse < StandardError; end + class HTTPHeaderSyntaxError < StandardError; end + # :startdoc: + + # == An HTTP client API for Ruby. + # + # Net::HTTP provides a rich library which can be used to build HTTP + # user-agents. For more details about HTTP see + # [RFC2616](http://www.ietf.org/rfc/rfc2616.txt) + # + # Net::HTTP is designed to work closely with URI. URI::HTTP#host, + # URI::HTTP#port and URI::HTTP#request_uri are designed to work with + # Net::HTTP. + # + # If you are only performing a few GET requests you should try OpenURI. + # + # == Simple Examples + # + # All examples assume you have loaded Net::HTTP with: + # + # require 'net/http' + # + # This will also require 'uri' so you don't need to require it separately. + # + # The Net::HTTP methods in the following section do not persist + # connections. They are not recommended if you are performing many HTTP + # requests. + # + # === GET + # + # Net::HTTP.get('example.com', '/index.html') # => String + # + # === GET by URI + # + # uri = URI('http://example.com/index.html?count=10') + # Net::HTTP.get(uri) # => String + # + # === GET with Dynamic Parameters + # + # uri = URI('http://example.com/index.html') + # params = { :limit => 10, :page => 3 } + # uri.query = URI.encode_www_form(params) + # + # res = Net::HTTP.get_response(uri) + # puts res.body if res.is_a?(Net::HTTPSuccess) + # + # === POST + # + # uri = URI('http://www.example.com/search.cgi') + # res = Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50') + # puts res.body + # + # === POST with Multiple Values + # + # uri = URI('http://www.example.com/search.cgi') + # res = Net::HTTP.post_form(uri, 'q' => ['ruby', 'perl'], 'max' => '50') + # puts res.body + # + # == How to use Net::HTTP + # + # The following example code can be used as the basis of a HTTP user-agent + # which can perform a variety of request types using persistent + # connections. + # + # uri = URI('http://example.com/some_path?query=string') + # + # Net::HTTP.start(uri.host, uri.port) do |http| + # request = Net::HTTP::Get.new uri + # + # response = http.request request # Net::HTTPResponse object + # end + # + # Net::HTTP::start immediately creates a connection to an HTTP server which + # is kept open for the duration of the block. The connection will remain + # open for multiple requests in the block if the server indicates it + # supports persistent connections. + # + # The request types Net::HTTP supports are listed below in the section "HTTP + # Request Classes". + # + # If you wish to re-use a connection across multiple HTTP requests without + # automatically closing it you can use ::new instead of ::start. #request + # will automatically open a connection to the server if one is not currently + # open. You can manually close the connection with #finish. + # + # For all the Net::HTTP request objects and shortcut request methods you may + # supply either a String for the request path or a URI from which Net::HTTP + # will extract the request path. + # + # === Response Data + # + # uri = URI('http://example.com/index.html') + # res = Net::HTTP.get_response(uri) + # + # # Headers + # res['Set-Cookie'] # => String + # res.get_fields('set-cookie') # => Array + # res.to_hash['set-cookie'] # => Array + # puts "Headers: #{res.to_hash.inspect}" + # + # # Status + # puts res.code # => '200' + # puts res.message # => 'OK' + # puts res.class.name # => 'HTTPOK' + # + # # Body + # puts res.body if res.response_body_permitted? + # + # === Following Redirection + # + # Each Net::HTTPResponse object belongs to a class for its response code. + # + # For example, all 2XX responses are instances of a Net::HTTPSuccess + # subclass, a 3XX response is an instance of a Net::HTTPRedirection + # subclass and a 200 response is an instance of the Net::HTTPOK class. For + # details of response classes, see the section "HTTP Response Classes" + # below. + # + # Using a case statement you can handle various types of responses properly: + # + # def fetch(uri_str, limit = 10) + # # You should choose a better exception. + # raise ArgumentError, 'too many HTTP redirects' if limit == 0 + # + # response = Net::HTTP.get_response(URI(uri_str)) + # + # case response + # when Net::HTTPSuccess then + # response + # when Net::HTTPRedirection then + # location = response['location'] + # warn "redirected to #{location}" + # fetch(location, limit - 1) + # else + # response.value + # end + # end + # + # print fetch('http://www.ruby-lang.org') + # + # === POST + # + # A POST can be made using the Net::HTTP::Post request class. This example + # creates a urlencoded POST body: + # + # uri = URI('http://www.example.com/todo.cgi') + # req = Net::HTTP::Post.new(uri) + # req.set_form_data('from' => '2005-01-01', 'to' => '2005-03-31') + # + # res = Net::HTTP.start(uri.hostname, uri.port) do |http| + # http.request(req) + # end + # + # case res + # when Net::HTTPSuccess, Net::HTTPRedirection + # # OK + # else + # res.value + # end + # + # At this time Net::HTTP does not support multipart/form-data. To send + # multipart/form-data use Net::HTTPRequest#body= and + # Net::HTTPRequest#content_type=: + # + # req = Net::HTTP::Post.new(uri) + # req.body = multipart_data + # req.content_type = 'multipart/form-data' + # + # Other requests that can contain a body such as PUT can be created in the + # same way using the corresponding request class (Net::HTTP::Put). + # + # === Setting Headers + # + # The following example performs a conditional GET using the + # If-Modified-Since header. If the files has not been modified since the + # time in the header a Not Modified response will be returned. See RFC 2616 + # section 9.3 for further details. + # + # uri = URI('http://example.com/cached_response') + # file = File.stat 'cached_response' + # + # req = Net::HTTP::Get.new(uri) + # req['If-Modified-Since'] = file.mtime.rfc2822 + # + # res = Net::HTTP.start(uri.hostname, uri.port) {|http| + # http.request(req) + # } + # + # open 'cached_response', 'w' do |io| + # io.write res.body + # end if res.is_a?(Net::HTTPSuccess) + # + # === Basic Authentication + # + # Basic authentication is performed according to + # [RFC2617](http://www.ietf.org/rfc/rfc2617.txt) + # + # uri = URI('http://example.com/index.html?key=value') + # + # req = Net::HTTP::Get.new(uri) + # req.basic_auth 'user', 'pass' + # + # res = Net::HTTP.start(uri.hostname, uri.port) {|http| + # http.request(req) + # } + # puts res.body + # + # === Streaming Response Bodies + # + # By default Net::HTTP reads an entire response into memory. If you are + # handling large files or wish to implement a progress bar you can instead + # stream the body directly to an IO. + # + # uri = URI('http://example.com/large_file') + # + # Net::HTTP.start(uri.host, uri.port) do |http| + # request = Net::HTTP::Get.new uri + # + # http.request request do |response| + # open 'large_file', 'w' do |io| + # response.read_body do |chunk| + # io.write chunk + # end + # end + # end + # end + # + # === HTTPS + # + # HTTPS is enabled for an HTTP connection by Net::HTTP#use_ssl=. + # + # uri = URI('https://secure.example.com/some_path?query=string') + # + # Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http| + # request = Net::HTTP::Get.new uri + # response = http.request request # Net::HTTPResponse object + # end + # + # Or if you simply want to make a GET request, you may pass in an URI + # object that has a HTTPS URL. Net::HTTP automatically turn on TLS + # verification if the URI object has a 'https' URI scheme. + # + # uri = URI('https://example.com/') + # Net::HTTP.get(uri) # => String + # + # In previous versions of Ruby you would need to require 'net/https' to use + # HTTPS. This is no longer true. + # + # === Proxies + # + # Net::HTTP will automatically create a proxy from the +http_proxy+ + # environment variable if it is present. To disable use of +http_proxy+, + # pass +nil+ for the proxy address. + # + # You may also create a custom proxy: + # + # proxy_addr = 'your.proxy.host' + # proxy_port = 8080 + # + # Net::HTTP.new('example.com', nil, proxy_addr, proxy_port).start { |http| + # # always proxy via your.proxy.addr:8080 + # } + # + # See Net::HTTP.new for further details and examples such as proxies that + # require a username and password. + # + # === Compression + # + # Net::HTTP automatically adds Accept-Encoding for compression of response + # bodies and automatically decompresses gzip and deflate responses unless a + # Range header was sent. + # + # Compression can be disabled through the Accept-Encoding: identity header. + # + # == HTTP Request Classes + # + # Here is the HTTP request class hierarchy. + # + # * Net::HTTPRequest + # * Net::HTTP::Get + # * Net::HTTP::Head + # * Net::HTTP::Post + # * Net::HTTP::Patch + # * Net::HTTP::Put + # * Net::HTTP::Proppatch + # * Net::HTTP::Lock + # * Net::HTTP::Unlock + # * Net::HTTP::Options + # * Net::HTTP::Propfind + # * Net::HTTP::Delete + # * Net::HTTP::Move + # * Net::HTTP::Copy + # * Net::HTTP::Mkcol + # * Net::HTTP::Trace + # + # == HTTP Response Classes + # + # Here is HTTP response class hierarchy. All classes are defined in Net + # module and are subclasses of Net::HTTPResponse. + # + # HTTPUnknownResponse:: For unhandled HTTP extensions + # HTTPInformation:: 1xx + # HTTPContinue:: 100 + # HTTPSwitchProtocol:: 101 + # HTTPSuccess:: 2xx + # HTTPOK:: 200 + # HTTPCreated:: 201 + # HTTPAccepted:: 202 + # HTTPNonAuthoritativeInformation:: 203 + # HTTPNoContent:: 204 + # HTTPResetContent:: 205 + # HTTPPartialContent:: 206 + # HTTPMultiStatus:: 207 + # HTTPIMUsed:: 226 + # HTTPRedirection:: 3xx + # HTTPMultipleChoices:: 300 + # HTTPMovedPermanently:: 301 + # HTTPFound:: 302 + # HTTPSeeOther:: 303 + # HTTPNotModified:: 304 + # HTTPUseProxy:: 305 + # HTTPTemporaryRedirect:: 307 + # HTTPClientError:: 4xx + # HTTPBadRequest:: 400 + # HTTPUnauthorized:: 401 + # HTTPPaymentRequired:: 402 + # HTTPForbidden:: 403 + # HTTPNotFound:: 404 + # HTTPMethodNotAllowed:: 405 + # HTTPNotAcceptable:: 406 + # HTTPProxyAuthenticationRequired:: 407 + # HTTPRequestTimeOut:: 408 + # HTTPConflict:: 409 + # HTTPGone:: 410 + # HTTPLengthRequired:: 411 + # HTTPPreconditionFailed:: 412 + # HTTPRequestEntityTooLarge:: 413 + # HTTPRequestURITooLong:: 414 + # HTTPUnsupportedMediaType:: 415 + # HTTPRequestedRangeNotSatisfiable:: 416 + # HTTPExpectationFailed:: 417 + # HTTPUnprocessableEntity:: 422 + # HTTPLocked:: 423 + # HTTPFailedDependency:: 424 + # HTTPUpgradeRequired:: 426 + # HTTPPreconditionRequired:: 428 + # HTTPTooManyRequests:: 429 + # HTTPRequestHeaderFieldsTooLarge:: 431 + # HTTPUnavailableForLegalReasons:: 451 + # HTTPServerError:: 5xx + # HTTPInternalServerError:: 500 + # HTTPNotImplemented:: 501 + # HTTPBadGateway:: 502 + # HTTPServiceUnavailable:: 503 + # HTTPGatewayTimeOut:: 504 + # HTTPVersionNotSupported:: 505 + # HTTPInsufficientStorage:: 507 + # HTTPNetworkAuthenticationRequired:: 511 + # + # There is also the Net::HTTPBadResponse exception which is raised when + # there is a protocol error. + # + class HTTP < Protocol + + # :stopdoc: + Revision = %q$Revision: 61155 $.split[1] + HTTPVersion = '1.1' + begin + require 'zlib' + require 'stringio' #for our purposes (unpacking gzip) lump these together + HAVE_ZLIB=true + rescue LoadError + HAVE_ZLIB=false + end + # :startdoc: + + # Turns on net/http 1.2 (Ruby 1.8) features. + # Defaults to ON in Ruby 1.8 or later. + def HTTP.version_1_2 + true + end + + # Returns true if net/http is in version 1.2 mode. + # Defaults to true. + def HTTP.version_1_2? + true + end + + def HTTP.version_1_1? #:nodoc: + false + end + + class << HTTP + alias is_version_1_1? version_1_1? #:nodoc: + alias is_version_1_2? version_1_2? #:nodoc: + end + + # + # short cut methods + # + + # + # Gets the body text from the target and outputs it to $stdout. The + # target can either be specified as + # (+uri+), or as (+host+, +path+, +port+ = 80); so: + # + # Net::HTTP.get_print URI('http://www.example.com/index.html') + # + # or: + # + # Net::HTTP.get_print 'www.example.com', '/index.html' + # + def HTTP.get_print(uri_or_host, path = nil, port = nil) + get_response(uri_or_host, path, port) {|res| + res.read_body do |chunk| + $stdout.print chunk + end + } + nil + end + + # Sends a GET request to the target and returns the HTTP response + # as a string. The target can either be specified as + # (+uri+), or as (+host+, +path+, +port+ = 80); so: + # + # print Net::HTTP.get(URI('http://www.example.com/index.html')) + # + # or: + # + # print Net::HTTP.get('www.example.com', '/index.html') + # + def HTTP.get(uri_or_host, path = nil, port = nil) + get_response(uri_or_host, path, port).body + end + + # Sends a GET request to the target and returns the HTTP response + # as a Net::HTTPResponse object. The target can either be specified as + # (+uri+), or as (+host+, +path+, +port+ = 80); so: + # + # res = Net::HTTP.get_response(URI('http://www.example.com/index.html')) + # print res.body + # + # or: + # + # res = Net::HTTP.get_response('www.example.com', '/index.html') + # print res.body + # + def HTTP.get_response(uri_or_host, path = nil, port = nil, &block) + if path + host = uri_or_host + new(host, port || HTTP.default_port).start {|http| + return http.request_get(path, &block) + } + else + uri = uri_or_host + start(uri.hostname, uri.port, + :use_ssl => uri.scheme == 'https') {|http| + return http.request_get(uri, &block) + } + end + end + + # Posts data to the specified URI object. + # + # Example: + # + # require 'net/http' + # require 'uri' + # + # Net::HTTP.post URI('http://www.example.com/api/search'), + # { "q" => "ruby", "max" => "50" }.to_json, + # "Content-Type" => "application/json" + # + def HTTP.post(url, data, header = nil) + start(url.hostname, url.port, + :use_ssl => url.scheme == 'https' ) {|http| + http.post(url, data, header) + } + end + + # Posts HTML form data to the specified URI object. + # The form data must be provided as a Hash mapping from String to String. + # Example: + # + # { "cmd" => "search", "q" => "ruby", "max" => "50" } + # + # This method also does Basic Authentication iff +url+.user exists. + # But userinfo for authentication is deprecated (RFC3986). + # So this feature will be removed. + # + # Example: + # + # require 'net/http' + # require 'uri' + # + # Net::HTTP.post_form URI('http://www.example.com/search.cgi'), + # { "q" => "ruby", "max" => "50" } + # + def HTTP.post_form(url, params) + req = Post.new(url) + req.form_data = params + req.basic_auth url.user, url.password if url.user + start(url.hostname, url.port, + :use_ssl => url.scheme == 'https' ) {|http| + http.request(req) + } + end + + # + # HTTP session management + # + + # The default port to use for HTTP requests; defaults to 80. + def HTTP.default_port + http_default_port() + end + + # The default port to use for HTTP requests; defaults to 80. + def HTTP.http_default_port + 80 + end + + # The default port to use for HTTPS requests; defaults to 443. + def HTTP.https_default_port + 443 + end + + def HTTP.socket_type #:nodoc: obsolete + BufferedIO + end + + # :call-seq: + # HTTP.start(address, port, p_addr, p_port, p_user, p_pass, &block) + # HTTP.start(address, port=nil, p_addr=:ENV, p_port=nil, p_user=nil, p_pass=nil, opt, &block) + # + # Creates a new Net::HTTP object, then additionally opens the TCP + # connection and HTTP session. + # + # Arguments are the following: + # _address_ :: hostname or IP address of the server + # _port_ :: port of the server + # _p_addr_ :: address of proxy + # _p_port_ :: port of proxy + # _p_user_ :: user of proxy + # _p_pass_ :: pass of proxy + # _opt_ :: optional hash + # + # _opt_ sets following values by its accessor. + # The keys are ca_file, ca_path, cert, cert_store, ciphers, + # close_on_empty_response, key, open_timeout, read_timeout, ssl_timeout, + # ssl_version, use_ssl, verify_callback, verify_depth and verify_mode. + # If you set :use_ssl as true, you can use https and default value of + # verify_mode is set as OpenSSL::SSL::VERIFY_PEER. + # + # If the optional block is given, the newly + # created Net::HTTP object is passed to it and closed when the + # block finishes. In this case, the return value of this method + # is the return value of the block. If no block is given, the + # return value of this method is the newly created Net::HTTP object + # itself, and the caller is responsible for closing it upon completion + # using the finish() method. + def HTTP.start(address, *arg, &block) # :yield: +http+ + arg.pop if opt = Hash.try_convert(arg[-1]) + port, p_addr, p_port, p_user, p_pass = *arg + p_addr = :ENV if arg.size < 2 + port = https_default_port if !port && opt && opt[:use_ssl] + http = new(address, port, p_addr, p_port, p_user, p_pass) + + if opt + if opt[:use_ssl] + opt = {verify_mode: OpenSSL::SSL::VERIFY_PEER}.update(opt) + end + http.methods.grep(/\A(\w+)=\z/) do |meth| + key = $1.to_sym + opt.key?(key) or next + http.__send__(meth, opt[key]) + end + end + + http.start(&block) + end + + class << HTTP + alias newobj new # :nodoc: + end + + # Creates a new Net::HTTP object without opening a TCP connection or + # HTTP session. + # + # The +address+ should be a DNS hostname or IP address, the +port+ is the + # port the server operates on. If no +port+ is given the default port for + # HTTP or HTTPS is used. + # + # If none of the +p_+ arguments are given, the proxy host and port are + # taken from the +http_proxy+ environment variable (or its uppercase + # equivalent) if present. If the proxy requires authentication you must + # supply it by hand. See URI::Generic#find_proxy for details of proxy + # detection from the environment. To disable proxy detection set +p_addr+ + # to nil. + # + # If you are connecting to a custom proxy, +p_addr+ specifies the DNS name + # or IP address of the proxy host, +p_port+ the port to use to access the + # proxy, +p_user+ and +p_pass+ the username and password if authorization + # is required to use the proxy, and p_no_proxy hosts which do not + # use the proxy. + # + def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil) + http = super address, port + + if proxy_class? then # from Net::HTTP::Proxy() + http.proxy_from_env = @proxy_from_env + http.proxy_address = @proxy_address + http.proxy_port = @proxy_port + http.proxy_user = @proxy_user + http.proxy_pass = @proxy_pass + elsif p_addr == :ENV then + http.proxy_from_env = true + else + if p_addr && p_no_proxy && !URI::Generic.use_proxy?(p_addr, p_addr, p_port, p_no_proxy) + p_addr = nil + p_port = nil + end + http.proxy_address = p_addr + http.proxy_port = p_port || default_port + http.proxy_user = p_user + http.proxy_pass = p_pass + end + + http + end + + # Creates a new Net::HTTP object for the specified server address, + # without opening the TCP connection or initializing the HTTP session. + # The +address+ should be a DNS hostname or IP address. + def initialize(address, port = nil) + @address = address + @port = (port || HTTP.default_port) + @local_host = nil + @local_port = nil + @curr_http_version = HTTPVersion + @keep_alive_timeout = 2 + @last_communicated = nil + @close_on_empty_response = false + @socket = nil + @started = false + @open_timeout = 60 + @read_timeout = 60 + @continue_timeout = nil + @max_retries = 1 + @debug_output = nil + + @proxy_from_env = false + @proxy_uri = nil + @proxy_address = nil + @proxy_port = nil + @proxy_user = nil + @proxy_pass = nil + + @use_ssl = false + @ssl_context = nil + @ssl_session = nil + @sspi_enabled = false + SSL_IVNAMES.each do |ivname| + instance_variable_set ivname, nil + end + end + + def inspect + "#<#{self.class} #{@address}:#{@port} open=#{started?}>" + end + + # *WARNING* This method opens a serious security hole. + # Never use this method in production code. + # + # Sets an output stream for debugging. + # + # http = Net::HTTP.new(hostname) + # http.set_debug_output $stderr + # http.start { .... } + # + def set_debug_output(output) + warn 'Net::HTTP#set_debug_output called after HTTP started', uplevel: 1 if started? + @debug_output = output + end + + # The DNS host name or IP address to connect to. + attr_reader :address + + # The port number to connect to. + attr_reader :port + + # The local host used to establish the connection. + attr_accessor :local_host + + # The local port used to establish the connection. + attr_accessor :local_port + + attr_writer :proxy_from_env + attr_writer :proxy_address + attr_writer :proxy_port + attr_writer :proxy_user + attr_writer :proxy_pass + + # Number of seconds to wait for the connection to open. Any number + # may be used, including Floats for fractional seconds. If the HTTP + # object cannot open a connection in this many seconds, it raises a + # Net::OpenTimeout exception. The default value is 60 seconds. + attr_accessor :open_timeout + + # Number of seconds to wait for one block to be read (via one read(2) + # call). Any number may be used, including Floats for fractional + # seconds. If the HTTP object cannot read data in this many seconds, + # it raises a Net::ReadTimeout exception. The default value is 60 seconds. + attr_reader :read_timeout + + # Maximum number of times to retry an idempotent request in case of + # Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET, + # Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError, + # Timeout::Error. + # Should be a non-negative integer number. Zero means no retries. + # The default value is 1. + def max_retries=(retries) + retries = retries.to_int + if retries < 0 + raise ArgumentError, 'max_retries should be non-negative integer number' + end + @max_retries = retries + end + + attr_reader :max_retries + + # Setter for the read_timeout attribute. + def read_timeout=(sec) + @socket.read_timeout = sec if @socket + @read_timeout = sec + end + + # Seconds to wait for 100 Continue response. If the HTTP object does not + # receive a response in this many seconds it sends the request body. The + # default value is +nil+. + attr_reader :continue_timeout + + # Setter for the continue_timeout attribute. + def continue_timeout=(sec) + @socket.continue_timeout = sec if @socket + @continue_timeout = sec + end + + # Seconds to reuse the connection of the previous request. + # If the idle time is less than this Keep-Alive Timeout, + # Net::HTTP reuses the TCP/IP socket used by the previous communication. + # The default value is 2 seconds. + attr_accessor :keep_alive_timeout + + # Returns true if the HTTP session has been started. + def started? + @started + end + + alias active? started? #:nodoc: obsolete + + attr_accessor :close_on_empty_response + + # Returns true if SSL/TLS is being used with HTTP. + def use_ssl? + @use_ssl + end + + # Turn on/off SSL. + # This flag must be set before starting session. + # If you change use_ssl value after session started, + # a Net::HTTP object raises IOError. + def use_ssl=(flag) + flag = flag ? true : false + if started? and @use_ssl != flag + raise IOError, "use_ssl value changed, but session already started" + end + @use_ssl = flag + end + + SSL_IVNAMES = [ + :@ca_file, + :@ca_path, + :@cert, + :@cert_store, + :@ciphers, + :@key, + :@ssl_timeout, + :@ssl_version, + :@min_version, + :@max_version, + :@verify_callback, + :@verify_depth, + :@verify_mode, + ] + SSL_ATTRIBUTES = [ + :ca_file, + :ca_path, + :cert, + :cert_store, + :ciphers, + :key, + :ssl_timeout, + :ssl_version, + :min_version, + :max_version, + :verify_callback, + :verify_depth, + :verify_mode, + ] + + # Sets path of a CA certification file in PEM format. + # + # The file can contain several CA certificates. + attr_accessor :ca_file + + # Sets path of a CA certification directory containing certifications in + # PEM format. + attr_accessor :ca_path + + # Sets an OpenSSL::X509::Certificate object as client certificate. + # (This method is appeared in Michal Rokos's OpenSSL extension). + attr_accessor :cert + + # Sets the X509::Store to verify peer certificate. + attr_accessor :cert_store + + # Sets the available ciphers. See OpenSSL::SSL::SSLContext#ciphers= + attr_accessor :ciphers + + # Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. + # (This method is appeared in Michal Rokos's OpenSSL extension.) + attr_accessor :key + + # Sets the SSL timeout seconds. + attr_accessor :ssl_timeout + + # Sets the SSL version. See OpenSSL::SSL::SSLContext#ssl_version= + attr_accessor :ssl_version + + # Sets the minimum SSL version. See OpenSSL::SSL::SSLContext#min_version= + attr_accessor :min_version + + # Sets the maximum SSL version. See OpenSSL::SSL::SSLContext#max_version= + attr_accessor :max_version + + # Sets the verify callback for the server certification verification. + attr_accessor :verify_callback + + # Sets the maximum depth for the certificate chain verification. + attr_accessor :verify_depth + + # Sets the flags for server the certification verification at beginning of + # SSL/TLS session. + # + # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable. + attr_accessor :verify_mode + + # Returns the X.509 certificates the server presented. + def peer_cert + if not use_ssl? or not @socket + return nil + end + @socket.io.peer_cert + end + + # Opens a TCP connection and HTTP session. + # + # When this method is called with a block, it passes the Net::HTTP + # object to the block, and closes the TCP connection and HTTP session + # after the block has been executed. + # + # When called with a block, it returns the return value of the + # block; otherwise, it returns self. + # + def start # :yield: http + raise IOError, 'HTTP session already opened' if @started + if block_given? + begin + do_start + return yield(self) + ensure + do_finish + end + end + do_start + self + end + + def do_start + connect + @started = true + end + private :do_start + + def connect + if proxy? then + conn_address = proxy_address + conn_port = proxy_port + else + conn_address = address + conn_port = port + end + + D "opening connection to #{conn_address}:#{conn_port}..." + s = Timeout.timeout(@open_timeout, Net::OpenTimeout) { + begin + TCPSocket.open(conn_address, conn_port, @local_host, @local_port) + rescue => e + raise e, "Failed to open TCP connection to " + + "#{conn_address}:#{conn_port} (#{e.message})" + end + } + s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) + D "opened" + if use_ssl? + if proxy? + plain_sock = BufferedIO.new(s, read_timeout: @read_timeout, + continue_timeout: @continue_timeout, + debug_output: @debug_output) + buf = "CONNECT #{@address}:#{@port} HTTP/#{HTTPVersion}\r\n" + buf << "Host: #{@address}:#{@port}\r\n" + if proxy_user + credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0') + buf << "Proxy-Authorization: Basic #{credential}\r\n" + end + buf << "\r\n" + plain_sock.write(buf) + HTTPResponse.read_new(plain_sock).value + # assuming nothing left in buffers after successful CONNECT response + end + + ssl_parameters = Hash.new + iv_list = instance_variables + SSL_IVNAMES.each_with_index do |ivname, i| + if iv_list.include?(ivname) and + value = instance_variable_get(ivname) + ssl_parameters[SSL_ATTRIBUTES[i]] = value if value + end + end + @ssl_context = OpenSSL::SSL::SSLContext.new + @ssl_context.set_params(ssl_parameters) + @ssl_context.session_cache_mode = + OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | + OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE + @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess } + D "starting SSL for #{conn_address}:#{conn_port}..." + s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) + s.sync_close = true + # Server Name Indication (SNI) RFC 3546 + s.hostname = @address if s.respond_to? :hostname= + if @ssl_session and + Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout + s.session = @ssl_session + end + ssl_socket_connect(s, @open_timeout) + if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE + s.post_connection_check(@address) + end + D "SSL established" + end + @socket = BufferedIO.new(s, read_timeout: @read_timeout, + continue_timeout: @continue_timeout, + debug_output: @debug_output) + on_connect + rescue => exception + if s + D "Conn close because of connect error #{exception}" + s.close + end + raise + end + private :connect + + def on_connect + end + private :on_connect + + # Finishes the HTTP session and closes the TCP connection. + # Raises IOError if the session has not been started. + def finish + raise IOError, 'HTTP session not yet started' unless started? + do_finish + end + + def do_finish + @started = false + @socket.close if @socket + @socket = nil + end + private :do_finish + + # + # proxy + # + + public + + # no proxy + @is_proxy_class = false + @proxy_from_env = false + @proxy_addr = nil + @proxy_port = nil + @proxy_user = nil + @proxy_pass = nil + + # Creates an HTTP proxy class which behaves like Net::HTTP, but + # performs all access via the specified proxy. + # + # This class is obsolete. You may pass these same parameters directly to + # Net::HTTP.new. See Net::HTTP.new for details of the arguments. + def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil) + return self unless p_addr + + Class.new(self) { + @is_proxy_class = true + + if p_addr == :ENV then + @proxy_from_env = true + @proxy_address = nil + @proxy_port = nil + else + @proxy_from_env = false + @proxy_address = p_addr + @proxy_port = p_port || default_port + end + + @proxy_user = p_user + @proxy_pass = p_pass + } + end + + class << HTTP + # returns true if self is a class which was created by HTTP::Proxy. + def proxy_class? + defined?(@is_proxy_class) ? @is_proxy_class : false + end + + # Address of proxy host. If Net::HTTP does not use a proxy, nil. + attr_reader :proxy_address + + # Port number of proxy host. If Net::HTTP does not use a proxy, nil. + attr_reader :proxy_port + + # User name for accessing proxy. If Net::HTTP does not use a proxy, nil. + attr_reader :proxy_user + + # User password for accessing proxy. If Net::HTTP does not use a proxy, + # nil. + attr_reader :proxy_pass + end + + # True if requests for this connection will be proxied + def proxy? + !!(@proxy_from_env ? proxy_uri : @proxy_address) + end + + # True if the proxy for this connection is determined from the environment + def proxy_from_env? + @proxy_from_env + end + + # The proxy URI determined from the environment for this connection. + def proxy_uri # :nodoc: + return if @proxy_uri == false + @proxy_uri ||= URI::HTTP.new( + "http".freeze, nil, address, port, nil, nil, nil, nil, nil + ).find_proxy || false + @proxy_uri || nil + end + + # The address of the proxy server, if one is configured. + def proxy_address + if @proxy_from_env then + proxy_uri&.hostname + else + @proxy_address + end + end + + # The port of the proxy server, if one is configured. + def proxy_port + if @proxy_from_env then + proxy_uri&.port + else + @proxy_port + end + end + + # [Bug #12921] + if /linux|freebsd|darwin/ =~ RUBY_PLATFORM + ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE = true + else + ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE = false + end + + # The username of the proxy server, if one is configured. + def proxy_user + if ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE && @proxy_from_env + proxy_uri&.user + else + @proxy_user + end + end + + # The password of the proxy server, if one is configured. + def proxy_pass + if ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE && @proxy_from_env + proxy_uri&.password + else + @proxy_pass + end + end + + alias proxyaddr proxy_address #:nodoc: obsolete + alias proxyport proxy_port #:nodoc: obsolete + + private + + # without proxy, obsolete + + def conn_address # :nodoc: + address() + end + + def conn_port # :nodoc: + port() + end + + def edit_path(path) + if proxy? + if path.start_with?("ftp://") || use_ssl? + path + else + "http://#{addr_port}#{path}" + end + else + path + end + end + + # + # HTTP operations + # + + public + + # Retrieves data from +path+ on the connected-to host which may be an + # absolute path String or a URI to extract the path from. + # + # +initheader+ must be a Hash like { 'Accept' => '*/*', ... }, + # and it defaults to an empty hash. + # If +initheader+ doesn't have the key 'accept-encoding', then + # a value of "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" is used, + # so that gzip compression is used in preference to deflate + # compression, which is used in preference to no compression. + # Ruby doesn't have libraries to support the compress (Lempel-Ziv) + # compression, so that is not supported. The intent of this is + # to reduce bandwidth by default. If this routine sets up + # compression, then it does the decompression also, removing + # the header as well to prevent confusion. Otherwise + # it leaves the body as it found it. + # + # This method returns a Net::HTTPResponse object. + # + # If called with a block, yields each fragment of the + # entity body in turn as a string as it is read from + # the socket. Note that in this case, the returned response + # object will *not* contain a (meaningful) body. + # + # +dest+ argument is obsolete. + # It still works but you must not use it. + # + # This method never raises an exception. + # + # response = http.get('/index.html') + # + # # using block + # File.open('result.txt', 'w') {|f| + # http.get('/~foo/') do |str| + # f.write str + # end + # } + # + def get(path, initheader = nil, dest = nil, &block) # :yield: +body_segment+ + res = nil + request(Get.new(path, initheader)) {|r| + r.read_body dest, &block + res = r + } + res + end + + # Gets only the header from +path+ on the connected-to host. + # +header+ is a Hash like { 'Accept' => '*/*', ... }. + # + # This method returns a Net::HTTPResponse object. + # + # This method never raises an exception. + # + # response = nil + # Net::HTTP.start('some.www.server', 80) {|http| + # response = http.head('/index.html') + # } + # p response['content-type'] + # + def head(path, initheader = nil) + request(Head.new(path, initheader)) + end + + # Posts +data+ (must be a String) to +path+. +header+ must be a Hash + # like { 'Accept' => '*/*', ... }. + # + # This method returns a Net::HTTPResponse object. + # + # If called with a block, yields each fragment of the + # entity body in turn as a string as it is read from + # the socket. Note that in this case, the returned response + # object will *not* contain a (meaningful) body. + # + # +dest+ argument is obsolete. + # It still works but you must not use it. + # + # This method never raises exception. + # + # response = http.post('/cgi-bin/search.rb', 'query=foo') + # + # # using block + # File.open('result.txt', 'w') {|f| + # http.post('/cgi-bin/search.rb', 'query=foo') do |str| + # f.write str + # end + # } + # + # You should set Content-Type: header field for POST. + # If no Content-Type: field given, this method uses + # "application/x-www-form-urlencoded" by default. + # + def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ + send_entity(path, data, initheader, dest, Post, &block) + end + + # Sends a PATCH request to the +path+ and gets a response, + # as an HTTPResponse object. + def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ + send_entity(path, data, initheader, dest, Patch, &block) + end + + def put(path, data, initheader = nil) #:nodoc: + request(Put.new(path, initheader), data) + end + + # Sends a PROPPATCH request to the +path+ and gets a response, + # as an HTTPResponse object. + def proppatch(path, body, initheader = nil) + request(Proppatch.new(path, initheader), body) + end + + # Sends a LOCK request to the +path+ and gets a response, + # as an HTTPResponse object. + def lock(path, body, initheader = nil) + request(Lock.new(path, initheader), body) + end + + # Sends a UNLOCK request to the +path+ and gets a response, + # as an HTTPResponse object. + def unlock(path, body, initheader = nil) + request(Unlock.new(path, initheader), body) + end + + # Sends a OPTIONS request to the +path+ and gets a response, + # as an HTTPResponse object. + def options(path, initheader = nil) + request(Options.new(path, initheader)) + end + + # Sends a PROPFIND request to the +path+ and gets a response, + # as an HTTPResponse object. + def propfind(path, body = nil, initheader = {'Depth' => '0'}) + request(Propfind.new(path, initheader), body) + end + + # Sends a DELETE request to the +path+ and gets a response, + # as an HTTPResponse object. + def delete(path, initheader = {'Depth' => 'Infinity'}) + request(Delete.new(path, initheader)) + end + + # Sends a MOVE request to the +path+ and gets a response, + # as an HTTPResponse object. + def move(path, initheader = nil) + request(Move.new(path, initheader)) + end + + # Sends a COPY request to the +path+ and gets a response, + # as an HTTPResponse object. + def copy(path, initheader = nil) + request(Copy.new(path, initheader)) + end + + # Sends a MKCOL request to the +path+ and gets a response, + # as an HTTPResponse object. + def mkcol(path, body = nil, initheader = nil) + request(Mkcol.new(path, initheader), body) + end + + # Sends a TRACE request to the +path+ and gets a response, + # as an HTTPResponse object. + def trace(path, initheader = nil) + request(Trace.new(path, initheader)) + end + + # Sends a GET request to the +path+. + # Returns the response as a Net::HTTPResponse object. + # + # When called with a block, passes an HTTPResponse object to the block. + # The body of the response will not have been read yet; + # the block can process it using HTTPResponse#read_body, + # if desired. + # + # Returns the response. + # + # This method never raises Net::* exceptions. + # + # response = http.request_get('/index.html') + # # The entity body is already read in this case. + # p response['content-type'] + # puts response.body + # + # # Using a block + # http.request_get('/index.html') {|response| + # p response['content-type'] + # response.read_body do |str| # read body now + # print str + # end + # } + # + def request_get(path, initheader = nil, &block) # :yield: +response+ + request(Get.new(path, initheader), &block) + end + + # Sends a HEAD request to the +path+ and returns the response + # as a Net::HTTPResponse object. + # + # Returns the response. + # + # This method never raises Net::* exceptions. + # + # response = http.request_head('/index.html') + # p response['content-type'] + # + def request_head(path, initheader = nil, &block) + request(Head.new(path, initheader), &block) + end + + # Sends a POST request to the +path+. + # + # Returns the response as a Net::HTTPResponse object. + # + # When called with a block, the block is passed an HTTPResponse + # object. The body of that response will not have been read yet; + # the block can process it using HTTPResponse#read_body, if desired. + # + # Returns the response. + # + # This method never raises Net::* exceptions. + # + # # example + # response = http.request_post('/cgi-bin/nice.rb', 'datadatadata...') + # p response.status + # puts response.body # body is already read in this case + # + # # using block + # http.request_post('/cgi-bin/nice.rb', 'datadatadata...') {|response| + # p response.status + # p response['content-type'] + # response.read_body do |str| # read body now + # print str + # end + # } + # + def request_post(path, data, initheader = nil, &block) # :yield: +response+ + request Post.new(path, initheader), data, &block + end + + def request_put(path, data, initheader = nil, &block) #:nodoc: + request Put.new(path, initheader), data, &block + end + + alias get2 request_get #:nodoc: obsolete + alias head2 request_head #:nodoc: obsolete + alias post2 request_post #:nodoc: obsolete + alias put2 request_put #:nodoc: obsolete + + + # Sends an HTTP request to the HTTP server. + # Also sends a DATA string if +data+ is given. + # + # Returns a Net::HTTPResponse object. + # + # This method never raises Net::* exceptions. + # + # response = http.send_request('GET', '/index.html') + # puts response.body + # + def send_request(name, path, data = nil, header = nil) + has_response_body = name != 'HEAD' + r = HTTPGenericRequest.new(name,(data ? true : false),has_response_body,path,header) + request r, data + end + + # Sends an HTTPRequest object +req+ to the HTTP server. + # + # If +req+ is a Net::HTTP::Post or Net::HTTP::Put request containing + # data, the data is also sent. Providing data for a Net::HTTP::Head or + # Net::HTTP::Get request results in an ArgumentError. + # + # Returns an HTTPResponse object. + # + # When called with a block, passes an HTTPResponse object to the block. + # The body of the response will not have been read yet; + # the block can process it using HTTPResponse#read_body, + # if desired. + # + # This method never raises Net::* exceptions. + # + def request(req, body = nil, &block) # :yield: +response+ + unless started? + start { + req['connection'] ||= 'close' + return request(req, body, &block) + } + end + if proxy_user() + req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl? + end + req.set_body_internal body + res = transport_request(req, &block) + if sspi_auth?(res) + sspi_auth(req) + res = transport_request(req, &block) + end + res + end + + private + + # Executes a request which uses a representation + # and returns its body. + def send_entity(path, data, initheader, dest, type, &block) + res = nil + request(type.new(path, initheader), data) {|r| + r.read_body dest, &block + res = r + } + res + end + + IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/ # :nodoc: + + def transport_request(req) + count = 0 + begin + begin_transport req + res = catch(:response) { + req.exec @socket, @curr_http_version, edit_path(req.path) + begin + res = HTTPResponse.read_new(@socket) + res.decode_content = req.decode_content + end while res.kind_of?(HTTPInformation) + + res.uri = req.uri + + res + } + res.reading_body(@socket, req.response_body_permitted?) { + yield res if block_given? + } + rescue Net::OpenTimeout + raise + rescue Net::ReadTimeout, IOError, EOFError, + Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, + # avoid a dependency on OpenSSL + defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError, + Timeout::Error => exception + if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method) + count += 1 + @socket.close if @socket + D "Conn close because of error #{exception}, and retry" + retry + end + D "Conn close because of error #{exception}" + @socket.close if @socket + raise + end + + end_transport req, res + res + rescue => exception + D "Conn close because of error #{exception}" + @socket.close if @socket + raise exception + end + + def begin_transport(req) + if @socket.closed? + connect + elsif @last_communicated + if @last_communicated + @keep_alive_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC) + D 'Conn close because of keep_alive_timeout' + @socket.close + connect + elsif @socket.io.to_io.wait_readable(0) && @socket.eof? + D "Conn close because of EOF" + @socket.close + connect + end + end + + if not req.response_body_permitted? and @close_on_empty_response + req['connection'] ||= 'close' + end + + req.update_uri address, port, use_ssl? + req['host'] ||= addr_port() + end + + def end_transport(req, res) + @curr_http_version = res.http_version + @last_communicated = nil + if @socket.closed? + D 'Conn socket closed' + elsif not res.body and @close_on_empty_response + D 'Conn close' + @socket.close + elsif keep_alive?(req, res) + D 'Conn keep-alive' + @last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC) + else + D 'Conn close' + @socket.close + end + end + + def keep_alive?(req, res) + return false if req.connection_close? + if @curr_http_version <= '1.0' + res.connection_keep_alive? + else # HTTP/1.1 or later + not res.connection_close? + end + end + + def sspi_auth?(res) + return false unless @sspi_enabled + if res.kind_of?(HTTPProxyAuthenticationRequired) and + proxy? and res["Proxy-Authenticate"].include?("Negotiate") + begin + require 'win32/sspi' + true + rescue LoadError + false + end + else + false + end + end + + def sspi_auth(req) + n = Win32::SSPI::NegotiateAuth.new + req["Proxy-Authorization"] = "Negotiate #{n.get_initial_token}" + # Some versions of ISA will close the connection if this isn't present. + req["Connection"] = "Keep-Alive" + req["Proxy-Connection"] = "Keep-Alive" + res = transport_request(req) + authphrase = res["Proxy-Authenticate"] or return res + req["Proxy-Authorization"] = "Negotiate #{n.complete_authentication(authphrase)}" + rescue => err + raise HTTPAuthenticationError.new('HTTP authentication failed', err) + end + + # + # utils + # + + private + + def addr_port + addr = address + addr = "[#{addr}]" if addr.include?(":") + default_port = use_ssl? ? HTTP.https_default_port : HTTP.http_default_port + default_port == port ? addr : "#{addr}:#{port}" + end + + def D(msg) + return unless @debug_output + @debug_output << msg + @debug_output << "\n" + end + end + +end + +require_relative 'http/exceptions' + +require_relative 'http/header' + +require_relative 'http/generic_request' +require_relative 'http/request' +require_relative 'http/requests' + +require_relative 'http/response' +require_relative 'http/responses' + +require_relative 'http/proxy_delta' + +require_relative 'http/backward' diff --git a/gxg/standard/net/http/backward.rb b/gxg/standard/net/http/backward.rb new file mode 100644 index 0000000..9e24eae --- /dev/null +++ b/gxg/standard/net/http/backward.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: false +# for backward compatibility + +# :enddoc: + +class Net::HTTP + ProxyMod = ProxyDelta +end + +module Net + HTTPSession = Net::HTTP +end + +module Net::NetPrivate + HTTPRequest = ::Net::HTTPRequest +end + +Net::HTTPInformationCode = Net::HTTPInformation +Net::HTTPSuccessCode = Net::HTTPSuccess +Net::HTTPRedirectionCode = Net::HTTPRedirection +Net::HTTPRetriableCode = Net::HTTPRedirection +Net::HTTPClientErrorCode = Net::HTTPClientError +Net::HTTPFatalErrorCode = Net::HTTPClientError +Net::HTTPServerErrorCode = Net::HTTPServerError +Net::HTTPResponceReceiver = Net::HTTPResponse + diff --git a/gxg/standard/net/http/exceptions.rb b/gxg/standard/net/http/exceptions.rb new file mode 100644 index 0000000..0d34526 --- /dev/null +++ b/gxg/standard/net/http/exceptions.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: false +# Net::HTTP exception class. +# You cannot use Net::HTTPExceptions directly; instead, you must use +# its subclasses. +module Net::HTTPExceptions + def initialize(msg, res) #:nodoc: + super msg + @response = res + end + attr_reader :response + alias data response #:nodoc: obsolete +end +class Net::HTTPError < Net::ProtocolError + include Net::HTTPExceptions +end +class Net::HTTPRetriableError < Net::ProtoRetriableError + include Net::HTTPExceptions +end +class Net::HTTPServerException < Net::ProtoServerError + # We cannot use the name "HTTPServerError", it is the name of the response. + include Net::HTTPExceptions +end +class Net::HTTPFatalError < Net::ProtoFatalError + include Net::HTTPExceptions +end + diff --git a/gxg/standard/net/http/generic_request.rb b/gxg/standard/net/http/generic_request.rb new file mode 100644 index 0000000..526cc33 --- /dev/null +++ b/gxg/standard/net/http/generic_request.rb @@ -0,0 +1,338 @@ +# frozen_string_literal: false +# HTTPGenericRequest is the parent of the HTTPRequest class. +# Do not use this directly; use a subclass of HTTPRequest. +# +# Mixes in the HTTPHeader module to provide easier access to HTTP headers. +# +class Net::HTTPGenericRequest + + include Net::HTTPHeader + + def initialize(m, reqbody, resbody, uri_or_path, initheader = nil) + @method = m + @request_has_body = reqbody + @response_has_body = resbody + + if URI === uri_or_path then + @uri = uri_or_path.dup + host = @uri.hostname.dup + host << ":".freeze << @uri.port.to_s if @uri.port != @uri.default_port + @path = uri_or_path.request_uri + raise ArgumentError, "no HTTP request path given" unless @path + else + @uri = nil + host = nil + raise ArgumentError, "no HTTP request path given" unless uri_or_path + raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty? + @path = uri_or_path.dup + end + + @decode_content = false + + if @response_has_body and Net::HTTP::HAVE_ZLIB then + if !initheader || + !initheader.keys.any? { |k| + %w[accept-encoding range].include? k.downcase + } then + @decode_content = true + initheader = initheader ? initheader.dup : {} + initheader["accept-encoding"] = + "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" + end + end + + initialize_http_header initheader + self['Accept'] ||= '*/*' + self['User-Agent'] ||= 'Ruby' + self['Host'] ||= host if host + @body = nil + @body_stream = nil + @body_data = nil + end + + attr_reader :method + attr_reader :path + attr_reader :uri + + # Automatically set to false if the user sets the Accept-Encoding header. + # This indicates they wish to handle Content-encoding in responses + # themselves. + attr_reader :decode_content + + def inspect + "\#<#{self.class} #{@method}>" + end + + ## + # Don't automatically decode response content-encoding if the user indicates + # they want to handle it. + + def []=(key, val) # :nodoc: + @decode_content = false if key.downcase == 'accept-encoding' + + super key, val + end + + def request_body_permitted? + @request_has_body + end + + def response_body_permitted? + @response_has_body + end + + def body_exist? + warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE + response_body_permitted? + end + + attr_reader :body + + def body=(str) + @body = str + @body_stream = nil + @body_data = nil + str + end + + attr_reader :body_stream + + def body_stream=(input) + @body = nil + @body_stream = input + @body_data = nil + input + end + + def set_body_internal(str) #:nodoc: internal use only + raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream) + self.body = str if str + if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted? + self.body = '' + end + end + + # + # write + # + + def exec(sock, ver, path) #:nodoc: internal use only + if @body + send_request_with_body sock, ver, path, @body + elsif @body_stream + send_request_with_body_stream sock, ver, path, @body_stream + elsif @body_data + send_request_with_body_data sock, ver, path, @body_data + else + write_header sock, ver, path + end + end + + def update_uri(addr, port, ssl) # :nodoc: internal use only + # reflect the connection and @path to @uri + return unless @uri + + if ssl + scheme = 'https'.freeze + klass = URI::HTTPS + else + scheme = 'http'.freeze + klass = URI::HTTP + end + + if host = self['host'] + host.sub!(/:.*/s, ''.freeze) + elsif host = @uri.host + else + host = addr + end + # convert the class of the URI + if @uri.is_a?(klass) + @uri.host = host + @uri.port = port + else + @uri = klass.new( + scheme, @uri.userinfo, + host, port, nil, + @uri.path, nil, @uri.query, nil) + end + end + + private + + class Chunker #:nodoc: + def initialize(sock) + @sock = sock + @prev = nil + end + + def write(buf) + # avoid memcpy() of buf, buf can huge and eat memory bandwidth + @sock.write("#{buf.bytesize.to_s(16)}\r\n") + rv = @sock.write(buf) + @sock.write("\r\n") + rv + end + + def finish + @sock.write("0\r\n\r\n") + end + end + + def send_request_with_body(sock, ver, path, body) + self.content_length = body.bytesize + delete 'Transfer-Encoding' + supply_default_content_type + write_header sock, ver, path + wait_for_continue sock, ver if sock.continue_timeout + sock.write body + end + + def send_request_with_body_stream(sock, ver, path, f) + unless content_length() or chunked? + raise ArgumentError, + "Content-Length not given and Transfer-Encoding is not `chunked'" + end + supply_default_content_type + write_header sock, ver, path + wait_for_continue sock, ver if sock.continue_timeout + if chunked? + chunker = Chunker.new(sock) + IO.copy_stream(f, chunker) + chunker.finish + else + # copy_stream can sendfile() to sock.io unless we use SSL. + # If sock.io is an SSLSocket, copy_stream will hit SSL_write() + IO.copy_stream(f, sock.io) + end + end + + def send_request_with_body_data(sock, ver, path, params) + if /\Amultipart\/form-data\z/i !~ self.content_type + self.content_type = 'application/x-www-form-urlencoded' + return send_request_with_body(sock, ver, path, URI.encode_www_form(params)) + end + + opt = @form_option.dup + require 'securerandom' unless defined?(SecureRandom) + opt[:boundary] ||= SecureRandom.urlsafe_base64(40) + self.set_content_type(self.content_type, boundary: opt[:boundary]) + if chunked? + write_header sock, ver, path + encode_multipart_form_data(sock, params, opt) + else + require 'tempfile' + file = Tempfile.new('multipart') + file.binmode + encode_multipart_form_data(file, params, opt) + file.rewind + self.content_length = file.size + write_header sock, ver, path + IO.copy_stream(file, sock) + file.close(true) + end + end + + def encode_multipart_form_data(out, params, opt) + charset = opt[:charset] + boundary = opt[:boundary] + require 'securerandom' unless defined?(SecureRandom) + boundary ||= SecureRandom.urlsafe_base64(40) + chunked_p = chunked? + + buf = '' + params.each do |key, value, h={}| + key = quote_string(key, charset) + filename = + h.key?(:filename) ? h[:filename] : + value.respond_to?(:to_path) ? File.basename(value.to_path) : + nil + + buf << "--#{boundary}\r\n" + if filename + filename = quote_string(filename, charset) + type = h[:content_type] || 'application/octet-stream' + buf << "Content-Disposition: form-data; " \ + "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \ + "Content-Type: #{type}\r\n\r\n" + if !out.respond_to?(:write) || !value.respond_to?(:read) + # if +out+ is not an IO or +value+ is not an IO + buf << (value.respond_to?(:read) ? value.read : value) + elsif value.respond_to?(:size) && chunked_p + # if +out+ is an IO and +value+ is a File, use IO.copy_stream + flush_buffer(out, buf, chunked_p) + out << "%x\r\n" % value.size if chunked_p + IO.copy_stream(value, out) + out << "\r\n" if chunked_p + else + # +out+ is an IO, and +value+ is not a File but an IO + flush_buffer(out, buf, chunked_p) + 1 while flush_buffer(out, value.read(4096), chunked_p) + end + else + # non-file field: + # HTML5 says, "The parts of the generated multipart/form-data + # resource that correspond to non-file fields must not have a + # Content-Type header specified." + buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n" + buf << (value.respond_to?(:read) ? value.read : value) + end + buf << "\r\n" + end + buf << "--#{boundary}--\r\n" + flush_buffer(out, buf, chunked_p) + out << "0\r\n\r\n" if chunked_p + end + + def quote_string(str, charset) + str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset + str.gsub(/[\\"]/, '\\\\\&') + end + + def flush_buffer(out, buf, chunked_p) + return unless buf + out << "%x\r\n"%buf.bytesize if chunked_p + out << buf + out << "\r\n" if chunked_p + buf.clear + end + + def supply_default_content_type + return if content_type() + warn 'net/http: Content-Type did not set; using application/x-www-form-urlencoded', uplevel: 1 if $VERBOSE + set_content_type 'application/x-www-form-urlencoded' + end + + ## + # Waits up to the continue timeout for a response from the server provided + # we're speaking HTTP 1.1 and are expecting a 100-continue response. + + def wait_for_continue(sock, ver) + if ver >= '1.1' and @header['expect'] and + @header['expect'].include?('100-continue') + if sock.io.to_io.wait_readable(sock.continue_timeout) + res = Net::HTTPResponse.read_new(sock) + unless res.kind_of?(Net::HTTPContinue) + res.decode_content = @decode_content + throw :response, res + end + end + end + end + + def write_header(sock, ver, path) + reqline = "#{@method} #{path} HTTP/#{ver}" + if /[\r\n]/ =~ reqline + raise ArgumentError, "A Request-Line must not contain CR or LF" + end + buf = "" + buf << reqline << "\r\n" + each_capitalized do |k,v| + buf << "#{k}: #{v}\r\n" + end + buf << "\r\n" + sock.write buf + end + +end + diff --git a/gxg/standard/net/http/header.rb b/gxg/standard/net/http/header.rb new file mode 100644 index 0000000..96d898c --- /dev/null +++ b/gxg/standard/net/http/header.rb @@ -0,0 +1,494 @@ +# frozen_string_literal: false +# The HTTPHeader module defines methods for reading and writing +# HTTP headers. +# +# It is used as a mixin by other classes, to provide hash-like +# access to HTTP header values. Unlike raw hash access, HTTPHeader +# provides access via case-insensitive keys. It also provides +# methods for accessing commonly-used HTTP header values in more +# convenient formats. +# +module Net::HTTPHeader + + def initialize_http_header(initheader) + @header = {} + return unless initheader + initheader.each do |key, value| + warn "net/http: duplicated HTTP header: #{key}", uplevel: 1 if key?(key) and $VERBOSE + if value.nil? + warn "net/http: nil HTTP header: #{key}", uplevel: 1 if $VERBOSE + else + value = value.strip # raise error for invalid byte sequences + if value.count("\r\n") > 0 + raise ArgumentError, 'header field value cannot include CR/LF' + end + @header[key.downcase] = [value] + end + end + end + + def size #:nodoc: obsolete + @header.size + end + + alias length size #:nodoc: obsolete + + # Returns the header field corresponding to the case-insensitive key. + # For example, a key of "Content-Type" might return "text/html" + def [](key) + a = @header[key.downcase] or return nil + a.join(', ') + end + + # Sets the header field corresponding to the case-insensitive key. + def []=(key, val) + unless val + @header.delete key.downcase + return val + end + set_field(key, val) + end + + # [Ruby 1.8.3] + # Adds a value to a named header field, instead of replacing its value. + # Second argument +val+ must be a String. + # See also #[]=, #[] and #get_fields. + # + # request.add_field 'X-My-Header', 'a' + # p request['X-My-Header'] #=> "a" + # p request.get_fields('X-My-Header') #=> ["a"] + # request.add_field 'X-My-Header', 'b' + # p request['X-My-Header'] #=> "a, b" + # p request.get_fields('X-My-Header') #=> ["a", "b"] + # request.add_field 'X-My-Header', 'c' + # p request['X-My-Header'] #=> "a, b, c" + # p request.get_fields('X-My-Header') #=> ["a", "b", "c"] + # + def add_field(key, val) + if @header.key?(key.downcase) + append_field_value(@header[key.downcase], val) + else + set_field(key, val) + end + end + + private def set_field(key, val) + case val + when Enumerable + ary = [] + append_field_value(ary, val) + @header[key.downcase] = ary + else + val = val.to_s # for compatibility use to_s instead of to_str + if val.b.count("\r\n") > 0 + raise ArgumentError, 'header field value cannot include CR/LF' + end + @header[key.downcase] = [val] + end + end + + private def append_field_value(ary, val) + case val + when Enumerable + val.each{|x| append_field_value(ary, x)} + else + val = val.to_s + if /[\r\n]/n.match?(val.b) + raise ArgumentError, 'header field value cannot include CR/LF' + end + ary.push val + end + end + + # [Ruby 1.8.3] + # Returns an array of header field strings corresponding to the + # case-insensitive +key+. This method allows you to get duplicated + # header fields without any processing. See also #[]. + # + # p response.get_fields('Set-Cookie') + # #=> ["session=al98axx; expires=Fri, 31-Dec-1999 23:58:23", + # "query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"] + # p response['Set-Cookie'] + # #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23" + # + def get_fields(key) + return nil unless @header[key.downcase] + @header[key.downcase].dup + end + + # Returns the header field corresponding to the case-insensitive key. + # Returns the default value +args+, or the result of the block, or + # raises an IndexError if there's no header field named +key+ + # See Hash#fetch + def fetch(key, *args, &block) #:yield: +key+ + a = @header.fetch(key.downcase, *args, &block) + a.kind_of?(Array) ? a.join(', ') : a + end + + # Iterates through the header names and values, passing in the name + # and value to the code block supplied. + # + # Returns an enumerator if no block is given. + # + # Example: + # + # response.header.each_header {|key,value| puts "#{key} = #{value}" } + # + def each_header #:yield: +key+, +value+ + block_given? or return enum_for(__method__) { @header.size } + @header.each do |k,va| + yield k, va.join(', ') + end + end + + alias each each_header + + # Iterates through the header names in the header, passing + # each header name to the code block. + # + # Returns an enumerator if no block is given. + def each_name(&block) #:yield: +key+ + block_given? or return enum_for(__method__) { @header.size } + @header.each_key(&block) + end + + alias each_key each_name + + # Iterates through the header names in the header, passing + # capitalized header names to the code block. + # + # Note that header names are capitalized systematically; + # capitalization may not match that used by the remote HTTP + # server in its response. + # + # Returns an enumerator if no block is given. + def each_capitalized_name #:yield: +key+ + block_given? or return enum_for(__method__) { @header.size } + @header.each_key do |k| + yield capitalize(k) + end + end + + # Iterates through header values, passing each value to the + # code block. + # + # Returns an enumerator if no block is given. + def each_value #:yield: +value+ + block_given? or return enum_for(__method__) { @header.size } + @header.each_value do |va| + yield va.join(', ') + end + end + + # Removes a header field, specified by case-insensitive key. + def delete(key) + @header.delete(key.downcase) + end + + # true if +key+ header exists. + def key?(key) + @header.key?(key.downcase) + end + + # Returns a Hash consisting of header names and array of values. + # e.g. + # {"cache-control" => ["private"], + # "content-type" => ["text/html"], + # "date" => ["Wed, 22 Jun 2005 22:11:50 GMT"]} + def to_hash + @header.dup + end + + # As for #each_header, except the keys are provided in capitalized form. + # + # Note that header names are capitalized systematically; + # capitalization may not match that used by the remote HTTP + # server in its response. + # + # Returns an enumerator if no block is given. + def each_capitalized + block_given? or return enum_for(__method__) { @header.size } + @header.each do |k,v| + yield capitalize(k), v.join(', ') + end + end + + alias canonical_each each_capitalized + + def capitalize(name) + name.to_s.split(/-/).map {|s| s.capitalize }.join('-') + end + private :capitalize + + # Returns an Array of Range objects which represent the Range: + # HTTP header field, or +nil+ if there is no such header. + def range + return nil unless @header['range'] + + value = self['Range'] + # byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec ) + # *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] ) + # corrected collected ABNF + # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1 + # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C + # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5 + unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value + raise Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'" + end + + byte_range_set = $1 + result = byte_range_set.split(/,/).map {|spec| + m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or + raise Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'" + d1 = m[1].to_i + d2 = m[2].to_i + if m[1] and m[2] + if d1 > d2 + raise Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'" + end + d1..d2 + elsif m[1] + d1..-1 + elsif m[2] + -d2..-1 + else + raise Net::HTTPHeaderSyntaxError, 'range is not specified' + end + } + # if result.empty? + # byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec + # but above regexp already denies it. + if result.size == 1 && result[0].begin == 0 && result[0].end == -1 + raise Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length' + end + result + end + + # Sets the HTTP Range: header. + # Accepts either a Range object as a single argument, + # or a beginning index and a length from that index. + # Example: + # + # req.range = (0..1023) + # req.set_range 0, 1023 + # + def set_range(r, e = nil) + unless r + @header.delete 'range' + return r + end + r = (r...r+e) if e + case r + when Numeric + n = r.to_i + rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}") + when Range + first = r.first + last = r.end + last -= 1 if r.exclude_end? + if last == -1 + rangestr = (first > 0 ? "#{first}-" : "-#{-first}") + else + raise Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0 + raise Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0 + raise Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last + rangestr = "#{first}-#{last}" + end + else + raise TypeError, 'Range/Integer is required' + end + @header['range'] = ["bytes=#{rangestr}"] + r + end + + alias range= set_range + + # Returns an Integer object which represents the HTTP Content-Length: + # header field, or +nil+ if that field was not provided. + def content_length + return nil unless key?('Content-Length') + len = self['Content-Length'].slice(/\d+/) or + raise Net::HTTPHeaderSyntaxError, 'wrong Content-Length format' + len.to_i + end + + def content_length=(len) + unless len + @header.delete 'content-length' + return nil + end + @header['content-length'] = [len.to_i.to_s] + end + + # Returns "true" if the "transfer-encoding" header is present and + # set to "chunked". This is an HTTP/1.1 feature, allowing the + # the content to be sent in "chunks" without at the outset + # stating the entire content length. + def chunked? + return false unless @header['transfer-encoding'] + field = self['Transfer-Encoding'] + (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false + end + + # Returns a Range object which represents the value of the Content-Range: + # header field. + # For a partial entity body, this indicates where this fragment + # fits inside the full entity body, as range of byte offsets. + def content_range + return nil unless @header['content-range'] + m = %ri.match(self['Content-Range']) or + raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format' + m[1].to_i .. m[2].to_i + end + + # The length of the range represented in Content-Range: header. + def range_length + r = content_range() or return nil + r.end - r.begin + 1 + end + + # Returns a content type string such as "text/html". + # This method returns nil if Content-Type: header field does not exist. + def content_type + return nil unless main_type() + if sub_type() + then "#{main_type()}/#{sub_type()}" + else main_type() + end + end + + # Returns a content type string such as "text". + # This method returns nil if Content-Type: header field does not exist. + def main_type + return nil unless @header['content-type'] + self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip + end + + # Returns a content type string such as "html". + # This method returns nil if Content-Type: header field does not exist + # or sub-type is not given (e.g. "Content-Type: text"). + def sub_type + return nil unless @header['content-type'] + _, sub = *self['Content-Type'].split(';').first.to_s.split('/') + return nil unless sub + sub.strip + end + + # Any parameters specified for the content type, returned as a Hash. + # For example, a header of Content-Type: text/html; charset=EUC-JP + # would result in type_params returning {'charset' => 'EUC-JP'} + def type_params + result = {} + list = self['Content-Type'].to_s.split(';') + list.shift + list.each do |param| + k, v = *param.split('=', 2) + result[k.strip] = v.strip + end + result + end + + # Sets the content type in an HTTP header. + # The +type+ should be a full HTTP content type, e.g. "text/html". + # The +params+ are an optional Hash of parameters to add after the + # content type, e.g. {'charset' => 'iso-8859-1'} + def set_content_type(type, params = {}) + @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')] + end + + alias content_type= set_content_type + + # Set header fields and a body from HTML form data. + # +params+ should be an Array of Arrays or + # a Hash containing HTML form data. + # Optional argument +sep+ means data record separator. + # + # Values are URL encoded as necessary and the content-type is set to + # application/x-www-form-urlencoded + # + # Example: + # http.form_data = {"q" => "ruby", "lang" => "en"} + # http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"} + # http.set_form_data({"q" => "ruby", "lang" => "en"}, ';') + # + def set_form_data(params, sep = '&') + query = URI.encode_www_form(params) + query.gsub!(/&/, sep) if sep != '&' + self.body = query + self.content_type = 'application/x-www-form-urlencoded' + end + + alias form_data= set_form_data + + # Set an HTML form data set. + # +params+ is the form data set; it is an Array of Arrays or a Hash + # +enctype is the type to encode the form data set. + # It is application/x-www-form-urlencoded or multipart/form-data. + # +formopt+ is an optional hash to specify the detail. + # + # boundary:: the boundary of the multipart message + # charset:: the charset of the message. All names and the values of + # non-file fields are encoded as the charset. + # + # Each item of params is an array and contains following items: + # +name+:: the name of the field + # +value+:: the value of the field, it should be a String or a File + # +opt+:: an optional hash to specify additional information + # + # Each item is a file field or a normal field. + # If +value+ is a File object or the +opt+ have a filename key, + # the item is treated as a file field. + # + # If Transfer-Encoding is set as chunked, this send the request in + # chunked encoding. Because chunked encoding is HTTP/1.1 feature, + # you must confirm the server to support HTTP/1.1 before sending it. + # + # Example: + # http.set_form([["q", "ruby"], ["lang", "en"]]) + # + # See also RFC 2388, RFC 2616, HTML 4.01, and HTML5 + # + def set_form(params, enctype='application/x-www-form-urlencoded', formopt={}) + @body_data = params + @body = nil + @body_stream = nil + @form_option = formopt + case enctype + when /\Aapplication\/x-www-form-urlencoded\z/i, + /\Amultipart\/form-data\z/i + self.content_type = enctype + else + raise ArgumentError, "invalid enctype: #{enctype}" + end + end + + # Set the Authorization: header for "Basic" authorization. + def basic_auth(account, password) + @header['authorization'] = [basic_encode(account, password)] + end + + # Set Proxy-Authorization: header for "Basic" authorization. + def proxy_basic_auth(account, password) + @header['proxy-authorization'] = [basic_encode(account, password)] + end + + def basic_encode(account, password) + 'Basic ' + ["#{account}:#{password}"].pack('m0') + end + private :basic_encode + + def connection_close? + token = /(?:\A|,)\s*close\s*(?:\z|,)/i + @header['connection']&.grep(token) {return true} + @header['proxy-connection']&.grep(token) {return true} + false + end + + def connection_keep_alive? + token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i + @header['connection']&.grep(token) {return true} + @header['proxy-connection']&.grep(token) {return true} + false + end + +end diff --git a/gxg/standard/net/http/proxy_delta.rb b/gxg/standard/net/http/proxy_delta.rb new file mode 100644 index 0000000..a2f770e --- /dev/null +++ b/gxg/standard/net/http/proxy_delta.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: false +module Net::HTTP::ProxyDelta #:nodoc: internal use only + private + + def conn_address + proxy_address() + end + + def conn_port + proxy_port() + end + + def edit_path(path) + use_ssl? ? path : "http://#{addr_port()}#{path}" + end +end + diff --git a/gxg/standard/net/http/request.rb b/gxg/standard/net/http/request.rb new file mode 100644 index 0000000..1e86f3e --- /dev/null +++ b/gxg/standard/net/http/request.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: false +# HTTP request class. +# This class wraps together the request header and the request path. +# You cannot use this class directly. Instead, you should use one of its +# subclasses: Net::HTTP::Get, Net::HTTP::Post, Net::HTTP::Head. +# +class Net::HTTPRequest < Net::HTTPGenericRequest + # Creates an HTTP request object for +path+. + # + # +initheader+ are the default headers to use. Net::HTTP adds + # Accept-Encoding to enable compression of the response body unless + # Accept-Encoding or Range are supplied in +initheader+. + + def initialize(path, initheader = nil) + super self.class::METHOD, + self.class::REQUEST_HAS_BODY, + self.class::RESPONSE_HAS_BODY, + path, initheader + end +end + diff --git a/gxg/standard/net/http/requests.rb b/gxg/standard/net/http/requests.rb new file mode 100644 index 0000000..d4c80a3 --- /dev/null +++ b/gxg/standard/net/http/requests.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: false +# +# HTTP/1.1 methods --- RFC2616 +# + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Get < Net::HTTPRequest + METHOD = 'GET' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Head < Net::HTTPRequest + METHOD = 'HEAD' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = false +end + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Post < Net::HTTPRequest + METHOD = 'POST' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Put < Net::HTTPRequest + METHOD = 'PUT' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Delete < Net::HTTPRequest + METHOD = 'DELETE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Options < Net::HTTPRequest + METHOD = 'OPTIONS' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Trace < Net::HTTPRequest + METHOD = 'TRACE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# +# PATCH method --- RFC5789 +# + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Patch < Net::HTTPRequest + METHOD = 'PATCH' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# +# WebDAV methods --- RFC2518 +# + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Propfind < Net::HTTPRequest + METHOD = 'PROPFIND' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Proppatch < Net::HTTPRequest + METHOD = 'PROPPATCH' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Mkcol < Net::HTTPRequest + METHOD = 'MKCOL' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Copy < Net::HTTPRequest + METHOD = 'COPY' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Move < Net::HTTPRequest + METHOD = 'MOVE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Lock < Net::HTTPRequest + METHOD = 'LOCK' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Unlock < Net::HTTPRequest + METHOD = 'UNLOCK' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + diff --git a/gxg/standard/net/http/response.rb b/gxg/standard/net/http/response.rb new file mode 100644 index 0000000..6a78272 --- /dev/null +++ b/gxg/standard/net/http/response.rb @@ -0,0 +1,419 @@ +# frozen_string_literal: false +# HTTP response class. +# +# This class wraps together the response header and the response body (the +# entity requested). +# +# It mixes in the HTTPHeader module, which provides access to response +# header values both via hash-like methods and via individual readers. +# +# Note that each possible HTTP response code defines its own +# HTTPResponse subclass. These are listed below. +# +# All classes are defined under the Net module. Indentation indicates +# inheritance. For a list of the classes see Net::HTTP. +# +# +class Net::HTTPResponse + class << self + # true if the response has a body. + def body_permitted? + self::HAS_BODY + end + + def exception_type # :nodoc: internal use only + self::EXCEPTION_TYPE + end + + def read_new(sock) #:nodoc: internal use only + httpv, code, msg = read_status_line(sock) + res = response_class(code).new(httpv, code, msg) + each_response_header(sock) do |k,v| + res.add_field k, v + end + res + end + + private + + def read_status_line(sock) + str = sock.readline + m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or + raise Net::HTTPBadResponse, "wrong status line: #{str.dump}" + m.captures + end + + def response_class(code) + CODE_TO_OBJ[code] or + CODE_CLASS_TO_OBJ[code[0,1]] or + Net::HTTPUnknownResponse + end + + def each_response_header(sock) + key = value = nil + while true + line = sock.readuntil("\n", true).sub(/\s+\z/, '') + break if line.empty? + if line[0] == ?\s or line[0] == ?\t and value + value << ' ' unless value.empty? + value << line.strip + else + yield key, value if key + key, value = line.strip.split(/\s*:\s*/, 2) + raise Net::HTTPBadResponse, 'wrong header line format' if value.nil? + end + end + yield key, value if key + end + end + + # next is to fix bug in RDoc, where the private inside class << self + # spills out. + public + + include Net::HTTPHeader + + def initialize(httpv, code, msg) #:nodoc: internal use only + @http_version = httpv + @code = code + @message = msg + initialize_http_header nil + @body = nil + @read = false + @uri = nil + @decode_content = false + end + + # The HTTP version supported by the server. + attr_reader :http_version + + # The HTTP result code string. For example, '302'. You can also + # determine the response type by examining which response subclass + # the response object is an instance of. + attr_reader :code + + # The HTTP result message sent by the server. For example, 'Not Found'. + attr_reader :message + alias msg message # :nodoc: obsolete + + # The URI used to fetch this response. The response URI is only available + # if a URI was used to create the request. + attr_reader :uri + + # Set to true automatically when the request did not contain an + # Accept-Encoding header from the user. + attr_accessor :decode_content + + def inspect + "#<#{self.class} #{@code} #{@message} readbody=#{@read}>" + end + + # + # response <-> exception relationship + # + + def code_type #:nodoc: + self.class + end + + def error! #:nodoc: + message = @code + message += ' ' + @message.dump if @message + raise error_type().new(message, self) + end + + def error_type #:nodoc: + self.class::EXCEPTION_TYPE + end + + # Raises an HTTP error if the response is not 2xx (success). + def value + error! unless self.kind_of?(Net::HTTPSuccess) + end + + def uri= uri # :nodoc: + @uri = uri.dup if uri + end + + # + # header (for backward compatibility only; DO NOT USE) + # + + def response #:nodoc: + warn "Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE + self + end + + def header #:nodoc: + warn "Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE + self + end + + def read_header #:nodoc: + warn "Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE + self + end + + # + # body + # + + def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only + @socket = sock + @body_exist = reqmethodallowbody && self.class.body_permitted? + begin + yield + self.body # ensure to read body + ensure + @socket = nil + end + end + + # Gets the entity body returned by the remote HTTP server. + # + # If a block is given, the body is passed to the block, and + # the body is provided in fragments, as it is read in from the socket. + # + # Calling this method a second or subsequent time for the same + # HTTPResponse object will return the value already read. + # + # http.request_get('/index.html') {|res| + # puts res.read_body + # } + # + # http.request_get('/index.html') {|res| + # p res.read_body.object_id # 538149362 + # p res.read_body.object_id # 538149362 + # } + # + # # using iterator + # http.request_get('/index.html') {|res| + # res.read_body do |segment| + # print segment + # end + # } + # + def read_body(dest = nil, &block) + if @read + raise IOError, "#{self.class}\#read_body called twice" if dest or block + return @body + end + to = procdest(dest, block) + stream_check + if @body_exist + read_body_0 to + @body = to + else + @body = nil + end + @read = true + + @body + end + + # Returns the full entity body. + # + # Calling this method a second or subsequent time will return the + # string already read. + # + # http.request_get('/index.html') {|res| + # puts res.body + # } + # + # http.request_get('/index.html') {|res| + # p res.body.object_id # 538149362 + # p res.body.object_id # 538149362 + # } + # + def body + read_body() + end + + # Because it may be necessary to modify the body, Eg, decompression + # this method facilitates that. + def body=(value) + @body = value + end + + alias entity body #:nodoc: obsolete + + private + + ## + # Checks for a supported Content-Encoding header and yields an Inflate + # wrapper for this response's socket when zlib is present. If the + # Content-Encoding is not supported or zlib is missing, the plain socket is + # yielded. + # + # If a Content-Range header is present, a plain socket is yielded as the + # bytes in the range may not be a complete deflate block. + + def inflater # :nodoc: + return yield @socket unless Net::HTTP::HAVE_ZLIB + return yield @socket unless @decode_content + return yield @socket if self['content-range'] + + v = self['content-encoding'] + case v&.downcase + when 'deflate', 'gzip', 'x-gzip' then + self.delete 'content-encoding' + + inflate_body_io = Inflater.new(@socket) + + begin + yield inflate_body_io + ensure + orig_err = $! + begin + inflate_body_io.finish + rescue => err + raise orig_err || err + end + end + when 'none', 'identity' then + self.delete 'content-encoding' + + yield @socket + else + yield @socket + end + end + + def read_body_0(dest) + inflater do |inflate_body_io| + if chunked? + read_chunked dest, inflate_body_io + return + end + + @socket = inflate_body_io + + clen = content_length() + if clen + @socket.read clen, dest, true # ignore EOF + return + end + clen = range_length() + if clen + @socket.read clen, dest + return + end + @socket.read_all dest + end + end + + ## + # read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF, + # etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip + # encoded. + # + # See RFC 2616 section 3.6.1 for definitions + + def read_chunked(dest, chunk_data_io) # :nodoc: + total = 0 + while true + line = @socket.readline + hexlen = line.slice(/[0-9a-fA-F]+/) or + raise Net::HTTPBadResponse, "wrong chunk size line: #{line}" + len = hexlen.hex + break if len == 0 + begin + chunk_data_io.read len, dest + ensure + total += len + @socket.read 2 # \r\n + end + end + until @socket.readline.empty? + # none + end + end + + def stream_check + raise IOError, 'attempt to read body out of block' if @socket.closed? + end + + def procdest(dest, block) + raise ArgumentError, 'both arg and block given for HTTP method' if + dest and block + if block + Net::ReadAdapter.new(block) + else + dest || '' + end + end + + ## + # Inflater is a wrapper around Net::BufferedIO that transparently inflates + # zlib and gzip streams. + + class Inflater # :nodoc: + + ## + # Creates a new Inflater wrapping +socket+ + + def initialize socket + @socket = socket + # zlib with automatic gzip detection + @inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS) + end + + ## + # Finishes the inflate stream. + + def finish + return if @inflate.total_in == 0 + @inflate.finish + end + + ## + # Returns a Net::ReadAdapter that inflates each read chunk into +dest+. + # + # This allows a large response body to be inflated without storing the + # entire body in memory. + + def inflate_adapter(dest) + if dest.respond_to?(:set_encoding) + dest.set_encoding(Encoding::ASCII_8BIT) + elsif dest.respond_to?(:force_encoding) + dest.force_encoding(Encoding::ASCII_8BIT) + end + block = proc do |compressed_chunk| + @inflate.inflate(compressed_chunk) do |chunk| + dest << chunk + end + end + + Net::ReadAdapter.new(block) + end + + ## + # Reads +clen+ bytes from the socket, inflates them, then writes them to + # +dest+. +ignore_eof+ is passed down to Net::BufferedIO#read + # + # Unlike Net::BufferedIO#read, this method returns more than +clen+ bytes. + # At this time there is no way for a user of Net::HTTPResponse to read a + # specific number of bytes from the HTTP response body, so this internal + # API does not return the same number of bytes as were requested. + # + # See https://bugs.ruby-lang.org/issues/6492 for further discussion. + + def read clen, dest, ignore_eof = false + temp_dest = inflate_adapter(dest) + + @socket.read clen, temp_dest, ignore_eof + end + + ## + # Reads the rest of the socket, inflates it, then writes it to +dest+. + + def read_all dest + temp_dest = inflate_adapter(dest) + + @socket.read_all temp_dest + end + + end + +end + diff --git a/gxg/standard/net/http/responses.rb b/gxg/standard/net/http/responses.rb new file mode 100644 index 0000000..c4259e1 --- /dev/null +++ b/gxg/standard/net/http/responses.rb @@ -0,0 +1,299 @@ +# frozen_string_literal: false +# :stopdoc: +# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml +class Net::HTTPUnknownResponse < Net::HTTPResponse + HAS_BODY = true + EXCEPTION_TYPE = Net::HTTPError +end +class Net::HTTPInformation < Net::HTTPResponse # 1xx + HAS_BODY = false + EXCEPTION_TYPE = Net::HTTPError +end +class Net::HTTPSuccess < Net::HTTPResponse # 2xx + HAS_BODY = true + EXCEPTION_TYPE = Net::HTTPError +end +class Net::HTTPRedirection < Net::HTTPResponse # 3xx + HAS_BODY = true + EXCEPTION_TYPE = Net::HTTPRetriableError +end +class Net::HTTPClientError < Net::HTTPResponse # 4xx + HAS_BODY = true + EXCEPTION_TYPE = Net::HTTPServerException # for backward compatibility +end +class Net::HTTPServerError < Net::HTTPResponse # 5xx + HAS_BODY = true + EXCEPTION_TYPE = Net::HTTPFatalError # for backward compatibility +end + +class Net::HTTPContinue < Net::HTTPInformation # 100 + HAS_BODY = false +end +class Net::HTTPSwitchProtocol < Net::HTTPInformation # 101 + HAS_BODY = false +end +class Net::HTTPProcessing < Net::HTTPInformation # 102 + HAS_BODY = false +end + +class Net::HTTPOK < Net::HTTPSuccess # 200 + HAS_BODY = true +end +class Net::HTTPCreated < Net::HTTPSuccess # 201 + HAS_BODY = true +end +class Net::HTTPAccepted < Net::HTTPSuccess # 202 + HAS_BODY = true +end +class Net::HTTPNonAuthoritativeInformation < Net::HTTPSuccess # 203 + HAS_BODY = true +end +class Net::HTTPNoContent < Net::HTTPSuccess # 204 + HAS_BODY = false +end +class Net::HTTPResetContent < Net::HTTPSuccess # 205 + HAS_BODY = false +end +class Net::HTTPPartialContent < Net::HTTPSuccess # 206 + HAS_BODY = true +end +class Net::HTTPMultiStatus < Net::HTTPSuccess # 207 - RFC 4918 + HAS_BODY = true +end +class Net::HTTPAlreadyReported < Net::HTTPSuccess # 208 - RFC 5842 + HAS_BODY = true +end +class Net::HTTPIMUsed < Net::HTTPSuccess # 226 - RFC 3229 + HAS_BODY = true +end + +class Net::HTTPMultipleChoices < Net::HTTPRedirection # 300 + HAS_BODY = true +end +Net::HTTPMultipleChoice = Net::HTTPMultipleChoices +class Net::HTTPMovedPermanently < Net::HTTPRedirection # 301 + HAS_BODY = true +end +class Net::HTTPFound < Net::HTTPRedirection # 302 + HAS_BODY = true +end +Net::HTTPMovedTemporarily = Net::HTTPFound +class Net::HTTPSeeOther < Net::HTTPRedirection # 303 + HAS_BODY = true +end +class Net::HTTPNotModified < Net::HTTPRedirection # 304 + HAS_BODY = false +end +class Net::HTTPUseProxy < Net::HTTPRedirection # 305 + HAS_BODY = false +end +# 306 Switch Proxy - no longer unused +class Net::HTTPTemporaryRedirect < Net::HTTPRedirection # 307 + HAS_BODY = true +end +class Net::HTTPPermanentRedirect < Net::HTTPRedirection # 308 + HAS_BODY = true +end + +class Net::HTTPBadRequest < Net::HTTPClientError # 400 + HAS_BODY = true +end +class Net::HTTPUnauthorized < Net::HTTPClientError # 401 + HAS_BODY = true +end +class Net::HTTPPaymentRequired < Net::HTTPClientError # 402 + HAS_BODY = true +end +class Net::HTTPForbidden < Net::HTTPClientError # 403 + HAS_BODY = true +end +class Net::HTTPNotFound < Net::HTTPClientError # 404 + HAS_BODY = true +end +class Net::HTTPMethodNotAllowed < Net::HTTPClientError # 405 + HAS_BODY = true +end +class Net::HTTPNotAcceptable < Net::HTTPClientError # 406 + HAS_BODY = true +end +class Net::HTTPProxyAuthenticationRequired < Net::HTTPClientError # 407 + HAS_BODY = true +end +class Net::HTTPRequestTimeOut < Net::HTTPClientError # 408 + HAS_BODY = true +end +class Net::HTTPConflict < Net::HTTPClientError # 409 + HAS_BODY = true +end +class Net::HTTPGone < Net::HTTPClientError # 410 + HAS_BODY = true +end +class Net::HTTPLengthRequired < Net::HTTPClientError # 411 + HAS_BODY = true +end +class Net::HTTPPreconditionFailed < Net::HTTPClientError # 412 + HAS_BODY = true +end +class Net::HTTPRequestEntityTooLarge < Net::HTTPClientError # 413 + HAS_BODY = true +end +class Net::HTTPRequestURITooLong < Net::HTTPClientError # 414 + HAS_BODY = true +end +Net::HTTPRequestURITooLarge = Net::HTTPRequestURITooLong +class Net::HTTPUnsupportedMediaType < Net::HTTPClientError # 415 + HAS_BODY = true +end +class Net::HTTPRequestedRangeNotSatisfiable < Net::HTTPClientError # 416 + HAS_BODY = true +end +class Net::HTTPExpectationFailed < Net::HTTPClientError # 417 + HAS_BODY = true +end +# 418 I'm a teapot - RFC 2324; a joke RFC +# 420 Enhance Your Calm - Twitter +class Net::HTTPMisdirectedRequest < Net::HTTPClientError # 421 - RFC 7540 + HAS_BODY = true +end +class Net::HTTPUnprocessableEntity < Net::HTTPClientError # 422 - RFC 4918 + HAS_BODY = true +end +class Net::HTTPLocked < Net::HTTPClientError # 423 - RFC 4918 + HAS_BODY = true +end +class Net::HTTPFailedDependency < Net::HTTPClientError # 424 - RFC 4918 + HAS_BODY = true +end +# 425 Unordered Collection - existed only in draft +class Net::HTTPUpgradeRequired < Net::HTTPClientError # 426 - RFC 2817 + HAS_BODY = true +end +class Net::HTTPPreconditionRequired < Net::HTTPClientError # 428 - RFC 6585 + HAS_BODY = true +end +class Net::HTTPTooManyRequests < Net::HTTPClientError # 429 - RFC 6585 + HAS_BODY = true +end +class Net::HTTPRequestHeaderFieldsTooLarge < Net::HTTPClientError # 431 - RFC 6585 + HAS_BODY = true +end +class Net::HTTPUnavailableForLegalReasons < Net::HTTPClientError # 451 - RFC 7725 + HAS_BODY = true +end +# 444 No Response - Nginx +# 449 Retry With - Microsoft +# 450 Blocked by Windows Parental Controls - Microsoft +# 499 Client Closed Request - Nginx + +class Net::HTTPInternalServerError < Net::HTTPServerError # 500 + HAS_BODY = true +end +class Net::HTTPNotImplemented < Net::HTTPServerError # 501 + HAS_BODY = true +end +class Net::HTTPBadGateway < Net::HTTPServerError # 502 + HAS_BODY = true +end +class Net::HTTPServiceUnavailable < Net::HTTPServerError # 503 + HAS_BODY = true +end +class Net::HTTPGatewayTimeOut < Net::HTTPServerError # 504 + HAS_BODY = true +end +class Net::HTTPVersionNotSupported < Net::HTTPServerError # 505 + HAS_BODY = true +end +class Net::HTTPVariantAlsoNegotiates < Net::HTTPServerError # 506 + HAS_BODY = true +end +class Net::HTTPInsufficientStorage < Net::HTTPServerError # 507 - RFC 4918 + HAS_BODY = true +end +class Net::HTTPLoopDetected < Net::HTTPServerError # 508 - RFC 5842 + HAS_BODY = true +end +# 509 Bandwidth Limit Exceeded - Apache bw/limited extension +class Net::HTTPNotExtended < Net::HTTPServerError # 510 - RFC 2774 + HAS_BODY = true +end +class Net::HTTPNetworkAuthenticationRequired < Net::HTTPServerError # 511 - RFC 6585 + HAS_BODY = true +end + +class Net::HTTPResponse + CODE_CLASS_TO_OBJ = { + '1' => Net::HTTPInformation, + '2' => Net::HTTPSuccess, + '3' => Net::HTTPRedirection, + '4' => Net::HTTPClientError, + '5' => Net::HTTPServerError + } + CODE_TO_OBJ = { + '100' => Net::HTTPContinue, + '101' => Net::HTTPSwitchProtocol, + '102' => Net::HTTPProcessing, + + '200' => Net::HTTPOK, + '201' => Net::HTTPCreated, + '202' => Net::HTTPAccepted, + '203' => Net::HTTPNonAuthoritativeInformation, + '204' => Net::HTTPNoContent, + '205' => Net::HTTPResetContent, + '206' => Net::HTTPPartialContent, + '207' => Net::HTTPMultiStatus, + '208' => Net::HTTPAlreadyReported, + '226' => Net::HTTPIMUsed, + + '300' => Net::HTTPMultipleChoices, + '301' => Net::HTTPMovedPermanently, + '302' => Net::HTTPFound, + '303' => Net::HTTPSeeOther, + '304' => Net::HTTPNotModified, + '305' => Net::HTTPUseProxy, + '307' => Net::HTTPTemporaryRedirect, + '308' => Net::HTTPPermanentRedirect, + + '400' => Net::HTTPBadRequest, + '401' => Net::HTTPUnauthorized, + '402' => Net::HTTPPaymentRequired, + '403' => Net::HTTPForbidden, + '404' => Net::HTTPNotFound, + '405' => Net::HTTPMethodNotAllowed, + '406' => Net::HTTPNotAcceptable, + '407' => Net::HTTPProxyAuthenticationRequired, + '408' => Net::HTTPRequestTimeOut, + '409' => Net::HTTPConflict, + '410' => Net::HTTPGone, + '411' => Net::HTTPLengthRequired, + '412' => Net::HTTPPreconditionFailed, + '413' => Net::HTTPRequestEntityTooLarge, + '414' => Net::HTTPRequestURITooLong, + '415' => Net::HTTPUnsupportedMediaType, + '416' => Net::HTTPRequestedRangeNotSatisfiable, + '417' => Net::HTTPExpectationFailed, + '421' => Net::HTTPMisdirectedRequest, + '422' => Net::HTTPUnprocessableEntity, + '423' => Net::HTTPLocked, + '424' => Net::HTTPFailedDependency, + '426' => Net::HTTPUpgradeRequired, + '428' => Net::HTTPPreconditionRequired, + '429' => Net::HTTPTooManyRequests, + '431' => Net::HTTPRequestHeaderFieldsTooLarge, + '451' => Net::HTTPUnavailableForLegalReasons, + + '500' => Net::HTTPInternalServerError, + '501' => Net::HTTPNotImplemented, + '502' => Net::HTTPBadGateway, + '503' => Net::HTTPServiceUnavailable, + '504' => Net::HTTPGatewayTimeOut, + '505' => Net::HTTPVersionNotSupported, + '506' => Net::HTTPVariantAlsoNegotiates, + '507' => Net::HTTPInsufficientStorage, + '508' => Net::HTTPLoopDetected, + '510' => Net::HTTPNotExtended, + '511' => Net::HTTPNetworkAuthenticationRequired, + } +end + +# :startdoc: + diff --git a/gxg/standard/net/http/status.rb b/gxg/standard/net/http/status.rb new file mode 100644 index 0000000..c7a4c0c --- /dev/null +++ b/gxg/standard/net/http/status.rb @@ -0,0 +1,83 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'net/http' + +if $0 == __FILE__ + require 'open-uri' + IO.foreach(__FILE__) do |line| + puts line + break if line.start_with?('end') + end + puts + puts "Net::HTTP::STATUS_CODES = {" + url = "https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv" + URI(url).read.each_line do |line| + code, mes, = line.split(',') + next if ['(Unused)', 'Unassigned', 'Description'].include?(mes) + puts " #{code} => '#{mes}'," + end + puts "}" +end + +Net::HTTP::STATUS_CODES = { + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + 226 => 'IM Used', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Payload Too Large', + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 421 => 'Misdirected Request', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 510 => 'Not Extended', + 511 => 'Network Authentication Required', +} diff --git a/gxg/standard/net/https.rb b/gxg/standard/net/https.rb new file mode 100644 index 0000000..58cb6dd --- /dev/null +++ b/gxg/standard/net/https.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: false +=begin + += net/https -- SSL/TLS enhancement for Net::HTTP. + + This file has been merged with net/http. There is no longer any need to + require 'net/https' to use HTTPS. + + See Net::HTTP for details on how to make HTTPS connections. + +== Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2001 GOTOU Yuuzou + All rights reserved. + +== Licence + This program is licensed under the same licence as Ruby. + (See the file 'LICENCE'.) + +=end + +require 'net/http' +require 'openssl' diff --git a/gxg/standard/net/imap.rb b/gxg/standard/net/imap.rb new file mode 100644 index 0000000..3d33b94 --- /dev/null +++ b/gxg/standard/net/imap.rb @@ -0,0 +1,3724 @@ +# frozen_string_literal: true +# +# = net/imap.rb +# +# Copyright (C) 2000 Shugo Maeda +# +# This library is distributed under the terms of the Ruby license. +# You can freely distribute/modify this library. +# +# Documentation: Shugo Maeda, with RDoc conversion and overview by William +# Webber. +# +# See Net::IMAP for documentation. +# + + +require "socket" +require "monitor" +require "digest/md5" +require "strscan" +require 'net/protocol' +begin + require "openssl" +rescue LoadError +end + +module Net + + # + # Net::IMAP implements Internet Message Access Protocol (IMAP) client + # functionality. The protocol is described in [IMAP]. + # + # == IMAP Overview + # + # An IMAP client connects to a server, and then authenticates + # itself using either #authenticate() or #login(). Having + # authenticated itself, there is a range of commands + # available to it. Most work with mailboxes, which may be + # arranged in an hierarchical namespace, and each of which + # contains zero or more messages. How this is implemented on + # the server is implementation-dependent; on a UNIX server, it + # will frequently be implemented as files in mailbox format + # within a hierarchy of directories. + # + # To work on the messages within a mailbox, the client must + # first select that mailbox, using either #select() or (for + # read-only access) #examine(). Once the client has successfully + # selected a mailbox, they enter _selected_ state, and that + # mailbox becomes the _current_ mailbox, on which mail-item + # related commands implicitly operate. + # + # Messages have two sorts of identifiers: message sequence + # numbers and UIDs. + # + # Message sequence numbers number messages within a mailbox + # from 1 up to the number of items in the mailbox. If a new + # message arrives during a session, it receives a sequence + # number equal to the new size of the mailbox. If messages + # are expunged from the mailbox, remaining messages have their + # sequence numbers "shuffled down" to fill the gaps. + # + # UIDs, on the other hand, are permanently guaranteed not to + # identify another message within the same mailbox, even if + # the existing message is deleted. UIDs are required to + # be assigned in ascending (but not necessarily sequential) + # order within a mailbox; this means that if a non-IMAP client + # rearranges the order of mailitems within a mailbox, the + # UIDs have to be reassigned. An IMAP client thus cannot + # rearrange message orders. + # + # == Examples of Usage + # + # === List sender and subject of all recent messages in the default mailbox + # + # imap = Net::IMAP.new('mail.example.com') + # imap.authenticate('LOGIN', 'joe_user', 'joes_password') + # imap.examine('INBOX') + # imap.search(["RECENT"]).each do |message_id| + # envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"] + # puts "#{envelope.from[0].name}: \t#{envelope.subject}" + # end + # + # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03" + # + # imap = Net::IMAP.new('mail.example.com') + # imap.authenticate('LOGIN', 'joe_user', 'joes_password') + # imap.select('Mail/sent-mail') + # if not imap.list('Mail/', 'sent-apr03') + # imap.create('Mail/sent-apr03') + # end + # imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id| + # imap.copy(message_id, "Mail/sent-apr03") + # imap.store(message_id, "+FLAGS", [:Deleted]) + # end + # imap.expunge + # + # == Thread Safety + # + # Net::IMAP supports concurrent threads. For example, + # + # imap = Net::IMAP.new("imap.foo.net", "imap2") + # imap.authenticate("cram-md5", "bar", "password") + # imap.select("inbox") + # fetch_thread = Thread.start { imap.fetch(1..-1, "UID") } + # search_result = imap.search(["BODY", "hello"]) + # fetch_result = fetch_thread.value + # imap.disconnect + # + # This script invokes the FETCH command and the SEARCH command concurrently. + # + # == Errors + # + # An IMAP server can send three different types of responses to indicate + # failure: + # + # NO:: the attempted command could not be successfully completed. For + # instance, the username/password used for logging in are incorrect; + # the selected mailbox does not exist; etc. + # + # BAD:: the request from the client does not follow the server's + # understanding of the IMAP protocol. This includes attempting + # commands from the wrong client state; for instance, attempting + # to perform a SEARCH command without having SELECTed a current + # mailbox. It can also signal an internal server + # failure (such as a disk crash) has occurred. + # + # BYE:: the server is saying goodbye. This can be part of a normal + # logout sequence, and can be used as part of a login sequence + # to indicate that the server is (for some reason) unwilling + # to accept your connection. As a response to any other command, + # it indicates either that the server is shutting down, or that + # the server is timing out the client connection due to inactivity. + # + # These three error response are represented by the errors + # Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, and + # Net::IMAP::ByeResponseError, all of which are subclasses of + # Net::IMAP::ResponseError. Essentially, all methods that involve + # sending a request to the server can generate one of these errors. + # Only the most pertinent instances have been documented below. + # + # Because the IMAP class uses Sockets for communication, its methods + # are also susceptible to the various errors that can occur when + # working with sockets. These are generally represented as + # Errno errors. For instance, any method that involves sending a + # request to the server and/or receiving a response from it could + # raise an Errno::EPIPE error if the network connection unexpectedly + # goes down. See the socket(7), ip(7), tcp(7), socket(2), connect(2), + # and associated man pages. + # + # Finally, a Net::IMAP::DataFormatError is thrown if low-level data + # is found to be in an incorrect format (for instance, when converting + # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is + # thrown if a server response is non-parseable. + # + # + # == References + # + # [[IMAP]] + # M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1", + # RFC 2060, December 1996. (Note: since obsoleted by RFC 3501) + # + # [[LANGUAGE-TAGS]] + # Alvestrand, H., "Tags for the Identification of + # Languages", RFC 1766, March 1995. + # + # [[MD5]] + # Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC + # 1864, October 1995. + # + # [[MIME-IMB]] + # Freed, N., and N. Borenstein, "MIME (Multipurpose Internet + # Mail Extensions) Part One: Format of Internet Message Bodies", RFC + # 2045, November 1996. + # + # [[RFC-822]] + # Crocker, D., "Standard for the Format of ARPA Internet Text + # Messages", STD 11, RFC 822, University of Delaware, August 1982. + # + # [[RFC-2087]] + # Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997. + # + # [[RFC-2086]] + # Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997. + # + # [[RFC-2195]] + # Klensin, J., Catoe, R., and Krumviede, P., "IMAP/POP AUTHorize Extension + # for Simple Challenge/Response", RFC 2195, September 1997. + # + # [[SORT-THREAD-EXT]] + # Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - SORT and THREAD + # Extensions", draft-ietf-imapext-sort, May 2003. + # + # [[OSSL]] + # http://www.openssl.org + # + # [[RSSL]] + # http://savannah.gnu.org/projects/rubypki + # + # [[UTF7]] + # Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of + # Unicode", RFC 2152, May 1997. + # + class IMAP < Protocol + include MonitorMixin + if defined?(OpenSSL::SSL) + include OpenSSL + include SSL + end + + # Returns an initial greeting response from the server. + attr_reader :greeting + + # Returns recorded untagged responses. For example: + # + # imap.select("inbox") + # p imap.responses["EXISTS"][-1] + # #=> 2 + # p imap.responses["UIDVALIDITY"][-1] + # #=> 968263756 + attr_reader :responses + + # Returns all response handlers. + attr_reader :response_handlers + + # Seconds to wait until a connection is opened. + # If the IMAP object cannot open a connection within this time, + # it raises a Net::OpenTimeout exception. The default value is 30 seconds. + attr_reader :open_timeout + + # The thread to receive exceptions. + attr_accessor :client_thread + + # Flag indicating a message has been seen. + SEEN = :Seen + + # Flag indicating a message has been answered. + ANSWERED = :Answered + + # Flag indicating a message has been flagged for special or urgent + # attention. + FLAGGED = :Flagged + + # Flag indicating a message has been marked for deletion. This + # will occur when the mailbox is closed or expunged. + DELETED = :Deleted + + # Flag indicating a message is only a draft or work-in-progress version. + DRAFT = :Draft + + # Flag indicating that the message is "recent," meaning that this + # session is the first session in which the client has been notified + # of this message. + RECENT = :Recent + + # Flag indicating that a mailbox context name cannot contain + # children. + NOINFERIORS = :Noinferiors + + # Flag indicating that a mailbox is not selected. + NOSELECT = :Noselect + + # Flag indicating that a mailbox has been marked "interesting" by + # the server; this commonly indicates that the mailbox contains + # new messages. + MARKED = :Marked + + # Flag indicating that the mailbox does not contains new messages. + UNMARKED = :Unmarked + + # Returns the debug mode. + def self.debug + return @@debug + end + + # Sets the debug mode. + def self.debug=(val) + return @@debug = val + end + + # Returns the max number of flags interned to symbols. + def self.max_flag_count + return @@max_flag_count + end + + # Sets the max number of flags interned to symbols. + def self.max_flag_count=(count) + @@max_flag_count = count + end + + # Adds an authenticator for Net::IMAP#authenticate. +auth_type+ + # is the type of authentication this authenticator supports + # (for instance, "LOGIN"). The +authenticator+ is an object + # which defines a process() method to handle authentication with + # the server. See Net::IMAP::LoginAuthenticator, + # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator + # for examples. + # + # + # If +auth_type+ refers to an existing authenticator, it will be + # replaced by the new one. + def self.add_authenticator(auth_type, authenticator) + @@authenticators[auth_type] = authenticator + end + + # The default port for IMAP connections, port 143 + def self.default_port + return PORT + end + + # The default port for IMAPS connections, port 993 + def self.default_tls_port + return SSL_PORT + end + + class << self + alias default_imap_port default_port + alias default_imaps_port default_tls_port + alias default_ssl_port default_tls_port + end + + # Disconnects from the server. + def disconnect + return if disconnected? + begin + begin + # try to call SSL::SSLSocket#io. + @sock.io.shutdown + rescue NoMethodError + # @sock is not an SSL::SSLSocket. + @sock.shutdown + end + rescue Errno::ENOTCONN + # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms. + rescue Exception => e + @receiver_thread.raise(e) + end + @receiver_thread.join + synchronize do + @sock.close + end + raise e if e + end + + # Returns true if disconnected from the server. + def disconnected? + return @sock.closed? + end + + # Sends a CAPABILITY command, and returns an array of + # capabilities that the server supports. Each capability + # is a string. See [IMAP] for a list of possible + # capabilities. + # + # Note that the Net::IMAP class does not modify its + # behaviour according to the capabilities of the server; + # it is up to the user of the class to ensure that + # a certain capability is supported by a server before + # using it. + def capability + synchronize do + send_command("CAPABILITY") + return @responses.delete("CAPABILITY")[-1] + end + end + + # Sends a NOOP command to the server. It does nothing. + def noop + send_command("NOOP") + end + + # Sends a LOGOUT command to inform the server that the client is + # done with the connection. + def logout + send_command("LOGOUT") + end + + # Sends a STARTTLS command to start TLS session. + def starttls(options = {}, verify = true) + send_command("STARTTLS") do |resp| + if resp.kind_of?(TaggedResponse) && resp.name == "OK" + begin + # for backward compatibility + certs = options.to_str + options = create_ssl_params(certs, verify) + rescue NoMethodError + end + start_tls_session(options) + end + end + end + + # Sends an AUTHENTICATE command to authenticate the client. + # The +auth_type+ parameter is a string that represents + # the authentication mechanism to be used. Currently Net::IMAP + # supports the authentication mechanisms: + # + # LOGIN:: login using cleartext user and password. + # CRAM-MD5:: login with cleartext user and encrypted password + # (see [RFC-2195] for a full description). This + # mechanism requires that the server have the user's + # password stored in clear-text password. + # + # For both of these mechanisms, there should be two +args+: username + # and (cleartext) password. A server may not support one or the other + # of these mechanisms; check #capability() for a capability of + # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5". + # + # Authentication is done using the appropriate authenticator object: + # see @@authenticators for more information on plugging in your own + # authenticator. + # + # For example: + # + # imap.authenticate('LOGIN', user, password) + # + # A Net::IMAP::NoResponseError is raised if authentication fails. + def authenticate(auth_type, *args) + auth_type = auth_type.upcase + unless @@authenticators.has_key?(auth_type) + raise ArgumentError, + format('unknown auth type - "%s"', auth_type) + end + authenticator = @@authenticators[auth_type].new(*args) + send_command("AUTHENTICATE", auth_type) do |resp| + if resp.instance_of?(ContinuationRequest) + data = authenticator.process(resp.data.text.unpack("m")[0]) + s = [data].pack("m0") + send_string_data(s) + put_string(CRLF) + end + end + end + + # Sends a LOGIN command to identify the client and carries + # the plaintext +password+ authenticating this +user+. Note + # that, unlike calling #authenticate() with an +auth_type+ + # of "LOGIN", #login() does *not* use the login authenticator. + # + # A Net::IMAP::NoResponseError is raised if authentication fails. + def login(user, password) + send_command("LOGIN", user, password) + end + + # Sends a SELECT command to select a +mailbox+ so that messages + # in the +mailbox+ can be accessed. + # + # After you have selected a mailbox, you may retrieve the + # number of items in that mailbox from @responses["EXISTS"][-1], + # and the number of recent messages from @responses["RECENT"][-1]. + # Note that these values can change if new messages arrive + # during a session; see #add_response_handler() for a way of + # detecting this event. + # + # A Net::IMAP::NoResponseError is raised if the mailbox does not + # exist or is for some reason non-selectable. + def select(mailbox) + synchronize do + @responses.clear + send_command("SELECT", mailbox) + end + end + + # Sends a EXAMINE command to select a +mailbox+ so that messages + # in the +mailbox+ can be accessed. Behaves the same as #select(), + # except that the selected +mailbox+ is identified as read-only. + # + # A Net::IMAP::NoResponseError is raised if the mailbox does not + # exist or is for some reason non-examinable. + def examine(mailbox) + synchronize do + @responses.clear + send_command("EXAMINE", mailbox) + end + end + + # Sends a CREATE command to create a new +mailbox+. + # + # A Net::IMAP::NoResponseError is raised if a mailbox with that name + # cannot be created. + def create(mailbox) + send_command("CREATE", mailbox) + end + + # Sends a DELETE command to remove the +mailbox+. + # + # A Net::IMAP::NoResponseError is raised if a mailbox with that name + # cannot be deleted, either because it does not exist or because the + # client does not have permission to delete it. + def delete(mailbox) + send_command("DELETE", mailbox) + end + + # Sends a RENAME command to change the name of the +mailbox+ to + # +newname+. + # + # A Net::IMAP::NoResponseError is raised if a mailbox with the + # name +mailbox+ cannot be renamed to +newname+ for whatever + # reason; for instance, because +mailbox+ does not exist, or + # because there is already a mailbox with the name +newname+. + def rename(mailbox, newname) + send_command("RENAME", mailbox, newname) + end + + # Sends a SUBSCRIBE command to add the specified +mailbox+ name to + # the server's set of "active" or "subscribed" mailboxes as returned + # by #lsub(). + # + # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be + # subscribed to; for instance, because it does not exist. + def subscribe(mailbox) + send_command("SUBSCRIBE", mailbox) + end + + # Sends a UNSUBSCRIBE command to remove the specified +mailbox+ name + # from the server's set of "active" or "subscribed" mailboxes. + # + # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be + # unsubscribed from; for instance, because the client is not currently + # subscribed to it. + def unsubscribe(mailbox) + send_command("UNSUBSCRIBE", mailbox) + end + + # Sends a LIST command, and returns a subset of names from + # the complete set of all names available to the client. + # +refname+ provides a context (for instance, a base directory + # in a directory-based mailbox hierarchy). +mailbox+ specifies + # a mailbox or (via wildcards) mailboxes under that context. + # Two wildcards may be used in +mailbox+: '*', which matches + # all characters *including* the hierarchy delimiter (for instance, + # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%', + # which matches all characters *except* the hierarchy delimiter. + # + # If +refname+ is empty, +mailbox+ is used directly to determine + # which mailboxes to match. If +mailbox+ is empty, the root + # name of +refname+ and the hierarchy delimiter are returned. + # + # The return value is an array of +Net::IMAP::MailboxList+. For example: + # + # imap.create("foo/bar") + # imap.create("foo/baz") + # p imap.list("", "foo/%") + # #=> [#, \\ + # #, \\ + # #] + def list(refname, mailbox) + synchronize do + send_command("LIST", refname, mailbox) + return @responses.delete("LIST") + end + end + + # Sends a XLIST command, and returns a subset of names from + # the complete set of all names available to the client. + # +refname+ provides a context (for instance, a base directory + # in a directory-based mailbox hierarchy). +mailbox+ specifies + # a mailbox or (via wildcards) mailboxes under that context. + # Two wildcards may be used in +mailbox+: '*', which matches + # all characters *including* the hierarchy delimiter (for instance, + # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%', + # which matches all characters *except* the hierarchy delimiter. + # + # If +refname+ is empty, +mailbox+ is used directly to determine + # which mailboxes to match. If +mailbox+ is empty, the root + # name of +refname+ and the hierarchy delimiter are returned. + # + # The XLIST command is like the LIST command except that the flags + # returned refer to the function of the folder/mailbox, e.g. :Sent + # + # The return value is an array of +Net::IMAP::MailboxList+. For example: + # + # imap.create("foo/bar") + # imap.create("foo/baz") + # p imap.xlist("", "foo/%") + # #=> [#, \\ + # #, \\ + # #] + def xlist(refname, mailbox) + synchronize do + send_command("XLIST", refname, mailbox) + return @responses.delete("XLIST") + end + end + + # Sends the GETQUOTAROOT command along with the specified +mailbox+. + # This command is generally available to both admin and user. + # If this mailbox exists, it returns an array containing objects of type + # Net::IMAP::MailboxQuotaRoot and Net::IMAP::MailboxQuota. + def getquotaroot(mailbox) + synchronize do + send_command("GETQUOTAROOT", mailbox) + result = [] + result.concat(@responses.delete("QUOTAROOT")) + result.concat(@responses.delete("QUOTA")) + return result + end + end + + # Sends the GETQUOTA command along with specified +mailbox+. + # If this mailbox exists, then an array containing a + # Net::IMAP::MailboxQuota object is returned. This + # command is generally only available to server admin. + def getquota(mailbox) + synchronize do + send_command("GETQUOTA", mailbox) + return @responses.delete("QUOTA") + end + end + + # Sends a SETQUOTA command along with the specified +mailbox+ and + # +quota+. If +quota+ is nil, then +quota+ will be unset for that + # mailbox. Typically one needs to be logged in as a server admin + # for this to work. The IMAP quota commands are described in + # [RFC-2087]. + def setquota(mailbox, quota) + if quota.nil? + data = '()' + else + data = '(STORAGE ' + quota.to_s + ')' + end + send_command("SETQUOTA", mailbox, RawData.new(data)) + end + + # Sends the SETACL command along with +mailbox+, +user+ and the + # +rights+ that user is to have on that mailbox. If +rights+ is nil, + # then that user will be stripped of any rights to that mailbox. + # The IMAP ACL commands are described in [RFC-2086]. + def setacl(mailbox, user, rights) + if rights.nil? + send_command("SETACL", mailbox, user, "") + else + send_command("SETACL", mailbox, user, rights) + end + end + + # Send the GETACL command along with a specified +mailbox+. + # If this mailbox exists, an array containing objects of + # Net::IMAP::MailboxACLItem will be returned. + def getacl(mailbox) + synchronize do + send_command("GETACL", mailbox) + return @responses.delete("ACL")[-1] + end + end + + # Sends a LSUB command, and returns a subset of names from the set + # of names that the user has declared as being "active" or + # "subscribed." +refname+ and +mailbox+ are interpreted as + # for #list(). + # The return value is an array of +Net::IMAP::MailboxList+. + def lsub(refname, mailbox) + synchronize do + send_command("LSUB", refname, mailbox) + return @responses.delete("LSUB") + end + end + + # Sends a STATUS command, and returns the status of the indicated + # +mailbox+. +attr+ is a list of one or more attributes whose + # statuses are to be requested. Supported attributes include: + # + # MESSAGES:: the number of messages in the mailbox. + # RECENT:: the number of recent messages in the mailbox. + # UNSEEN:: the number of unseen messages in the mailbox. + # + # The return value is a hash of attributes. For example: + # + # p imap.status("inbox", ["MESSAGES", "RECENT"]) + # #=> {"RECENT"=>0, "MESSAGES"=>44} + # + # A Net::IMAP::NoResponseError is raised if status values + # for +mailbox+ cannot be returned; for instance, because it + # does not exist. + def status(mailbox, attr) + synchronize do + send_command("STATUS", mailbox, attr) + return @responses.delete("STATUS")[-1].attr + end + end + + # Sends a APPEND command to append the +message+ to the end of + # the +mailbox+. The optional +flags+ argument is an array of + # flags initially passed to the new message. The optional + # +date_time+ argument specifies the creation time to assign to the + # new message; it defaults to the current time. + # For example: + # + # imap.append("inbox", <:: a set of message sequence numbers. ',' indicates + # an interval, ':' indicates a range. For instance, + # '2,10:12,15' means "2,10,11,12,15". + # + # BEFORE :: messages with an internal date strictly before + # . The date argument has a format similar + # to 8-Aug-2002. + # + # BODY :: messages that contain within their body. + # + # CC :: messages containing in their CC field. + # + # FROM :: messages that contain in their FROM field. + # + # NEW:: messages with the \Recent, but not the \Seen, flag set. + # + # NOT :: negate the following search key. + # + # OR :: "or" two search keys together. + # + # ON :: messages with an internal date exactly equal to , + # which has a format similar to 8-Aug-2002. + # + # SINCE :: messages with an internal date on or after . + # + # SUBJECT :: messages with in their subject. + # + # TO :: messages with in their TO field. + # + # For example: + # + # p imap.search(["SUBJECT", "hello", "NOT", "NEW"]) + # #=> [1, 6, 7, 8] + def search(keys, charset = nil) + return search_internal("SEARCH", keys, charset) + end + + # Similar to #search(), but returns unique identifiers. + def uid_search(keys, charset = nil) + return search_internal("UID SEARCH", keys, charset) + end + + # Sends a FETCH command to retrieve data associated with a message + # in the mailbox. + # + # The +set+ parameter is a number or a range between two numbers, + # or an array of those. The number is a message sequence number, + # where -1 represents a '*' for use in range notation like 100..-1 + # being interpreted as '100:*'. Beware that the +exclude_end?+ + # property of a Range object is ignored, and the contents of a + # range are independent of the order of the range endpoints as per + # the protocol specification, so 1...5, 5..1 and 5...1 are all + # equivalent to 1..5. + # + # +attr+ is a list of attributes to fetch; see the documentation + # for Net::IMAP::FetchData for a list of valid attributes. + # + # The return value is an array of Net::IMAP::FetchData or nil + # (instead of an empty array) if there is no matching message. + # + # For example: + # + # p imap.fetch(6..8, "UID") + # #=> [#98}>, \\ + # #99}>, \\ + # #100}>] + # p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]") + # #=> [#"Subject: test\r\n\r\n"}>] + # data = imap.uid_fetch(98, ["RFC822.SIZE", "INTERNALDATE"])[0] + # p data.seqno + # #=> 6 + # p data.attr["RFC822.SIZE"] + # #=> 611 + # p data.attr["INTERNALDATE"] + # #=> "12-Oct-2000 22:40:59 +0900" + # p data.attr["UID"] + # #=> 98 + def fetch(set, attr, mod = nil) + return fetch_internal("FETCH", set, attr, mod) + end + + # Similar to #fetch(), but +set+ contains unique identifiers. + def uid_fetch(set, attr, mod = nil) + return fetch_internal("UID FETCH", set, attr, mod) + end + + # Sends a STORE command to alter data associated with messages + # in the mailbox, in particular their flags. The +set+ parameter + # is a number, an array of numbers, or a Range object. Each number + # is a message sequence number. +attr+ is the name of a data item + # to store: 'FLAGS' will replace the message's flag list + # with the provided one, '+FLAGS' will add the provided flags, + # and '-FLAGS' will remove them. +flags+ is a list of flags. + # + # The return value is an array of Net::IMAP::FetchData. For example: + # + # p imap.store(6..8, "+FLAGS", [:Deleted]) + # #=> [#[:Seen, :Deleted]}>, \\ + # #[:Seen, :Deleted]}>, \\ + # #[:Seen, :Deleted]}>] + def store(set, attr, flags) + return store_internal("STORE", set, attr, flags) + end + + # Similar to #store(), but +set+ contains unique identifiers. + def uid_store(set, attr, flags) + return store_internal("UID STORE", set, attr, flags) + end + + # Sends a COPY command to copy the specified message(s) to the end + # of the specified destination +mailbox+. The +set+ parameter is + # a number, an array of numbers, or a Range object. The number is + # a message sequence number. + def copy(set, mailbox) + copy_internal("COPY", set, mailbox) + end + + # Similar to #copy(), but +set+ contains unique identifiers. + def uid_copy(set, mailbox) + copy_internal("UID COPY", set, mailbox) + end + + # Sends a MOVE command to move the specified message(s) to the end + # of the specified destination +mailbox+. The +set+ parameter is + # a number, an array of numbers, or a Range object. The number is + # a message sequence number. + # The IMAP MOVE extension is described in [RFC-6851]. + def move(set, mailbox) + copy_internal("MOVE", set, mailbox) + end + + # Similar to #move(), but +set+ contains unique identifiers. + def uid_move(set, mailbox) + copy_internal("UID MOVE", set, mailbox) + end + + # Sends a SORT command to sort messages in the mailbox. + # Returns an array of message sequence numbers. For example: + # + # p imap.sort(["FROM"], ["ALL"], "US-ASCII") + # #=> [1, 2, 3, 5, 6, 7, 8, 4, 9] + # p imap.sort(["DATE"], ["SUBJECT", "hello"], "US-ASCII") + # #=> [6, 7, 8, 1] + # + # See [SORT-THREAD-EXT] for more details. + def sort(sort_keys, search_keys, charset) + return sort_internal("SORT", sort_keys, search_keys, charset) + end + + # Similar to #sort(), but returns an array of unique identifiers. + def uid_sort(sort_keys, search_keys, charset) + return sort_internal("UID SORT", sort_keys, search_keys, charset) + end + + # Adds a response handler. For example, to detect when + # the server sends a new EXISTS response (which normally + # indicates new messages being added to the mailbox), + # add the following handler after selecting the + # mailbox: + # + # imap.add_response_handler { |resp| + # if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS" + # puts "Mailbox now has #{resp.data} messages" + # end + # } + # + def add_response_handler(handler = Proc.new) + @response_handlers.push(handler) + end + + # Removes the response handler. + def remove_response_handler(handler) + @response_handlers.delete(handler) + end + + # Similar to #search(), but returns message sequence numbers in threaded + # format, as a Net::IMAP::ThreadMember tree. The supported algorithms + # are: + # + # ORDEREDSUBJECT:: split into single-level threads according to subject, + # ordered by date. + # REFERENCES:: split into threads by parent/child relationships determined + # by which message is a reply to which. + # + # Unlike #search(), +charset+ is a required argument. US-ASCII + # and UTF-8 are sample values. + # + # See [SORT-THREAD-EXT] for more details. + def thread(algorithm, search_keys, charset) + return thread_internal("THREAD", algorithm, search_keys, charset) + end + + # Similar to #thread(), but returns unique identifiers instead of + # message sequence numbers. + def uid_thread(algorithm, search_keys, charset) + return thread_internal("UID THREAD", algorithm, search_keys, charset) + end + + # Sends an IDLE command that waits for notifications of new or expunged + # messages. Yields responses from the server during the IDLE. + # + # Use #idle_done() to leave IDLE. + # + # If +timeout+ is given, this method returns after +timeout+ seconds passed. + # +timeout+ can be used for keep-alive. For example, the following code + # checks the connection for each 60 seconds. + # + # loop do + # imap.idle(60) do |res| + # ... + # end + # end + def idle(timeout = nil, &response_handler) + raise LocalJumpError, "no block given" unless response_handler + + response = nil + + synchronize do + tag = Thread.current[:net_imap_tag] = generate_tag + put_string("#{tag} IDLE#{CRLF}") + + begin + add_response_handler(response_handler) + @idle_done_cond = new_cond + @idle_done_cond.wait(timeout) + @idle_done_cond = nil + if @receiver_thread_terminating + raise @exception || Net::IMAP::Error.new("connection closed") + end + ensure + unless @receiver_thread_terminating + remove_response_handler(response_handler) + put_string("DONE#{CRLF}") + response = get_tagged_response(tag, "IDLE") + end + end + end + + return response + end + + # Leaves IDLE. + def idle_done + synchronize do + if @idle_done_cond.nil? + raise Net::IMAP::Error, "not during IDLE" + end + @idle_done_cond.signal + end + end + + # Decode a string from modified UTF-7 format to UTF-8. + # + # UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a + # slightly modified version of this to encode mailbox names + # containing non-ASCII characters; see [IMAP] section 5.1.3. + # + # Net::IMAP does _not_ automatically encode and decode + # mailbox names to and from UTF-7. + def self.decode_utf7(s) + return s.gsub(/&([^-]+)?-/n) { + if $1 + ($1.tr(",", "/") + "===").unpack("m")[0].encode(Encoding::UTF_8, Encoding::UTF_16BE) + else + "&" + end + } + end + + # Encode a string from UTF-8 format to modified UTF-7. + def self.encode_utf7(s) + return s.gsub(/(&)|[^\x20-\x7e]+/) { + if $1 + "&-" + else + base64 = [$&.encode(Encoding::UTF_16BE)].pack("m0") + "&" + base64.delete("=").tr("/", ",") + "-" + end + }.force_encoding("ASCII-8BIT") + end + + # Formats +time+ as an IMAP-style date. + def self.format_date(time) + return time.strftime('%d-%b-%Y') + end + + # Formats +time+ as an IMAP-style date-time. + def self.format_datetime(time) + return time.strftime('%d-%b-%Y %H:%M %z') + end + + private + + CRLF = "\r\n" # :nodoc: + PORT = 143 # :nodoc: + SSL_PORT = 993 # :nodoc: + + @@debug = false + @@authenticators = {} + @@max_flag_count = 10000 + + # :call-seq: + # Net::IMAP.new(host, options = {}) + # + # Creates a new Net::IMAP object and connects it to the specified + # +host+. + # + # +options+ is an option hash, each key of which is a symbol. + # + # The available options are: + # + # port:: Port number (default value is 143 for imap, or 993 for imaps) + # ssl:: If options[:ssl] is true, then an attempt will be made + # to use SSL (now TLS) to connect to the server. For this to work + # OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] extensions need to + # be installed. + # If options[:ssl] is a hash, it's passed to + # OpenSSL::SSL::SSLContext#set_params as parameters. + # open_timeout:: Seconds to wait until a connection is opened + # + # The most common errors are: + # + # Errno::ECONNREFUSED:: Connection refused by +host+ or an intervening + # firewall. + # Errno::ETIMEDOUT:: Connection timed out (possibly due to packets + # being dropped by an intervening firewall). + # Errno::ENETUNREACH:: There is no route to that network. + # SocketError:: Hostname not known or other socket error. + # Net::IMAP::ByeResponseError:: The connected to the host was successful, but + # it immediately said goodbye. + def initialize(host, port_or_options = {}, + usessl = false, certs = nil, verify = true) + super() + @host = host + begin + options = port_or_options.to_hash + rescue NoMethodError + # for backward compatibility + options = {} + options[:port] = port_or_options + if usessl + options[:ssl] = create_ssl_params(certs, verify) + end + end + @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT) + @tag_prefix = "RUBY" + @tagno = 0 + @open_timeout = options[:open_timeout] || 30 + @parser = ResponseParser.new + @sock = tcp_socket(@host, @port) + begin + if options[:ssl] + start_tls_session(options[:ssl]) + @usessl = true + else + @usessl = false + end + @responses = Hash.new([].freeze) + @tagged_responses = {} + @response_handlers = [] + @tagged_response_arrival = new_cond + @continued_command_tag = nil + @continuation_request_arrival = new_cond + @continuation_request_exception = nil + @idle_done_cond = nil + @logout_command_tag = nil + @debug_output_bol = true + @exception = nil + + @greeting = get_response + if @greeting.nil? + raise Error, "connection closed" + end + if @greeting.name == "BYE" + raise ByeResponseError, @greeting + end + + @client_thread = Thread.current + @receiver_thread = Thread.start { + begin + receive_responses + rescue Exception + end + } + @receiver_thread_terminating = false + rescue Exception + @sock.close + raise + end + end + + def tcp_socket(host, port) + Socket.tcp(host, port, :connect_timeout => @open_timeout) + rescue Errno::ETIMEDOUT + raise Net::OpenTimeout, "Timeout to open TCP connection to " + + "#{host}:#{port} (exceeds #{@open_timeout} seconds)" + end + + def receive_responses + connection_closed = false + until connection_closed + synchronize do + @exception = nil + end + begin + resp = get_response + rescue Exception => e + synchronize do + @sock.close + @exception = e + end + break + end + unless resp + synchronize do + @exception = EOFError.new("end of file reached") + end + break + end + begin + synchronize do + case resp + when TaggedResponse + @tagged_responses[resp.tag] = resp + @tagged_response_arrival.broadcast + case resp.tag + when @logout_command_tag + return + when @continued_command_tag + @continuation_request_exception = + RESPONSE_ERRORS[resp.name].new(resp) + @continuation_request_arrival.signal + end + when UntaggedResponse + record_response(resp.name, resp.data) + if resp.data.instance_of?(ResponseText) && + (code = resp.data.code) + record_response(code.name, code.data) + end + if resp.name == "BYE" && @logout_command_tag.nil? + @sock.close + @exception = ByeResponseError.new(resp) + connection_closed = true + end + when ContinuationRequest + @continuation_request_arrival.signal + end + @response_handlers.each do |handler| + handler.call(resp) + end + end + rescue Exception => e + @exception = e + synchronize do + @tagged_response_arrival.broadcast + @continuation_request_arrival.broadcast + end + end + end + synchronize do + @receiver_thread_terminating = true + @tagged_response_arrival.broadcast + @continuation_request_arrival.broadcast + if @idle_done_cond + @idle_done_cond.signal + end + end + end + + def get_tagged_response(tag, cmd) + until @tagged_responses.key?(tag) + raise @exception if @exception + @tagged_response_arrival.wait + end + resp = @tagged_responses.delete(tag) + case resp.name + when /\A(?:NO)\z/ni + raise NoResponseError, resp + when /\A(?:BAD)\z/ni + raise BadResponseError, resp + else + return resp + end + end + + def get_response + buff = String.new + while true + s = @sock.gets(CRLF) + break unless s + buff.concat(s) + if /\{(\d+)\}\r\n/n =~ s + s = @sock.read($1.to_i) + buff.concat(s) + else + break + end + end + return nil if buff.length == 0 + if @@debug + $stderr.print(buff.gsub(/^/n, "S: ")) + end + return @parser.parse(buff) + end + + def record_response(name, data) + unless @responses.has_key?(name) + @responses[name] = [] + end + @responses[name].push(data) + end + + def send_command(cmd, *args, &block) + synchronize do + args.each do |i| + validate_data(i) + end + tag = generate_tag + put_string(tag + " " + cmd) + args.each do |i| + put_string(" ") + send_data(i, tag) + end + put_string(CRLF) + if cmd == "LOGOUT" + @logout_command_tag = tag + end + if block + add_response_handler(block) + end + begin + return get_tagged_response(tag, cmd) + ensure + if block + remove_response_handler(block) + end + end + end + end + + def generate_tag + @tagno += 1 + return format("%s%04d", @tag_prefix, @tagno) + end + + def put_string(str) + @sock.print(str) + if @@debug + if @debug_output_bol + $stderr.print("C: ") + end + $stderr.print(str.gsub(/\n(?!\z)/n, "\nC: ")) + if /\r\n\z/n.match(str) + @debug_output_bol = true + else + @debug_output_bol = false + end + end + end + + def validate_data(data) + case data + when nil + when String + when Integer + NumValidator.ensure_number(data) + when Array + if data[0] == 'CHANGEDSINCE' + NumValidator.ensure_mod_sequence_value(data[1]) + else + data.each do |i| + validate_data(i) + end + end + when Time + when Symbol + else + data.validate + end + end + + def send_data(data, tag = nil) + case data + when nil + put_string("NIL") + when String + send_string_data(data) + when Integer + send_number_data(data) + when Array + send_list_data(data) + when Time + send_time_data(data) + when Symbol + send_symbol_data(data) + else + data.send_data(self, tag) + end + end + + def send_string_data(str) + case str + when "" + put_string('""') + when /[\x80-\xff\r\n]/n + # literal + send_literal(str) + when /[(){ \x00-\x1f\x7f%*"\\]/n + # quoted string + send_quoted_string(str) + else + put_string(str) + end + end + + def send_quoted_string(str) + put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"') + end + + def send_literal(str, tag) + synchronize do + put_string("{" + str.bytesize.to_s + "}" + CRLF) + @continued_command_tag = tag + @continuation_request_exception = nil + begin + @continuation_request_arrival.wait + e = @continuation_request_exception || @exception + raise e if e + put_string(str) + ensure + @continued_command_tag = nil + @continuation_request_exception = nil + end + end + end + + def send_number_data(num) + put_string(num.to_s) + end + + def send_list_data(list) + put_string("(") + first = true + list.each do |i| + if first + first = false + else + put_string(" ") + end + send_data(i) + end + put_string(")") + end + + DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) + + def send_time_data(time) + t = time.dup.gmtime + s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"', + t.day, DATE_MONTH[t.month - 1], t.year, + t.hour, t.min, t.sec) + put_string(s) + end + + def send_symbol_data(symbol) + put_string("\\" + symbol.to_s) + end + + def search_internal(cmd, keys, charset) + if keys.instance_of?(String) + keys = [RawData.new(keys)] + else + normalize_searching_criteria(keys) + end + synchronize do + if charset + send_command(cmd, "CHARSET", charset, *keys) + else + send_command(cmd, *keys) + end + return @responses.delete("SEARCH")[-1] + end + end + + def fetch_internal(cmd, set, attr, mod = nil) + case attr + when String then + attr = RawData.new(attr) + when Array then + attr = attr.map { |arg| + arg.is_a?(String) ? RawData.new(arg) : arg + } + end + + synchronize do + @responses.delete("FETCH") + if mod + send_command(cmd, MessageSet.new(set), attr, mod) + else + send_command(cmd, MessageSet.new(set), attr) + end + return @responses.delete("FETCH") + end + end + + def store_internal(cmd, set, attr, flags) + if attr.instance_of?(String) + attr = RawData.new(attr) + end + synchronize do + @responses.delete("FETCH") + send_command(cmd, MessageSet.new(set), attr, flags) + return @responses.delete("FETCH") + end + end + + def copy_internal(cmd, set, mailbox) + send_command(cmd, MessageSet.new(set), mailbox) + end + + def sort_internal(cmd, sort_keys, search_keys, charset) + if search_keys.instance_of?(String) + search_keys = [RawData.new(search_keys)] + else + normalize_searching_criteria(search_keys) + end + normalize_searching_criteria(search_keys) + synchronize do + send_command(cmd, sort_keys, charset, *search_keys) + return @responses.delete("SORT")[-1] + end + end + + def thread_internal(cmd, algorithm, search_keys, charset) + if search_keys.instance_of?(String) + search_keys = [RawData.new(search_keys)] + else + normalize_searching_criteria(search_keys) + end + normalize_searching_criteria(search_keys) + send_command(cmd, algorithm, charset, *search_keys) + return @responses.delete("THREAD")[-1] + end + + def normalize_searching_criteria(keys) + keys.collect! do |i| + case i + when -1, Range, Array + MessageSet.new(i) + else + i + end + end + end + + def create_ssl_params(certs = nil, verify = true) + params = {} + if certs + if File.file?(certs) + params[:ca_file] = certs + elsif File.directory?(certs) + params[:ca_path] = certs + end + end + if verify + params[:verify_mode] = VERIFY_PEER + else + params[:verify_mode] = VERIFY_NONE + end + return params + end + + def start_tls_session(params = {}) + unless defined?(OpenSSL::SSL) + raise "SSL extension not installed" + end + if @sock.kind_of?(OpenSSL::SSL::SSLSocket) + raise RuntimeError, "already using SSL" + end + begin + params = params.to_hash + rescue NoMethodError + params = {} + end + context = SSLContext.new + context.set_params(params) + if defined?(VerifyCallbackProc) + context.verify_callback = VerifyCallbackProc + end + @sock = SSLSocket.new(@sock, context) + @sock.sync_close = true + ssl_socket_connect(@sock, @open_timeout) + if context.verify_mode != VERIFY_NONE + @sock.post_connection_check(@host) + end + end + + class RawData # :nodoc: + def send_data(imap, tag) + imap.send(:put_string, @data) + end + + def validate + end + + private + + def initialize(data) + @data = data + end + end + + class Atom # :nodoc: + def send_data(imap, tag) + imap.send(:put_string, @data) + end + + def validate + end + + private + + def initialize(data) + @data = data + end + end + + class QuotedString # :nodoc: + def send_data(imap, tag) + imap.send(:send_quoted_string, @data) + end + + def validate + end + + private + + def initialize(data) + @data = data + end + end + + class Literal # :nodoc: + def send_data(imap, tag) + imap.send(:send_literal, @data, tag) + end + + def validate + end + + private + + def initialize(data) + @data = data + end + end + + class MessageSet # :nodoc: + def send_data(imap, tag) + imap.send(:put_string, format_internal(@data)) + end + + def validate + validate_internal(@data) + end + + private + + def initialize(data) + @data = data + end + + def format_internal(data) + case data + when "*" + return data + when Integer + if data == -1 + return "*" + else + return data.to_s + end + when Range + return format_internal(data.first) + + ":" + format_internal(data.last) + when Array + return data.collect {|i| format_internal(i)}.join(",") + when ThreadMember + return data.seqno.to_s + + ":" + data.children.collect {|i| format_internal(i).join(",")} + end + end + + def validate_internal(data) + case data + when "*" + when Integer + NumValidator.ensure_nz_number(data) + when Range + when Array + data.each do |i| + validate_internal(i) + end + when ThreadMember + data.children.each do |i| + validate_internal(i) + end + else + raise DataFormatError, data.inspect + end + end + end + + # Common validators of number and nz_number types + module NumValidator # :nodoc + class << self + # Check is passed argument valid 'number' in RFC 3501 terminology + def valid_number?(num) + # [RFC 3501] + # number = 1*DIGIT + # ; Unsigned 32-bit integer + # ; (0 <= n < 4,294,967,296) + num >= 0 && num < 4294967296 + end + + # Check is passed argument valid 'nz_number' in RFC 3501 terminology + def valid_nz_number?(num) + # [RFC 3501] + # nz-number = digit-nz *DIGIT + # ; Non-zero unsigned 32-bit integer + # ; (0 < n < 4,294,967,296) + num != 0 && valid_number?(num) + end + + # Check is passed argument valid 'mod_sequence_value' in RFC 4551 terminology + def valid_mod_sequence_value?(num) + # mod-sequence-value = 1*DIGIT + # ; Positive unsigned 64-bit integer + # ; (mod-sequence) + # ; (1 <= n < 18,446,744,073,709,551,615) + num >= 1 && num < 18446744073709551615 + end + + # Ensure argument is 'number' or raise DataFormatError + def ensure_number(num) + return if valid_number?(num) + + msg = "number must be unsigned 32-bit integer: #{num}" + raise DataFormatError, msg + end + + # Ensure argument is 'nz_number' or raise DataFormatError + def ensure_nz_number(num) + return if valid_nz_number?(num) + + msg = "nz_number must be non-zero unsigned 32-bit integer: #{num}" + raise DataFormatError, msg + end + + # Ensure argument is 'mod_sequence_value' or raise DataFormatError + def ensure_mod_sequence_value(num) + return if valid_mod_sequence_value?(num) + + msg = "mod_sequence_value must be unsigned 64-bit integer: #{num}" + raise DataFormatError, msg + end + end + end + + # Net::IMAP::ContinuationRequest represents command continuation requests. + # + # The command continuation request response is indicated by a "+" token + # instead of a tag. This form of response indicates that the server is + # ready to accept the continuation of a command from the client. The + # remainder of this response is a line of text. + # + # continue_req ::= "+" SPACE (resp_text / base64) + # + # ==== Fields: + # + # data:: Returns the data (Net::IMAP::ResponseText). + # + # raw_data:: Returns the raw data string. + ContinuationRequest = Struct.new(:data, :raw_data) + + # Net::IMAP::UntaggedResponse represents untagged responses. + # + # Data transmitted by the server to the client and status responses + # that do not indicate command completion are prefixed with the token + # "*", and are called untagged responses. + # + # response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye / + # mailbox_data / message_data / capability_data) + # + # ==== Fields: + # + # name:: Returns the name, such as "FLAGS", "LIST", or "FETCH". + # + # data:: Returns the data such as an array of flag symbols, + # a (()) object. + # + # raw_data:: Returns the raw data string. + UntaggedResponse = Struct.new(:name, :data, :raw_data) + + # Net::IMAP::TaggedResponse represents tagged responses. + # + # The server completion result response indicates the success or + # failure of the operation. It is tagged with the same tag as the + # client command which began the operation. + # + # response_tagged ::= tag SPACE resp_cond_state CRLF + # + # tag ::= 1* + # + # resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text + # + # ==== Fields: + # + # tag:: Returns the tag. + # + # name:: Returns the name, one of "OK", "NO", or "BAD". + # + # data:: Returns the data. See (()). + # + # raw_data:: Returns the raw data string. + # + TaggedResponse = Struct.new(:tag, :name, :data, :raw_data) + + # Net::IMAP::ResponseText represents texts of responses. + # The text may be prefixed by the response code. + # + # resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text) + # ;; text SHOULD NOT begin with "[" or "=" + # + # ==== Fields: + # + # code:: Returns the response code. See (()). + # + # text:: Returns the text. + # + ResponseText = Struct.new(:code, :text) + + # Net::IMAP::ResponseCode represents response codes. + # + # resp_text_code ::= "ALERT" / "PARSE" / + # "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" / + # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" / + # "UIDVALIDITY" SPACE nz_number / + # "UNSEEN" SPACE nz_number / + # atom [SPACE 1*] + # + # ==== Fields: + # + # name:: Returns the name, such as "ALERT", "PERMANENTFLAGS", or "UIDVALIDITY". + # + # data:: Returns the data, if it exists. + # + ResponseCode = Struct.new(:name, :data) + + # Net::IMAP::MailboxList represents contents of the LIST response. + # + # mailbox_list ::= "(" #("\Marked" / "\Noinferiors" / + # "\Noselect" / "\Unmarked" / flag_extension) ")" + # SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox + # + # ==== Fields: + # + # attr:: Returns the name attributes. Each name attribute is a symbol + # capitalized by String#capitalize, such as :Noselect (not :NoSelect). + # + # delim:: Returns the hierarchy delimiter. + # + # name:: Returns the mailbox name. + # + MailboxList = Struct.new(:attr, :delim, :name) + + # Net::IMAP::MailboxQuota represents contents of GETQUOTA response. + # This object can also be a response to GETQUOTAROOT. In the syntax + # specification below, the delimiter used with the "#" construct is a + # single space (SPACE). + # + # quota_list ::= "(" #quota_resource ")" + # + # quota_resource ::= atom SPACE number SPACE number + # + # quota_response ::= "QUOTA" SPACE astring SPACE quota_list + # + # ==== Fields: + # + # mailbox:: The mailbox with the associated quota. + # + # usage:: Current storage usage of the mailbox. + # + # quota:: Quota limit imposed on the mailbox. + # + MailboxQuota = Struct.new(:mailbox, :usage, :quota) + + # Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT + # response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.) + # + # quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring) + # + # ==== Fields: + # + # mailbox:: The mailbox with the associated quota. + # + # quotaroots:: Zero or more quotaroots that affect the quota on the + # specified mailbox. + # + MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots) + + # Net::IMAP::MailboxACLItem represents the response from GETACL. + # + # acl_data ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights) + # + # identifier ::= astring + # + # rights ::= astring + # + # ==== Fields: + # + # user:: Login name that has certain rights to the mailbox + # that was specified with the getacl command. + # + # rights:: The access rights the indicated user has to the + # mailbox. + # + MailboxACLItem = Struct.new(:user, :rights, :mailbox) + + # Net::IMAP::StatusData represents the contents of the STATUS response. + # + # ==== Fields: + # + # mailbox:: Returns the mailbox name. + # + # attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT", + # "UIDVALIDITY", "UNSEEN". Each value is a number. + # + StatusData = Struct.new(:mailbox, :attr) + + # Net::IMAP::FetchData represents the contents of the FETCH response. + # + # ==== Fields: + # + # seqno:: Returns the message sequence number. + # (Note: not the unique identifier, even for the UID command response.) + # + # attr:: Returns a hash. Each key is a data item name, and each value is + # its value. + # + # The current data items are: + # + # [BODY] + # A form of BODYSTRUCTURE without extension data. + # [BODY[
]<>] + # A string expressing the body contents of the specified section. + # [BODYSTRUCTURE] + # An object that describes the [MIME-IMB] body structure of a message. + # See Net::IMAP::BodyTypeBasic, Net::IMAP::BodyTypeText, + # Net::IMAP::BodyTypeMessage, Net::IMAP::BodyTypeMultipart. + # [ENVELOPE] + # A Net::IMAP::Envelope object that describes the envelope + # structure of a message. + # [FLAGS] + # A array of flag symbols that are set for this message. Flag symbols + # are capitalized by String#capitalize. + # [INTERNALDATE] + # A string representing the internal date of the message. + # [RFC822] + # Equivalent to BODY[]. + # [RFC822.HEADER] + # Equivalent to BODY.PEEK[HEADER]. + # [RFC822.SIZE] + # A number expressing the [RFC-822] size of the message. + # [RFC822.TEXT] + # Equivalent to BODY[TEXT]. + # [UID] + # A number expressing the unique identifier of the message. + # + FetchData = Struct.new(:seqno, :attr) + + # Net::IMAP::Envelope represents envelope structures of messages. + # + # ==== Fields: + # + # date:: Returns a string that represents the date. + # + # subject:: Returns a string that represents the subject. + # + # from:: Returns an array of Net::IMAP::Address that represents the from. + # + # sender:: Returns an array of Net::IMAP::Address that represents the sender. + # + # reply_to:: Returns an array of Net::IMAP::Address that represents the reply-to. + # + # to:: Returns an array of Net::IMAP::Address that represents the to. + # + # cc:: Returns an array of Net::IMAP::Address that represents the cc. + # + # bcc:: Returns an array of Net::IMAP::Address that represents the bcc. + # + # in_reply_to:: Returns a string that represents the in-reply-to. + # + # message_id:: Returns a string that represents the message-id. + # + Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to, + :to, :cc, :bcc, :in_reply_to, :message_id) + + # + # Net::IMAP::Address represents electronic mail addresses. + # + # ==== Fields: + # + # name:: Returns the phrase from [RFC-822] mailbox. + # + # route:: Returns the route from [RFC-822] route-addr. + # + # mailbox:: nil indicates end of [RFC-822] group. + # If non-nil and host is nil, returns [RFC-822] group name. + # Otherwise, returns [RFC-822] local-part. + # + # host:: nil indicates [RFC-822] group syntax. + # Otherwise, returns [RFC-822] domain name. + # + Address = Struct.new(:name, :route, :mailbox, :host) + + # + # Net::IMAP::ContentDisposition represents Content-Disposition fields. + # + # ==== Fields: + # + # dsp_type:: Returns the disposition type. + # + # param:: Returns a hash that represents parameters of the Content-Disposition + # field. + # + ContentDisposition = Struct.new(:dsp_type, :param) + + # Net::IMAP::ThreadMember represents a thread-node returned + # by Net::IMAP#thread. + # + # ==== Fields: + # + # seqno:: The sequence number of this message. + # + # children:: An array of Net::IMAP::ThreadMember objects for mail + # items that are children of this in the thread. + # + ThreadMember = Struct.new(:seqno, :children) + + # Net::IMAP::BodyTypeBasic represents basic body structures of messages. + # + # ==== Fields: + # + # media_type:: Returns the content media type name as defined in [MIME-IMB]. + # + # subtype:: Returns the content subtype name as defined in [MIME-IMB]. + # + # param:: Returns a hash that represents parameters as defined in [MIME-IMB]. + # + # content_id:: Returns a string giving the content id as defined in [MIME-IMB]. + # + # description:: Returns a string giving the content description as defined in + # [MIME-IMB]. + # + # encoding:: Returns a string giving the content transfer encoding as defined in + # [MIME-IMB]. + # + # size:: Returns a number giving the size of the body in octets. + # + # md5:: Returns a string giving the body MD5 value as defined in [MD5]. + # + # disposition:: Returns a Net::IMAP::ContentDisposition object giving + # the content disposition. + # + # language:: Returns a string or an array of strings giving the body + # language value as defined in [LANGUAGE-TAGS]. + # + # extension:: Returns extension data. + # + # multipart?:: Returns false. + # + class BodyTypeBasic < Struct.new(:media_type, :subtype, + :param, :content_id, + :description, :encoding, :size, + :md5, :disposition, :language, + :extension) + def multipart? + return false + end + + # Obsolete: use +subtype+ instead. Calling this will + # generate a warning message to +stderr+, then return + # the value of +subtype+. + def media_subtype + warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1) + return subtype + end + end + + # Net::IMAP::BodyTypeText represents TEXT body structures of messages. + # + # ==== Fields: + # + # lines:: Returns the size of the body in text lines. + # + # And Net::IMAP::BodyTypeText has all fields of Net::IMAP::BodyTypeBasic. + # + class BodyTypeText < Struct.new(:media_type, :subtype, + :param, :content_id, + :description, :encoding, :size, + :lines, + :md5, :disposition, :language, + :extension) + def multipart? + return false + end + + # Obsolete: use +subtype+ instead. Calling this will + # generate a warning message to +stderr+, then return + # the value of +subtype+. + def media_subtype + warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1) + return subtype + end + end + + # Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages. + # + # ==== Fields: + # + # envelope:: Returns a Net::IMAP::Envelope giving the envelope structure. + # + # body:: Returns an object giving the body structure. + # + # And Net::IMAP::BodyTypeMessage has all methods of Net::IMAP::BodyTypeText. + # + class BodyTypeMessage < Struct.new(:media_type, :subtype, + :param, :content_id, + :description, :encoding, :size, + :envelope, :body, :lines, + :md5, :disposition, :language, + :extension) + def multipart? + return false + end + + # Obsolete: use +subtype+ instead. Calling this will + # generate a warning message to +stderr+, then return + # the value of +subtype+. + def media_subtype + warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1) + return subtype + end + end + + # Net::IMAP::BodyTypeAttachment represents attachment body structures + # of messages. + # + # ==== Fields: + # + # media_type:: Returns the content media type name. + # + # subtype:: Returns +nil+. + # + # param:: Returns a hash that represents parameters. + # + # multipart?:: Returns false. + # + class BodyTypeAttachment < Struct.new(:media_type, :subtype, + :param) + def multipart? + return false + end + end + + # Net::IMAP::BodyTypeMultipart represents multipart body structures + # of messages. + # + # ==== Fields: + # + # media_type:: Returns the content media type name as defined in [MIME-IMB]. + # + # subtype:: Returns the content subtype name as defined in [MIME-IMB]. + # + # parts:: Returns multiple parts. + # + # param:: Returns a hash that represents parameters as defined in [MIME-IMB]. + # + # disposition:: Returns a Net::IMAP::ContentDisposition object giving + # the content disposition. + # + # language:: Returns a string or an array of strings giving the body + # language value as defined in [LANGUAGE-TAGS]. + # + # extension:: Returns extension data. + # + # multipart?:: Returns true. + # + class BodyTypeMultipart < Struct.new(:media_type, :subtype, + :parts, + :param, :disposition, :language, + :extension) + def multipart? + return true + end + + # Obsolete: use +subtype+ instead. Calling this will + # generate a warning message to +stderr+, then return + # the value of +subtype+. + def media_subtype + warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1) + return subtype + end + end + + class BodyTypeExtension < Struct.new(:media_type, :subtype, + :params, :content_id, + :description, :encoding, :size) + def multipart? + return false + end + end + + class ResponseParser # :nodoc: + def initialize + @str = nil + @pos = nil + @lex_state = nil + @token = nil + @flag_symbols = {} + end + + def parse(str) + @str = str + @pos = 0 + @lex_state = EXPR_BEG + @token = nil + return response + end + + private + + EXPR_BEG = :EXPR_BEG + EXPR_DATA = :EXPR_DATA + EXPR_TEXT = :EXPR_TEXT + EXPR_RTEXT = :EXPR_RTEXT + EXPR_CTEXT = :EXPR_CTEXT + + T_SPACE = :SPACE + T_NIL = :NIL + T_NUMBER = :NUMBER + T_ATOM = :ATOM + T_QUOTED = :QUOTED + T_LPAR = :LPAR + T_RPAR = :RPAR + T_BSLASH = :BSLASH + T_STAR = :STAR + T_LBRA = :LBRA + T_RBRA = :RBRA + T_LITERAL = :LITERAL + T_PLUS = :PLUS + T_PERCENT = :PERCENT + T_CRLF = :CRLF + T_EOF = :EOF + T_TEXT = :TEXT + + BEG_REGEXP = /\G(?:\ +(?# 1: SPACE )( +)|\ +(?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\ +(?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\ +(?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\ +(?# 5: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\ +(?# 6: LPAR )(\()|\ +(?# 7: RPAR )(\))|\ +(?# 8: BSLASH )(\\)|\ +(?# 9: STAR )(\*)|\ +(?# 10: LBRA )(\[)|\ +(?# 11: RBRA )(\])|\ +(?# 12: LITERAL )\{(\d+)\}\r\n|\ +(?# 13: PLUS )(\+)|\ +(?# 14: PERCENT )(%)|\ +(?# 15: CRLF )(\r\n)|\ +(?# 16: EOF )(\z))/ni + + DATA_REGEXP = /\G(?:\ +(?# 1: SPACE )( )|\ +(?# 2: NIL )(NIL)|\ +(?# 3: NUMBER )(\d+)|\ +(?# 4: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\ +(?# 5: LITERAL )\{(\d+)\}\r\n|\ +(?# 6: LPAR )(\()|\ +(?# 7: RPAR )(\)))/ni + + TEXT_REGEXP = /\G(?:\ +(?# 1: TEXT )([^\x00\r\n]*))/ni + + RTEXT_REGEXP = /\G(?:\ +(?# 1: LBRA )(\[)|\ +(?# 2: TEXT )([^\x00\r\n]*))/ni + + CTEXT_REGEXP = /\G(?:\ +(?# 1: TEXT )([^\x00\r\n\]]*))/ni + + Token = Struct.new(:symbol, :value) + + def response + token = lookahead + case token.symbol + when T_PLUS + result = continue_req + when T_STAR + result = response_untagged + else + result = response_tagged + end + while lookahead.symbol == T_SPACE + # Ignore trailing space for Microsoft Exchange Server + shift_token + end + match(T_CRLF) + match(T_EOF) + return result + end + + def continue_req + match(T_PLUS) + token = lookahead + if token.symbol == T_SPACE + shift_token + return ContinuationRequest.new(resp_text, @str) + else + return ContinuationRequest.new(ResponseText.new(nil, ""), @str) + end + end + + def response_untagged + match(T_STAR) + match(T_SPACE) + token = lookahead + if token.symbol == T_NUMBER + return numeric_response + elsif token.symbol == T_ATOM + case token.value + when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni + return response_cond + when /\A(?:FLAGS)\z/ni + return flags_response + when /\A(?:LIST|LSUB|XLIST)\z/ni + return list_response + when /\A(?:QUOTA)\z/ni + return getquota_response + when /\A(?:QUOTAROOT)\z/ni + return getquotaroot_response + when /\A(?:ACL)\z/ni + return getacl_response + when /\A(?:SEARCH|SORT)\z/ni + return search_response + when /\A(?:THREAD)\z/ni + return thread_response + when /\A(?:STATUS)\z/ni + return status_response + when /\A(?:CAPABILITY)\z/ni + return capability_response + else + return text_response + end + else + parse_error("unexpected token %s", token.symbol) + end + end + + def response_tagged + tag = atom + match(T_SPACE) + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + return TaggedResponse.new(tag, name, resp_text, @str) + end + + def response_cond + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + return UntaggedResponse.new(name, resp_text, @str) + end + + def numeric_response + n = number + match(T_SPACE) + token = match(T_ATOM) + name = token.value.upcase + case name + when "EXISTS", "RECENT", "EXPUNGE" + return UntaggedResponse.new(name, n, @str) + when "FETCH" + shift_token + match(T_SPACE) + data = FetchData.new(n, msg_att(n)) + return UntaggedResponse.new(name, data, @str) + end + end + + def msg_att(n) + match(T_LPAR) + attr = {} + while true + token = lookahead + case token.symbol + when T_RPAR + shift_token + break + when T_SPACE + shift_token + next + end + case token.value + when /\A(?:ENVELOPE)\z/ni + name, val = envelope_data + when /\A(?:FLAGS)\z/ni + name, val = flags_data + when /\A(?:INTERNALDATE)\z/ni + name, val = internaldate_data + when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni + name, val = rfc822_text + when /\A(?:RFC822\.SIZE)\z/ni + name, val = rfc822_size + when /\A(?:BODY(?:STRUCTURE)?)\z/ni + name, val = body_data + when /\A(?:UID)\z/ni + name, val = uid_data + when /\A(?:MODSEQ)\z/ni + name, val = modseq_data + else + parse_error("unknown attribute `%s' for {%d}", token.value, n) + end + attr[name] = val + end + return attr + end + + def envelope_data + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + return name, envelope + end + + def envelope + @lex_state = EXPR_DATA + token = lookahead + if token.symbol == T_NIL + shift_token + result = nil + else + match(T_LPAR) + date = nstring + match(T_SPACE) + subject = nstring + match(T_SPACE) + from = address_list + match(T_SPACE) + sender = address_list + match(T_SPACE) + reply_to = address_list + match(T_SPACE) + to = address_list + match(T_SPACE) + cc = address_list + match(T_SPACE) + bcc = address_list + match(T_SPACE) + in_reply_to = nstring + match(T_SPACE) + message_id = nstring + match(T_RPAR) + result = Envelope.new(date, subject, from, sender, reply_to, + to, cc, bcc, in_reply_to, message_id) + end + @lex_state = EXPR_BEG + return result + end + + def flags_data + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + return name, flag_list + end + + def internaldate_data + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + token = match(T_QUOTED) + return name, token.value + end + + def rfc822_text + token = match(T_ATOM) + name = token.value.upcase + token = lookahead + if token.symbol == T_LBRA + shift_token + match(T_RBRA) + end + match(T_SPACE) + return name, nstring + end + + def rfc822_size + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + return name, number + end + + def body_data + token = match(T_ATOM) + name = token.value.upcase + token = lookahead + if token.symbol == T_SPACE + shift_token + return name, body + end + name.concat(section) + token = lookahead + if token.symbol == T_ATOM + name.concat(token.value) + shift_token + end + match(T_SPACE) + data = nstring + return name, data + end + + def body + @lex_state = EXPR_DATA + token = lookahead + if token.symbol == T_NIL + shift_token + result = nil + else + match(T_LPAR) + token = lookahead + if token.symbol == T_LPAR + result = body_type_mpart + else + result = body_type_1part + end + match(T_RPAR) + end + @lex_state = EXPR_BEG + return result + end + + def body_type_1part + token = lookahead + case token.value + when /\A(?:TEXT)\z/ni + return body_type_text + when /\A(?:MESSAGE)\z/ni + return body_type_msg + when /\A(?:ATTACHMENT)\z/ni + return body_type_attachment + when /\A(?:MIXED)\z/ni + return body_type_mixed + else + return body_type_basic + end + end + + def body_type_basic + mtype, msubtype = media_type + token = lookahead + if token.symbol == T_RPAR + return BodyTypeBasic.new(mtype, msubtype) + end + match(T_SPACE) + param, content_id, desc, enc, size = body_fields + md5, disposition, language, extension = body_ext_1part + return BodyTypeBasic.new(mtype, msubtype, + param, content_id, + desc, enc, size, + md5, disposition, language, extension) + end + + def body_type_text + mtype, msubtype = media_type + match(T_SPACE) + param, content_id, desc, enc, size = body_fields + match(T_SPACE) + lines = number + md5, disposition, language, extension = body_ext_1part + return BodyTypeText.new(mtype, msubtype, + param, content_id, + desc, enc, size, + lines, + md5, disposition, language, extension) + end + + def body_type_msg + mtype, msubtype = media_type + match(T_SPACE) + param, content_id, desc, enc, size = body_fields + + token = lookahead + if token.symbol == T_RPAR + # If this is not message/rfc822, we shouldn't apply the RFC822 + # spec to it. We should handle anything other than + # message/rfc822 using multipart extension data [rfc3501] (i.e. + # the data itself won't be returned, we would have to retrieve it + # with BODYSTRUCTURE instead of with BODY + + # Also, sometimes a message/rfc822 is included as a large + # attachment instead of having all of the other details + # (e.g. attaching a .eml file to an email) + if msubtype == "RFC822" + return BodyTypeMessage.new(mtype, msubtype, param, content_id, + desc, enc, size, nil, nil, nil, nil, + nil, nil, nil) + else + return BodyTypeExtension.new(mtype, msubtype, + param, content_id, + desc, enc, size) + end + end + + match(T_SPACE) + env = envelope + match(T_SPACE) + b = body + match(T_SPACE) + lines = number + md5, disposition, language, extension = body_ext_1part + return BodyTypeMessage.new(mtype, msubtype, + param, content_id, + desc, enc, size, + env, b, lines, + md5, disposition, language, extension) + end + + def body_type_attachment + mtype = case_insensitive_string + match(T_SPACE) + param = body_fld_param + return BodyTypeAttachment.new(mtype, nil, param) + end + + def body_type_mixed + mtype = "MULTIPART" + msubtype = case_insensitive_string + param, disposition, language, extension = body_ext_mpart + return BodyTypeBasic.new(mtype, msubtype, param, nil, nil, nil, nil, nil, disposition, language, extension) + end + + def body_type_mpart + parts = [] + while true + token = lookahead + if token.symbol == T_SPACE + shift_token + break + end + parts.push(body) + end + mtype = "MULTIPART" + msubtype = case_insensitive_string + param, disposition, language, extension = body_ext_mpart + return BodyTypeMultipart.new(mtype, msubtype, parts, + param, disposition, language, + extension) + end + + def media_type + mtype = case_insensitive_string + token = lookahead + if token.symbol != T_SPACE + return mtype, nil + end + match(T_SPACE) + msubtype = case_insensitive_string + return mtype, msubtype + end + + def body_fields + param = body_fld_param + match(T_SPACE) + content_id = nstring + match(T_SPACE) + desc = nstring + match(T_SPACE) + enc = case_insensitive_string + match(T_SPACE) + size = number + return param, content_id, desc, enc, size + end + + def body_fld_param + token = lookahead + if token.symbol == T_NIL + shift_token + return nil + end + match(T_LPAR) + param = {} + while true + token = lookahead + case token.symbol + when T_RPAR + shift_token + break + when T_SPACE + shift_token + end + name = case_insensitive_string + match(T_SPACE) + val = string + param[name] = val + end + return param + end + + def body_ext_1part + token = lookahead + if token.symbol == T_SPACE + shift_token + else + return nil + end + md5 = nstring + + token = lookahead + if token.symbol == T_SPACE + shift_token + else + return md5 + end + disposition = body_fld_dsp + + token = lookahead + if token.symbol == T_SPACE + shift_token + else + return md5, disposition + end + language = body_fld_lang + + token = lookahead + if token.symbol == T_SPACE + shift_token + else + return md5, disposition, language + end + + extension = body_extensions + return md5, disposition, language, extension + end + + def body_ext_mpart + token = lookahead + if token.symbol == T_SPACE + shift_token + else + return nil + end + param = body_fld_param + + token = lookahead + if token.symbol == T_SPACE + shift_token + else + return param + end + disposition = body_fld_dsp + + token = lookahead + if token.symbol == T_SPACE + shift_token + else + return param, disposition + end + language = body_fld_lang + + token = lookahead + if token.symbol == T_SPACE + shift_token + else + return param, disposition, language + end + + extension = body_extensions + return param, disposition, language, extension + end + + def body_fld_dsp + token = lookahead + if token.symbol == T_NIL + shift_token + return nil + end + match(T_LPAR) + dsp_type = case_insensitive_string + match(T_SPACE) + param = body_fld_param + match(T_RPAR) + return ContentDisposition.new(dsp_type, param) + end + + def body_fld_lang + token = lookahead + if token.symbol == T_LPAR + shift_token + result = [] + while true + token = lookahead + case token.symbol + when T_RPAR + shift_token + return result + when T_SPACE + shift_token + end + result.push(case_insensitive_string) + end + else + lang = nstring + if lang + return lang.upcase + else + return lang + end + end + end + + def body_extensions + result = [] + while true + token = lookahead + case token.symbol + when T_RPAR + return result + when T_SPACE + shift_token + end + result.push(body_extension) + end + end + + def body_extension + token = lookahead + case token.symbol + when T_LPAR + shift_token + result = body_extensions + match(T_RPAR) + return result + when T_NUMBER + return number + else + return nstring + end + end + + def section + str = String.new + token = match(T_LBRA) + str.concat(token.value) + token = match(T_ATOM, T_NUMBER, T_RBRA) + if token.symbol == T_RBRA + str.concat(token.value) + return str + end + str.concat(token.value) + token = lookahead + if token.symbol == T_SPACE + shift_token + str.concat(token.value) + token = match(T_LPAR) + str.concat(token.value) + while true + token = lookahead + case token.symbol + when T_RPAR + str.concat(token.value) + shift_token + break + when T_SPACE + shift_token + str.concat(token.value) + end + str.concat(format_string(astring)) + end + end + token = match(T_RBRA) + str.concat(token.value) + return str + end + + def format_string(str) + case str + when "" + return '""' + when /[\x80-\xff\r\n]/n + # literal + return "{" + str.bytesize.to_s + "}" + CRLF + str + when /[(){ \x00-\x1f\x7f%*"\\]/n + # quoted string + return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"' + else + # atom + return str + end + end + + def uid_data + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + return name, number + end + + def modseq_data + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + match(T_LPAR) + modseq = number + match(T_RPAR) + return name, modseq + end + + def text_response + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + @lex_state = EXPR_TEXT + token = match(T_TEXT) + @lex_state = EXPR_BEG + return UntaggedResponse.new(name, token.value) + end + + def flags_response + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + return UntaggedResponse.new(name, flag_list, @str) + end + + def list_response + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + return UntaggedResponse.new(name, mailbox_list, @str) + end + + def mailbox_list + attr = flag_list + match(T_SPACE) + token = match(T_QUOTED, T_NIL) + if token.symbol == T_NIL + delim = nil + else + delim = token.value + end + match(T_SPACE) + name = astring + return MailboxList.new(attr, delim, name) + end + + def getquota_response + # If quota never established, get back + # `NO Quota root does not exist'. + # If quota removed, get `()' after the + # folder spec with no mention of `STORAGE'. + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + mailbox = astring + match(T_SPACE) + match(T_LPAR) + token = lookahead + case token.symbol + when T_RPAR + shift_token + data = MailboxQuota.new(mailbox, nil, nil) + return UntaggedResponse.new(name, data, @str) + when T_ATOM + shift_token + match(T_SPACE) + token = match(T_NUMBER) + usage = token.value + match(T_SPACE) + token = match(T_NUMBER) + quota = token.value + match(T_RPAR) + data = MailboxQuota.new(mailbox, usage, quota) + return UntaggedResponse.new(name, data, @str) + else + parse_error("unexpected token %s", token.symbol) + end + end + + def getquotaroot_response + # Similar to getquota, but only admin can use getquota. + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + mailbox = astring + quotaroots = [] + while true + token = lookahead + break unless token.symbol == T_SPACE + shift_token + quotaroots.push(astring) + end + data = MailboxQuotaRoot.new(mailbox, quotaroots) + return UntaggedResponse.new(name, data, @str) + end + + def getacl_response + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + mailbox = astring + data = [] + token = lookahead + if token.symbol == T_SPACE + shift_token + while true + token = lookahead + case token.symbol + when T_CRLF + break + when T_SPACE + shift_token + end + user = astring + match(T_SPACE) + rights = astring + data.push(MailboxACLItem.new(user, rights, mailbox)) + end + end + return UntaggedResponse.new(name, data, @str) + end + + def search_response + token = match(T_ATOM) + name = token.value.upcase + token = lookahead + if token.symbol == T_SPACE + shift_token + data = [] + while true + token = lookahead + case token.symbol + when T_CRLF + break + when T_SPACE + shift_token + when T_NUMBER + data.push(number) + when T_LPAR + # TODO: include the MODSEQ value in a response + shift_token + match(T_ATOM) + match(T_SPACE) + match(T_NUMBER) + match(T_RPAR) + end + end + else + data = [] + end + return UntaggedResponse.new(name, data, @str) + end + + def thread_response + token = match(T_ATOM) + name = token.value.upcase + token = lookahead + + if token.symbol == T_SPACE + threads = [] + + while true + shift_token + token = lookahead + + case token.symbol + when T_LPAR + threads << thread_branch(token) + when T_CRLF + break + end + end + else + # no member + threads = [] + end + + return UntaggedResponse.new(name, threads, @str) + end + + def thread_branch(token) + rootmember = nil + lastmember = nil + + while true + shift_token # ignore first T_LPAR + token = lookahead + + case token.symbol + when T_NUMBER + # new member + newmember = ThreadMember.new(number, []) + if rootmember.nil? + rootmember = newmember + else + lastmember.children << newmember + end + lastmember = newmember + when T_SPACE + # do nothing + when T_LPAR + if rootmember.nil? + # dummy member + lastmember = rootmember = ThreadMember.new(nil, []) + end + + lastmember.children << thread_branch(token) + when T_RPAR + break + end + end + + return rootmember + end + + def status_response + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + mailbox = astring + match(T_SPACE) + match(T_LPAR) + attr = {} + while true + token = lookahead + case token.symbol + when T_RPAR + shift_token + break + when T_SPACE + shift_token + end + token = match(T_ATOM) + key = token.value.upcase + match(T_SPACE) + val = number + attr[key] = val + end + data = StatusData.new(mailbox, attr) + return UntaggedResponse.new(name, data, @str) + end + + def capability_response + token = match(T_ATOM) + name = token.value.upcase + match(T_SPACE) + data = [] + while true + token = lookahead + case token.symbol + when T_CRLF + break + when T_SPACE + shift_token + next + end + data.push(atom.upcase) + end + return UntaggedResponse.new(name, data, @str) + end + + def resp_text + @lex_state = EXPR_RTEXT + token = lookahead + if token.symbol == T_LBRA + code = resp_text_code + else + code = nil + end + token = match(T_TEXT) + @lex_state = EXPR_BEG + return ResponseText.new(code, token.value) + end + + def resp_text_code + @lex_state = EXPR_BEG + match(T_LBRA) + token = match(T_ATOM) + name = token.value.upcase + case name + when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n + result = ResponseCode.new(name, nil) + when /\A(?:PERMANENTFLAGS)\z/n + match(T_SPACE) + result = ResponseCode.new(name, flag_list) + when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n + match(T_SPACE) + result = ResponseCode.new(name, number) + else + token = lookahead + if token.symbol == T_SPACE + shift_token + @lex_state = EXPR_CTEXT + token = match(T_TEXT) + @lex_state = EXPR_BEG + result = ResponseCode.new(name, token.value) + else + result = ResponseCode.new(name, nil) + end + end + match(T_RBRA) + @lex_state = EXPR_RTEXT + return result + end + + def address_list + token = lookahead + if token.symbol == T_NIL + shift_token + return nil + else + result = [] + match(T_LPAR) + while true + token = lookahead + case token.symbol + when T_RPAR + shift_token + break + when T_SPACE + shift_token + end + result.push(address) + end + return result + end + end + + ADDRESS_REGEXP = /\G\ +(?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ +(?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ +(?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ +(?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\ +\)/ni + + def address + match(T_LPAR) + if @str.index(ADDRESS_REGEXP, @pos) + # address does not include literal. + @pos = $~.end(0) + name = $1 + route = $2 + mailbox = $3 + host = $4 + for s in [name, route, mailbox, host] + if s + s.gsub!(/\\(["\\])/n, "\\1") + end + end + else + name = nstring + match(T_SPACE) + route = nstring + match(T_SPACE) + mailbox = nstring + match(T_SPACE) + host = nstring + match(T_RPAR) + end + return Address.new(name, route, mailbox, host) + end + + FLAG_REGEXP = /\ +(?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\ +(?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n + + def flag_list + if @str.index(/\(([^)]*)\)/ni, @pos) + @pos = $~.end(0) + return $1.scan(FLAG_REGEXP).collect { |flag, atom| + if atom + atom + else + symbol = flag.capitalize.untaint.intern + @flag_symbols[symbol] = true + if @flag_symbols.length > IMAP.max_flag_count + raise FlagCountError, "number of flag symbols exceeded" + end + symbol + end + } + else + parse_error("invalid flag list") + end + end + + def nstring + token = lookahead + if token.symbol == T_NIL + shift_token + return nil + else + return string + end + end + + def astring + token = lookahead + if string_token?(token) + return string + else + return atom + end + end + + def string + token = lookahead + if token.symbol == T_NIL + shift_token + return nil + end + token = match(T_QUOTED, T_LITERAL) + return token.value + end + + STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL] + + def string_token?(token) + return STRING_TOKENS.include?(token.symbol) + end + + def case_insensitive_string + token = lookahead + if token.symbol == T_NIL + shift_token + return nil + end + token = match(T_QUOTED, T_LITERAL) + return token.value.upcase + end + + def atom + result = String.new + while true + token = lookahead + if atom_token?(token) + result.concat(token.value) + shift_token + else + if result.empty? + parse_error("unexpected token %s", token.symbol) + else + return result + end + end + end + end + + ATOM_TOKENS = [ + T_ATOM, + T_NUMBER, + T_NIL, + T_LBRA, + T_RBRA, + T_PLUS + ] + + def atom_token?(token) + return ATOM_TOKENS.include?(token.symbol) + end + + def number + token = lookahead + if token.symbol == T_NIL + shift_token + return nil + end + token = match(T_NUMBER) + return token.value.to_i + end + + def nil_atom + match(T_NIL) + return nil + end + + def match(*args) + token = lookahead + unless args.include?(token.symbol) + parse_error('unexpected token %s (expected %s)', + token.symbol.id2name, + args.collect {|i| i.id2name}.join(" or ")) + end + shift_token + return token + end + + def lookahead + unless @token + @token = next_token + end + return @token + end + + def shift_token + @token = nil + end + + def next_token + case @lex_state + when EXPR_BEG + if @str.index(BEG_REGEXP, @pos) + @pos = $~.end(0) + if $1 + return Token.new(T_SPACE, $+) + elsif $2 + return Token.new(T_NIL, $+) + elsif $3 + return Token.new(T_NUMBER, $+) + elsif $4 + return Token.new(T_ATOM, $+) + elsif $5 + return Token.new(T_QUOTED, + $+.gsub(/\\(["\\])/n, "\\1")) + elsif $6 + return Token.new(T_LPAR, $+) + elsif $7 + return Token.new(T_RPAR, $+) + elsif $8 + return Token.new(T_BSLASH, $+) + elsif $9 + return Token.new(T_STAR, $+) + elsif $10 + return Token.new(T_LBRA, $+) + elsif $11 + return Token.new(T_RBRA, $+) + elsif $12 + len = $+.to_i + val = @str[@pos, len] + @pos += len + return Token.new(T_LITERAL, val) + elsif $13 + return Token.new(T_PLUS, $+) + elsif $14 + return Token.new(T_PERCENT, $+) + elsif $15 + return Token.new(T_CRLF, $+) + elsif $16 + return Token.new(T_EOF, $+) + else + parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid") + end + else + @str.index(/\S*/n, @pos) + parse_error("unknown token - %s", $&.dump) + end + when EXPR_DATA + if @str.index(DATA_REGEXP, @pos) + @pos = $~.end(0) + if $1 + return Token.new(T_SPACE, $+) + elsif $2 + return Token.new(T_NIL, $+) + elsif $3 + return Token.new(T_NUMBER, $+) + elsif $4 + return Token.new(T_QUOTED, + $+.gsub(/\\(["\\])/n, "\\1")) + elsif $5 + len = $+.to_i + val = @str[@pos, len] + @pos += len + return Token.new(T_LITERAL, val) + elsif $6 + return Token.new(T_LPAR, $+) + elsif $7 + return Token.new(T_RPAR, $+) + else + parse_error("[Net::IMAP BUG] DATA_REGEXP is invalid") + end + else + @str.index(/\S*/n, @pos) + parse_error("unknown token - %s", $&.dump) + end + when EXPR_TEXT + if @str.index(TEXT_REGEXP, @pos) + @pos = $~.end(0) + if $1 + return Token.new(T_TEXT, $+) + else + parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid") + end + else + @str.index(/\S*/n, @pos) + parse_error("unknown token - %s", $&.dump) + end + when EXPR_RTEXT + if @str.index(RTEXT_REGEXP, @pos) + @pos = $~.end(0) + if $1 + return Token.new(T_LBRA, $+) + elsif $2 + return Token.new(T_TEXT, $+) + else + parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid") + end + else + @str.index(/\S*/n, @pos) + parse_error("unknown token - %s", $&.dump) + end + when EXPR_CTEXT + if @str.index(CTEXT_REGEXP, @pos) + @pos = $~.end(0) + if $1 + return Token.new(T_TEXT, $+) + else + parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid") + end + else + @str.index(/\S*/n, @pos) #/ + parse_error("unknown token - %s", $&.dump) + end + else + parse_error("invalid @lex_state - %s", @lex_state.inspect) + end + end + + def parse_error(fmt, *args) + if IMAP.debug + $stderr.printf("@str: %s\n", @str.dump) + $stderr.printf("@pos: %d\n", @pos) + $stderr.printf("@lex_state: %s\n", @lex_state) + if @token + $stderr.printf("@token.symbol: %s\n", @token.symbol) + $stderr.printf("@token.value: %s\n", @token.value.inspect) + end + end + raise ResponseParseError, format(fmt, *args) + end + end + + # Authenticator for the "LOGIN" authentication type. See + # #authenticate(). + class LoginAuthenticator + def process(data) + case @state + when STATE_USER + @state = STATE_PASSWORD + return @user + when STATE_PASSWORD + return @password + end + end + + private + + STATE_USER = :USER + STATE_PASSWORD = :PASSWORD + + def initialize(user, password) + @user = user + @password = password + @state = STATE_USER + end + end + add_authenticator "LOGIN", LoginAuthenticator + + # Authenticator for the "PLAIN" authentication type. See + # #authenticate(). + class PlainAuthenticator + def process(data) + return "\0#{@user}\0#{@password}" + end + + private + + def initialize(user, password) + @user = user + @password = password + end + end + add_authenticator "PLAIN", PlainAuthenticator + + # Authenticator for the "CRAM-MD5" authentication type. See + # #authenticate(). + class CramMD5Authenticator + def process(challenge) + digest = hmac_md5(challenge, @password) + return @user + " " + digest + end + + private + + def initialize(user, password) + @user = user + @password = password + end + + def hmac_md5(text, key) + if key.length > 64 + key = Digest::MD5.digest(key) + end + + k_ipad = key + "\0" * (64 - key.length) + k_opad = key + "\0" * (64 - key.length) + for i in 0..63 + k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr + k_opad[i] = (k_opad[i].ord ^ 0x5c).chr + end + + digest = Digest::MD5.digest(k_ipad + text) + + return Digest::MD5.hexdigest(k_opad + digest) + end + end + add_authenticator "CRAM-MD5", CramMD5Authenticator + + # Authenticator for the "DIGEST-MD5" authentication type. See + # #authenticate(). + class DigestMD5Authenticator + def process(challenge) + case @stage + when STAGE_ONE + @stage = STAGE_TWO + sparams = {} + c = StringScanner.new(challenge) + while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/) + k, v = c[1], c[2] + if v =~ /^"(.*)"$/ + v = $1 + if v =~ /,/ + v = v.split(',') + end + end + sparams[k] = v + end + + raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0 + raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth") + + response = { + :nonce => sparams['nonce'], + :username => @user, + :realm => sparams['realm'], + :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]), + :'digest-uri' => 'imap/' + sparams['realm'], + :qop => 'auth', + :maxbuf => 65535, + :nc => "%08d" % nc(sparams['nonce']), + :charset => sparams['charset'], + } + + response[:authzid] = @authname unless @authname.nil? + + # now, the real thing + a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') ) + + a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':') + a1 << ':' + response[:authzid] unless response[:authzid].nil? + + a2 = "AUTHENTICATE:" + response[:'digest-uri'] + a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/ + + response[:response] = Digest::MD5.hexdigest( + [ + Digest::MD5.hexdigest(a1), + response.values_at(:nonce, :nc, :cnonce, :qop), + Digest::MD5.hexdigest(a2) + ].join(':') + ) + + return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',') + when STAGE_TWO + @stage = nil + # if at the second stage, return an empty string + if challenge =~ /rspauth=/ + return '' + else + raise ResponseParseError, challenge + end + else + raise ResponseParseError, challenge + end + end + + def initialize(user, password, authname = nil) + @user, @password, @authname = user, password, authname + @nc, @stage = {}, STAGE_ONE + end + + private + + STAGE_ONE = :stage_one + STAGE_TWO = :stage_two + + def nc(nonce) + if @nc.has_key? nonce + @nc[nonce] = @nc[nonce] + 1 + else + @nc[nonce] = 1 + end + return @nc[nonce] + end + + # some responses need quoting + def qdval(k, v) + return if k.nil? or v.nil? + if %w"username authzid realm nonce cnonce digest-uri qop".include? k + v.gsub!(/([\\"])/, "\\\1") + return '%s="%s"' % [k, v] + else + return '%s=%s' % [k, v] + end + end + end + add_authenticator "DIGEST-MD5", DigestMD5Authenticator + + # Superclass of IMAP errors. + class Error < StandardError + end + + # Error raised when data is in the incorrect format. + class DataFormatError < Error + end + + # Error raised when a response from the server is non-parseable. + class ResponseParseError < Error + end + + # Superclass of all errors used to encapsulate "fail" responses + # from the server. + class ResponseError < Error + + # The response that caused this error + attr_accessor :response + + def initialize(response) + @response = response + + super @response.data.text + end + + end + + # Error raised upon a "NO" response from the server, indicating + # that the client command could not be completed successfully. + class NoResponseError < ResponseError + end + + # Error raised upon a "BAD" response from the server, indicating + # that the client command violated the IMAP protocol, or an internal + # server failure has occurred. + class BadResponseError < ResponseError + end + + # Error raised upon a "BYE" response from the server, indicating + # that the client is not being allowed to login, or has been timed + # out due to inactivity. + class ByeResponseError < ResponseError + end + + RESPONSE_ERRORS = Hash.new(ResponseError) + RESPONSE_ERRORS["NO"] = NoResponseError + RESPONSE_ERRORS["BAD"] = BadResponseError + + # Error raised when too many flags are interned to symbols. + class FlagCountError < Error + end + end +end diff --git a/gxg/standard/net/pop.rb b/gxg/standard/net/pop.rb new file mode 100644 index 0000000..c437bcc --- /dev/null +++ b/gxg/standard/net/pop.rb @@ -0,0 +1,1022 @@ +# frozen_string_literal: true +# = net/pop.rb +# +# Copyright (c) 1999-2007 Yukihiro Matsumoto. +# +# Copyright (c) 1999-2007 Minero Aoki. +# +# Written & maintained by Minero Aoki . +# +# Documented by William Webber and Minero Aoki. +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms as Ruby itself, +# Ruby Distribute License. +# +# NOTE: You can find Japanese version of this document at: +# http://docs.ruby-lang.org/ja/latest/library/net=2fpop.html +# +# $Id: pop.rb 59472 2017-08-02 15:10:42Z kazu $ +# +# See Net::POP3 for documentation. +# + +require 'net/protocol' +require 'digest/md5' +require 'timeout' + +begin + require "openssl" +rescue LoadError +end + +module Net + + # Non-authentication POP3 protocol error + # (reply code "-ERR", except authentication). + class POPError < ProtocolError; end + + # POP3 authentication error. + class POPAuthenticationError < ProtoAuthError; end + + # Unexpected response from the server. + class POPBadResponse < POPError; end + + # + # == What is This Library? + # + # This library provides functionality for retrieving + # email via POP3, the Post Office Protocol version 3. For details + # of POP3, see [RFC1939] (http://www.ietf.org/rfc/rfc1939.txt). + # + # == Examples + # + # === Retrieving Messages + # + # This example retrieves messages from the server and deletes them + # on the server. + # + # Messages are written to files named 'inbox/1', 'inbox/2', .... + # Replace 'pop.example.com' with your POP3 server address, and + # 'YourAccount' and 'YourPassword' with the appropriate account + # details. + # + # require 'net/pop' + # + # pop = Net::POP3.new('pop.example.com') + # pop.start('YourAccount', 'YourPassword') # (1) + # if pop.mails.empty? + # puts 'No mail.' + # else + # i = 0 + # pop.each_mail do |m| # or "pop.mails.each ..." # (2) + # File.open("inbox/#{i}", 'w') do |f| + # f.write m.pop + # end + # m.delete + # i += 1 + # end + # puts "#{pop.mails.size} mails popped." + # end + # pop.finish # (3) + # + # 1. Call Net::POP3#start and start POP session. + # 2. Access messages by using POP3#each_mail and/or POP3#mails. + # 3. Close POP session by calling POP3#finish or use the block form of #start. + # + # === Shortened Code + # + # The example above is very verbose. You can shorten the code by using + # some utility methods. First, the block form of Net::POP3.start can + # be used instead of POP3.new, POP3#start and POP3#finish. + # + # require 'net/pop' + # + # Net::POP3.start('pop.example.com', 110, + # 'YourAccount', 'YourPassword') do |pop| + # if pop.mails.empty? + # puts 'No mail.' + # else + # i = 0 + # pop.each_mail do |m| # or "pop.mails.each ..." + # File.open("inbox/#{i}", 'w') do |f| + # f.write m.pop + # end + # m.delete + # i += 1 + # end + # puts "#{pop.mails.size} mails popped." + # end + # end + # + # POP3#delete_all is an alternative for #each_mail and #delete. + # + # require 'net/pop' + # + # Net::POP3.start('pop.example.com', 110, + # 'YourAccount', 'YourPassword') do |pop| + # if pop.mails.empty? + # puts 'No mail.' + # else + # i = 1 + # pop.delete_all do |m| + # File.open("inbox/#{i}", 'w') do |f| + # f.write m.pop + # end + # i += 1 + # end + # end + # end + # + # And here is an even shorter example. + # + # require 'net/pop' + # + # i = 0 + # Net::POP3.delete_all('pop.example.com', 110, + # 'YourAccount', 'YourPassword') do |m| + # File.open("inbox/#{i}", 'w') do |f| + # f.write m.pop + # end + # i += 1 + # end + # + # === Memory Space Issues + # + # All the examples above get each message as one big string. + # This example avoids this. + # + # require 'net/pop' + # + # i = 1 + # Net::POP3.delete_all('pop.example.com', 110, + # 'YourAccount', 'YourPassword') do |m| + # File.open("inbox/#{i}", 'w') do |f| + # m.pop do |chunk| # get a message little by little. + # f.write chunk + # end + # i += 1 + # end + # end + # + # === Using APOP + # + # The net/pop library supports APOP authentication. + # To use APOP, use the Net::APOP class instead of the Net::POP3 class. + # You can use the utility method, Net::POP3.APOP(). For example: + # + # require 'net/pop' + # + # # Use APOP authentication if $isapop == true + # pop = Net::POP3.APOP($isapop).new('apop.example.com', 110) + # pop.start('YourAccount', 'YourPassword') do |pop| + # # Rest of the code is the same. + # end + # + # === Fetch Only Selected Mail Using 'UIDL' POP Command + # + # If your POP server provides UIDL functionality, + # you can grab only selected mails from the POP server. + # e.g. + # + # def need_pop?( id ) + # # determine if we need pop this mail... + # end + # + # Net::POP3.start('pop.example.com', 110, + # 'Your account', 'Your password') do |pop| + # pop.mails.select { |m| need_pop?(m.unique_id) }.each do |m| + # do_something(m.pop) + # end + # end + # + # The POPMail#unique_id() method returns the unique-id of the message as a + # String. Normally the unique-id is a hash of the message. + # + class POP3 < Protocol + + # svn revision of this library + Revision = %q$Revision: 59472 $.split[1] + + # + # Class Parameters + # + + # returns the port for POP3 + def POP3.default_port + default_pop3_port() + end + + # The default port for POP3 connections, port 110 + def POP3.default_pop3_port + 110 + end + + # The default port for POP3S connections, port 995 + def POP3.default_pop3s_port + 995 + end + + def POP3.socket_type #:nodoc: obsolete + Net::InternetMessageIO + end + + # + # Utilities + # + + # Returns the APOP class if +isapop+ is true; otherwise, returns + # the POP class. For example: + # + # # Example 1 + # pop = Net::POP3::APOP($is_apop).new(addr, port) + # + # # Example 2 + # Net::POP3::APOP($is_apop).start(addr, port) do |pop| + # .... + # end + # + def POP3.APOP(isapop) + isapop ? APOP : POP3 + end + + # Starts a POP3 session and iterates over each POPMail object, + # yielding it to the +block+. + # This method is equivalent to: + # + # Net::POP3.start(address, port, account, password) do |pop| + # pop.each_mail do |m| + # yield m + # end + # end + # + # This method raises a POPAuthenticationError if authentication fails. + # + # === Example + # + # Net::POP3.foreach('pop.example.com', 110, + # 'YourAccount', 'YourPassword') do |m| + # file.write m.pop + # m.delete if $DELETE + # end + # + def POP3.foreach(address, port = nil, + account = nil, password = nil, + isapop = false, &block) # :yields: message + start(address, port, account, password, isapop) {|pop| + pop.each_mail(&block) + } + end + + # Starts a POP3 session and deletes all messages on the server. + # If a block is given, each POPMail object is yielded to it before + # being deleted. + # + # This method raises a POPAuthenticationError if authentication fails. + # + # === Example + # + # Net::POP3.delete_all('pop.example.com', 110, + # 'YourAccount', 'YourPassword') do |m| + # file.write m.pop + # end + # + def POP3.delete_all(address, port = nil, + account = nil, password = nil, + isapop = false, &block) + start(address, port, account, password, isapop) {|pop| + pop.delete_all(&block) + } + end + + # Opens a POP3 session, attempts authentication, and quits. + # + # This method raises POPAuthenticationError if authentication fails. + # + # === Example: normal POP3 + # + # Net::POP3.auth_only('pop.example.com', 110, + # 'YourAccount', 'YourPassword') + # + # === Example: APOP + # + # Net::POP3.auth_only('pop.example.com', 110, + # 'YourAccount', 'YourPassword', true) + # + def POP3.auth_only(address, port = nil, + account = nil, password = nil, + isapop = false) + new(address, port, isapop).auth_only account, password + end + + # Starts a pop3 session, attempts authentication, and quits. + # This method must not be called while POP3 session is opened. + # This method raises POPAuthenticationError if authentication fails. + def auth_only(account, password) + raise IOError, 'opening previously opened POP session' if started? + start(account, password) { + ; + } + end + + # + # SSL + # + + @ssl_params = nil + + # :call-seq: + # Net::POP.enable_ssl(params = {}) + # + # Enable SSL for all new instances. + # +params+ is passed to OpenSSL::SSLContext#set_params. + def POP3.enable_ssl(*args) + @ssl_params = create_ssl_params(*args) + end + + # Constructs proper parameters from arguments + def POP3.create_ssl_params(verify_or_params = {}, certs = nil) + begin + params = verify_or_params.to_hash + rescue NoMethodError + params = {} + params[:verify_mode] = verify_or_params + if certs + if File.file?(certs) + params[:ca_file] = certs + elsif File.directory?(certs) + params[:ca_path] = certs + end + end + end + return params + end + + # Disable SSL for all new instances. + def POP3.disable_ssl + @ssl_params = nil + end + + # returns the SSL Parameters + # + # see also POP3.enable_ssl + def POP3.ssl_params + return @ssl_params + end + + # returns +true+ if POP3.ssl_params is set + def POP3.use_ssl? + return !@ssl_params.nil? + end + + # returns whether verify_mode is enable from POP3.ssl_params + def POP3.verify + return @ssl_params[:verify_mode] + end + + # returns the :ca_file or :ca_path from POP3.ssl_params + def POP3.certs + return @ssl_params[:ca_file] || @ssl_params[:ca_path] + end + + # + # Session management + # + + # Creates a new POP3 object and open the connection. Equivalent to + # + # Net::POP3.new(address, port, isapop).start(account, password) + # + # If +block+ is provided, yields the newly-opened POP3 object to it, + # and automatically closes it at the end of the session. + # + # === Example + # + # Net::POP3.start(addr, port, account, password) do |pop| + # pop.each_mail do |m| + # file.write m.pop + # m.delete + # end + # end + # + def POP3.start(address, port = nil, + account = nil, password = nil, + isapop = false, &block) # :yield: pop + new(address, port, isapop).start(account, password, &block) + end + + # Creates a new POP3 object. + # + # +address+ is the hostname or ip address of your POP3 server. + # + # The optional +port+ is the port to connect to. + # + # The optional +isapop+ specifies whether this connection is going + # to use APOP authentication; it defaults to +false+. + # + # This method does *not* open the TCP connection. + def initialize(addr, port = nil, isapop = false) + @address = addr + @ssl_params = POP3.ssl_params + @port = port + @apop = isapop + + @command = nil + @socket = nil + @started = false + @open_timeout = 30 + @read_timeout = 60 + @debug_output = nil + + @mails = nil + @n_mails = nil + @n_bytes = nil + end + + # Does this instance use APOP authentication? + def apop? + @apop + end + + # does this instance use SSL? + def use_ssl? + return !@ssl_params.nil? + end + + # :call-seq: + # Net::POP#enable_ssl(params = {}) + # + # Enables SSL for this instance. Must be called before the connection is + # established to have any effect. + # +params[:port]+ is port to establish the SSL connection on; Defaults to 995. + # +params+ (except :port) is passed to OpenSSL::SSLContext#set_params. + def enable_ssl(verify_or_params = {}, certs = nil, port = nil) + begin + @ssl_params = verify_or_params.to_hash.dup + @port = @ssl_params.delete(:port) || @port + rescue NoMethodError + @ssl_params = POP3.create_ssl_params(verify_or_params, certs) + @port = port || @port + end + end + + # Disable SSL for all new instances. + def disable_ssl + @ssl_params = nil + end + + # Provide human-readable stringification of class state. + def inspect + "#<#{self.class} #{@address}:#{@port} open=#{@started}>" + end + + # *WARNING*: This method causes a serious security hole. + # Use this method only for debugging. + # + # Set an output stream for debugging. + # + # === Example + # + # pop = Net::POP.new(addr, port) + # pop.set_debug_output $stderr + # pop.start(account, passwd) do |pop| + # .... + # end + # + def set_debug_output(arg) + @debug_output = arg + end + + # The address to connect to. + attr_reader :address + + # The port number to connect to. + def port + return @port || (use_ssl? ? POP3.default_pop3s_port : POP3.default_pop3_port) + end + + # Seconds to wait until a connection is opened. + # If the POP3 object cannot open a connection within this time, + # it raises a Net::OpenTimeout exception. The default value is 30 seconds. + attr_accessor :open_timeout + + # Seconds to wait until reading one block (by one read(1) call). + # If the POP3 object cannot complete a read() within this time, + # it raises a Net::ReadTimeout exception. The default value is 60 seconds. + attr_reader :read_timeout + + # Set the read timeout. + def read_timeout=(sec) + @command.socket.read_timeout = sec if @command + @read_timeout = sec + end + + # +true+ if the POP3 session has started. + def started? + @started + end + + alias active? started? #:nodoc: obsolete + + # Starts a POP3 session. + # + # When called with block, gives a POP3 object to the block and + # closes the session after block call finishes. + # + # This method raises a POPAuthenticationError if authentication fails. + def start(account, password) # :yield: pop + raise IOError, 'POP session already started' if @started + if block_given? + begin + do_start account, password + return yield(self) + ensure + do_finish + end + else + do_start account, password + return self + end + end + + # internal method for Net::POP3.start + def do_start(account, password) # :nodoc: + s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do + TCPSocket.open(@address, port) + end + if use_ssl? + raise 'openssl library not installed' unless defined?(OpenSSL) + context = OpenSSL::SSL::SSLContext.new + context.set_params(@ssl_params) + s = OpenSSL::SSL::SSLSocket.new(s, context) + s.sync_close = true + ssl_socket_connect(s, @open_timeout) + if context.verify_mode != OpenSSL::SSL::VERIFY_NONE + s.post_connection_check(@address) + end + end + @socket = InternetMessageIO.new(s, + read_timeout: @read_timeout, + debug_output: @debug_output) + logging "POP session started: #{@address}:#{@port} (#{@apop ? 'APOP' : 'POP'})" + on_connect + @command = POP3Command.new(@socket) + if apop? + @command.apop account, password + else + @command.auth account, password + end + @started = true + ensure + # Authentication failed, clean up connection. + unless @started + s.close if s + @socket = nil + @command = nil + end + end + private :do_start + + # Does nothing + def on_connect # :nodoc: + end + private :on_connect + + # Finishes a POP3 session and closes TCP connection. + def finish + raise IOError, 'POP session not yet started' unless started? + do_finish + end + + # nil's out the: + # - mails + # - number counter for mails + # - number counter for bytes + # - quits the current command, if any + def do_finish # :nodoc: + @mails = nil + @n_mails = nil + @n_bytes = nil + @command.quit if @command + ensure + @started = false + @command = nil + @socket.close if @socket + @socket = nil + end + private :do_finish + + # Returns the current command. + # + # Raises IOError if there is no active socket + def command # :nodoc: + raise IOError, 'POP session not opened yet' \ + if not @socket or @socket.closed? + @command + end + private :command + + # + # POP protocol wrapper + # + + # Returns the number of messages on the POP server. + def n_mails + return @n_mails if @n_mails + @n_mails, @n_bytes = command().stat + @n_mails + end + + # Returns the total size in bytes of all the messages on the POP server. + def n_bytes + return @n_bytes if @n_bytes + @n_mails, @n_bytes = command().stat + @n_bytes + end + + # Returns an array of Net::POPMail objects, representing all the + # messages on the server. This array is renewed when the session + # restarts; otherwise, it is fetched from the server the first time + # this method is called (directly or indirectly) and cached. + # + # This method raises a POPError if an error occurs. + def mails + return @mails.dup if @mails + if n_mails() == 0 + # some popd raises error for LIST on the empty mailbox. + @mails = [] + return [] + end + + @mails = command().list.map {|num, size| + POPMail.new(num, size, self, command()) + } + @mails.dup + end + + # Yields each message to the passed-in block in turn. + # Equivalent to: + # + # pop3.mails.each do |popmail| + # .... + # end + # + # This method raises a POPError if an error occurs. + def each_mail(&block) # :yield: message + mails().each(&block) + end + + alias each each_mail + + # Deletes all messages on the server. + # + # If called with a block, yields each message in turn before deleting it. + # + # === Example + # + # n = 1 + # pop.delete_all do |m| + # File.open("inbox/#{n}") do |f| + # f.write m.pop + # end + # n += 1 + # end + # + # This method raises a POPError if an error occurs. + # + def delete_all # :yield: message + mails().each do |m| + yield m if block_given? + m.delete unless m.deleted? + end + end + + # Resets the session. This clears all "deleted" marks from messages. + # + # This method raises a POPError if an error occurs. + def reset + command().rset + mails().each do |m| + m.instance_eval { + @deleted = false + } + end + end + + def set_all_uids #:nodoc: internal use only (called from POPMail#uidl) + uidl = command().uidl + @mails.each {|m| m.uid = uidl[m.number] } + end + + # debugging output for +msg+ + def logging(msg) + @debug_output << msg + "\n" if @debug_output + end + + end # class POP3 + + # class aliases + POP = POP3 # :nodoc: + POPSession = POP3 # :nodoc: + POP3Session = POP3 # :nodoc: + + # + # This class is equivalent to POP3, except that it uses APOP authentication. + # + class APOP < POP3 + # Always returns true. + def apop? + true + end + end + + # class aliases + APOPSession = APOP + + # + # This class represents a message which exists on the POP server. + # Instances of this class are created by the POP3 class; they should + # not be directly created by the user. + # + class POPMail + + def initialize(num, len, pop, cmd) #:nodoc: + @number = num + @length = len + @pop = pop + @command = cmd + @deleted = false + @uid = nil + end + + # The sequence number of the message on the server. + attr_reader :number + + # The length of the message in octets. + attr_reader :length + alias size length + + # Provide human-readable stringification of class state. + def inspect + "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>" + end + + # + # This method fetches the message. If called with a block, the + # message is yielded to the block one chunk at a time. If called + # without a block, the message is returned as a String. The optional + # +dest+ argument will be prepended to the returned String; this + # argument is essentially obsolete. + # + # === Example without block + # + # POP3.start('pop.example.com', 110, + # 'YourAccount', 'YourPassword') do |pop| + # n = 1 + # pop.mails.each do |popmail| + # File.open("inbox/#{n}", 'w') do |f| + # f.write popmail.pop + # end + # popmail.delete + # n += 1 + # end + # end + # + # === Example with block + # + # POP3.start('pop.example.com', 110, + # 'YourAccount', 'YourPassword') do |pop| + # n = 1 + # pop.mails.each do |popmail| + # File.open("inbox/#{n}", 'w') do |f| + # popmail.pop do |chunk| #### + # f.write chunk + # end + # end + # n += 1 + # end + # end + # + # This method raises a POPError if an error occurs. + # + def pop( dest = '', &block ) # :yield: message_chunk + if block_given? + @command.retr(@number, &block) + nil + else + @command.retr(@number) do |chunk| + dest << chunk + end + dest + end + end + + alias all pop #:nodoc: obsolete + alias mail pop #:nodoc: obsolete + + # Fetches the message header and +lines+ lines of body. + # + # The optional +dest+ argument is obsolete. + # + # This method raises a POPError if an error occurs. + def top(lines, dest = '') + @command.top(@number, lines) do |chunk| + dest << chunk + end + dest + end + + # Fetches the message header. + # + # The optional +dest+ argument is obsolete. + # + # This method raises a POPError if an error occurs. + def header(dest = '') + top(0, dest) + end + + # Marks a message for deletion on the server. Deletion does not + # actually occur until the end of the session; deletion may be + # cancelled for _all_ marked messages by calling POP3#reset(). + # + # This method raises a POPError if an error occurs. + # + # === Example + # + # POP3.start('pop.example.com', 110, + # 'YourAccount', 'YourPassword') do |pop| + # n = 1 + # pop.mails.each do |popmail| + # File.open("inbox/#{n}", 'w') do |f| + # f.write popmail.pop + # end + # popmail.delete #### + # n += 1 + # end + # end + # + def delete + @command.dele @number + @deleted = true + end + + alias delete! delete #:nodoc: obsolete + + # True if the mail has been deleted. + def deleted? + @deleted + end + + # Returns the unique-id of the message. + # Normally the unique-id is a hash string of the message. + # + # This method raises a POPError if an error occurs. + def unique_id + return @uid if @uid + @pop.set_all_uids + @uid + end + + alias uidl unique_id + + def uid=(uid) #:nodoc: internal use only + @uid = uid + end + + end # class POPMail + + + class POP3Command #:nodoc: internal use only + + def initialize(sock) + @socket = sock + @error_occurred = false + res = check_response(critical { recv_response() }) + @apop_stamp = res.slice(/<[!-~]+@[!-~]+>/) + end + + attr_reader :socket + + def inspect + "#<#{self.class} socket=#{@socket}>" + end + + def auth(account, password) + check_response_auth(critical { + check_response_auth(get_response('USER %s', account)) + get_response('PASS %s', password) + }) + end + + def apop(account, password) + raise POPAuthenticationError, 'not APOP server; cannot login' \ + unless @apop_stamp + check_response_auth(critical { + get_response('APOP %s %s', + account, + Digest::MD5.hexdigest(@apop_stamp + password)) + }) + end + + def list + critical { + getok 'LIST' + list = [] + @socket.each_list_item do |line| + m = /\A(\d+)[ \t]+(\d+)/.match(line) or + raise POPBadResponse, "bad response: #{line}" + list.push [m[1].to_i, m[2].to_i] + end + return list + } + end + + def stat + res = check_response(critical { get_response('STAT') }) + m = /\A\+OK\s+(\d+)\s+(\d+)/.match(res) or + raise POPBadResponse, "wrong response format: #{res}" + [m[1].to_i, m[2].to_i] + end + + def rset + check_response(critical { get_response('RSET') }) + end + + def top(num, lines = 0, &block) + critical { + getok('TOP %d %d', num, lines) + @socket.each_message_chunk(&block) + } + end + + def retr(num, &block) + critical { + getok('RETR %d', num) + @socket.each_message_chunk(&block) + } + end + + def dele(num) + check_response(critical { get_response('DELE %d', num) }) + end + + def uidl(num = nil) + if num + res = check_response(critical { get_response('UIDL %d', num) }) + return res.split(/ /)[1] + else + critical { + getok('UIDL') + table = {} + @socket.each_list_item do |line| + num, uid = line.split + table[num.to_i] = uid + end + return table + } + end + end + + def quit + check_response(critical { get_response('QUIT') }) + end + + private + + def getok(fmt, *fargs) + @socket.writeline sprintf(fmt, *fargs) + check_response(recv_response()) + end + + def get_response(fmt, *fargs) + @socket.writeline sprintf(fmt, *fargs) + recv_response() + end + + def recv_response + @socket.readline + end + + def check_response(res) + raise POPError, res unless /\A\+OK/i =~ res + res + end + + def check_response_auth(res) + raise POPAuthenticationError, res unless /\A\+OK/i =~ res + res + end + + def critical + return '+OK dummy ok response' if @error_occurred + begin + return yield() + rescue Exception + @error_occurred = true + raise + end + end + + end # class POP3Command + +end # module Net diff --git a/gxg/standard/net/protocol.rb b/gxg/standard/net/protocol.rb new file mode 100644 index 0000000..efbe6bd --- /dev/null +++ b/gxg/standard/net/protocol.rb @@ -0,0 +1,442 @@ +# frozen_string_literal: true +# +# = net/protocol.rb +# +#-- +# Copyright (c) 1999-2004 Yukihiro Matsumoto +# Copyright (c) 1999-2004 Minero Aoki +# +# written and maintained by Minero Aoki +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms as Ruby itself, +# Ruby Distribute License or GNU General Public License. +# +# $Id: protocol.rb 58846 2017-05-22 07:36:00Z normal $ +#++ +# +# WARNING: This file is going to remove. +# Do not rely on the implementation written in this file. +# + +require 'socket' +require 'timeout' +require 'io/wait' + +module Net # :nodoc: + + class Protocol #:nodoc: internal use only + private + def Protocol.protocol_param(name, val) + module_eval(<<-End, __FILE__, __LINE__ + 1) + def #{name} + #{val} + end + End + end + + def ssl_socket_connect(s, timeout) + if timeout + while true + raise Net::OpenTimeout if timeout <= 0 + start = Process.clock_gettime Process::CLOCK_MONOTONIC + # to_io is required because SSLSocket doesn't have wait_readable yet + case s.connect_nonblock(exception: false) + when :wait_readable; s.to_io.wait_readable(timeout) + when :wait_writable; s.to_io.wait_writable(timeout) + else; break + end + timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + end + else + s.connect + end + end + end + + + class ProtocolError < StandardError; end + class ProtoSyntaxError < ProtocolError; end + class ProtoFatalError < ProtocolError; end + class ProtoUnknownError < ProtocolError; end + class ProtoServerError < ProtocolError; end + class ProtoAuthError < ProtocolError; end + class ProtoCommandError < ProtocolError; end + class ProtoRetriableError < ProtocolError; end + ProtocRetryError = ProtoRetriableError + + ## + # OpenTimeout, a subclass of Timeout::Error, is raised if a connection cannot + # be created within the open_timeout. + + class OpenTimeout < Timeout::Error; end + + ## + # ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the + # response cannot be read within the read_timeout. + + class ReadTimeout < Timeout::Error; end + + + class BufferedIO #:nodoc: internal use only + def initialize(io, read_timeout: 60, continue_timeout: nil, debug_output: nil) + @io = io + @read_timeout = read_timeout + @continue_timeout = continue_timeout + @debug_output = debug_output + @rbuf = ''.dup + end + + attr_reader :io + attr_accessor :read_timeout + attr_accessor :continue_timeout + attr_accessor :debug_output + + def inspect + "#<#{self.class} io=#{@io}>" + end + + def eof? + @io.eof? + end + + def closed? + @io.closed? + end + + def close + @io.close + end + + # + # Read + # + + public + + def read(len, dest = ''.dup, ignore_eof = false) + LOG "reading #{len} bytes..." + read_bytes = 0 + begin + while read_bytes + @rbuf.size < len + s = rbuf_consume(@rbuf.size) + read_bytes += s.size + dest << s + rbuf_fill + end + s = rbuf_consume(len - read_bytes) + read_bytes += s.size + dest << s + rescue EOFError + raise unless ignore_eof + end + LOG "read #{read_bytes} bytes" + dest + end + + def read_all(dest = ''.dup) + LOG 'reading all...' + read_bytes = 0 + begin + while true + s = rbuf_consume(@rbuf.size) + read_bytes += s.size + dest << s + rbuf_fill + end + rescue EOFError + ; + end + LOG "read #{read_bytes} bytes" + dest + end + + def readuntil(terminator, ignore_eof = false) + begin + until idx = @rbuf.index(terminator) + rbuf_fill + end + return rbuf_consume(idx + terminator.size) + rescue EOFError + raise unless ignore_eof + return rbuf_consume(@rbuf.size) + end + end + + def readline + readuntil("\n").chop + end + + private + + BUFSIZE = 1024 * 16 + + def rbuf_fill + case rv = @io.read_nonblock(BUFSIZE, exception: false) + when String + @rbuf << rv + rv.clear + return + when :wait_readable + @io.to_io.wait_readable(@read_timeout) or raise Net::ReadTimeout + # continue looping + when :wait_writable + # OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable. + # http://www.openssl.org/support/faq.html#PROG10 + @io.to_io.wait_writable(@read_timeout) or raise Net::ReadTimeout + # continue looping + when nil + raise EOFError, 'end of file reached' + end while true + end + + def rbuf_consume(len) + s = @rbuf.slice!(0, len) + @debug_output << %Q[-> #{s.dump}\n] if @debug_output + s + end + + # + # Write + # + + public + + def write(str) + writing { + write0 str + } + end + + alias << write + + def writeline(str) + writing { + write0 str + "\r\n" + } + end + + private + + def writing + @written_bytes = 0 + @debug_output << '<- ' if @debug_output + yield + @debug_output << "\n" if @debug_output + bytes = @written_bytes + @written_bytes = nil + bytes + end + + def write0(str) + @debug_output << str.dump if @debug_output + len = @io.write(str) + @written_bytes += len + len + end + + # + # Logging + # + + private + + def LOG_off + @save_debug_out = @debug_output + @debug_output = nil + end + + def LOG_on + @debug_output = @save_debug_out + end + + def LOG(msg) + return unless @debug_output + @debug_output << msg + "\n" + end + end + + + class InternetMessageIO < BufferedIO #:nodoc: internal use only + def initialize(*) + super + @wbuf = nil + end + + # + # Read + # + + def each_message_chunk + LOG 'reading message...' + LOG_off() + read_bytes = 0 + while (line = readuntil("\r\n")) != ".\r\n" + read_bytes += line.size + yield line.sub(/\A\./, '') + end + LOG_on() + LOG "read message (#{read_bytes} bytes)" + end + + # *library private* (cannot handle 'break') + def each_list_item + while (str = readuntil("\r\n")) != ".\r\n" + yield str.chop + end + end + + def write_message_0(src) + prev = @written_bytes + each_crlf_line(src) do |line| + write0 dot_stuff(line) + end + @written_bytes - prev + end + + # + # Write + # + + def write_message(src) + LOG "writing message from #{src.class}" + LOG_off() + len = writing { + using_each_crlf_line { + write_message_0 src + } + } + LOG_on() + LOG "wrote #{len} bytes" + len + end + + def write_message_by_block(&block) + LOG 'writing message from block' + LOG_off() + len = writing { + using_each_crlf_line { + begin + block.call(WriteAdapter.new(self, :write_message_0)) + rescue LocalJumpError + # allow `break' from writer block + end + } + } + LOG_on() + LOG "wrote #{len} bytes" + len + end + + private + + def dot_stuff(s) + s.sub(/\A\./, '..') + end + + def using_each_crlf_line + @wbuf = ''.dup + yield + if not @wbuf.empty? # unterminated last line + write0 dot_stuff(@wbuf.chomp) + "\r\n" + elsif @written_bytes == 0 # empty src + write0 "\r\n" + end + write0 ".\r\n" + @wbuf = nil + end + + def each_crlf_line(src) + buffer_filling(@wbuf, src) do + while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/) + yield line.chomp("\n") + "\r\n" + end + end + end + + def buffer_filling(buf, src) + case src + when String # for speeding up. + 0.step(src.size - 1, 1024) do |i| + buf << src[i, 1024] + yield + end + when File # for speeding up. + while s = src.read(1024) + buf << s + yield + end + else # generic reader + src.each do |str| + buf << str + yield if buf.size > 1024 + end + yield unless buf.empty? + end + end + end + + + # + # The writer adapter class + # + class WriteAdapter + def initialize(socket, method) + @socket = socket + @method_id = method + end + + def inspect + "#<#{self.class} socket=#{@socket.inspect}>" + end + + def write(str) + @socket.__send__(@method_id, str) + end + + alias print write + + def <<(str) + write str + self + end + + def puts(str = '') + write str.chomp("\n") + "\n" + end + + def printf(*args) + write sprintf(*args) + end + end + + + class ReadAdapter #:nodoc: internal use only + def initialize(block) + @block = block + end + + def inspect + "#<#{self.class}>" + end + + def <<(str) + call_block(str, &@block) if @block + end + + private + + # This method is needed because @block must be called by yield, + # not Proc#call. You can see difference when using `break' in + # the block. + def call_block(str) + yield str + end + end + + + module NetPrivate #:nodoc: obsolete + Socket = ::Net::InternetMessageIO + end + +end # module Net diff --git a/gxg/standard/net/smtp.rb b/gxg/standard/net/smtp.rb new file mode 100644 index 0000000..8b00fd7 --- /dev/null +++ b/gxg/standard/net/smtp.rb @@ -0,0 +1,1078 @@ +# frozen_string_literal: true +# = net/smtp.rb +# +# Copyright (c) 1999-2007 Yukihiro Matsumoto. +# +# Copyright (c) 1999-2007 Minero Aoki. +# +# Written & maintained by Minero Aoki . +# +# Documented by William Webber and Minero Aoki. +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms as Ruby itself. +# +# $Id: smtp.rb 59381 2017-07-20 23:34:03Z kazu $ +# +# See Net::SMTP for documentation. +# + +require 'net/protocol' +require 'digest/md5' +require 'timeout' +begin + require 'openssl' +rescue LoadError +end + +module Net + + # Module mixed in to all SMTP error classes + module SMTPError + # This *class* is a module for backward compatibility. + # In later release, this module becomes a class. + end + + # Represents an SMTP authentication error. + class SMTPAuthenticationError < ProtoAuthError + include SMTPError + end + + # Represents SMTP error code 420 or 450, a temporary error. + class SMTPServerBusy < ProtoServerError + include SMTPError + end + + # Represents an SMTP command syntax error (error code 500) + class SMTPSyntaxError < ProtoSyntaxError + include SMTPError + end + + # Represents a fatal SMTP error (error code 5xx, except for 500) + class SMTPFatalError < ProtoFatalError + include SMTPError + end + + # Unexpected reply code returned from server. + class SMTPUnknownError < ProtoUnknownError + include SMTPError + end + + # Command is not supported on server. + class SMTPUnsupportedCommand < ProtocolError + include SMTPError + end + + # + # == What is This Library? + # + # This library provides functionality to send internet + # mail via SMTP, the Simple Mail Transfer Protocol. For details of + # SMTP itself, see [RFC2821] (http://www.ietf.org/rfc/rfc2821.txt). + # + # == What is This Library NOT? + # + # This library does NOT provide functions to compose internet mails. + # You must create them by yourself. If you want better mail support, + # try RubyMail or TMail or search for alternatives in + # {RubyGems.org}[https://rubygems.org/] or {The Ruby + # Toolbox}[https://www.ruby-toolbox.com/]. + # + # FYI: the official documentation on internet mail is: [RFC2822] (http://www.ietf.org/rfc/rfc2822.txt). + # + # == Examples + # + # === Sending Messages + # + # You must open a connection to an SMTP server before sending messages. + # The first argument is the address of your SMTP server, and the second + # argument is the port number. Using SMTP.start with a block is the simplest + # way to do this. This way, the SMTP connection is closed automatically + # after the block is executed. + # + # require 'net/smtp' + # Net::SMTP.start('your.smtp.server', 25) do |smtp| + # # Use the SMTP object smtp only in this block. + # end + # + # Replace 'your.smtp.server' with your SMTP server. Normally + # your system manager or internet provider supplies a server + # for you. + # + # Then you can send messages. + # + # msgstr = < + # To: Destination Address + # Subject: test message + # Date: Sat, 23 Jun 2001 16:26:43 +0900 + # Message-Id: + # + # This is a test message. + # END_OF_MESSAGE + # + # require 'net/smtp' + # Net::SMTP.start('your.smtp.server', 25) do |smtp| + # smtp.send_message msgstr, + # 'your@mail.address', + # 'his_address@example.com' + # end + # + # === Closing the Session + # + # You MUST close the SMTP session after sending messages, by calling + # the #finish method: + # + # # using SMTP#finish + # smtp = Net::SMTP.start('your.smtp.server', 25) + # smtp.send_message msgstr, 'from@address', 'to@address' + # smtp.finish + # + # You can also use the block form of SMTP.start/SMTP#start. This closes + # the SMTP session automatically: + # + # # using block form of SMTP.start + # Net::SMTP.start('your.smtp.server', 25) do |smtp| + # smtp.send_message msgstr, 'from@address', 'to@address' + # end + # + # I strongly recommend this scheme. This form is simpler and more robust. + # + # === HELO domain + # + # In almost all situations, you must provide a third argument + # to SMTP.start/SMTP#start. This is the domain name which you are on + # (the host to send mail from). It is called the "HELO domain". + # The SMTP server will judge whether it should send or reject + # the SMTP session by inspecting the HELO domain. + # + # Net::SMTP.start('your.smtp.server', 25, + # 'mail.from.domain') { |smtp| ... } + # + # === SMTP Authentication + # + # The Net::SMTP class supports three authentication schemes; + # PLAIN, LOGIN and CRAM MD5. (SMTP Authentication: [RFC2554]) + # To use SMTP authentication, pass extra arguments to + # SMTP.start/SMTP#start. + # + # # PLAIN + # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain', + # 'Your Account', 'Your Password', :plain) + # # LOGIN + # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain', + # 'Your Account', 'Your Password', :login) + # + # # CRAM MD5 + # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain', + # 'Your Account', 'Your Password', :cram_md5) + # + class SMTP < Protocol + + Revision = %q$Revision: 59381 $.split[1] + + # The default SMTP port number, 25. + def SMTP.default_port + 25 + end + + # The default mail submission port number, 587. + def SMTP.default_submission_port + 587 + end + + # The default SMTPS port number, 465. + def SMTP.default_tls_port + 465 + end + + class << self + alias default_ssl_port default_tls_port + end + + def SMTP.default_ssl_context + OpenSSL::SSL::SSLContext.new + end + + # + # Creates a new Net::SMTP object. + # + # +address+ is the hostname or ip address of your SMTP + # server. +port+ is the port to connect to; it defaults to + # port 25. + # + # This method does not open the TCP connection. You can use + # SMTP.start instead of SMTP.new if you want to do everything + # at once. Otherwise, follow SMTP.new with SMTP#start. + # + def initialize(address, port = nil) + @address = address + @port = (port || SMTP.default_port) + @esmtp = true + @capabilities = nil + @socket = nil + @started = false + @open_timeout = 30 + @read_timeout = 60 + @error_occurred = false + @debug_output = nil + @tls = false + @starttls = false + @ssl_context = nil + end + + # Provide human-readable stringification of class state. + def inspect + "#<#{self.class} #{@address}:#{@port} started=#{@started}>" + end + + # + # Set whether to use ESMTP or not. This should be done before + # calling #start. Note that if #start is called in ESMTP mode, + # and the connection fails due to a ProtocolError, the SMTP + # object will automatically switch to plain SMTP mode and + # retry (but not vice versa). + # + attr_accessor :esmtp + + # +true+ if the SMTP object uses ESMTP (which it does by default). + alias :esmtp? :esmtp + + # true if server advertises STARTTLS. + # You cannot get valid value before opening SMTP session. + def capable_starttls? + capable?('STARTTLS') + end + + def capable?(key) + return nil unless @capabilities + @capabilities[key] ? true : false + end + private :capable? + + # true if server advertises AUTH PLAIN. + # You cannot get valid value before opening SMTP session. + def capable_plain_auth? + auth_capable?('PLAIN') + end + + # true if server advertises AUTH LOGIN. + # You cannot get valid value before opening SMTP session. + def capable_login_auth? + auth_capable?('LOGIN') + end + + # true if server advertises AUTH CRAM-MD5. + # You cannot get valid value before opening SMTP session. + def capable_cram_md5_auth? + auth_capable?('CRAM-MD5') + end + + def auth_capable?(type) + return nil unless @capabilities + return false unless @capabilities['AUTH'] + @capabilities['AUTH'].include?(type) + end + private :auth_capable? + + # Returns supported authentication methods on this server. + # You cannot get valid value before opening SMTP session. + def capable_auth_types + return [] unless @capabilities + return [] unless @capabilities['AUTH'] + @capabilities['AUTH'] + end + + # true if this object uses SMTP/TLS (SMTPS). + def tls? + @tls + end + + alias ssl? tls? + + # Enables SMTP/TLS (SMTPS: SMTP over direct TLS connection) for + # this object. Must be called before the connection is established + # to have any effect. +context+ is a OpenSSL::SSL::SSLContext object. + def enable_tls(context = SMTP.default_ssl_context) + raise 'openssl library not installed' unless defined?(OpenSSL) + raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls + @tls = true + @ssl_context = context + end + + alias enable_ssl enable_tls + + # Disables SMTP/TLS for this object. Must be called before the + # connection is established to have any effect. + def disable_tls + @tls = false + @ssl_context = nil + end + + alias disable_ssl disable_tls + + # Returns truth value if this object uses STARTTLS. + # If this object always uses STARTTLS, returns :always. + # If this object uses STARTTLS when the server support TLS, returns :auto. + def starttls? + @starttls + end + + # true if this object uses STARTTLS. + def starttls_always? + @starttls == :always + end + + # true if this object uses STARTTLS when server advertises STARTTLS. + def starttls_auto? + @starttls == :auto + end + + # Enables SMTP/TLS (STARTTLS) for this object. + # +context+ is a OpenSSL::SSL::SSLContext object. + def enable_starttls(context = SMTP.default_ssl_context) + raise 'openssl library not installed' unless defined?(OpenSSL) + raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls + @starttls = :always + @ssl_context = context + end + + # Enables SMTP/TLS (STARTTLS) for this object if server accepts. + # +context+ is a OpenSSL::SSL::SSLContext object. + def enable_starttls_auto(context = SMTP.default_ssl_context) + raise 'openssl library not installed' unless defined?(OpenSSL) + raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls + @starttls = :auto + @ssl_context = context + end + + # Disables SMTP/TLS (STARTTLS) for this object. Must be called + # before the connection is established to have any effect. + def disable_starttls + @starttls = false + @ssl_context = nil + end + + # The address of the SMTP server to connect to. + attr_reader :address + + # The port number of the SMTP server to connect to. + attr_reader :port + + # Seconds to wait while attempting to open a connection. + # If the connection cannot be opened within this time, a + # Net::OpenTimeout is raised. The default value is 30 seconds. + attr_accessor :open_timeout + + # Seconds to wait while reading one block (by one read(2) call). + # If the read(2) call does not complete within this time, a + # Net::ReadTimeout is raised. The default value is 60 seconds. + attr_reader :read_timeout + + # Set the number of seconds to wait until timing-out a read(2) + # call. + def read_timeout=(sec) + @socket.read_timeout = sec if @socket + @read_timeout = sec + end + + # + # WARNING: This method causes serious security holes. + # Use this method for only debugging. + # + # Set an output stream for debug logging. + # You must call this before #start. + # + # # example + # smtp = Net::SMTP.new(addr, port) + # smtp.set_debug_output $stderr + # smtp.start do |smtp| + # .... + # end + # + def debug_output=(arg) + @debug_output = arg + end + + alias set_debug_output debug_output= + + # + # SMTP session control + # + + # + # Creates a new Net::SMTP object and connects to the server. + # + # This method is equivalent to: + # + # Net::SMTP.new(address, port).start(helo_domain, account, password, authtype) + # + # === Example + # + # Net::SMTP.start('your.smtp.server') do |smtp| + # smtp.send_message msgstr, 'from@example.com', ['dest@example.com'] + # end + # + # === Block Usage + # + # If called with a block, the newly-opened Net::SMTP object is yielded + # to the block, and automatically closed when the block finishes. If called + # without a block, the newly-opened Net::SMTP object is returned to + # the caller, and it is the caller's responsibility to close it when + # finished. + # + # === Parameters + # + # +address+ is the hostname or ip address of your smtp server. + # + # +port+ is the port to connect to; it defaults to port 25. + # + # +helo+ is the _HELO_ _domain_ provided by the client to the + # server (see overview comments); it defaults to 'localhost'. + # + # The remaining arguments are used for SMTP authentication, if required + # or desired. +user+ is the account name; +secret+ is your password + # or other authentication token; and +authtype+ is the authentication + # type, one of :plain, :login, or :cram_md5. See the discussion of + # SMTP Authentication in the overview notes. + # + # === Errors + # + # This method may raise: + # + # * Net::SMTPAuthenticationError + # * Net::SMTPServerBusy + # * Net::SMTPSyntaxError + # * Net::SMTPFatalError + # * Net::SMTPUnknownError + # * Net::OpenTimeout + # * Net::ReadTimeout + # * IOError + # + def SMTP.start(address, port = nil, helo = 'localhost', + user = nil, secret = nil, authtype = nil, + &block) # :yield: smtp + new(address, port).start(helo, user, secret, authtype, &block) + end + + # +true+ if the SMTP session has been started. + def started? + @started + end + + # + # Opens a TCP connection and starts the SMTP session. + # + # === Parameters + # + # +helo+ is the _HELO_ _domain_ that you'll dispatch mails from; see + # the discussion in the overview notes. + # + # If both of +user+ and +secret+ are given, SMTP authentication + # will be attempted using the AUTH command. +authtype+ specifies + # the type of authentication to attempt; it must be one of + # :login, :plain, and :cram_md5. See the notes on SMTP Authentication + # in the overview. + # + # === Block Usage + # + # When this methods is called with a block, the newly-started SMTP + # object is yielded to the block, and automatically closed after + # the block call finishes. Otherwise, it is the caller's + # responsibility to close the session when finished. + # + # === Example + # + # This is very similar to the class method SMTP.start. + # + # require 'net/smtp' + # smtp = Net::SMTP.new('smtp.mail.server', 25) + # smtp.start(helo_domain, account, password, authtype) do |smtp| + # smtp.send_message msgstr, 'from@example.com', ['dest@example.com'] + # end + # + # The primary use of this method (as opposed to SMTP.start) + # is probably to set debugging (#set_debug_output) or ESMTP + # (#esmtp=), which must be done before the session is + # started. + # + # === Errors + # + # If session has already been started, an IOError will be raised. + # + # This method may raise: + # + # * Net::SMTPAuthenticationError + # * Net::SMTPServerBusy + # * Net::SMTPSyntaxError + # * Net::SMTPFatalError + # * Net::SMTPUnknownError + # * Net::OpenTimeout + # * Net::ReadTimeout + # * IOError + # + def start(helo = 'localhost', + user = nil, secret = nil, authtype = nil) # :yield: smtp + if block_given? + begin + do_start helo, user, secret, authtype + return yield(self) + ensure + do_finish + end + else + do_start helo, user, secret, authtype + return self + end + end + + # Finishes the SMTP session and closes TCP connection. + # Raises IOError if not started. + def finish + raise IOError, 'not yet started' unless started? + do_finish + end + + private + + def tcp_socket(address, port) + TCPSocket.open address, port + end + + def do_start(helo_domain, user, secret, authtype) + raise IOError, 'SMTP session already started' if @started + if user or secret + check_auth_method(authtype || DEFAULT_AUTH_TYPE) + check_auth_args user, secret + end + s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do + tcp_socket(@address, @port) + end + logging "Connection opened: #{@address}:#{@port}" + @socket = new_internet_message_io(tls? ? tlsconnect(s) : s) + check_response critical { recv_response() } + do_helo helo_domain + if starttls_always? or (capable_starttls? and starttls_auto?) + unless capable_starttls? + raise SMTPUnsupportedCommand, + "STARTTLS is not supported on this server" + end + starttls + @socket = new_internet_message_io(tlsconnect(s)) + # helo response may be different after STARTTLS + do_helo helo_domain + end + authenticate user, secret, (authtype || DEFAULT_AUTH_TYPE) if user + @started = true + ensure + unless @started + # authentication failed, cancel connection. + s.close if s + @socket = nil + end + end + + def ssl_socket(socket, context) + OpenSSL::SSL::SSLSocket.new socket, context + end + + def tlsconnect(s) + verified = false + s = ssl_socket(s, @ssl_context) + logging "TLS connection started" + s.sync_close = true + ssl_socket_connect(s, @open_timeout) + if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE + s.post_connection_check(@address) + end + verified = true + s + ensure + s.close unless verified + end + + def new_internet_message_io(s) + InternetMessageIO.new(s, read_timeout: @read_timeout, + debug_output: @debug_output) + end + + def do_helo(helo_domain) + res = @esmtp ? ehlo(helo_domain) : helo(helo_domain) + @capabilities = res.capabilities + rescue SMTPError + if @esmtp + @esmtp = false + @error_occurred = false + retry + end + raise + end + + def do_finish + quit if @socket and not @socket.closed? and not @error_occurred + ensure + @started = false + @error_occurred = false + @socket.close if @socket + @socket = nil + end + + # + # Message Sending + # + + public + + # + # Sends +msgstr+ as a message. Single CR ("\r") and LF ("\n") found + # in the +msgstr+, are converted into the CR LF pair. You cannot send a + # binary message with this method. +msgstr+ should include both + # the message headers and body. + # + # +from_addr+ is a String representing the source mail address. + # + # +to_addr+ is a String or Strings or Array of Strings, representing + # the destination mail address or addresses. + # + # === Example + # + # Net::SMTP.start('smtp.example.com') do |smtp| + # smtp.send_message msgstr, + # 'from@example.com', + # ['dest@example.com', 'dest2@example.com'] + # end + # + # === Errors + # + # This method may raise: + # + # * Net::SMTPServerBusy + # * Net::SMTPSyntaxError + # * Net::SMTPFatalError + # * Net::SMTPUnknownError + # * Net::ReadTimeout + # * IOError + # + def send_message(msgstr, from_addr, *to_addrs) + raise IOError, 'closed session' unless @socket + mailfrom from_addr + rcptto_list(to_addrs) {data msgstr} + end + + alias send_mail send_message + alias sendmail send_message # obsolete + + # + # Opens a message writer stream and gives it to the block. + # The stream is valid only in the block, and has these methods: + # + # puts(str = ''):: outputs STR and CR LF. + # print(str):: outputs STR. + # printf(fmt, *args):: outputs sprintf(fmt,*args). + # write(str):: outputs STR and returns the length of written bytes. + # <<(str):: outputs STR and returns self. + # + # If a single CR ("\r") or LF ("\n") is found in the message, + # it is converted to the CR LF pair. You cannot send a binary + # message with this method. + # + # === Parameters + # + # +from_addr+ is a String representing the source mail address. + # + # +to_addr+ is a String or Strings or Array of Strings, representing + # the destination mail address or addresses. + # + # === Example + # + # Net::SMTP.start('smtp.example.com', 25) do |smtp| + # smtp.open_message_stream('from@example.com', ['dest@example.com']) do |f| + # f.puts 'From: from@example.com' + # f.puts 'To: dest@example.com' + # f.puts 'Subject: test message' + # f.puts + # f.puts 'This is a test message.' + # end + # end + # + # === Errors + # + # This method may raise: + # + # * Net::SMTPServerBusy + # * Net::SMTPSyntaxError + # * Net::SMTPFatalError + # * Net::SMTPUnknownError + # * Net::ReadTimeout + # * IOError + # + def open_message_stream(from_addr, *to_addrs, &block) # :yield: stream + raise IOError, 'closed session' unless @socket + mailfrom from_addr + rcptto_list(to_addrs) {data(&block)} + end + + alias ready open_message_stream # obsolete + + # + # Authentication + # + + public + + DEFAULT_AUTH_TYPE = :plain + + def authenticate(user, secret, authtype = DEFAULT_AUTH_TYPE) + check_auth_method authtype + check_auth_args user, secret + send auth_method(authtype), user, secret + end + + def auth_plain(user, secret) + check_auth_args user, secret + res = critical { + get_response('AUTH PLAIN ' + base64_encode("\0#{user}\0#{secret}")) + } + check_auth_response res + res + end + + def auth_login(user, secret) + check_auth_args user, secret + res = critical { + check_auth_continue get_response('AUTH LOGIN') + check_auth_continue get_response(base64_encode(user)) + get_response(base64_encode(secret)) + } + check_auth_response res + res + end + + def auth_cram_md5(user, secret) + check_auth_args user, secret + res = critical { + res0 = get_response('AUTH CRAM-MD5') + check_auth_continue res0 + crammed = cram_md5_response(secret, res0.cram_md5_challenge) + get_response(base64_encode("#{user} #{crammed}")) + } + check_auth_response res + res + end + + private + + def check_auth_method(type) + unless respond_to?(auth_method(type), true) + raise ArgumentError, "wrong authentication type #{type}" + end + end + + def auth_method(type) + "auth_#{type.to_s.downcase}".intern + end + + def check_auth_args(user, secret, authtype = DEFAULT_AUTH_TYPE) + unless user + raise ArgumentError, 'SMTP-AUTH requested but missing user name' + end + unless secret + raise ArgumentError, 'SMTP-AUTH requested but missing secret phrase' + end + end + + def base64_encode(str) + # expects "str" may not become too long + [str].pack('m0') + end + + IMASK = 0x36 + OMASK = 0x5c + + # CRAM-MD5: [RFC2195] + def cram_md5_response(secret, challenge) + tmp = Digest::MD5.digest(cram_secret(secret, IMASK) + challenge) + Digest::MD5.hexdigest(cram_secret(secret, OMASK) + tmp) + end + + CRAM_BUFSIZE = 64 + + def cram_secret(secret, mask) + secret = Digest::MD5.digest(secret) if secret.size > CRAM_BUFSIZE + buf = secret.ljust(CRAM_BUFSIZE, "\0") + 0.upto(buf.size - 1) do |i| + buf[i] = (buf[i].ord ^ mask).chr + end + buf + end + + # + # SMTP command dispatcher + # + + public + + # Aborts the current mail transaction + + def rset + getok('RSET') + end + + def starttls + getok('STARTTLS') + end + + def helo(domain) + getok("HELO #{domain}") + end + + def ehlo(domain) + getok("EHLO #{domain}") + end + + def mailfrom(from_addr) + if $SAFE > 0 + raise SecurityError, 'tainted from_addr' if from_addr.tainted? + end + getok("MAIL FROM:<#{from_addr}>") + end + + def rcptto_list(to_addrs) + raise ArgumentError, 'mail destination not given' if to_addrs.empty? + ok_users = [] + unknown_users = [] + to_addrs.flatten.each do |addr| + begin + rcptto addr + rescue SMTPAuthenticationError + unknown_users << addr.dump + else + ok_users << addr + end + end + raise ArgumentError, 'mail destination not given' if ok_users.empty? + ret = yield + unless unknown_users.empty? + raise SMTPAuthenticationError, "failed to deliver for #{unknown_users.join(', ')}" + end + ret + end + + def rcptto(to_addr) + if $SAFE > 0 + raise SecurityError, 'tainted to_addr' if to_addr.tainted? + end + getok("RCPT TO:<#{to_addr}>") + end + + # This method sends a message. + # If +msgstr+ is given, sends it as a message. + # If block is given, yield a message writer stream. + # You must write message before the block is closed. + # + # # Example 1 (by string) + # smtp.data(< +# All rights reserved. +# +# = Licence +# This program is licensed under the same licence as Ruby. +# (See the file 'LICENCE'.) +#++ + +module OpenSSL + class BN + include Comparable + + def pretty_print(q) + q.object_group(self) { + q.text ' ' + q.text to_i.to_s + } + end + end # BN +end # OpenSSL + +## +#-- +# Add double dispatch to Integer +#++ +class Integer + # Casts an Integer as an OpenSSL::BN + # + # See `man bn` for more info. + def to_bn + OpenSSL::BN::new(self) + end +end # Integer diff --git a/gxg/standard/openssl/buffering.rb b/gxg/standard/openssl/buffering.rb new file mode 100644 index 0000000..ccbcbf1 --- /dev/null +++ b/gxg/standard/openssl/buffering.rb @@ -0,0 +1,455 @@ +# coding: binary +# frozen_string_literal: false +#-- +#= Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2001 GOTOU YUUZOU +# All rights reserved. +# +#= Licence +# This program is licensed under the same licence as Ruby. +# (See the file 'LICENCE'.) +#++ + +## +# OpenSSL IO buffering mix-in module. +# +# This module allows an OpenSSL::SSL::SSLSocket to behave like an IO. +# +# You typically won't use this module directly, you can see it implemented in +# OpenSSL::SSL::SSLSocket. + +module OpenSSL::Buffering + include Enumerable + + ## + # The "sync mode" of the SSLSocket. + # + # See IO#sync for full details. + + attr_accessor :sync + + ## + # Default size to read from or write to the SSLSocket for buffer operations. + + BLOCK_SIZE = 1024*16 + + ## + # Creates an instance of OpenSSL's buffering IO module. + + def initialize(*) + super + @eof = false + @rbuffer = "" + @sync = @io.sync + end + + # + # for reading. + # + private + + ## + # Fills the buffer from the underlying SSLSocket + + def fill_rbuff + begin + @rbuffer << self.sysread(BLOCK_SIZE) + rescue Errno::EAGAIN + retry + rescue EOFError + @eof = true + end + end + + ## + # Consumes _size_ bytes from the buffer + + def consume_rbuff(size=nil) + if @rbuffer.empty? + nil + else + size = @rbuffer.size unless size + ret = @rbuffer[0, size] + @rbuffer[0, size] = "" + ret + end + end + + public + + ## + # Reads _size_ bytes from the stream. If _buf_ is provided it must + # reference a string which will receive the data. + # + # See IO#read for full details. + + def read(size=nil, buf=nil) + if size == 0 + if buf + buf.clear + return buf + else + return "" + end + end + until @eof + break if size && size <= @rbuffer.size + fill_rbuff + end + ret = consume_rbuff(size) || "" + if buf + buf.replace(ret) + ret = buf + end + (size && ret.empty?) ? nil : ret + end + + ## + # Reads at most _maxlen_ bytes from the stream. If _buf_ is provided it + # must reference a string which will receive the data. + # + # See IO#readpartial for full details. + + def readpartial(maxlen, buf=nil) + if maxlen == 0 + if buf + buf.clear + return buf + else + return "" + end + end + if @rbuffer.empty? + begin + return sysread(maxlen, buf) + rescue Errno::EAGAIN + retry + end + end + ret = consume_rbuff(maxlen) + if buf + buf.replace(ret) + ret = buf + end + ret + end + + ## + # Reads at most _maxlen_ bytes in the non-blocking manner. + # + # When no data can be read without blocking it raises + # OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable. + # + # IO::WaitReadable means SSL needs to read internally so read_nonblock + # should be called again when the underlying IO is readable. + # + # IO::WaitWritable means SSL needs to write internally so read_nonblock + # should be called again after the underlying IO is writable. + # + # OpenSSL::Buffering#read_nonblock needs two rescue clause as follows: + # + # # emulates blocking read (readpartial). + # begin + # result = ssl.read_nonblock(maxlen) + # rescue IO::WaitReadable + # IO.select([io]) + # retry + # rescue IO::WaitWritable + # IO.select(nil, [io]) + # retry + # end + # + # Note that one reason that read_nonblock writes to the underlying IO is + # when the peer requests a new TLS/SSL handshake. See openssl the FAQ for + # more details. http://www.openssl.org/support/faq.html + # + # By specifying a keyword argument _exception_ to +false+, you can indicate + # that read_nonblock should not raise an IO::Wait*able exception, but + # return the symbol +:wait_writable+ or +:wait_readable+ instead. At EOF, + # it will return +nil+ instead of raising EOFError. + + def read_nonblock(maxlen, buf=nil, exception: true) + if maxlen == 0 + if buf + buf.clear + return buf + else + return "" + end + end + if @rbuffer.empty? + return sysread_nonblock(maxlen, buf, exception: exception) + end + ret = consume_rbuff(maxlen) + if buf + buf.replace(ret) + ret = buf + end + ret + end + + ## + # Reads the next "line" from the stream. Lines are separated by _eol_. If + # _limit_ is provided the result will not be longer than the given number of + # bytes. + # + # _eol_ may be a String or Regexp. + # + # Unlike IO#gets the line read will not be assigned to +$_+. + # + # Unlike IO#gets the separator must be provided if a limit is provided. + + def gets(eol=$/, limit=nil) + idx = @rbuffer.index(eol) + until @eof + break if idx + fill_rbuff + idx = @rbuffer.index(eol) + end + if eol.is_a?(Regexp) + size = idx ? idx+$&.size : nil + else + size = idx ? idx+eol.size : nil + end + if size && limit && limit >= 0 + size = [size, limit].min + end + consume_rbuff(size) + end + + ## + # Executes the block for every line in the stream where lines are separated + # by _eol_. + # + # See also #gets + + def each(eol=$/) + while line = self.gets(eol) + yield line + end + end + alias each_line each + + ## + # Reads lines from the stream which are separated by _eol_. + # + # See also #gets + + def readlines(eol=$/) + ary = [] + while line = self.gets(eol) + ary << line + end + ary + end + + ## + # Reads a line from the stream which is separated by _eol_. + # + # Raises EOFError if at end of file. + + def readline(eol=$/) + raise EOFError if eof? + gets(eol) + end + + ## + # Reads one character from the stream. Returns nil if called at end of + # file. + + def getc + read(1) + end + + ## + # Calls the given block once for each byte in the stream. + + def each_byte # :yields: byte + while c = getc + yield(c.ord) + end + end + + ## + # Reads a one-character string from the stream. Raises an EOFError at end + # of file. + + def readchar + raise EOFError if eof? + getc + end + + ## + # Pushes character _c_ back onto the stream such that a subsequent buffered + # character read will return it. + # + # Unlike IO#getc multiple bytes may be pushed back onto the stream. + # + # Has no effect on unbuffered reads (such as #sysread). + + def ungetc(c) + @rbuffer[0,0] = c.chr + end + + ## + # Returns true if the stream is at file which means there is no more data to + # be read. + + def eof? + fill_rbuff if !@eof && @rbuffer.empty? + @eof && @rbuffer.empty? + end + alias eof eof? + + # + # for writing. + # + private + + ## + # Writes _s_ to the buffer. When the buffer is full or #sync is true the + # buffer is flushed to the underlying socket. + + def do_write(s) + @wbuffer = "" unless defined? @wbuffer + @wbuffer << s + @wbuffer.force_encoding(Encoding::BINARY) + @sync ||= false + if @sync or @wbuffer.size > BLOCK_SIZE or @wbuffer.rindex("\n") + until @wbuffer.empty? + begin + nwrote = syswrite(@wbuffer) + rescue Errno::EAGAIN + retry + end + @wbuffer[0, nwrote] = "" + end + end + end + + public + + ## + # Writes _s_ to the stream. If the argument is not a String it will be + # converted using +.to_s+ method. Returns the number of bytes written. + + def write(*s) + s.inject(0) do |written, str| + do_write(str) + written + str.bytesize + end + end + + ## + # Writes _s_ in the non-blocking manner. + # + # If there is buffered data, it is flushed first. This may block. + # + # write_nonblock returns number of bytes written to the SSL connection. + # + # When no data can be written without blocking it raises + # OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable. + # + # IO::WaitReadable means SSL needs to read internally so write_nonblock + # should be called again after the underlying IO is readable. + # + # IO::WaitWritable means SSL needs to write internally so write_nonblock + # should be called again after underlying IO is writable. + # + # So OpenSSL::Buffering#write_nonblock needs two rescue clause as follows. + # + # # emulates blocking write. + # begin + # result = ssl.write_nonblock(str) + # rescue IO::WaitReadable + # IO.select([io]) + # retry + # rescue IO::WaitWritable + # IO.select(nil, [io]) + # retry + # end + # + # Note that one reason that write_nonblock reads from the underlying IO + # is when the peer requests a new TLS/SSL handshake. See the openssl FAQ + # for more details. http://www.openssl.org/support/faq.html + # + # By specifying a keyword argument _exception_ to +false+, you can indicate + # that write_nonblock should not raise an IO::Wait*able exception, but + # return the symbol +:wait_writable+ or +:wait_readable+ instead. + + def write_nonblock(s, exception: true) + flush + syswrite_nonblock(s, exception: exception) + end + + ## + # Writes _s_ to the stream. _s_ will be converted to a String using + # +.to_s+ method. + + def <<(s) + do_write(s) + self + end + + ## + # Writes _args_ to the stream along with a record separator. + # + # See IO#puts for full details. + + def puts(*args) + s = "" + if args.empty? + s << "\n" + end + args.each{|arg| + s << arg.to_s + s.sub!(/(? +# All rights reserved. +# +# = Licence +# This program is licensed under the same licence as Ruby. +# (See the file 'LICENCE'.) +#++ + +module OpenSSL + class Cipher + %w(AES CAST5 BF DES IDEA RC2 RC4 RC5).each{|name| + klass = Class.new(Cipher){ + define_method(:initialize){|*args| + cipher_name = args.inject(name){|n, arg| "#{n}-#{arg}" } + super(cipher_name.downcase) + } + } + const_set(name, klass) + } + + %w(128 192 256).each{|keylen| + klass = Class.new(Cipher){ + define_method(:initialize){|mode = "CBC"| + super("aes-#{keylen}-#{mode}".downcase) + } + } + const_set("AES#{keylen}", klass) + } + + # call-seq: + # cipher.random_key -> key + # + # Generate a random key with OpenSSL::Random.random_bytes and sets it to + # the cipher, and returns it. + # + # You must call #encrypt or #decrypt before calling this method. + def random_key + str = OpenSSL::Random.random_bytes(self.key_len) + self.key = str + end + + # call-seq: + # cipher.random_iv -> iv + # + # Generate a random IV with OpenSSL::Random.random_bytes and sets it to the + # cipher, and returns it. + # + # You must call #encrypt or #decrypt before calling this method. + def random_iv + str = OpenSSL::Random.random_bytes(self.iv_len) + self.iv = str + end + + # Deprecated. + # + # This class is only provided for backwards compatibility. + # Use OpenSSL::Cipher. + class Cipher < Cipher; end + deprecate_constant :Cipher + end # Cipher +end # OpenSSL diff --git a/gxg/standard/openssl/config.rb b/gxg/standard/openssl/config.rb new file mode 100644 index 0000000..48d8be0 --- /dev/null +++ b/gxg/standard/openssl/config.rb @@ -0,0 +1,474 @@ +# frozen_string_literal: false +=begin += Ruby-space definitions that completes C-space funcs for Config + += Info + Copyright (C) 2010 Hiroshi Nakamura + += Licence + This program is licensed under the same licence as Ruby. + (See the file 'LICENCE'.) + +=end + +require 'stringio' + +module OpenSSL + ## + # = OpenSSL::Config + # + # Configuration for the openssl library. + # + # Many system's installation of openssl library will depend on your system + # configuration. See the value of OpenSSL::Config::DEFAULT_CONFIG_FILE for + # the location of the file for your host. + # + # See also http://www.openssl.org/docs/apps/config.html + class Config + include Enumerable + + class << self + + ## + # Parses a given _string_ as a blob that contains configuration for + # OpenSSL. + # + # If the source of the IO is a file, then consider using #parse_config. + def parse(string) + c = new() + parse_config(StringIO.new(string)).each do |section, hash| + c[section] = hash + end + c + end + + ## + # load is an alias to ::new + alias load new + + ## + # Parses the configuration data read from _io_, see also #parse. + # + # Raises a ConfigError on invalid configuration data. + def parse_config(io) + begin + parse_config_lines(io) + rescue ConfigError => e + e.message.replace("error in line #{io.lineno}: " + e.message) + raise + end + end + + def get_key_string(data, section, key) # :nodoc: + if v = data[section] && data[section][key] + return v + elsif section == 'ENV' + if v = ENV[key] + return v + end + end + if v = data['default'] && data['default'][key] + return v + end + end + + private + + def parse_config_lines(io) + section = 'default' + data = {section => {}} + while definition = get_definition(io) + definition = clear_comments(definition) + next if definition.empty? + if definition[0] == ?[ + if /\[([^\]]*)\]/ =~ definition + section = $1.strip + data[section] ||= {} + else + raise ConfigError, "missing close square bracket" + end + else + if /\A([^:\s]*)(?:::([^:\s]*))?\s*=(.*)\z/ =~ definition + if $2 + section = $1 + key = $2 + else + key = $1 + end + value = unescape_value(data, section, $3) + (data[section] ||= {})[key] = value.strip + else + raise ConfigError, "missing equal sign" + end + end + end + data + end + + # escape with backslash + QUOTE_REGEXP_SQ = /\A([^'\\]*(?:\\.[^'\\]*)*)'/ + # escape with backslash and doubled dq + QUOTE_REGEXP_DQ = /\A([^"\\]*(?:""[^"\\]*|\\.[^"\\]*)*)"/ + # escaped char map + ESCAPE_MAP = { + "r" => "\r", + "n" => "\n", + "b" => "\b", + "t" => "\t", + } + + def unescape_value(data, section, value) + scanned = [] + while m = value.match(/['"\\$]/) + scanned << m.pre_match + c = m[0] + value = m.post_match + case c + when "'" + if m = value.match(QUOTE_REGEXP_SQ) + scanned << m[1].gsub(/\\(.)/, '\\1') + value = m.post_match + else + break + end + when '"' + if m = value.match(QUOTE_REGEXP_DQ) + scanned << m[1].gsub(/""/, '').gsub(/\\(.)/, '\\1') + value = m.post_match + else + break + end + when "\\" + c = value.slice!(0, 1) + scanned << (ESCAPE_MAP[c] || c) + when "$" + ref, value = extract_reference(value) + refsec = section + if ref.index('::') + refsec, ref = ref.split('::', 2) + end + if v = get_key_string(data, refsec, ref) + scanned << v + else + raise ConfigError, "variable has no value" + end + else + raise 'must not reaced' + end + end + scanned << value + scanned.join + end + + def extract_reference(value) + rest = '' + if m = value.match(/\(([^)]*)\)|\{([^}]*)\}/) + value = m[1] || m[2] + rest = m.post_match + elsif [?(, ?{].include?(value[0]) + raise ConfigError, "no close brace" + end + if m = value.match(/[a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)?/) + return m[0], m.post_match + rest + else + raise + end + end + + def clear_comments(line) + # FCOMMENT + if m = line.match(/\A([\t\n\f ]*);.*\z/) + return m[1] + end + # COMMENT + scanned = [] + while m = line.match(/[#'"\\]/) + scanned << m.pre_match + c = m[0] + line = m.post_match + case c + when '#' + line = nil + break + when "'", '"' + regexp = (c == "'") ? QUOTE_REGEXP_SQ : QUOTE_REGEXP_DQ + scanned << c + if m = line.match(regexp) + scanned << m[0] + line = m.post_match + else + scanned << line + line = nil + break + end + when "\\" + scanned << c + scanned << line.slice!(0, 1) + else + raise 'must not reaced' + end + end + scanned << line + scanned.join + end + + def get_definition(io) + if line = get_line(io) + while /[^\\]\\\z/ =~ line + if extra = get_line(io) + line += extra + else + break + end + end + return line.strip + end + end + + def get_line(io) + if line = io.gets + line.gsub(/[\r\n]*/, '') + end + end + end + + ## + # Creates an instance of OpenSSL's configuration class. + # + # This can be used in contexts like OpenSSL::X509::ExtensionFactory.config= + # + # If the optional _filename_ parameter is provided, then it is read in and + # parsed via #parse_config. + # + # This can raise IO exceptions based on the access, or availability of the + # file. A ConfigError exception may be raised depending on the validity of + # the data being configured. + # + def initialize(filename = nil) + @data = {} + if filename + File.open(filename.to_s) do |file| + Config.parse_config(file).each do |section, hash| + self[section] = hash + end + end + end + end + + ## + # Gets the value of _key_ from the given _section_ + # + # Given the following configurating file being loaded: + # + # config = OpenSSL::Config.load('foo.cnf') + # #=> # + # puts config.to_s + # #=> [ default ] + # # foo=bar + # + # You can get a specific value from the config if you know the _section_ + # and _key_ like so: + # + # config.get_value('default','foo') + # #=> "bar" + # + def get_value(section, key) + if section.nil? + raise TypeError.new('nil not allowed') + end + section = 'default' if section.empty? + get_key_string(section, key) + end + + ## + # + # *Deprecated* + # + # Use #get_value instead + def value(arg1, arg2 = nil) # :nodoc: + warn('Config#value is deprecated; use Config#get_value') + if arg2.nil? + section, key = 'default', arg1 + else + section, key = arg1, arg2 + end + section ||= 'default' + section = 'default' if section.empty? + get_key_string(section, key) + end + + ## + # Set the target _key_ with a given _value_ under a specific _section_. + # + # Given the following configurating file being loaded: + # + # config = OpenSSL::Config.load('foo.cnf') + # #=> # + # puts config.to_s + # #=> [ default ] + # # foo=bar + # + # You can set the value of _foo_ under the _default_ section to a new + # value: + # + # config.add_value('default', 'foo', 'buzz') + # #=> "buzz" + # puts config.to_s + # #=> [ default ] + # # foo=buzz + # + def add_value(section, key, value) + check_modify + (@data[section] ||= {})[key] = value + end + + ## + # Get a specific _section_ from the current configuration + # + # Given the following configurating file being loaded: + # + # config = OpenSSL::Config.load('foo.cnf') + # #=> # + # puts config.to_s + # #=> [ default ] + # # foo=bar + # + # You can get a hash of the specific section like so: + # + # config['default'] + # #=> {"foo"=>"bar"} + # + def [](section) + @data[section] || {} + end + + ## + # Deprecated + # + # Use #[] instead + def section(name) # :nodoc: + warn('Config#section is deprecated; use Config#[]') + @data[name] || {} + end + + ## + # Sets a specific _section_ name with a Hash _pairs_. + # + # Given the following configuration being created: + # + # config = OpenSSL::Config.new + # #=> # + # config['default'] = {"foo"=>"bar","baz"=>"buz"} + # #=> {"foo"=>"bar", "baz"=>"buz"} + # puts config.to_s + # #=> [ default ] + # # foo=bar + # # baz=buz + # + # It's important to note that this will essentially merge any of the keys + # in _pairs_ with the existing _section_. For example: + # + # config['default'] + # #=> {"foo"=>"bar", "baz"=>"buz"} + # config['default'] = {"foo" => "changed"} + # #=> {"foo"=>"changed"} + # config['default'] + # #=> {"foo"=>"changed", "baz"=>"buz"} + # + def []=(section, pairs) + check_modify + @data[section] ||= {} + pairs.each do |key, value| + self.add_value(section, key, value) + end + end + + ## + # Get the names of all sections in the current configuration + def sections + @data.keys + end + + ## + # Get the parsable form of the current configuration + # + # Given the following configuration being created: + # + # config = OpenSSL::Config.new + # #=> # + # config['default'] = {"foo"=>"bar","baz"=>"buz"} + # #=> {"foo"=>"bar", "baz"=>"buz"} + # puts config.to_s + # #=> [ default ] + # # foo=bar + # # baz=buz + # + # You can parse get the serialized configuration using #to_s and then parse + # it later: + # + # serialized_config = config.to_s + # # much later... + # new_config = OpenSSL::Config.parse(serialized_config) + # #=> # + # puts new_config + # #=> [ default ] + # foo=bar + # baz=buz + # + def to_s + ary = [] + @data.keys.sort.each do |section| + ary << "[ #{section} ]\n" + @data[section].keys.each do |key| + ary << "#{key}=#{@data[section][key]}\n" + end + ary << "\n" + end + ary.join + end + + ## + # For a block. + # + # Receive the section and its pairs for the current configuration. + # + # config.each do |section, key, value| + # # ... + # end + # + def each + @data.each do |section, hash| + hash.each do |key, value| + yield [section, key, value] + end + end + end + + ## + # String representation of this configuration object, including the class + # name and its sections. + def inspect + "#<#{self.class.name} sections=#{sections.inspect}>" + end + + protected + + def data # :nodoc: + @data + end + + private + + def initialize_copy(other) + @data = other.data.dup + end + + def check_modify + raise TypeError.new("Insecure: can't modify OpenSSL config") if frozen? + end + + def get_key_string(section, key) + Config.get_key_string(@data, section, key) + end + end +end diff --git a/gxg/standard/openssl/digest.rb b/gxg/standard/openssl/digest.rb new file mode 100644 index 0000000..b6744de --- /dev/null +++ b/gxg/standard/openssl/digest.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: false +#-- +# = Ruby-space predefined Digest subclasses +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos +# All rights reserved. +# +# = Licence +# This program is licensed under the same licence as Ruby. +# (See the file 'LICENCE'.) +#++ + +module OpenSSL + class Digest + + alg = %w(MD2 MD4 MD5 MDC2 RIPEMD160 SHA1 SHA224 SHA256 SHA384 SHA512) + if OPENSSL_VERSION_NUMBER < 0x10100000 + alg += %w(DSS DSS1 SHA) + end + + # Return the hash value computed with _name_ Digest. _name_ is either the + # long name or short name of a supported digest algorithm. + # + # === Examples + # + # OpenSSL::Digest.digest("SHA256", "abc") + # + # which is equivalent to: + # + # OpenSSL::Digest::SHA256.digest("abc") + + def self.digest(name, data) + super(data, name) + end + + alg.each{|name| + klass = Class.new(self) { + define_method(:initialize, ->(data = nil) {super(name, data)}) + } + singleton = (class << klass; self; end) + singleton.class_eval{ + define_method(:digest){|data| new.digest(data) } + define_method(:hexdigest){|data| new.hexdigest(data) } + } + const_set(name, klass) + } + + # Deprecated. + # + # This class is only provided for backwards compatibility. + # Use OpenSSL::Digest instead. + class Digest < Digest; end # :nodoc: + deprecate_constant :Digest + + end # Digest + + # Returns a Digest subclass by _name_ + # + # require 'openssl' + # + # OpenSSL::Digest("MD5") + # # => OpenSSL::Digest::MD5 + # + # Digest("Foo") + # # => NameError: wrong constant name Foo + + def Digest(name) + OpenSSL::Digest.const_get(name) + end + + module_function :Digest + +end # OpenSSL diff --git a/gxg/standard/openssl/pkcs5.rb b/gxg/standard/openssl/pkcs5.rb new file mode 100644 index 0000000..959447d --- /dev/null +++ b/gxg/standard/openssl/pkcs5.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: false +#-- +# Ruby/OpenSSL Project +# Copyright (C) 2017 Ruby/OpenSSL Project Authors +#++ + +module OpenSSL + module PKCS5 + module_function + + # OpenSSL::PKCS5.pbkdf2_hmac has been renamed to OpenSSL::KDF.pbkdf2_hmac. + # This method is provided for backwards compatibility. + def pbkdf2_hmac(pass, salt, iter, keylen, digest) + OpenSSL::KDF.pbkdf2_hmac(pass, salt: salt, iterations: iter, + length: keylen, hash: digest) + end + + def pbkdf2_hmac_sha1(pass, salt, iter, keylen) + pbkdf2_hmac(pass, salt, iter, keylen, "sha1") + end + end +end diff --git a/gxg/standard/openssl/pkey.rb b/gxg/standard/openssl/pkey.rb new file mode 100644 index 0000000..8a547c3 --- /dev/null +++ b/gxg/standard/openssl/pkey.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: false +#-- +# Ruby/OpenSSL Project +# Copyright (C) 2017 Ruby/OpenSSL Project Authors +#++ + +module OpenSSL::PKey + if defined?(EC) + class EC::Point + # :call-seq: + # point.to_bn([conversion_form]) -> OpenSSL::BN + # + # Returns the octet string representation of the EC point as an instance of + # OpenSSL::BN. + # + # If _conversion_form_ is not given, the _point_conversion_form_ attribute + # set to the group is used. + # + # See #to_octet_string for more information. + def to_bn(conversion_form = group.point_conversion_form) + OpenSSL::BN.new(to_octet_string(conversion_form), 2) + end + end + end +end diff --git a/gxg/standard/openssl/ssl.rb b/gxg/standard/openssl/ssl.rb new file mode 100644 index 0000000..355eb2e --- /dev/null +++ b/gxg/standard/openssl/ssl.rb @@ -0,0 +1,504 @@ +# frozen_string_literal: false +=begin += Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2001 GOTOU YUUZOU + All rights reserved. + += Licence + This program is licensed under the same licence as Ruby. + (See the file 'LICENCE'.) +=end + +require "openssl/buffering" +require "io/nonblock" +require "ipaddr" + +module OpenSSL + module SSL + class SSLContext + DEFAULT_PARAMS = { # :nodoc: + :min_version => OpenSSL::SSL::TLS1_VERSION, + :verify_mode => OpenSSL::SSL::VERIFY_PEER, + :verify_hostname => true, + :options => -> { + opts = OpenSSL::SSL::OP_ALL + opts &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS + opts |= OpenSSL::SSL::OP_NO_COMPRESSION + opts + }.call + } + + if defined?(OpenSSL::PKey::DH) + DEFAULT_2048 = OpenSSL::PKey::DH.new <<-_end_of_pem_ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA7E6kBrYiyvmKAMzQ7i8WvwVk9Y/+f8S7sCTN712KkK3cqd1jhJDY +JbrYeNV3kUIKhPxWHhObHKpD1R84UpL+s2b55+iMd6GmL7OYmNIT/FccKhTcveab +VBmZT86BZKYyf45hUF9FOuUM9xPzuK3Vd8oJQvfYMCd7LPC0taAEljQLR4Edf8E6 +YoaOffgTf5qxiwkjnlVZQc3whgnEt9FpVMvQ9eknyeGB5KHfayAc3+hUAvI3/Cr3 +1bNveX5wInh5GDx1FGhKBZ+s1H+aedudCm7sCgRwv8lKWYGiHzObSma8A86KG+MD +7Lo5JquQ3DlBodj3IDyPrxIv96lvRPFtAwIBAg== +-----END DH PARAMETERS----- + _end_of_pem_ + private_constant :DEFAULT_2048 + + DEFAULT_TMP_DH_CALLBACK = lambda { |ctx, is_export, keylen| # :nodoc: + warn "using default DH parameters." if $VERBOSE + DEFAULT_2048 + } + end + + if !(OpenSSL::OPENSSL_VERSION.start_with?("OpenSSL") && + OpenSSL::OPENSSL_VERSION_NUMBER >= 0x10100000) + DEFAULT_PARAMS.merge!( + ciphers: %w{ + ECDHE-ECDSA-AES128-GCM-SHA256 + ECDHE-RSA-AES128-GCM-SHA256 + ECDHE-ECDSA-AES256-GCM-SHA384 + ECDHE-RSA-AES256-GCM-SHA384 + DHE-RSA-AES128-GCM-SHA256 + DHE-DSS-AES128-GCM-SHA256 + DHE-RSA-AES256-GCM-SHA384 + DHE-DSS-AES256-GCM-SHA384 + ECDHE-ECDSA-AES128-SHA256 + ECDHE-RSA-AES128-SHA256 + ECDHE-ECDSA-AES128-SHA + ECDHE-RSA-AES128-SHA + ECDHE-ECDSA-AES256-SHA384 + ECDHE-RSA-AES256-SHA384 + ECDHE-ECDSA-AES256-SHA + ECDHE-RSA-AES256-SHA + DHE-RSA-AES128-SHA256 + DHE-RSA-AES256-SHA256 + DHE-RSA-AES128-SHA + DHE-RSA-AES256-SHA + DHE-DSS-AES128-SHA256 + DHE-DSS-AES256-SHA256 + DHE-DSS-AES128-SHA + DHE-DSS-AES256-SHA + AES128-GCM-SHA256 + AES256-GCM-SHA384 + AES128-SHA256 + AES256-SHA256 + AES128-SHA + AES256-SHA + }.join(":"), + ) + end + + DEFAULT_CERT_STORE = OpenSSL::X509::Store.new # :nodoc: + DEFAULT_CERT_STORE.set_default_paths + DEFAULT_CERT_STORE.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL + + # A callback invoked when DH parameters are required. + # + # The callback is invoked with the Session for the key exchange, an + # flag indicating the use of an export cipher and the keylength + # required. + # + # The callback must return an OpenSSL::PKey::DH instance of the correct + # key length. + + attr_accessor :tmp_dh_callback + + # A callback invoked at connect time to distinguish between multiple + # server names. + # + # The callback is invoked with an SSLSocket and a server name. The + # callback must return an SSLContext for the server name or nil. + attr_accessor :servername_cb + + # call-seq: + # SSLContext.new -> ctx + # SSLContext.new(:TLSv1) -> ctx + # SSLContext.new("SSLv23") -> ctx + # + # Creates a new SSL context. + # + # If an argument is given, #ssl_version= is called with the value. Note + # that this form is deprecated. New applications should use #min_version= + # and #max_version= as necessary. + def initialize(version = nil) + self.options |= OpenSSL::SSL::OP_ALL + self.ssl_version = version if version + end + + ## + # call-seq: + # ctx.set_params(params = {}) -> params + # + # Sets saner defaults optimized for the use with HTTP-like protocols. + # + # If a Hash _params_ is given, the parameters are overridden with it. + # The keys in _params_ must be assignment methods on SSLContext. + # + # If the verify_mode is not VERIFY_NONE and ca_file, ca_path and + # cert_store are not set then the system default certificate store is + # used. + def set_params(params={}) + params = DEFAULT_PARAMS.merge(params) + self.options = params.delete(:options) # set before min_version/max_version + params.each{|name, value| self.__send__("#{name}=", value) } + if self.verify_mode != OpenSSL::SSL::VERIFY_NONE + unless self.ca_file or self.ca_path or self.cert_store + self.cert_store = DEFAULT_CERT_STORE + end + end + return params + end + + # call-seq: + # ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION + # ctx.min_version = :TLS1_2 + # ctx.min_version = nil + # + # Sets the lower bound on the supported SSL/TLS protocol version. The + # version may be specified by an integer constant named + # OpenSSL::SSL::*_VERSION, a Symbol, or +nil+ which means "any version". + # + # Be careful that you don't overwrite OpenSSL::SSL::OP_NO_{SSL,TLS}v* + # options by #options= once you have called #min_version= or + # #max_version=. + # + # === Example + # ctx = OpenSSL::SSL::SSLContext.new + # ctx.min_version = OpenSSL::SSL::TLS1_1_VERSION + # ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + # + # sock = OpenSSL::SSL::SSLSocket.new(tcp_sock, ctx) + # sock.connect # Initiates a connection using either TLS 1.1 or TLS 1.2 + def min_version=(version) + set_minmax_proto_version(version, @max_proto_version ||= nil) + @min_proto_version = version + end + + # call-seq: + # ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + # ctx.max_version = :TLS1_2 + # ctx.max_version = nil + # + # Sets the upper bound of the supported SSL/TLS protocol version. See + # #min_version= for the possible values. + def max_version=(version) + set_minmax_proto_version(@min_proto_version ||= nil, version) + @max_proto_version = version + end + + # call-seq: + # ctx.ssl_version = :TLSv1 + # ctx.ssl_version = "SSLv23" + # + # Sets the SSL/TLS protocol version for the context. This forces + # connections to use only the specified protocol version. This is + # deprecated and only provided for backwards compatibility. Use + # #min_version= and #max_version= instead. + # + # === History + # As the name hints, this used to call the SSL_CTX_set_ssl_version() + # function which sets the SSL method used for connections created from + # the context. As of Ruby/OpenSSL 2.1, this accessor method is + # implemented to call #min_version= and #max_version= instead. + def ssl_version=(meth) + meth = meth.to_s if meth.is_a?(Symbol) + if /(?_client|_server)\z/ =~ meth + meth = $` + if $VERBOSE + warn "#{caller(1, 1)[0]}: method type #{type.inspect} is ignored" + end + end + version = METHODS_MAP[meth.intern] or + raise ArgumentError, "unknown SSL method `%s'" % meth + set_minmax_proto_version(version, version) + @min_proto_version = @max_proto_version = version + end + + METHODS_MAP = { + SSLv23: 0, + SSLv2: OpenSSL::SSL::SSL2_VERSION, + SSLv3: OpenSSL::SSL::SSL3_VERSION, + TLSv1: OpenSSL::SSL::TLS1_VERSION, + TLSv1_1: OpenSSL::SSL::TLS1_1_VERSION, + TLSv1_2: OpenSSL::SSL::TLS1_2_VERSION, + }.freeze + private_constant :METHODS_MAP + + # The list of available SSL/TLS methods. This constant is only provided + # for backwards compatibility. + METHODS = METHODS_MAP.flat_map { |name,| + [name, :"#{name}_client", :"#{name}_server"] + }.freeze + deprecate_constant :METHODS + end + + module SocketForwarder + def addr + to_io.addr + end + + def peeraddr + to_io.peeraddr + end + + def setsockopt(level, optname, optval) + to_io.setsockopt(level, optname, optval) + end + + def getsockopt(level, optname) + to_io.getsockopt(level, optname) + end + + def fcntl(*args) + to_io.fcntl(*args) + end + + def closed? + to_io.closed? + end + + def do_not_reverse_lookup=(flag) + to_io.do_not_reverse_lookup = flag + end + end + + def verify_certificate_identity(cert, hostname) + should_verify_common_name = true + cert.extensions.each{|ext| + next if ext.oid != "subjectAltName" + ostr = OpenSSL::ASN1.decode(ext.to_der).value.last + sequence = OpenSSL::ASN1.decode(ostr.value) + sequence.value.each{|san| + case san.tag + when 2 # dNSName in GeneralName (RFC5280) + should_verify_common_name = false + return true if verify_hostname(hostname, san.value) + when 7 # iPAddress in GeneralName (RFC5280) + should_verify_common_name = false + if san.value.size == 4 || san.value.size == 16 + begin + return true if san.value == IPAddr.new(hostname).hton + rescue IPAddr::InvalidAddressError + end + end + end + } + } + if should_verify_common_name + cert.subject.to_a.each{|oid, value| + if oid == "CN" + return true if verify_hostname(hostname, value) + end + } + end + return false + end + module_function :verify_certificate_identity + + def verify_hostname(hostname, san) # :nodoc: + # RFC 5280, IA5String is limited to the set of ASCII characters + return false unless san.ascii_only? + return false unless hostname.ascii_only? + + # See RFC 6125, section 6.4.1 + # Matching is case-insensitive. + san_parts = san.downcase.split(".") + + # TODO: this behavior should probably be more strict + return san == hostname if san_parts.size < 2 + + # Matching is case-insensitive. + host_parts = hostname.downcase.split(".") + + # RFC 6125, section 6.4.3, subitem 2. + # If the wildcard character is the only character of the left-most + # label in the presented identifier, the client SHOULD NOT compare + # against anything but the left-most label of the reference + # identifier (e.g., *.example.com would match foo.example.com but + # not bar.foo.example.com or example.com). + return false unless san_parts.size == host_parts.size + + # RFC 6125, section 6.4.3, subitem 1. + # The client SHOULD NOT attempt to match a presented identifier in + # which the wildcard character comprises a label other than the + # left-most label (e.g., do not match bar.*.example.net). + return false unless verify_wildcard(host_parts.shift, san_parts.shift) + + san_parts.join(".") == host_parts.join(".") + end + module_function :verify_hostname + + def verify_wildcard(domain_component, san_component) # :nodoc: + parts = san_component.split("*", -1) + + return false if parts.size > 2 + return san_component == domain_component if parts.size == 1 + + # RFC 6125, section 6.4.3, subitem 3. + # The client SHOULD NOT attempt to match a presented identifier + # where the wildcard character is embedded within an A-label or + # U-label of an internationalized domain name. + return false if domain_component.start_with?("xn--") && san_component != "*" + + parts[0].length + parts[1].length < domain_component.length && + domain_component.start_with?(parts[0]) && + domain_component.end_with?(parts[1]) + end + module_function :verify_wildcard + + class SSLSocket + include Buffering + include SocketForwarder + + attr_reader :hostname + + # The underlying IO object. + attr_reader :io + alias :to_io :io + + # The SSLContext object used in this connection. + attr_reader :context + + # Whether to close the underlying socket as well, when the SSL/TLS + # connection is shut down. This defaults to +false+. + attr_accessor :sync_close + + # call-seq: + # ssl.sysclose => nil + # + # Sends "close notify" to the peer and tries to shut down the SSL + # connection gracefully. + # + # If sync_close is set to +true+, the underlying IO is also closed. + def sysclose + return if closed? + stop + io.close if sync_close + end + + # call-seq: + # ssl.post_connection_check(hostname) -> true + # + # Perform hostname verification following RFC 6125. + # + # This method MUST be called after calling #connect to ensure that the + # hostname of a remote peer has been verified. + def post_connection_check(hostname) + if peer_cert.nil? + msg = "Peer verification enabled, but no certificate received." + if using_anon_cipher? + msg += " Anonymous cipher suite #{cipher[0]} was negotiated. " \ + "Anonymous suites must be disabled to use peer verification." + end + raise SSLError, msg + end + + unless OpenSSL::SSL.verify_certificate_identity(peer_cert, hostname) + raise SSLError, "hostname \"#{hostname}\" does not match the server certificate" + end + return true + end + + # call-seq: + # ssl.session -> aSession + # + # Returns the SSLSession object currently used, or nil if the session is + # not established. + def session + SSL::Session.new(self) + rescue SSL::Session::SessionError + nil + end + + private + + def using_anon_cipher? + ctx = OpenSSL::SSL::SSLContext.new + ctx.ciphers = "aNULL" + ctx.ciphers.include?(cipher) + end + + def client_cert_cb + @context.client_cert_cb + end + + def tmp_dh_callback + @context.tmp_dh_callback || OpenSSL::SSL::SSLContext::DEFAULT_TMP_DH_CALLBACK + end + + def tmp_ecdh_callback + @context.tmp_ecdh_callback + end + + def session_new_cb + @context.session_new_cb + end + + def session_get_cb + @context.session_get_cb + end + end + + ## + # SSLServer represents a TCP/IP server socket with Secure Sockets Layer. + class SSLServer + include SocketForwarder + # When true then #accept works exactly the same as TCPServer#accept + attr_accessor :start_immediately + + # Creates a new instance of SSLServer. + # * _srv_ is an instance of TCPServer. + # * _ctx_ is an instance of OpenSSL::SSL::SSLContext. + def initialize(svr, ctx) + @svr = svr + @ctx = ctx + unless ctx.session_id_context + # see #6137 - session id may not exceed 32 bytes + prng = ::Random.new($0.hash) + session_id = prng.bytes(16).unpack('H*')[0] + @ctx.session_id_context = session_id + end + @start_immediately = true + end + + # Returns the TCPServer passed to the SSLServer when initialized. + def to_io + @svr + end + + # See TCPServer#listen for details. + def listen(backlog=5) + @svr.listen(backlog) + end + + # See BasicSocket#shutdown for details. + def shutdown(how=Socket::SHUT_RDWR) + @svr.shutdown(how) + end + + # Works similar to TCPServer#accept. + def accept + # Socket#accept returns [socket, addrinfo]. + # TCPServer#accept returns a socket. + # The following comma strips addrinfo. + sock, = @svr.accept + begin + ssl = OpenSSL::SSL::SSLSocket.new(sock, @ctx) + ssl.sync_close = true + ssl.accept if @start_immediately + ssl + rescue Exception => ex + if ssl + ssl.close + else + sock.close + end + raise ex + end + end + + # See IO#close for details. + def close + @svr.close + end + end + end +end diff --git a/gxg/standard/openssl/x509.rb b/gxg/standard/openssl/x509.rb new file mode 100644 index 0000000..98358f9 --- /dev/null +++ b/gxg/standard/openssl/x509.rb @@ -0,0 +1,215 @@ +# frozen_string_literal: false +#-- +# = Ruby-space definitions that completes C-space funcs for X509 and subclasses +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos +# All rights reserved. +# +# = Licence +# This program is licensed under the same licence as Ruby. +# (See the file 'LICENCE'.) +#++ + +module OpenSSL + module X509 + class ExtensionFactory + def create_extension(*arg) + if arg.size > 1 + create_ext(*arg) + else + send("create_ext_from_"+arg[0].class.name.downcase, arg[0]) + end + end + + def create_ext_from_array(ary) + raise ExtensionError, "unexpected array form" if ary.size > 3 + create_ext(ary[0], ary[1], ary[2]) + end + + def create_ext_from_string(str) # "oid = critical, value" + oid, value = str.split(/=/, 2) + oid.strip! + value.strip! + create_ext(oid, value) + end + + def create_ext_from_hash(hash) + create_ext(hash["oid"], hash["value"], hash["critical"]) + end + end + + class Extension + def ==(other) + return false unless Extension === other + to_der == other.to_der + end + + def to_s # "oid = critical, value" + str = self.oid + str << " = " + str << "critical, " if self.critical? + str << self.value.gsub(/\n/, ", ") + end + + def to_h # {"oid"=>sn|ln, "value"=>value, "critical"=>true|false} + {"oid"=>self.oid,"value"=>self.value,"critical"=>self.critical?} + end + + def to_a + [ self.oid, self.value, self.critical? ] + end + end + + class Name + module RFC2253DN + Special = ',=+<>#;' + HexChar = /[0-9a-fA-F]/ + HexPair = /#{HexChar}#{HexChar}/ + HexString = /#{HexPair}+/ + Pair = /\\(?:[#{Special}]|\\|"|#{HexPair})/ + StringChar = /[^\\"#{Special}]/ + QuoteChar = /[^\\"]/ + AttributeType = /[a-zA-Z][0-9a-zA-Z]*|[0-9]+(?:\.[0-9]+)*/ + AttributeValue = / + (?!["#])((?:#{StringChar}|#{Pair})*)| + \#(#{HexString})| + "((?:#{QuoteChar}|#{Pair})*)" + /x + TypeAndValue = /\A(#{AttributeType})=#{AttributeValue}/ + + module_function + + def expand_pair(str) + return nil unless str + return str.gsub(Pair){ + pair = $& + case pair.size + when 2 then pair[1,1] + when 3 then Integer("0x#{pair[1,2]}").chr + else raise OpenSSL::X509::NameError, "invalid pair: #{str}" + end + } + end + + def expand_hexstring(str) + return nil unless str + der = str.gsub(HexPair){$&.to_i(16).chr } + a1 = OpenSSL::ASN1.decode(der) + return a1.value, a1.tag + end + + def expand_value(str1, str2, str3) + value = expand_pair(str1) + value, tag = expand_hexstring(str2) unless value + value = expand_pair(str3) unless value + return value, tag + end + + def scan(dn) + str = dn + ary = [] + while true + if md = TypeAndValue.match(str) + remain = md.post_match + type = md[1] + value, tag = expand_value(md[2], md[3], md[4]) rescue nil + if value + type_and_value = [type, value] + type_and_value.push(tag) if tag + ary.unshift(type_and_value) + if remain.length > 2 && remain[0] == ?, + str = remain[1..-1] + next + elsif remain.length > 2 && remain[0] == ?+ + raise OpenSSL::X509::NameError, + "multi-valued RDN is not supported: #{dn}" + elsif remain.empty? + break + end + end + end + msg_dn = dn[0, dn.length - str.length] + " =>" + str + raise OpenSSL::X509::NameError, "malformed RDN: #{msg_dn}" + end + return ary + end + end + + class << self + def parse_rfc2253(str, template=OBJECT_TYPE_TEMPLATE) + ary = OpenSSL::X509::Name::RFC2253DN.scan(str) + self.new(ary, template) + end + + def parse_openssl(str, template=OBJECT_TYPE_TEMPLATE) + if str.start_with?("/") + # /A=B/C=D format + ary = str[1..-1].split("/").map { |i| i.split("=", 2) } + else + # Comma-separated + ary = str.split(",").map { |i| i.strip.split("=", 2) } + end + self.new(ary, template) + end + + alias parse parse_openssl + end + + def pretty_print(q) + q.object_group(self) { + q.text ' ' + q.text to_s(OpenSSL::X509::Name::RFC2253) + } + end + end + + class Attribute + def ==(other) + return false unless Attribute === other + to_der == other.to_der + end + end + + class StoreContext + def cleanup + warn "(#{caller.first}) OpenSSL::X509::StoreContext#cleanup is deprecated with no replacement" if $VERBOSE + end + end + + class Certificate + def pretty_print(q) + q.object_group(self) { + q.breakable + q.text 'subject='; q.pp self.subject; q.text ','; q.breakable + q.text 'issuer='; q.pp self.issuer; q.text ','; q.breakable + q.text 'serial='; q.pp self.serial; q.text ','; q.breakable + q.text 'not_before='; q.pp self.not_before; q.text ','; q.breakable + q.text 'not_after='; q.pp self.not_after + } + end + end + + class CRL + def ==(other) + return false unless CRL === other + to_der == other.to_der + end + end + + class Revoked + def ==(other) + return false unless Revoked === other + to_der == other.to_der + end + end + + class Request + def ==(other) + return false unless Request === other + to_der == other.to_der + end + end + end +end diff --git a/gxg/standard/optionparser.rb b/gxg/standard/optionparser.rb new file mode 100644 index 0000000..4b9b40d --- /dev/null +++ b/gxg/standard/optionparser.rb @@ -0,0 +1,2 @@ +# frozen_string_literal: false +require_relative 'optparse' diff --git a/gxg/standard/optparse.rb b/gxg/standard/optparse.rb new file mode 100644 index 0000000..7440647 --- /dev/null +++ b/gxg/standard/optparse.rb @@ -0,0 +1,2161 @@ +# frozen_string_literal: false +# +# optparse.rb - command-line option analysis with the OptionParser class. +# +# Author:: Nobu Nakada +# Documentation:: Nobu Nakada and Gavin Sinclair. +# +# See OptionParser for documentation. +# + + +#-- +# == Developer Documentation (not for RDoc output) +# +# === Class tree +# +# - OptionParser:: front end +# - OptionParser::Switch:: each switches +# - OptionParser::List:: options list +# - OptionParser::ParseError:: errors on parsing +# - OptionParser::AmbiguousOption +# - OptionParser::NeedlessArgument +# - OptionParser::MissingArgument +# - OptionParser::InvalidOption +# - OptionParser::InvalidArgument +# - OptionParser::AmbiguousArgument +# +# === Object relationship diagram +# +# +--------------+ +# | OptionParser |<>-----+ +# +--------------+ | +--------+ +# | ,-| Switch | +# on_head -------->+---------------+ / +--------+ +# accept/reject -->| List |<|>- +# | |<|>- +----------+ +# on ------------->+---------------+ `-| argument | +# : : | class | +# +---------------+ |==========| +# on_tail -------->| | |pattern | +# +---------------+ |----------| +# OptionParser.accept ->| DefaultList | |converter | +# reject |(shared between| +----------+ +# | all instances)| +# +---------------+ +# +#++ +# +# == OptionParser +# +# === Introduction +# +# OptionParser is a class for command-line option analysis. It is much more +# advanced, yet also easier to use, than GetoptLong, and is a more Ruby-oriented +# solution. +# +# === Features +# +# 1. The argument specification and the code to handle it are written in the +# same place. +# 2. It can output an option summary; you don't need to maintain this string +# separately. +# 3. Optional and mandatory arguments are specified very gracefully. +# 4. Arguments can be automatically converted to a specified class. +# 5. Arguments can be restricted to a certain set. +# +# All of these features are demonstrated in the examples below. See +# #make_switch for full documentation. +# +# === Minimal example +# +# require 'optparse' +# +# options = {} +# OptionParser.new do |opts| +# opts.banner = "Usage: example.rb [options]" +# +# opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| +# options[:verbose] = v +# end +# end.parse! +# +# p options +# p ARGV +# +# === Generating Help +# +# OptionParser can be used to automatically generate help for the commands you +# write: +# +# require 'optparse' +# +# Options = Struct.new(:name) +# +# class Parser +# def self.parse(options) +# args = Options.new("world") +# +# opt_parser = OptionParser.new do |opts| +# opts.banner = "Usage: example.rb [options]" +# +# opts.on("-nNAME", "--name=NAME", "Name to say hello to") do |n| +# args.name = n +# end +# +# opts.on("-h", "--help", "Prints this help") do +# puts opts +# exit +# end +# end +# +# opt_parser.parse!(options) +# return args +# end +# end +# options = Parser.parse %w[--help] +# +# #=> +# # Usage: example.rb [options] +# # -n, --name=NAME Name to say hello to +# # -h, --help Prints this help +# +# === Required Arguments +# +# For options that require an argument, option specification strings may include an +# option name in all caps. If an option is used without the required argument, +# an exception will be raised. +# require 'optparse' +# +# options = {} +# OptionParser.new do |parser| +# parser.on("-r", "--require LIBRARY", +# "Require the LIBRARY before executing your script") do |lib| +# puts "You required #{lib}!" +# end +# end.parse! +# +# Used: +# +# bash-3.2$ ruby optparse-test.rb -r +# optparse-test.rb:9:in `
': missing argument: -r (OptionParser::MissingArgument) +# bash-3.2$ ruby optparse-test.rb -r my-library +# You required my-library! +# +# === Type Coercion +# +# OptionParser supports the ability to coerce command line arguments +# into objects for us. +# +# OptionParser comes with a few ready-to-use kinds of type +# coercion. They are: +# +# - Date -- Anything accepted by +Date.parse+ +# - DateTime -- Anything accepted by +DateTime.parse+ +# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+ +# - URI -- Anything accepted by +URI.parse+ +# - Shellwords -- Anything accepted by +Shellwords.shellwords+ +# - String -- Any non-empty string +# - Integer -- Any integer. Will convert octal. (e.g. 124, -3, 040) +# - Float -- Any float. (e.g. 10, 3.14, -100E+13) +# - Numeric -- Any integer, float, or rational (1, 3.4, 1/3) +# - DecimalInteger -- Like +Integer+, but no octal format. +# - OctalInteger -- Like +Integer+, but no decimal format. +# - DecimalNumeric -- Decimal integer or float. +# - TrueClass -- Accepts '+, yes, true, -, no, false' and +# defaults as +true+ +# - FalseClass -- Same as +TrueClass+, but defaults to +false+ +# - Array -- Strings separated by ',' (e.g. 1,2,3) +# - Regexp -- Regular expressions. Also includes options. +# +# We can also add our own coercions, which we will cover soon. +# +# ==== Using Built-in Conversions +# +# As an example, the built-in +Time+ conversion is used. The other built-in +# conversions behave in the same way. +# OptionParser will attempt to parse the argument +# as a +Time+. If it succeeds, that time will be passed to the +# handler block. Otherwise, an exception will be raised. +# +# require 'optparse' +# require 'optparse/time' +# OptionParser.new do |parser| +# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time| +# p time +# end +# end.parse! +# +# Used: +# bash-3.2$ ruby optparse-test.rb -t nonsense +# ... invalid argument: -t nonsense (OptionParser::InvalidArgument) +# from ... time.rb:5:in `block in ' +# from optparse-test.rb:31:in `
' +# bash-3.2$ ruby optparse-test.rb -t 10-11-12 +# 2010-11-12 00:00:00 -0500 +# bash-3.2$ ruby optparse-test.rb -t 9:30 +# 2014-08-13 09:30:00 -0400 +# +# ==== Creating Custom Conversions +# +# The +accept+ method on OptionParser may be used to create converters. +# It specifies which conversion block to call whenever a class is specified. +# The example below uses it to fetch a +User+ object before the +on+ handler receives it. +# +# require 'optparse' +# +# User = Struct.new(:id, :name) +# +# def find_user id +# not_found = ->{ raise "No User Found for id #{id}" } +# [ User.new(1, "Sam"), +# User.new(2, "Gandalf") ].find(not_found) do |u| +# u.id == id +# end +# end +# +# op = OptionParser.new +# op.accept(User) do |user_id| +# find_user user_id.to_i +# end +# +# op.on("--user ID", User) do |user| +# puts user +# end +# +# op.parse! +# +# output: +# bash-3.2$ ruby optparse-test.rb --user 1 +# # +# bash-3.2$ ruby optparse-test.rb --user 2 +# # +# bash-3.2$ ruby optparse-test.rb --user 3 +# optparse-test.rb:15:in `block in find_user': No User Found for id 3 (RuntimeError) +# === Complete example +# +# The following example is a complete Ruby program. You can run it and see the +# effect of specifying various options. This is probably the best way to learn +# the features of +optparse+. +# +# require 'optparse' +# require 'optparse/time' +# require 'ostruct' +# require 'pp' +# +# class OptparseExample +# Version = '1.0.0' +# +# CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary] +# CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" } +# +# class ScriptOptions +# attr_accessor :library, :inplace, :encoding, :transfer_type, +# :verbose, :extension, :delay, :time, :record_separator, +# :list +# +# def initialize +# self.library = [] +# self.inplace = false +# self.encoding = "utf8" +# self.transfer_type = :auto +# self.verbose = false +# end +# +# def define_options(parser) +# parser.banner = "Usage: example.rb [options]" +# parser.separator "" +# parser.separator "Specific options:" +# +# # add additional options +# perform_inplace_option(parser) +# delay_execution_option(parser) +# execute_at_time_option(parser) +# specify_record_separator_option(parser) +# list_example_option(parser) +# specify_encoding_option(parser) +# optional_option_argument_with_keyword_completion_option(parser) +# boolean_verbose_option(parser) +# +# parser.separator "" +# parser.separator "Common options:" +# # No argument, shows at tail. This will print an options summary. +# # Try it and see! +# parser.on_tail("-h", "--help", "Show this message") do +# puts parser +# exit +# end +# # Another typical switch to print the version. +# parser.on_tail("--version", "Show version") do +# puts Version +# exit +# end +# end +# +# def perform_inplace_option(parser) +# # Specifies an optional option argument +# parser.on("-i", "--inplace [EXTENSION]", +# "Edit ARGV files in place", +# "(make backup if EXTENSION supplied)") do |ext| +# self.inplace = true +# self.extension = ext || '' +# self.extension.sub!(/\A\.?(?=.)/, ".") # Ensure extension begins with dot. +# end +# end +# +# def delay_execution_option(parser) +# # Cast 'delay' argument to a Float. +# parser.on("--delay N", Float, "Delay N seconds before executing") do |n| +# self.delay = n +# end +# end +# +# def execute_at_time_option(parser) +# # Cast 'time' argument to a Time object. +# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time| +# self.time = time +# end +# end +# +# def specify_record_separator_option(parser) +# # Cast to octal integer. +# parser.on("-F", "--irs [OCTAL]", OptionParser::OctalInteger, +# "Specify record separator (default \\0)") do |rs| +# self.record_separator = rs +# end +# end +# +# def list_example_option(parser) +# # List of arguments. +# parser.on("--list x,y,z", Array, "Example 'list' of arguments") do |list| +# self.list = list +# end +# end +# +# def specify_encoding_option(parser) +# # Keyword completion. We are specifying a specific set of arguments (CODES +# # and CODE_ALIASES - notice the latter is a Hash), and the user may provide +# # the shortest unambiguous text. +# code_list = (CODE_ALIASES.keys + CODES).join(', ') +# parser.on("--code CODE", CODES, CODE_ALIASES, "Select encoding", +# "(#{code_list})") do |encoding| +# self.encoding = encoding +# end +# end +# +# def optional_option_argument_with_keyword_completion_option(parser) +# # Optional '--type' option argument with keyword completion. +# parser.on("--type [TYPE]", [:text, :binary, :auto], +# "Select transfer type (text, binary, auto)") do |t| +# self.transfer_type = t +# end +# end +# +# def boolean_verbose_option(parser) +# # Boolean switch. +# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v| +# self.verbose = v +# end +# end +# end +# +# # +# # Return a structure describing the options. +# # +# def parse(args) +# # The options specified on the command line will be collected in +# # *options*. +# +# @options = ScriptOptions.new +# @args = OptionParser.new do |parser| +# @options.define_options(parser) +# parser.parse!(args) +# end +# @options +# end +# +# attr_reader :parser, :options +# end # class OptparseExample +# +# example = OptparseExample.new +# options = example.parse(ARGV) +# pp options # example.options +# pp ARGV +# +# === Shell Completion +# +# For modern shells (e.g. bash, zsh, etc.), you can use shell +# completion for command line options. +# +# === Further documentation +# +# The above examples should be enough to learn how to use this class. If you +# have any questions, file a ticket at http://bugs.ruby-lang.org. +# +class OptionParser + # :stopdoc: + NoArgument = [NO_ARGUMENT = :NONE, nil].freeze + RequiredArgument = [REQUIRED_ARGUMENT = :REQUIRED, true].freeze + OptionalArgument = [OPTIONAL_ARGUMENT = :OPTIONAL, false].freeze + # :startdoc: + + # + # Keyword completion module. This allows partial arguments to be specified + # and resolved against a list of acceptable values. + # + module Completion + def self.regexp(key, icase) + Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), icase) + end + + def self.candidate(key, icase = false, pat = nil, &block) + pat ||= Completion.regexp(key, icase) + candidates = [] + block.call do |k, *v| + (if Regexp === k + kn = "".freeze + k === key + else + kn = defined?(k.id2name) ? k.id2name : k + pat === kn + end) or next + v << k if v.empty? + candidates << [k, v, kn] + end + candidates + end + + def candidate(key, icase = false, pat = nil) + Completion.candidate(key, icase, pat, &method(:each)) + end + + public + def complete(key, icase = false, pat = nil) + candidates = candidate(key, icase, pat, &method(:each)).sort_by {|k, v, kn| kn.size} + if candidates.size == 1 + canon, sw, * = candidates[0] + elsif candidates.size > 1 + canon, sw, cn = candidates.shift + candidates.each do |k, v, kn| + next if sw == v + if String === cn and String === kn + if cn.rindex(kn, 0) + canon, sw, cn = k, v, kn + next + elsif kn.rindex(cn, 0) + next + end + end + throw :ambiguous, key + end + end + if canon + block_given? or return key, *sw + yield(key, *sw) + end + end + + def convert(opt = nil, val = nil, *) + val + end + end + + + # + # Map from option/keyword string to object with completion. + # + class OptionMap < Hash + include Completion + end + + + # + # Individual switch class. Not important to the user. + # + # Defined within Switch are several Switch-derived classes: NoArgument, + # RequiredArgument, etc. + # + class Switch + attr_reader :pattern, :conv, :short, :long, :arg, :desc, :block + + # + # Guesses argument style from +arg+. Returns corresponding + # OptionParser::Switch class (OptionalArgument, etc.). + # + def self.guess(arg) + case arg + when "" + t = self + when /\A=?\[/ + t = Switch::OptionalArgument + when /\A\s+\[/ + t = Switch::PlacedArgument + else + t = Switch::RequiredArgument + end + self >= t or incompatible_argument_styles(arg, t) + t + end + + def self.incompatible_argument_styles(arg, t) + raise(ArgumentError, "#{arg}: incompatible argument styles\n #{self}, #{t}", + ParseError.filter_backtrace(caller(2))) + end + + def self.pattern + NilClass + end + + def initialize(pattern = nil, conv = nil, + short = nil, long = nil, arg = nil, + desc = ([] if short or long), block = Proc.new) + raise if Array === pattern + @pattern, @conv, @short, @long, @arg, @desc, @block = + pattern, conv, short, long, arg, desc, block + end + + # + # Parses +arg+ and returns rest of +arg+ and matched portion to the + # argument pattern. Yields when the pattern doesn't match substring. + # + def parse_arg(arg) + pattern or return nil, [arg] + unless m = pattern.match(arg) + yield(InvalidArgument, arg) + return arg, [] + end + if String === m + m = [s = m] + else + m = m.to_a + s = m[0] + return nil, m unless String === s + end + raise InvalidArgument, arg unless arg.rindex(s, 0) + return nil, m if s.length == arg.length + yield(InvalidArgument, arg) # didn't match whole arg + return arg[s.length..-1], m + end + private :parse_arg + + # + # Parses argument, converts and returns +arg+, +block+ and result of + # conversion. Yields at semi-error condition instead of raising an + # exception. + # + def conv_arg(arg, val = []) + if conv + val = conv.call(*val) + else + val = proc {|v| v}.call(*val) + end + return arg, block, val + end + private :conv_arg + + # + # Produces the summary text. Each line of the summary is yielded to the + # block (without newline). + # + # +sdone+:: Already summarized short style options keyed hash. + # +ldone+:: Already summarized long style options keyed hash. + # +width+:: Width of left side (option part). In other words, the right + # side (description part) starts after +width+ columns. + # +max+:: Maximum width of left side -> the options are filled within + # +max+ columns. + # +indent+:: Prefix string indents all summarized lines. + # + def summarize(sdone = [], ldone = [], width = 1, max = width - 1, indent = "") + sopts, lopts = [], [], nil + @short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short + @long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long + return if sopts.empty? and lopts.empty? # completely hidden + + left = [sopts.join(', ')] + right = desc.dup + + while s = lopts.shift + l = left[-1].length + s.length + l += arg.length if left.size == 1 && arg + l < max or sopts.empty? or left << '' + left[-1] << (left[-1].empty? ? ' ' * 4 : ', ') << s + end + + if arg + left[0] << (left[1] ? arg.sub(/\A(\[?)=/, '\1') + ',' : arg) + end + mlen = left.collect {|ss| ss.length}.max.to_i + while mlen > width and l = left.shift + mlen = left.collect {|ss| ss.length}.max.to_i if l.length == mlen + if l.length < width and (r = right[0]) and !r.empty? + l = l.to_s.ljust(width) + ' ' + r + right.shift + end + yield(indent + l) + end + + while begin l = left.shift; r = right.shift; l or r end + l = l.to_s.ljust(width) + ' ' + r if r and !r.empty? + yield(indent + l) + end + + self + end + + def add_banner(to) # :nodoc: + unless @short or @long + s = desc.join + to << " [" + s + "]..." unless s.empty? + end + to + end + + def match_nonswitch?(str) # :nodoc: + @pattern =~ str unless @short or @long + end + + # + # Main name of the switch. + # + def switch_name + (long.first || short.first).sub(/\A-+(?:\[no-\])?/, '') + end + + def compsys(sdone, ldone) # :nodoc: + sopts, lopts = [], [] + @short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short + @long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long + return if sopts.empty? and lopts.empty? # completely hidden + + (sopts+lopts).each do |opt| + # "(-x -c -r)-l[left justify]" \ + if /^--\[no-\](.+)$/ =~ opt + o = $1 + yield("--#{o}", desc.join("")) + yield("--no-#{o}", desc.join("")) + else + yield("#{opt}", desc.join("")) + end + end + end + + # + # Switch that takes no arguments. + # + class NoArgument < self + + # + # Raises an exception if any arguments given. + # + def parse(arg, argv) + yield(NeedlessArgument, arg) if arg + conv_arg(arg) + end + + def self.incompatible_argument_styles(*) + end + + def self.pattern + Object + end + end + + # + # Switch that takes an argument. + # + class RequiredArgument < self + + # + # Raises an exception if argument is not present. + # + def parse(arg, argv) + unless arg + raise MissingArgument if argv.empty? + arg = argv.shift + end + conv_arg(*parse_arg(arg, &method(:raise))) + end + end + + # + # Switch that can omit argument. + # + class OptionalArgument < self + + # + # Parses argument if given, or uses default value. + # + def parse(arg, argv, &error) + if arg + conv_arg(*parse_arg(arg, &error)) + else + conv_arg(arg) + end + end + end + + # + # Switch that takes an argument, which does not begin with '-'. + # + class PlacedArgument < self + + # + # Returns nil if argument is not present or begins with '-'. + # + def parse(arg, argv, &error) + if !(val = arg) and (argv.empty? or /\A-/ =~ (val = argv[0])) + return nil, block, nil + end + opt = (val = parse_arg(val, &error))[1] + val = conv_arg(*val) + if opt and !arg + argv.shift + else + val[0] = nil + end + val + end + end + end + + # + # Simple option list providing mapping from short and/or long option + # string to OptionParser::Switch and mapping from acceptable argument to + # matching pattern and converter pair. Also provides summary feature. + # + class List + # Map from acceptable argument types to pattern and converter pairs. + attr_reader :atype + + # Map from short style option switches to actual switch objects. + attr_reader :short + + # Map from long style option switches to actual switch objects. + attr_reader :long + + # List of all switches and summary string. + attr_reader :list + + # + # Just initializes all instance variables. + # + def initialize + @atype = {} + @short = OptionMap.new + @long = OptionMap.new + @list = [] + end + + # + # See OptionParser.accept. + # + def accept(t, pat = /.*/m, &block) + if pat + pat.respond_to?(:match) or + raise TypeError, "has no `match'", ParseError.filter_backtrace(caller(2)) + else + pat = t if t.respond_to?(:match) + end + unless block + block = pat.method(:convert).to_proc if pat.respond_to?(:convert) + end + @atype[t] = [pat, block] + end + + # + # See OptionParser.reject. + # + def reject(t) + @atype.delete(t) + end + + # + # Adds +sw+ according to +sopts+, +lopts+ and +nlopts+. + # + # +sw+:: OptionParser::Switch instance to be added. + # +sopts+:: Short style option list. + # +lopts+:: Long style option list. + # +nlopts+:: Negated long style options list. + # + def update(sw, sopts, lopts, nsw = nil, nlopts = nil) + sopts.each {|o| @short[o] = sw} if sopts + lopts.each {|o| @long[o] = sw} if lopts + nlopts.each {|o| @long[o] = nsw} if nsw and nlopts + used = @short.invert.update(@long.invert) + @list.delete_if {|o| Switch === o and !used[o]} + end + private :update + + # + # Inserts +switch+ at the head of the list, and associates short, long + # and negated long options. Arguments are: + # + # +switch+:: OptionParser::Switch instance to be inserted. + # +short_opts+:: List of short style options. + # +long_opts+:: List of long style options. + # +nolong_opts+:: List of long style options with "no-" prefix. + # + # prepend(switch, short_opts, long_opts, nolong_opts) + # + def prepend(*args) + update(*args) + @list.unshift(args[0]) + end + + # + # Appends +switch+ at the tail of the list, and associates short, long + # and negated long options. Arguments are: + # + # +switch+:: OptionParser::Switch instance to be inserted. + # +short_opts+:: List of short style options. + # +long_opts+:: List of long style options. + # +nolong_opts+:: List of long style options with "no-" prefix. + # + # append(switch, short_opts, long_opts, nolong_opts) + # + def append(*args) + update(*args) + @list.push(args[0]) + end + + # + # Searches +key+ in +id+ list. The result is returned or yielded if a + # block is given. If it isn't found, nil is returned. + # + def search(id, key) + if list = __send__(id) + val = list.fetch(key) {return nil} + block_given? ? yield(val) : val + end + end + + # + # Searches list +id+ for +opt+ and the optional patterns for completion + # +pat+. If +icase+ is true, the search is case insensitive. The result + # is returned or yielded if a block is given. If it isn't found, nil is + # returned. + # + def complete(id, opt, icase = false, *pat, &block) + __send__(id).complete(opt, icase, *pat, &block) + end + + # + # Iterates over each option, passing the option to the +block+. + # + def each_option(&block) + list.each(&block) + end + + # + # Creates the summary table, passing each line to the +block+ (without + # newline). The arguments +args+ are passed along to the summarize + # method which is called on every option. + # + def summarize(*args, &block) + sum = [] + list.reverse_each do |opt| + if opt.respond_to?(:summarize) # perhaps OptionParser::Switch + s = [] + opt.summarize(*args) {|l| s << l} + sum.concat(s.reverse) + elsif !opt or opt.empty? + sum << "" + elsif opt.respond_to?(:each_line) + sum.concat([*opt.each_line].reverse) + else + sum.concat([*opt.each].reverse) + end + end + sum.reverse_each(&block) + end + + def add_banner(to) # :nodoc: + list.each do |opt| + if opt.respond_to?(:add_banner) + opt.add_banner(to) + end + end + to + end + + def compsys(*args, &block) # :nodoc: + list.each do |opt| + if opt.respond_to?(:compsys) + opt.compsys(*args, &block) + end + end + end + end + + # + # Hash with completion search feature. See OptionParser::Completion. + # + class CompletingHash < Hash + include Completion + + # + # Completion for hash key. + # + def match(key) + *values = fetch(key) { + raise AmbiguousArgument, catch(:ambiguous) {return complete(key)} + } + return key, *values + end + end + + # :stopdoc: + + # + # Enumeration of acceptable argument styles. Possible values are: + # + # NO_ARGUMENT:: The switch takes no arguments. (:NONE) + # REQUIRED_ARGUMENT:: The switch requires an argument. (:REQUIRED) + # OPTIONAL_ARGUMENT:: The switch requires an optional argument. (:OPTIONAL) + # + # Use like --switch=argument (long style) or -Xargument (short style). For + # short style, only portion matched to argument pattern is treated as + # argument. + # + ArgumentStyle = {} + NoArgument.each {|el| ArgumentStyle[el] = Switch::NoArgument} + RequiredArgument.each {|el| ArgumentStyle[el] = Switch::RequiredArgument} + OptionalArgument.each {|el| ArgumentStyle[el] = Switch::OptionalArgument} + ArgumentStyle.freeze + + # + # Switches common used such as '--', and also provides default + # argument classes + # + DefaultList = List.new + DefaultList.short['-'] = Switch::NoArgument.new {} + DefaultList.long[''] = Switch::NoArgument.new {throw :terminate} + + + COMPSYS_HEADER = <<'XXX' # :nodoc: + +typeset -A opt_args +local context state line + +_arguments -s -S \ +XXX + + def compsys(to, name = File.basename($0)) # :nodoc: + to << "#compdef #{name}\n" + to << COMPSYS_HEADER + visit(:compsys, {}, {}) {|o, d| + to << %Q[ "#{o}[#{d.gsub(/[\"\[\]]/, '\\\\\&')}]" \\\n] + } + to << " '*:file:_files' && return 0\n" + end + + # + # Default options for ARGV, which never appear in option summary. + # + Officious = {} + + # + # --help + # Shows option summary. + # + Officious['help'] = proc do |parser| + Switch::NoArgument.new do |arg| + puts parser.help + exit + end + end + + # + # --*-completion-bash=WORD + # Shows candidates for command line completion. + # + Officious['*-completion-bash'] = proc do |parser| + Switch::RequiredArgument.new do |arg| + puts parser.candidate(arg) + exit + end + end + + # + # --*-completion-zsh[=NAME:FILE] + # Creates zsh completion file. + # + Officious['*-completion-zsh'] = proc do |parser| + Switch::OptionalArgument.new do |arg| + parser.compsys(STDOUT, arg) + exit + end + end + + # + # --version + # Shows version string if Version is defined. + # + Officious['version'] = proc do |parser| + Switch::OptionalArgument.new do |pkg| + if pkg + begin + require 'standard/optparse/version' + rescue LoadError + else + show_version(*pkg.split(/,/)) or + abort("#{parser.program_name}: no version found in package #{pkg}") + exit + end + end + v = parser.ver or abort("#{parser.program_name}: version unknown") + puts v + exit + end + end + + # :startdoc: + + # + # Class methods + # + + # + # Initializes a new instance and evaluates the optional block in context + # of the instance. Arguments +args+ are passed to #new, see there for + # description of parameters. + # + # This method is *deprecated*, its behavior corresponds to the older #new + # method. + # + def self.with(*args, &block) + opts = new(*args) + opts.instance_eval(&block) + opts + end + + # + # Returns an incremented value of +default+ according to +arg+. + # + def self.inc(arg, default = nil) + case arg + when Integer + arg.nonzero? + when nil + default.to_i + 1 + end + end + def inc(*args) + self.class.inc(*args) + end + + # + # Initializes the instance and yields itself if called with a block. + # + # +banner+:: Banner message. + # +width+:: Summary width. + # +indent+:: Summary indent. + # + def initialize(banner = nil, width = 32, indent = ' ' * 4) + @stack = [DefaultList, List.new, List.new] + @program_name = nil + @banner = banner + @summary_width = width + @summary_indent = indent + @default_argv = ARGV + add_officious + yield self if block_given? + end + + def add_officious # :nodoc: + list = base() + Officious.each do |opt, block| + list.long[opt] ||= block.call(self) + end + end + + # + # Terminates option parsing. Optional parameter +arg+ is a string pushed + # back to be the first non-option argument. + # + def terminate(arg = nil) + self.class.terminate(arg) + end + def self.terminate(arg = nil) + throw :terminate, arg + end + + @stack = [DefaultList] + def self.top() DefaultList end + + # + # Directs to accept specified class +t+. The argument string is passed to + # the block in which it should be converted to the desired class. + # + # +t+:: Argument class specifier, any object including Class. + # +pat+:: Pattern for argument, defaults to +t+ if it responds to match. + # + # accept(t, pat, &block) + # + def accept(*args, &blk) top.accept(*args, &blk) end + # + # See #accept. + # + def self.accept(*args, &blk) top.accept(*args, &blk) end + + # + # Directs to reject specified class argument. + # + # +t+:: Argument class specifier, any object including Class. + # + # reject(t) + # + def reject(*args, &blk) top.reject(*args, &blk) end + # + # See #reject. + # + def self.reject(*args, &blk) top.reject(*args, &blk) end + + # + # Instance methods + # + + # Heading banner preceding summary. + attr_writer :banner + + # Program name to be emitted in error message and default banner, + # defaults to $0. + attr_writer :program_name + + # Width for option list portion of summary. Must be Numeric. + attr_accessor :summary_width + + # Indentation for summary. Must be String (or have + String method). + attr_accessor :summary_indent + + # Strings to be parsed in default. + attr_accessor :default_argv + + # + # Heading banner preceding summary. + # + def banner + unless @banner + @banner = "Usage: #{program_name} [options]" + visit(:add_banner, @banner) + end + @banner + end + + # + # Program name to be emitted in error message and default banner, defaults + # to $0. + # + def program_name + @program_name || File.basename($0, '.*') + end + + # for experimental cascading :-) + alias set_banner banner= + alias set_program_name program_name= + alias set_summary_width summary_width= + alias set_summary_indent summary_indent= + + # Version + attr_writer :version + # Release code + attr_writer :release + + # + # Version + # + def version + @version || (defined?(::Version) && ::Version) + end + + # + # Release code + # + def release + @release || (defined?(::Release) && ::Release) || (defined?(::RELEASE) && ::RELEASE) + end + + # + # Returns version string from program_name, version and release. + # + def ver + if v = version + str = "#{program_name} #{[v].join('.')}" + str << " (#{v})" if v = release + str + end + end + + def warn(mesg = $!) + super("#{program_name}: #{mesg}") + end + + def abort(mesg = $!) + super("#{program_name}: #{mesg}") + end + + # + # Subject of #on / #on_head, #accept / #reject + # + def top + @stack[-1] + end + + # + # Subject of #on_tail. + # + def base + @stack[1] + end + + # + # Pushes a new List. + # + def new + @stack.push(List.new) + if block_given? + yield self + else + self + end + end + + # + # Removes the last List. + # + def remove + @stack.pop + end + + # + # Puts option summary into +to+ and returns +to+. Yields each line if + # a block is given. + # + # +to+:: Output destination, which must have method <<. Defaults to []. + # +width+:: Width of left side, defaults to @summary_width. + # +max+:: Maximum length allowed for left side, defaults to +width+ - 1. + # +indent+:: Indentation, defaults to @summary_indent. + # + def summarize(to = [], width = @summary_width, max = width - 1, indent = @summary_indent, &blk) + blk ||= proc {|l| to << (l.index($/, -1) ? l : l + $/)} + visit(:summarize, {}, {}, width, max, indent, &blk) + to + end + + # + # Returns option summary string. + # + def help; summarize("#{banner}".sub(/\n?\z/, "\n")) end + alias to_s help + + # + # Returns option summary list. + # + def to_a; summarize("#{banner}".split(/^/)) end + + # + # Checks if an argument is given twice, in which case an ArgumentError is + # raised. Called from OptionParser#switch only. + # + # +obj+:: New argument. + # +prv+:: Previously specified argument. + # +msg+:: Exception message. + # + def notwice(obj, prv, msg) + unless !prv or prv == obj + raise(ArgumentError, "argument #{msg} given twice: #{obj}", + ParseError.filter_backtrace(caller(2))) + end + obj + end + private :notwice + + SPLAT_PROC = proc {|*a| a.length <= 1 ? a.first : a} # :nodoc: + # + # Creates an OptionParser::Switch from the parameters. The parsed argument + # value is passed to the given block, where it can be processed. + # + # See at the beginning of OptionParser for some full examples. + # + # +opts+ can include the following elements: + # + # [Argument style:] + # One of the following: + # :NONE, :REQUIRED, :OPTIONAL + # + # [Argument pattern:] + # Acceptable option argument format, must be pre-defined with + # OptionParser.accept or OptionParser#accept, or Regexp. This can appear + # once or assigned as String if not present, otherwise causes an + # ArgumentError. Examples: + # Float, Time, Array + # + # [Possible argument values:] + # Hash or Array. + # [:text, :binary, :auto] + # %w[iso-2022-jp shift_jis euc-jp utf8 binary] + # { "jis" => "iso-2022-jp", "sjis" => "shift_jis" } + # + # [Long style switch:] + # Specifies a long style switch which takes a mandatory, optional or no + # argument. It's a string of the following form: + # "--switch=MANDATORY" or "--switch MANDATORY" + # "--switch[=OPTIONAL]" + # "--switch" + # + # [Short style switch:] + # Specifies short style switch which takes a mandatory, optional or no + # argument. It's a string of the following form: + # "-xMANDATORY" + # "-x[OPTIONAL]" + # "-x" + # There is also a special form which matches character range (not full + # set of regular expression): + # "-[a-z]MANDATORY" + # "-[a-z][OPTIONAL]" + # "-[a-z]" + # + # [Argument style and description:] + # Instead of specifying mandatory or optional arguments directly in the + # switch parameter, this separate parameter can be used. + # "=MANDATORY" + # "=[OPTIONAL]" + # + # [Description:] + # Description string for the option. + # "Run verbosely" + # + # [Handler:] + # Handler for the parsed argument value. Either give a block or pass a + # Proc or Method as an argument. + # + def make_switch(opts, block = nil) + short, long, nolong, style, pattern, conv, not_pattern, not_conv, not_style = [], [], [] + ldesc, sdesc, desc, arg = [], [], [] + default_style = Switch::NoArgument + default_pattern = nil + klass = nil + q, a = nil + has_arg = false + + opts.each do |o| + # argument class + next if search(:atype, o) do |pat, c| + klass = notwice(o, klass, 'type') + if not_style and not_style != Switch::NoArgument + not_pattern, not_conv = pat, c + else + default_pattern, conv = pat, c + end + end + + # directly specified pattern(any object possible to match) + if (!(String === o || Symbol === o)) and o.respond_to?(:match) + pattern = notwice(o, pattern, 'pattern') + if pattern.respond_to?(:convert) + conv = pattern.method(:convert).to_proc + else + conv = SPLAT_PROC + end + next + end + + # anything others + case o + when Proc, Method + block = notwice(o, block, 'block') + when Array, Hash + case pattern + when CompletingHash + when nil + pattern = CompletingHash.new + conv = pattern.method(:convert).to_proc if pattern.respond_to?(:convert) + else + raise ArgumentError, "argument pattern given twice" + end + o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}} + when Module + raise ArgumentError, "unsupported argument type: #{o}", ParseError.filter_backtrace(caller(4)) + when *ArgumentStyle.keys + style = notwice(ArgumentStyle[o], style, 'style') + when /^--no-([^\[\]=\s]*)(.+)?/ + q, a = $1, $2 + o = notwice(a ? Object : TrueClass, klass, 'type') + not_pattern, not_conv = search(:atype, o) unless not_style + not_style = (not_style || default_style).guess(arg = a) if a + default_style = Switch::NoArgument + default_pattern, conv = search(:atype, FalseClass) unless default_pattern + ldesc << "--no-#{q}" + (q = q.downcase).tr!('_', '-') + long << "no-#{q}" + nolong << q + when /^--\[no-\]([^\[\]=\s]*)(.+)?/ + q, a = $1, $2 + o = notwice(a ? Object : TrueClass, klass, 'type') + if a + default_style = default_style.guess(arg = a) + default_pattern, conv = search(:atype, o) unless default_pattern + end + ldesc << "--[no-]#{q}" + (o = q.downcase).tr!('_', '-') + long << o + not_pattern, not_conv = search(:atype, FalseClass) unless not_style + not_style = Switch::NoArgument + nolong << "no-#{o}" + when /^--([^\[\]=\s]*)(.+)?/ + q, a = $1, $2 + if a + o = notwice(NilClass, klass, 'type') + default_style = default_style.guess(arg = a) + default_pattern, conv = search(:atype, o) unless default_pattern + end + ldesc << "--#{q}" + (o = q.downcase).tr!('_', '-') + long << o + when /^-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/ + q, a = $1, $2 + o = notwice(Object, klass, 'type') + if a + default_style = default_style.guess(arg = a) + default_pattern, conv = search(:atype, o) unless default_pattern + else + has_arg = true + end + sdesc << "-#{q}" + short << Regexp.new(q) + when /^-(.)(.+)?/ + q, a = $1, $2 + if a + o = notwice(NilClass, klass, 'type') + default_style = default_style.guess(arg = a) + default_pattern, conv = search(:atype, o) unless default_pattern + end + sdesc << "-#{q}" + short << q + when /^=/ + style = notwice(default_style.guess(arg = o), style, 'style') + default_pattern, conv = search(:atype, Object) unless default_pattern + else + desc.push(o) + end + end + + default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern + if !(short.empty? and long.empty?) + if has_arg and default_style == Switch::NoArgument + default_style = Switch::RequiredArgument + end + s = (style || default_style).new(pattern || default_pattern, + conv, sdesc, ldesc, arg, desc, block) + elsif !block + if style or pattern + raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller) + end + s = desc + else + short << pattern + s = (style || default_style).new(pattern, + conv, nil, nil, arg, desc, block) + end + return s, short, long, + (not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style), + nolong + end + + def define(*opts, &block) + top.append(*(sw = make_switch(opts, block))) + sw[0] + end + + # + # Add option switch and handler. See #make_switch for an explanation of + # parameters. + # + def on(*opts, &block) + define(*opts, &block) + self + end + alias def_option define + + def define_head(*opts, &block) + top.prepend(*(sw = make_switch(opts, block))) + sw[0] + end + + # + # Add option switch like with #on, but at head of summary. + # + def on_head(*opts, &block) + define_head(*opts, &block) + self + end + alias def_head_option define_head + + def define_tail(*opts, &block) + base.append(*(sw = make_switch(opts, block))) + sw[0] + end + + # + # Add option switch like with #on, but at tail of summary. + # + def on_tail(*opts, &block) + define_tail(*opts, &block) + self + end + alias def_tail_option define_tail + + # + # Add separator in summary. + # + def separator(string) + top.append(string, nil, nil) + end + + # + # Parses command line arguments +argv+ in order. When a block is given, + # each non-option argument is yielded. + # + # Returns the rest of +argv+ left unparsed. + # + def order(*argv, into: nil, &nonopt) + argv = argv[0].dup if argv.size == 1 and Array === argv[0] + order!(argv, into: into, &nonopt) + end + + # + # Same as #order, but removes switches destructively. + # Non-option arguments remain in +argv+. + # + def order!(argv = default_argv, into: nil, &nonopt) + setter = ->(name, val) {into[name.to_sym] = val} if into + parse_in_order(argv, setter, &nonopt) + end + + def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: + opt, arg, val, rest = nil + nonopt ||= proc {|a| throw :terminate, a} + argv.unshift(arg) if arg = catch(:terminate) { + while arg = argv.shift + case arg + # long option + when /\A--([^=]*)(?:=(.*))?/m + opt, rest = $1, $2 + opt.tr!('_', '-') + begin + sw, = complete(:long, opt, true) + rescue ParseError + raise $!.set_option(arg, true) + end + begin + opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)} + val = cb.call(val) if cb + setter.call(sw.switch_name, val) if setter + rescue ParseError + raise $!.set_option(arg, rest) + end + + # short option + when /\A-(.)((=).*|.+)?/m + eq, rest, opt = $3, $2, $1 + has_arg, val = eq, rest + begin + sw, = search(:short, opt) + unless sw + begin + sw, = complete(:short, opt) + # short option matched. + val = arg.sub(/\A-/, '') + has_arg = true + rescue InvalidOption + # if no short options match, try completion with long + # options. + sw, = complete(:long, opt) + eq ||= !rest + end + end + rescue ParseError + raise $!.set_option(arg, true) + end + begin + opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq} + raise InvalidOption, arg if has_arg and !eq and arg == "-#{opt}" + argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-') + val = cb.call(val) if cb + setter.call(sw.switch_name, val) if setter + rescue ParseError + raise $!.set_option(arg, arg.length > 2) + end + + # non-option argument + else + catch(:prune) do + visit(:each_option) do |sw0| + sw = sw0 + sw.block.call(arg) if Switch === sw and sw.match_nonswitch?(arg) + end + nonopt.call(arg) + end + end + end + + nil + } + + visit(:search, :short, nil) {|sw| sw.block.call(*argv) if !sw.pattern} + + argv + end + private :parse_in_order + + # + # Parses command line arguments +argv+ in permutation mode and returns + # list of non-option arguments. + # + def permute(*argv, into: nil) + argv = argv[0].dup if argv.size == 1 and Array === argv[0] + permute!(argv, into: into) + end + + # + # Same as #permute, but removes switches destructively. + # Non-option arguments remain in +argv+. + # + def permute!(argv = default_argv, into: nil) + nonopts = [] + order!(argv, into: into, &nonopts.method(:<<)) + argv[0, 0] = nonopts + argv + end + + # + # Parses command line arguments +argv+ in order when environment variable + # POSIXLY_CORRECT is set, and in permutation mode otherwise. + # + def parse(*argv, into: nil) + argv = argv[0].dup if argv.size == 1 and Array === argv[0] + parse!(argv, into: into) + end + + # + # Same as #parse, but removes switches destructively. + # Non-option arguments remain in +argv+. + # + def parse!(argv = default_argv, into: nil) + if ENV.include?('POSIXLY_CORRECT') + order!(argv, into: into) + else + permute!(argv, into: into) + end + end + + # + # Wrapper method for getopts.rb. + # + # params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option") + # # params["a"] = true # -a + # # params["b"] = "1" # -b1 + # # params["foo"] = "1" # --foo + # # params["bar"] = "x" # --bar x + # # params["zot"] = "z" # --zot Z + # + def getopts(*args) + argv = Array === args.first ? args.shift : default_argv + single_options, *long_options = *args + + result = {} + + single_options.scan(/(.)(:)?/) do |opt, val| + if val + result[opt] = nil + define("-#{opt} VAL") + else + result[opt] = false + define("-#{opt}") + end + end if single_options + + long_options.each do |arg| + arg, desc = arg.split(';', 2) + opt, val = arg.split(':', 2) + if val + result[opt] = val.empty? ? nil : val + define("--#{opt}=#{result[opt] || "VAL"}", *[desc].compact) + else + result[opt] = false + define("--#{opt}", *[desc].compact) + end + end + + parse_in_order(argv, result.method(:[]=)) + result + end + + # + # See #getopts. + # + def self.getopts(*args) + new.getopts(*args) + end + + # + # Traverses @stack, sending each element method +id+ with +args+ and + # +block+. + # + def visit(id, *args, &block) + @stack.reverse_each do |el| + el.send(id, *args, &block) + end + nil + end + private :visit + + # + # Searches +key+ in @stack for +id+ hash and returns or yields the result. + # + def search(id, key) + block_given = block_given? + visit(:search, id, key) do |k| + return block_given ? yield(k) : k + end + end + private :search + + # + # Completes shortened long style option switch and returns pair of + # canonical switch and switch descriptor OptionParser::Switch. + # + # +typ+:: Searching table. + # +opt+:: Searching key. + # +icase+:: Search case insensitive if true. + # +pat+:: Optional pattern for completion. + # + def complete(typ, opt, icase = false, *pat) + if pat.empty? + search(typ, opt) {|sw| return [sw, opt]} # exact match or... + end + raise AmbiguousOption, catch(:ambiguous) { + visit(:complete, typ, opt, icase, *pat) {|o, *sw| return sw} + raise InvalidOption, opt + } + end + private :complete + + def candidate(word) + list = [] + case word + when '-' + long = short = true + when /\A--/ + word, arg = word.split(/=/, 2) + argpat = Completion.regexp(arg, false) if arg and !arg.empty? + long = true + when /\A-/ + short = true + end + pat = Completion.regexp(word, long) + visit(:each_option) do |opt| + next unless Switch === opt + opts = (long ? opt.long : []) + (short ? opt.short : []) + opts = Completion.candidate(word, true, pat, &opts.method(:each)).map(&:first) if pat + if /\A=/ =~ opt.arg + opts.map! {|sw| sw + "="} + if arg and CompletingHash === opt.pattern + if opts = opt.pattern.candidate(arg, false, argpat) + opts.map!(&:last) + end + end + end + list.concat(opts) + end + list + end + + # + # Loads options from file names as +filename+. Does nothing when the file + # is not present. Returns whether successfully loaded. + # + # +filename+ defaults to basename of the program without suffix in a + # directory ~/.options. + # + def load(filename = nil) + begin + filename ||= File.expand_path(File.basename($0, '.*'), '~/.options') + rescue + return false + end + begin + parse(*IO.readlines(filename).each {|s| s.chomp!}) + true + rescue Errno::ENOENT, Errno::ENOTDIR + false + end + end + + # + # Parses environment variable +env+ or its uppercase with splitting like a + # shell. + # + # +env+ defaults to the basename of the program. + # + def environment(env = File.basename($0, '.*')) + env = ENV[env] || ENV[env.upcase] or return + require 'standard/shellwords' + parse(*Shellwords.shellwords(env)) + end + + # + # Acceptable argument classes + # + + # + # Any string and no conversion. This is fall-back. + # + accept(Object) {|s,|s or s.nil?} + + accept(NilClass) {|s,|s} + + # + # Any non-empty string, and no conversion. + # + accept(String, /.+/m) {|s,*|s} + + # + # Ruby/C-like integer, octal for 0-7 sequence, binary for 0b, hexadecimal + # for 0x, and decimal for others; with optional sign prefix. Converts to + # Integer. + # + decimal = '\d+(?:_\d+)*' + binary = 'b[01]+(?:_[01]+)*' + hex = 'x[\da-f]+(?:_[\da-f]+)*' + octal = "0(?:[0-7]+(?:_[0-7]+)*|#{binary}|#{hex})?" + integer = "#{octal}|#{decimal}" + + accept(Integer, %r"\A[-+]?(?:#{integer})\z"io) {|s,| + begin + Integer(s) + rescue ArgumentError + raise OptionParser::InvalidArgument, s + end if s + } + + # + # Float number format, and converts to Float. + # + float = "(?:#{decimal}(?=(.)?)(?:\\.(?:#{decimal})?)?|\\.#{decimal})(?:E[-+]?#{decimal})?" + floatpat = %r"\A[-+]?#{float}\z"io + accept(Float, floatpat) {|s,| s.to_f if s} + + # + # Generic numeric format, converts to Integer for integer format, Float + # for float format, and Rational for rational format. + # + real = "[-+]?(?:#{octal}|#{float})" + accept(Numeric, /\A(#{real})(?:\/(#{real}))?\z/io) {|s, d, f, n,| + if n + Rational(d, n) + elsif f + Float(s) + else + Integer(s) + end + } + + # + # Decimal integer format, to be converted to Integer. + # + DecimalInteger = /\A[-+]?#{decimal}\z/io + accept(DecimalInteger, DecimalInteger) {|s,| + begin + Integer(s, 10) + rescue ArgumentError + raise OptionParser::InvalidArgument, s + end if s + } + + # + # Ruby/C like octal/hexadecimal/binary integer format, to be converted to + # Integer. + # + OctalInteger = /\A[-+]?(?:[0-7]+(?:_[0-7]+)*|0(?:#{binary}|#{hex}))\z/io + accept(OctalInteger, OctalInteger) {|s,| + begin + Integer(s, 8) + rescue ArgumentError + raise OptionParser::InvalidArgument, s + end if s + } + + # + # Decimal integer/float number format, to be converted to Integer for + # integer format, Float for float format. + # + DecimalNumeric = floatpat # decimal integer is allowed as float also. + accept(DecimalNumeric, floatpat) {|s, f| + begin + if f + Float(s) + else + Integer(s) + end + rescue ArgumentError + raise OptionParser::InvalidArgument, s + end if s + } + + # + # Boolean switch, which means whether it is present or not, whether it is + # absent or not with prefix no-, or it takes an argument + # yes/no/true/false/+/-. + # + yesno = CompletingHash.new + %w[- no false].each {|el| yesno[el] = false} + %w[+ yes true].each {|el| yesno[el] = true} + yesno['nil'] = false # should be nil? + accept(TrueClass, yesno) {|arg, val| val == nil or val} + # + # Similar to TrueClass, but defaults to false. + # + accept(FalseClass, yesno) {|arg, val| val != nil and val} + + # + # List of strings separated by ",". + # + accept(Array) do |s, | + if s + s = s.split(',').collect {|ss| ss unless ss.empty?} + end + s + end + + # + # Regular expression with options. + # + accept(Regexp, %r"\A/((?:\\.|[^\\])*)/([[:alpha:]]+)?\z|.*") do |all, s, o| + f = 0 + if o + f |= Regexp::IGNORECASE if /i/ =~ o + f |= Regexp::MULTILINE if /m/ =~ o + f |= Regexp::EXTENDED if /x/ =~ o + k = o.delete("imx") + k = nil if k.empty? + end + Regexp.new(s || all, f, k) + end + + # + # Exceptions + # + + # + # Base class of exceptions from OptionParser. + # + class ParseError < RuntimeError + # Reason which caused the error. + Reason = 'parse error'.freeze + + def initialize(*args) + @args = args + @reason = nil + end + + attr_reader :args + attr_writer :reason + + # + # Pushes back erred argument(s) to +argv+. + # + def recover(argv) + argv[0, 0] = @args + argv + end + + def self.filter_backtrace(array) + unless $DEBUG + array.delete_if(&%r"\A#{Regexp.quote(__FILE__)}:"o.method(:=~)) + end + array + end + + def set_backtrace(array) + super(self.class.filter_backtrace(array)) + end + + def set_option(opt, eq) + if eq + @args[0] = opt + else + @args.unshift(opt) + end + self + end + + # + # Returns error reason. Override this for I18N. + # + def reason + @reason || self.class::Reason + end + + def inspect + "#<#{self.class}: #{args.join(' ')}>" + end + + # + # Default stringizing method to emit standard error message. + # + def message + reason + ': ' + args.join(' ') + end + + alias to_s message + end + + # + # Raises when ambiguously completable string is encountered. + # + class AmbiguousOption < ParseError + const_set(:Reason, 'ambiguous option'.freeze) + end + + # + # Raises when there is an argument for a switch which takes no argument. + # + class NeedlessArgument < ParseError + const_set(:Reason, 'needless argument'.freeze) + end + + # + # Raises when a switch with mandatory argument has no argument. + # + class MissingArgument < ParseError + const_set(:Reason, 'missing argument'.freeze) + end + + # + # Raises when switch is undefined. + # + class InvalidOption < ParseError + const_set(:Reason, 'invalid option'.freeze) + end + + # + # Raises when the given argument does not match required format. + # + class InvalidArgument < ParseError + const_set(:Reason, 'invalid argument'.freeze) + end + + # + # Raises when the given argument word can't be completed uniquely. + # + class AmbiguousArgument < InvalidArgument + const_set(:Reason, 'ambiguous argument'.freeze) + end + + # + # Miscellaneous + # + + # + # Extends command line arguments array (ARGV) to parse itself. + # + module Arguable + + # + # Sets OptionParser object, when +opt+ is +false+ or +nil+, methods + # OptionParser::Arguable#options and OptionParser::Arguable#options= are + # undefined. Thus, there is no ways to access the OptionParser object + # via the receiver object. + # + def options=(opt) + unless @optparse = opt + class << self + undef_method(:options) + undef_method(:options=) + end + end + end + + # + # Actual OptionParser object, automatically created if nonexistent. + # + # If called with a block, yields the OptionParser object and returns the + # result of the block. If an OptionParser::ParseError exception occurs + # in the block, it is rescued, a error message printed to STDERR and + # +nil+ returned. + # + def options + @optparse ||= OptionParser.new + @optparse.default_argv = self + block_given? or return @optparse + begin + yield @optparse + rescue ParseError + @optparse.warn $! + nil + end + end + + # + # Parses +self+ destructively in order and returns +self+ containing the + # rest arguments left unparsed. + # + def order!(&blk) options.order!(self, &blk) end + + # + # Parses +self+ destructively in permutation mode and returns +self+ + # containing the rest arguments left unparsed. + # + def permute!() options.permute!(self) end + + # + # Parses +self+ destructively and returns +self+ containing the + # rest arguments left unparsed. + # + def parse!() options.parse!(self) end + + # + # Substitution of getopts is possible as follows. Also see + # OptionParser#getopts. + # + # def getopts(*args) + # ($OPT = ARGV.getopts(*args)).each do |opt, val| + # eval "$OPT_#{opt.gsub(/[^A-Za-z0-9_]/, '_')} = val" + # end + # rescue OptionParser::ParseError + # end + # + def getopts(*args) + options.getopts(self, *args) + end + + # + # Initializes instance variable. + # + def self.extend_object(obj) + super + obj.instance_eval {@optparse = nil} + end + def initialize(*args) + super + @optparse = nil + end + end + + # + # Acceptable argument classes. Now contains DecimalInteger, OctalInteger + # and DecimalNumeric. See Acceptable argument classes (in source code). + # + module Acceptables + const_set(:DecimalInteger, OptionParser::DecimalInteger) + const_set(:OctalInteger, OptionParser::OctalInteger) + const_set(:DecimalNumeric, OptionParser::DecimalNumeric) + end +end + +# ARGV is arguable by OptionParser +ARGV.extend(OptionParser::Arguable) + +# An alias for OptionParser. +OptParse = OptionParser # :nodoc: diff --git a/gxg/standard/optparse/ac.rb b/gxg/standard/optparse/ac.rb new file mode 100644 index 0000000..fb0883f --- /dev/null +++ b/gxg/standard/optparse/ac.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: false +require 'optparse' + +class OptionParser::AC < OptionParser + private + + def _check_ac_args(name, block) + unless /\A\w[-\w]*\z/ =~ name + raise ArgumentError, name + end + unless block + raise ArgumentError, "no block given", ParseError.filter_backtrace(caller) + end + end + + def _ac_arg_enable(prefix, name, help_string, block) + _check_ac_args(name, block) + + sdesc = [] + ldesc = ["--#{prefix}-#{name}"] + desc = [help_string] + q = name.downcase + enable = Switch::NoArgument.new(nil, proc {true}, sdesc, ldesc, nil, desc, block) + disable = Switch::NoArgument.new(nil, proc {false}, sdesc, ldesc, nil, desc, block) + top.append(enable, [], ["enable-" + q], disable, ['disable-' + q]) + enable + end + + public + + def ac_arg_enable(name, help_string, &block) + _ac_arg_enable("enable", name, help_string, block) + end + + def ac_arg_disable(name, help_string, &block) + _ac_arg_enable("disable", name, help_string, block) + end + + def ac_arg_with(name, help_string, &block) + _check_ac_args(name, block) + + sdesc = [] + ldesc = ["--with-#{name}"] + desc = [help_string] + q = name.downcase + with = Switch::PlacedArgument.new(*search(:atype, String), sdesc, ldesc, nil, desc, block) + without = Switch::NoArgument.new(nil, proc {}, sdesc, ldesc, nil, desc, block) + top.append(with, [], ["with-" + q], without, ['without-' + q]) + with + end +end diff --git a/gxg/standard/optparse/date.rb b/gxg/standard/optparse/date.rb new file mode 100644 index 0000000..d6649c8 --- /dev/null +++ b/gxg/standard/optparse/date.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: false +require 'optparse' +require 'date' + +OptionParser.accept(DateTime) do |s,| + begin + DateTime.parse(s) if s + rescue ArgumentError + raise OptionParser::InvalidArgument, s + end +end +OptionParser.accept(Date) do |s,| + begin + Date.parse(s) if s + rescue ArgumentError + raise OptionParser::InvalidArgument, s + end +end diff --git a/gxg/standard/optparse/kwargs.rb b/gxg/standard/optparse/kwargs.rb new file mode 100644 index 0000000..ed58cc1 --- /dev/null +++ b/gxg/standard/optparse/kwargs.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +require 'optparse' + +class OptionParser + def define_by_keywords(options, meth, **opts) + meth.parameters.each do |type, name| + case type + when :key, :keyreq + op, cl = *(type == :key ? %w"[ ]" : ["", ""]) + define("--#{name}=#{op}#{name.upcase}#{cl}", *opts[name]) do |o| + options[name] = o + end + end + end + options + end +end diff --git a/gxg/standard/optparse/shellwords.rb b/gxg/standard/optparse/shellwords.rb new file mode 100644 index 0000000..bf31701 --- /dev/null +++ b/gxg/standard/optparse/shellwords.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: false +# -*- ruby -*- + +require 'shellwords' +require 'optparse' + +OptionParser.accept(Shellwords) {|s,| Shellwords.shellwords(s)} diff --git a/gxg/standard/optparse/time.rb b/gxg/standard/optparse/time.rb new file mode 100644 index 0000000..ffc6ff0 --- /dev/null +++ b/gxg/standard/optparse/time.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: false +require 'optparse' +require 'time' + +OptionParser.accept(Time) do |s,| + begin + (Time.httpdate(s) rescue Time.parse(s)) if s + rescue + raise OptionParser::InvalidArgument, s + end +end diff --git a/gxg/standard/optparse/uri.rb b/gxg/standard/optparse/uri.rb new file mode 100644 index 0000000..51550cf --- /dev/null +++ b/gxg/standard/optparse/uri.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: false +# -*- ruby -*- + +require 'optparse' +require 'uri' + +OptionParser.accept(URI) {|s,| URI.parse(s) if s} diff --git a/gxg/standard/optparse/version.rb b/gxg/standard/optparse/version.rb new file mode 100644 index 0000000..b869d8f --- /dev/null +++ b/gxg/standard/optparse/version.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: false +# OptionParser internal utility + +class << OptionParser + def show_version(*pkgs) + progname = ARGV.options.program_name + result = false + show = proc do |klass, cname, version| + str = "#{progname}" + unless klass == ::Object and cname == :VERSION + version = version.join(".") if Array === version + str << ": #{klass}" unless klass == Object + str << " version #{version}" + end + [:Release, :RELEASE].find do |rel| + if klass.const_defined?(rel) + str << " (#{klass.const_get(rel)})" + end + end + puts str + result = true + end + if pkgs.size == 1 and pkgs[0] == "all" + self.search_const(::Object, /\AV(?:ERSION|ersion)\z/) do |klass, cname, version| + unless cname[1] == ?e and klass.const_defined?(:Version) + show.call(klass, cname.intern, version) + end + end + else + pkgs.each do |pkg| + begin + pkg = pkg.split(/::|\//).inject(::Object) {|m, c| m.const_get(c)} + v = case + when pkg.const_defined?(:Version) + pkg.const_get(n = :Version) + when pkg.const_defined?(:VERSION) + pkg.const_get(n = :VERSION) + else + n = nil + "unknown" + end + show.call(pkg, n, v) + rescue NameError + end + end + end + result + end + + def each_const(path, base = ::Object) + path.split(/::|\//).inject(base) do |klass, name| + raise NameError, path unless Module === klass + klass.constants.grep(/#{name}/i) do |c| + klass.const_defined?(c) or next + klass.const_get(c) + end + end + end + + def search_const(klass, name) + klasses = [klass] + while klass = klasses.shift + klass.constants.each do |cname| + klass.const_defined?(cname) or next + const = klass.const_get(cname) + yield klass, cname, const if name === cname + klasses << const if Module === const and const != ::Object + end + end + end +end diff --git a/gxg/standard/prettyprint.rb b/gxg/standard/prettyprint.rb new file mode 100644 index 0000000..2a3ca90 --- /dev/null +++ b/gxg/standard/prettyprint.rb @@ -0,0 +1,556 @@ +# frozen_string_literal: true +# +# This class implements a pretty printing algorithm. It finds line breaks and +# nice indentations for grouped structure. +# +# By default, the class assumes that primitive elements are strings and each +# byte in the strings have single column in width. But it can be used for +# other situations by giving suitable arguments for some methods: +# * newline object and space generation block for PrettyPrint.new +# * optional width argument for PrettyPrint#text +# * PrettyPrint#breakable +# +# There are several candidate uses: +# * text formatting using proportional fonts +# * multibyte characters which has columns different to number of bytes +# * non-string formatting +# +# == Bugs +# * Box based formatting? +# * Other (better) model/algorithm? +# +# Report any bugs at http://bugs.ruby-lang.org +# +# == References +# Christian Lindig, Strictly Pretty, March 2000, +# http://www.st.cs.uni-sb.de/~lindig/papers/#pretty +# +# Philip Wadler, A prettier printer, March 1998, +# http://homepages.inf.ed.ac.uk/wadler/topics/language-design.html#prettier +# +# == Author +# Tanaka Akira +# +class PrettyPrint + + # This is a convenience method which is same as follows: + # + # begin + # q = PrettyPrint.new(output, maxwidth, newline, &genspace) + # ... + # q.flush + # output + # end + # + def PrettyPrint.format(output=''.dup, maxwidth=79, newline="\n", genspace=lambda {|n| ' ' * n}) + q = PrettyPrint.new(output, maxwidth, newline, &genspace) + yield q + q.flush + output + end + + # This is similar to PrettyPrint::format but the result has no breaks. + # + # +maxwidth+, +newline+ and +genspace+ are ignored. + # + # The invocation of +breakable+ in the block doesn't break a line and is + # treated as just an invocation of +text+. + # + def PrettyPrint.singleline_format(output=''.dup, maxwidth=nil, newline=nil, genspace=nil) + q = SingleLine.new(output) + yield q + output + end + + # Creates a buffer for pretty printing. + # + # +output+ is an output target. If it is not specified, '' is assumed. It + # should have a << method which accepts the first argument +obj+ of + # PrettyPrint#text, the first argument +sep+ of PrettyPrint#breakable, the + # first argument +newline+ of PrettyPrint.new, and the result of a given + # block for PrettyPrint.new. + # + # +maxwidth+ specifies maximum line length. If it is not specified, 79 is + # assumed. However actual outputs may overflow +maxwidth+ if long + # non-breakable texts are provided. + # + # +newline+ is used for line breaks. "\n" is used if it is not specified. + # + # The block is used to generate spaces. {|width| ' ' * width} is used if it + # is not given. + # + def initialize(output=''.dup, maxwidth=79, newline="\n", &genspace) + @output = output + @maxwidth = maxwidth + @newline = newline + @genspace = genspace || lambda {|n| ' ' * n} + + @output_width = 0 + @buffer_width = 0 + @buffer = [] + + root_group = Group.new(0) + @group_stack = [root_group] + @group_queue = GroupQueue.new(root_group) + @indent = 0 + end + + # The output object. + # + # This defaults to '', and should accept the << method + attr_reader :output + + # The maximum width of a line, before it is separated in to a newline + # + # This defaults to 79, and should be a Fixnum + attr_reader :maxwidth + + # The value that is appended to +output+ to add a new line. + # + # This defaults to "\n", and should be String + attr_reader :newline + + # A lambda or Proc, that takes one argument, of a Fixnum, and returns + # the corresponding number of spaces. + # + # By default this is: + # lambda {|n| ' ' * n} + attr_reader :genspace + + # The number of spaces to be indented + attr_reader :indent + + # The PrettyPrint::GroupQueue of groups in stack to be pretty printed + attr_reader :group_queue + + # Returns the group most recently added to the stack. + # + # Contrived example: + # out = "" + # => "" + # q = PrettyPrint.new(out) + # => #, @output_width=0, @buffer_width=0, @buffer=[], @group_stack=[#], @group_queue=#]]>, @indent=0> + # q.group { + # q.text q.current_group.inspect + # q.text q.newline + # q.group(q.current_group.depth + 1) { + # q.text q.current_group.inspect + # q.text q.newline + # q.group(q.current_group.depth + 1) { + # q.text q.current_group.inspect + # q.text q.newline + # q.group(q.current_group.depth + 1) { + # q.text q.current_group.inspect + # q.text q.newline + # } + # } + # } + # } + # => 284 + # puts out + # # + # # + # # + # # + def current_group + @group_stack.last + end + + # Breaks the buffer into lines that are shorter than #maxwidth + def break_outmost_groups + while @maxwidth < @output_width + @buffer_width + return unless group = @group_queue.deq + until group.breakables.empty? + data = @buffer.shift + @output_width = data.output(@output, @output_width) + @buffer_width -= data.width + end + while !@buffer.empty? && Text === @buffer.first + text = @buffer.shift + @output_width = text.output(@output, @output_width) + @buffer_width -= text.width + end + end + end + + # This adds +obj+ as a text of +width+ columns in width. + # + # If +width+ is not specified, obj.length is used. + # + def text(obj, width=obj.length) + if @buffer.empty? + @output << obj + @output_width += width + else + text = @buffer.last + unless Text === text + text = Text.new + @buffer << text + end + text.add(obj, width) + @buffer_width += width + break_outmost_groups + end + end + + # This is similar to #breakable except + # the decision to break or not is determined individually. + # + # Two #fill_breakable under a group may cause 4 results: + # (break,break), (break,non-break), (non-break,break), (non-break,non-break). + # This is different to #breakable because two #breakable under a group + # may cause 2 results: + # (break,break), (non-break,non-break). + # + # The text +sep+ is inserted if a line is not broken at this point. + # + # If +sep+ is not specified, " " is used. + # + # If +width+ is not specified, +sep.length+ is used. You will have to + # specify this when +sep+ is a multibyte character, for example. + # + def fill_breakable(sep=' ', width=sep.length) + group { breakable sep, width } + end + + # This says "you can break a line here if necessary", and a +width+\-column + # text +sep+ is inserted if a line is not broken at the point. + # + # If +sep+ is not specified, " " is used. + # + # If +width+ is not specified, +sep.length+ is used. You will have to + # specify this when +sep+ is a multibyte character, for example. + # + def breakable(sep=' ', width=sep.length) + group = @group_stack.last + if group.break? + flush + @output << @newline + @output << @genspace.call(@indent) + @output_width = @indent + @buffer_width = 0 + else + @buffer << Breakable.new(sep, width, self) + @buffer_width += width + break_outmost_groups + end + end + + # Groups line break hints added in the block. The line break hints are all + # to be used or not. + # + # If +indent+ is specified, the method call is regarded as nested by + # nest(indent) { ... }. + # + # If +open_obj+ is specified, text open_obj, open_width is called + # before grouping. If +close_obj+ is specified, text close_obj, + # close_width is called after grouping. + # + def group(indent=0, open_obj='', close_obj='', open_width=open_obj.length, close_width=close_obj.length) + text open_obj, open_width + group_sub { + nest(indent) { + yield + } + } + text close_obj, close_width + end + + # Takes a block and queues a new group that is indented 1 level further. + def group_sub + group = Group.new(@group_stack.last.depth + 1) + @group_stack.push group + @group_queue.enq group + begin + yield + ensure + @group_stack.pop + if group.breakables.empty? + @group_queue.delete group + end + end + end + + # Increases left margin after newline with +indent+ for line breaks added in + # the block. + # + def nest(indent) + @indent += indent + begin + yield + ensure + @indent -= indent + end + end + + # outputs buffered data. + # + def flush + @buffer.each {|data| + @output_width = data.output(@output, @output_width) + } + @buffer.clear + @buffer_width = 0 + end + + # The Text class is the means by which to collect strings from objects. + # + # This class is intended for internal use of the PrettyPrint buffers. + class Text # :nodoc: + + # Creates a new text object. + # + # This constructor takes no arguments. + # + # The workflow is to append a PrettyPrint::Text object to the buffer, and + # being able to call the buffer.last() to reference it. + # + # As there are objects, use PrettyPrint::Text#add to include the objects + # and the width to utilized by the String version of this object. + def initialize + @objs = [] + @width = 0 + end + + # The total width of the objects included in this Text object. + attr_reader :width + + # Render the String text of the objects that have been added to this Text object. + # + # Output the text to +out+, and increment the width to +output_width+ + def output(out, output_width) + @objs.each {|obj| out << obj} + output_width + @width + end + + # Include +obj+ in the objects to be pretty printed, and increment + # this Text object's total width by +width+ + def add(obj, width) + @objs << obj + @width += width + end + end + + # The Breakable class is used for breaking up object information + # + # This class is intended for internal use of the PrettyPrint buffers. + class Breakable # :nodoc: + + # Create a new Breakable object. + # + # Arguments: + # * +sep+ String of the separator + # * +width+ Fixnum width of the +sep+ + # * +q+ parent PrettyPrint object, to base from + def initialize(sep, width, q) + @obj = sep + @width = width + @pp = q + @indent = q.indent + @group = q.current_group + @group.breakables.push self + end + + # Holds the separator String + # + # The +sep+ argument from ::new + attr_reader :obj + + # The width of +obj+ / +sep+ + attr_reader :width + + # The number of spaces to indent. + # + # This is inferred from +q+ within PrettyPrint, passed in ::new + attr_reader :indent + + # Render the String text of the objects that have been added to this + # Breakable object. + # + # Output the text to +out+, and increment the width to +output_width+ + def output(out, output_width) + @group.breakables.shift + if @group.break? + out << @pp.newline + out << @pp.genspace.call(@indent) + @indent + else + @pp.group_queue.delete @group if @group.breakables.empty? + out << @obj + output_width + @width + end + end + end + + # The Group class is used for making indentation easier. + # + # While this class does neither the breaking into newlines nor indentation, + # it is used in a stack (as well as a queue) within PrettyPrint, to group + # objects. + # + # For information on using groups, see PrettyPrint#group + # + # This class is intended for internal use of the PrettyPrint buffers. + class Group # :nodoc: + # Create a Group object + # + # Arguments: + # * +depth+ - this group's relation to previous groups + def initialize(depth) + @depth = depth + @breakables = [] + @break = false + end + + # This group's relation to previous groups + attr_reader :depth + + # Array to hold the Breakable objects for this Group + attr_reader :breakables + + # Makes a break for this Group, and returns true + def break + @break = true + end + + # Boolean of whether this Group has made a break + def break? + @break + end + + # Boolean of whether this Group has been queried for being first + # + # This is used as a predicate, and ought to be called first. + def first? + if defined? @first + false + else + @first = false + true + end + end + end + + # The GroupQueue class is used for managing the queue of Group to be pretty + # printed. + # + # This queue groups the Group objects, based on their depth. + # + # This class is intended for internal use of the PrettyPrint buffers. + class GroupQueue # :nodoc: + # Create a GroupQueue object + # + # Arguments: + # * +groups+ - one or more PrettyPrint::Group objects + def initialize(*groups) + @queue = [] + groups.each {|g| enq g} + end + + # Enqueue +group+ + # + # This does not strictly append the group to the end of the queue, + # but instead adds it in line, base on the +group.depth+ + def enq(group) + depth = group.depth + @queue << [] until depth < @queue.length + @queue[depth] << group + end + + # Returns the outer group of the queue + def deq + @queue.each {|gs| + (gs.length-1).downto(0) {|i| + unless gs[i].breakables.empty? + group = gs.slice!(i, 1).first + group.break + return group + end + } + gs.each {|group| group.break} + gs.clear + } + return nil + end + + # Remote +group+ from this queue + def delete(group) + @queue[group.depth].delete(group) + end + end + + # PrettyPrint::SingleLine is used by PrettyPrint.singleline_format + # + # It is passed to be similar to a PrettyPrint object itself, by responding to: + # * #text + # * #breakable + # * #nest + # * #group + # * #flush + # * #first? + # + # but instead, the output has no line breaks + # + class SingleLine + # Create a PrettyPrint::SingleLine object + # + # Arguments: + # * +output+ - String (or similar) to store rendered text. Needs to respond to '<<' + # * +maxwidth+ - Argument position expected to be here for compatibility. + # This argument is a noop. + # * +newline+ - Argument position expected to be here for compatibility. + # This argument is a noop. + def initialize(output, maxwidth=nil, newline=nil) + @output = output + @first = [true] + end + + # Add +obj+ to the text to be output. + # + # +width+ argument is here for compatibility. It is a noop argument. + def text(obj, width=nil) + @output << obj + end + + # Appends +sep+ to the text to be output. By default +sep+ is ' ' + # + # +width+ argument is here for compatibility. It is a noop argument. + def breakable(sep=' ', width=nil) + @output << sep + end + + # Takes +indent+ arg, but does nothing with it. + # + # Yields to a block. + def nest(indent) # :nodoc: + yield + end + + # Opens a block for grouping objects to be pretty printed. + # + # Arguments: + # * +indent+ - noop argument. Present for compatibility. + # * +open_obj+ - text appended before the &blok. Default is '' + # * +close_obj+ - text appended after the &blok. Default is '' + # * +open_width+ - noop argument. Present for compatibility. + # * +close_width+ - noop argument. Present for compatibility. + def group(indent=nil, open_obj='', close_obj='', open_width=nil, close_width=nil) + @first.push true + @output << open_obj + yield + @output << close_obj + @first.pop + end + + # Method present for compatibility, but is a noop + def flush # :nodoc: + end + + # This is used as a predicate, and ought to be called first. + def first? + result = @first[-1] + @first[-1] = false + result + end + end +end diff --git a/gxg/standard/prime.rb b/gxg/standard/prime.rb new file mode 100644 index 0000000..2cd22da --- /dev/null +++ b/gxg/standard/prime.rb @@ -0,0 +1,467 @@ +# frozen_string_literal: false +# +# = prime.rb +# +# Prime numbers and factorization library. +# +# Copyright:: +# Copyright (c) 1998-2008 Keiju ISHITSUKA(SHL Japan Inc.) +# Copyright (c) 2008 Yuki Sonoda (Yugui) +# +# Documentation:: +# Yuki Sonoda +# + +require "singleton" +require "forwardable" + +class Integer + # Re-composes a prime factorization and returns the product. + # + # See Prime#int_from_prime_division for more details. + def Integer.from_prime_division(pd) + Prime.int_from_prime_division(pd) + end + + # Returns the factorization of +self+. + # + # See Prime#prime_division for more details. + def prime_division(generator = Prime::Generator23.new) + Prime.prime_division(self, generator) + end + + # Returns true if +self+ is a prime number, else returns false. + def prime? + return self >= 2 if self <= 3 + return true if self == 5 + return false unless 30.gcd(self) == 1 + (7..Integer.sqrt(self)).step(30) do |p| + return false if + self%(p) == 0 || self%(p+4) == 0 || self%(p+6) == 0 || self%(p+10) == 0 || + self%(p+12) == 0 || self%(p+16) == 0 || self%(p+22) == 0 || self%(p+24) == 0 + end + true + end + + # Iterates the given block over all prime numbers. + # + # See +Prime+#each for more details. + def Integer.each_prime(ubound, &block) # :yields: prime + Prime.each(ubound, &block) + end +end + +# +# The set of all prime numbers. +# +# == Example +# +# Prime.each(100) do |prime| +# p prime #=> 2, 3, 5, 7, 11, ...., 97 +# end +# +# Prime is Enumerable: +# +# Prime.first 5 # => [2, 3, 5, 7, 11] +# +# == Retrieving the instance +# +# For convenience, each instance method of +Prime+.instance can be accessed +# as a class method of +Prime+. +# +# e.g. +# Prime.instance.prime?(2) #=> true +# Prime.prime?(2) #=> true +# +# == Generators +# +# A "generator" provides an implementation of enumerating pseudo-prime +# numbers and it remembers the position of enumeration and upper bound. +# Furthermore, it is an external iterator of prime enumeration which is +# compatible with an Enumerator. +# +# +Prime+::+PseudoPrimeGenerator+ is the base class for generators. +# There are few implementations of generator. +# +# [+Prime+::+EratosthenesGenerator+] +# Uses eratosthenes' sieve. +# [+Prime+::+TrialDivisionGenerator+] +# Uses the trial division method. +# [+Prime+::+Generator23+] +# Generates all positive integers which are not divisible by either 2 or 3. +# This sequence is very bad as a pseudo-prime sequence. But this +# is faster and uses much less memory than the other generators. So, +# it is suitable for factorizing an integer which is not large but +# has many prime factors. e.g. for Prime#prime? . + +class Prime + include Enumerable + include Singleton + + class << self + extend Forwardable + include Enumerable + + def method_added(method) # :nodoc: + (class<< self;self;end).def_delegator :instance, method + end + end + + # Iterates the given block over all prime numbers. + # + # == Parameters + # + # +ubound+:: + # Optional. An arbitrary positive number. + # The upper bound of enumeration. The method enumerates + # prime numbers infinitely if +ubound+ is nil. + # +generator+:: + # Optional. An implementation of pseudo-prime generator. + # + # == Return value + # + # An evaluated value of the given block at the last time. + # Or an enumerator which is compatible to an +Enumerator+ + # if no block given. + # + # == Description + # + # Calls +block+ once for each prime number, passing the prime as + # a parameter. + # + # +ubound+:: + # Upper bound of prime numbers. The iterator stops after it + # yields all prime numbers p <= +ubound+. + # + def each(ubound = nil, generator = EratosthenesGenerator.new, &block) + generator.upper_bound = ubound + generator.each(&block) + end + + + # Returns true if +value+ is a prime number, else returns false. + # + # == Parameters + # + # +value+:: an arbitrary integer to be checked. + # +generator+:: optional. A pseudo-prime generator. + def prime?(value, generator = Prime::Generator23.new) + raise ArgumentError, "Expected a prime generator, got #{generator}" unless generator.respond_to? :each + raise ArgumentError, "Expected an integer, got #{value}" unless value.respond_to?(:integer?) && value.integer? + return false if value < 2 + generator.each do |num| + q,r = value.divmod num + return true if q < num + return false if r == 0 + end + end + + # Re-composes a prime factorization and returns the product. + # + # == Parameters + # +pd+:: Array of pairs of integers. The each internal + # pair consists of a prime number -- a prime factor -- + # and a natural number -- an exponent. + # + # == Example + # For [[p_1, e_1], [p_2, e_2], ...., [p_n, e_n]], it returns: + # + # p_1**e_1 * p_2**e_2 * .... * p_n**e_n. + # + # Prime.int_from_prime_division([[2,2], [3,1]]) #=> 12 + def int_from_prime_division(pd) + pd.inject(1){|value, (prime, index)| + value * prime**index + } + end + + # Returns the factorization of +value+. + # + # == Parameters + # +value+:: An arbitrary integer. + # +generator+:: Optional. A pseudo-prime generator. + # +generator+.succ must return the next + # pseudo-prime number in the ascending + # order. It must generate all prime numbers, + # but may also generate non prime numbers too. + # + # === Exceptions + # +ZeroDivisionError+:: when +value+ is zero. + # + # == Example + # For an arbitrary integer: + # + # n = p_1**e_1 * p_2**e_2 * .... * p_n**e_n, + # + # prime_division(n) returns: + # + # [[p_1, e_1], [p_2, e_2], ...., [p_n, e_n]]. + # + # Prime.prime_division(12) #=> [[2,2], [3,1]] + # + def prime_division(value, generator = Prime::Generator23.new) + raise ZeroDivisionError if value == 0 + if value < 0 + value = -value + pv = [[-1, 1]] + else + pv = [] + end + generator.each do |prime| + count = 0 + while (value1, mod = value.divmod(prime) + mod) == 0 + value = value1 + count += 1 + end + if count != 0 + pv.push [prime, count] + end + break if value1 <= prime + end + if value > 1 + pv.push [value, 1] + end + pv + end + + # An abstract class for enumerating pseudo-prime numbers. + # + # Concrete subclasses should override succ, next, rewind. + class PseudoPrimeGenerator + include Enumerable + + def initialize(ubound = nil) + @ubound = ubound + end + + def upper_bound=(ubound) + @ubound = ubound + end + def upper_bound + @ubound + end + + # returns the next pseudo-prime number, and move the internal + # position forward. + # + # +PseudoPrimeGenerator+#succ raises +NotImplementedError+. + def succ + raise NotImplementedError, "need to define `succ'" + end + + # alias of +succ+. + def next + raise NotImplementedError, "need to define `next'" + end + + # Rewinds the internal position for enumeration. + # + # See +Enumerator+#rewind. + def rewind + raise NotImplementedError, "need to define `rewind'" + end + + # Iterates the given block for each prime number. + def each + return self.dup unless block_given? + if @ubound + last_value = nil + loop do + prime = succ + break last_value if prime > @ubound + last_value = yield prime + end + else + loop do + yield succ + end + end + end + + # see +Enumerator+#with_index. + def with_index(offset = 0) + return enum_for(:with_index, offset) { Float::INFINITY } unless block_given? + return each_with_index(&proc) if offset == 0 + + each do |prime| + yield prime, offset + offset += 1 + end + end + + # see +Enumerator+#with_object. + def with_object(obj) + return enum_for(:with_object, obj) { Float::INFINITY } unless block_given? + each do |prime| + yield prime, obj + end + end + + def size + Float::INFINITY + end + end + + # An implementation of +PseudoPrimeGenerator+. + # + # Uses +EratosthenesSieve+. + class EratosthenesGenerator < PseudoPrimeGenerator + def initialize + @last_prime_index = -1 + super + end + + def succ + @last_prime_index += 1 + EratosthenesSieve.instance.get_nth_prime(@last_prime_index) + end + def rewind + initialize + end + alias next succ + end + + # An implementation of +PseudoPrimeGenerator+ which uses + # a prime table generated by trial division. + class TrialDivisionGenerator < PseudoPrimeGenerator + def initialize + @index = -1 + super + end + + def succ + TrialDivision.instance[@index += 1] + end + def rewind + initialize + end + alias next succ + end + + # Generates all integers which are greater than 2 and + # are not divisible by either 2 or 3. + # + # This is a pseudo-prime generator, suitable on + # checking primality of an integer by brute force + # method. + class Generator23 < PseudoPrimeGenerator + def initialize + @prime = 1 + @step = nil + super + end + + def succ + if (@step) + @prime += @step + @step = 6 - @step + else + case @prime + when 1; @prime = 2 + when 2; @prime = 3 + when 3; @prime = 5; @step = 2 + end + end + @prime + end + alias next succ + def rewind + initialize + end + end + + # Internal use. An implementation of prime table by trial division method. + class TrialDivision + include Singleton + + def initialize # :nodoc: + # These are included as class variables to cache them for later uses. If memory + # usage is a problem, they can be put in Prime#initialize as instance variables. + + # There must be no primes between @primes[-1] and @next_to_check. + @primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101] + # @next_to_check % 6 must be 1. + @next_to_check = 103 # @primes[-1] - @primes[-1] % 6 + 7 + @ulticheck_index = 3 # @primes.index(@primes.reverse.find {|n| + # n < Math.sqrt(@@next_to_check) }) + @ulticheck_next_squared = 121 # @primes[@ulticheck_index + 1] ** 2 + end + + # Returns the cached prime numbers. + def cache + @primes + end + alias primes cache + alias primes_so_far cache + + # Returns the +index+th prime number. + # + # +index+ is a 0-based index. + def [](index) + while index >= @primes.length + # Only check for prime factors up to the square root of the potential primes, + # but without the performance hit of an actual square root calculation. + if @next_to_check + 4 > @ulticheck_next_squared + @ulticheck_index += 1 + @ulticheck_next_squared = @primes.at(@ulticheck_index + 1) ** 2 + end + # Only check numbers congruent to one and five, modulo six. All others + + # are divisible by two or three. This also allows us to skip checking against + # two and three. + @primes.push @next_to_check if @primes[2..@ulticheck_index].find {|prime| @next_to_check % prime == 0 }.nil? + @next_to_check += 4 + @primes.push @next_to_check if @primes[2..@ulticheck_index].find {|prime| @next_to_check % prime == 0 }.nil? + @next_to_check += 2 + end + @primes[index] + end + end + + # Internal use. An implementation of Eratosthenes' sieve + class EratosthenesSieve + include Singleton + + def initialize + @primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101] + # @max_checked must be an even number + @max_checked = @primes.last + 1 + end + + def get_nth_prime(n) + compute_primes while @primes.size <= n + @primes[n] + end + + private + def compute_primes + # max_segment_size must be an even number + max_segment_size = 1e6.to_i + max_cached_prime = @primes.last + # do not double count primes if #compute_primes is interrupted + # by Timeout.timeout + @max_checked = max_cached_prime + 1 if max_cached_prime > @max_checked + + segment_min = @max_checked + segment_max = [segment_min + max_segment_size, max_cached_prime * 2].min + root = Integer.sqrt(segment_max) + + segment = ((segment_min + 1) .. segment_max).step(2).to_a + + (1..Float::INFINITY).each do |sieving| + prime = @primes[sieving] + break if prime > root + composite_index = (-(segment_min + 1 + prime) / 2) % prime + while composite_index < segment.size do + segment[composite_index] = nil + composite_index += prime + end + end + + @primes.concat(segment.compact!) + + @max_checked = segment_max + end + end +end diff --git a/gxg/standard/profiler.rb b/gxg/standard/profiler.rb new file mode 100644 index 0000000..b3c9f7f --- /dev/null +++ b/gxg/standard/profiler.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true +# Profile provides a way to Profile your Ruby application. +# +# Profiling your program is a way of determining which methods are called and +# how long each method takes to complete. This way you can detect which +# methods are possible bottlenecks. +# +# Profiling your program will slow down your execution time considerably, +# so activate it only when you need it. Don't confuse benchmarking with +# profiling. +# +# There are two ways to activate Profiling: +# +# == Command line +# +# Run your Ruby script with -rprofile: +# +# ruby -rprofile example.rb +# +# If you're profiling an executable in your $PATH you can use +# ruby -S: +# +# ruby -rprofile -S some_executable +# +# == From code +# +# Just require 'profile': +# +# require 'profile' +# +# def slow_method +# 5000.times do +# 9999999999999999*999999999 +# end +# end +# +# def fast_method +# 5000.times do +# 9999999999999999+999999999 +# end +# end +# +# slow_method +# fast_method +# +# The output in both cases is a report when the execution is over: +# +# ruby -rprofile example.rb +# +# % cumulative self self total +# time seconds seconds calls ms/call ms/call name +# 68.42 0.13 0.13 2 65.00 95.00 Integer#times +# 15.79 0.16 0.03 5000 0.01 0.01 Fixnum#* +# 15.79 0.19 0.03 5000 0.01 0.01 Fixnum#+ +# 0.00 0.19 0.00 2 0.00 0.00 IO#set_encoding +# 0.00 0.19 0.00 1 0.00 100.00 Object#slow_method +# 0.00 0.19 0.00 2 0.00 0.00 Module#method_added +# 0.00 0.19 0.00 1 0.00 90.00 Object#fast_method +# 0.00 0.19 0.00 1 0.00 190.00 #toplevel + +module Profiler__ + class Wrapper < Struct.new(:defined_class, :method_id, :hash) # :nodoc: + private :defined_class=, :method_id=, :hash= + + def initialize(klass, mid) + super(klass, mid, nil) + self.hash = Struct.instance_method(:hash).bind(self).call + end + + def to_s + "#{defined_class.inspect}#".sub(/\A\##\z/, '\1.') << method_id.to_s + end + alias inspect to_s + end + + # internal values + @@start = nil # the start time that profiling began + @@stacks = nil # the map of stacks keyed by thread + @@maps = nil # the map of call data keyed by thread, class and id. Call data contains the call count, total time, + PROFILE_CALL_PROC = TracePoint.new(*%i[call c_call b_call]) {|tp| # :nodoc: + now = Process.times[0] + stack = (@@stacks[Thread.current] ||= []) + stack.push [now, 0.0] + } + PROFILE_RETURN_PROC = TracePoint.new(*%i[return c_return b_return]) {|tp| # :nodoc: + now = Process.times[0] + key = Wrapper.new(tp.defined_class, tp.method_id) + stack = (@@stacks[Thread.current] ||= []) + if tick = stack.pop + threadmap = (@@maps[Thread.current] ||= {}) + data = (threadmap[key] ||= [0, 0.0, 0.0, key]) + data[0] += 1 + cost = now - tick[0] + data[1] += cost + data[2] += cost - tick[1] + stack[-1][1] += cost if stack[-1] + end + } +module_function + # Starts the profiler. + # + # See Profiler__ for more information. + def start_profile + @@start = Process.times[0] + @@stacks = {} + @@maps = {} + PROFILE_CALL_PROC.enable + PROFILE_RETURN_PROC.enable + end + # Stops the profiler. + # + # See Profiler__ for more information. + def stop_profile + PROFILE_CALL_PROC.disable + PROFILE_RETURN_PROC.disable + end + # Outputs the results from the profiler. + # + # See Profiler__ for more information. + def print_profile(f) + stop_profile + total = Process.times[0] - @@start + if total == 0 then total = 0.01 end + totals = {} + @@maps.values.each do |threadmap| + threadmap.each do |key, data| + total_data = (totals[key] ||= [0, 0.0, 0.0, key]) + total_data[0] += data[0] + total_data[1] += data[1] + total_data[2] += data[2] + end + end + + # Maybe we should show a per thread output and a totals view? + + data = totals.values + data = data.sort_by{|x| -x[2]} + sum = 0 + f.printf " %% cumulative self self total\n" + f.printf " time seconds seconds calls ms/call ms/call name\n" + for d in data + sum += d[2] + f.printf "%6.2f %8.2f %8.2f %8d ", d[2]/total*100, sum, d[2], d[0] + f.printf "%8.2f %8.2f %s\n", d[2]*1000/d[0], d[1]*1000/d[0], d[3] + end + f.printf "%6.2f %8.2f %8.2f %8d ", 0.0, total, 0.0, 1 # ??? + f.printf "%8.2f %8.2f %s\n", 0.0, total*1000, "#toplevel" # ??? + end +end diff --git a/gxg/standard/racc/parser.rb b/gxg/standard/racc/parser.rb new file mode 100644 index 0000000..0cdb42e --- /dev/null +++ b/gxg/standard/racc/parser.rb @@ -0,0 +1,623 @@ +# frozen_string_literal: false +#-- +# $originalId: parser.rb,v 1.8 2006/07/06 11:42:07 aamine Exp $ +# +# Copyright (c) 1999-2006 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the same terms of ruby. +# +# As a special exception, when this code is copied by Racc +# into a Racc output file, you may use that output file +# without restriction. +#++ + +module Racc + class ParseError < StandardError; end +end +unless defined?(::ParseError) + ParseError = Racc::ParseError +end + +# Racc is a LALR(1) parser generator. +# It is written in Ruby itself, and generates Ruby programs. +# +# == Command-line Reference +# +# racc [-ofilename] [--output-file=filename] +# [-erubypath] [--embedded=rubypath] +# [-v] [--verbose] +# [-Ofilename] [--log-file=filename] +# [-g] [--debug] +# [-E] [--embedded] +# [-l] [--no-line-convert] +# [-c] [--line-convert-all] +# [-a] [--no-omit-actions] +# [-C] [--check-only] +# [-S] [--output-status] +# [--version] [--copyright] [--help] grammarfile +# +# [+filename+] +# Racc grammar file. Any extension is permitted. +# [-o+outfile+, --output-file=+outfile+] +# A filename for output. default is <+filename+>.tab.rb +# [-O+filename+, --log-file=+filename+] +# Place logging output in file +filename+. +# Default log file name is <+filename+>.output. +# [-e+rubypath+, --executable=+rubypath+] +# output executable file(mode 755). where +path+ is the Ruby interpreter. +# [-v, --verbose] +# verbose mode. create +filename+.output file, like yacc's y.output file. +# [-g, --debug] +# add debug code to parser class. To display debugging information, +# use this '-g' option and set @yydebug true in parser class. +# [-E, --embedded] +# Output parser which doesn't need runtime files (racc/parser.rb). +# [-C, --check-only] +# Check syntax of racc grammar file and quit. +# [-S, --output-status] +# Print messages time to time while compiling. +# [-l, --no-line-convert] +# turns off line number converting. +# [-c, --line-convert-all] +# Convert line number of actions, inner, header and footer. +# [-a, --no-omit-actions] +# Call all actions, even if an action is empty. +# [--version] +# print Racc version and quit. +# [--copyright] +# Print copyright and quit. +# [--help] +# Print usage and quit. +# +# == Generating Parser Using Racc +# +# To compile Racc grammar file, simply type: +# +# $ racc parse.y +# +# This creates Ruby script file "parse.tab.y". The -o option can change the output filename. +# +# == Writing A Racc Grammar File +# +# If you want your own parser, you have to write a grammar file. +# A grammar file contains the name of your parser class, grammar for the parser, +# user code, and anything else. +# When writing a grammar file, yacc's knowledge is helpful. +# If you have not used yacc before, Racc is not too difficult. +# +# Here's an example Racc grammar file. +# +# class Calcparser +# rule +# target: exp { print val[0] } +# +# exp: exp '+' exp +# | exp '*' exp +# | '(' exp ')' +# | NUMBER +# end +# +# Racc grammar files resemble yacc files. +# But (of course), this is Ruby code. +# yacc's $$ is the 'result', $0, $1... is +# an array called 'val', and $-1, $-2... is an array called '_values'. +# +# See the {Grammar File Reference}[rdoc-ref:lib/racc/rdoc/grammar.en.rdoc] for +# more information on grammar files. +# +# == Parser +# +# Then you must prepare the parse entry method. There are two types of +# parse methods in Racc, Racc::Parser#do_parse and Racc::Parser#yyparse +# +# Racc::Parser#do_parse is simple. +# +# It's yyparse() of yacc, and Racc::Parser#next_token is yylex(). +# This method must returns an array like [TOKENSYMBOL, ITS_VALUE]. +# EOF is [false, false]. +# (TOKENSYMBOL is a Ruby symbol (taken from String#intern) by default. +# If you want to change this, see the grammar reference. +# +# Racc::Parser#yyparse is little complicated, but useful. +# It does not use Racc::Parser#next_token, instead it gets tokens from any iterator. +# +# For example, yyparse(obj, :scan) causes +# calling +obj#scan+, and you can return tokens by yielding them from +obj#scan+. +# +# == Debugging +# +# When debugging, "-v" or/and the "-g" option is helpful. +# +# "-v" creates verbose log file (.output). +# "-g" creates a "Verbose Parser". +# Verbose Parser prints the internal status when parsing. +# But it's _not_ automatic. +# You must use -g option and set +@yydebug+ to +true+ in order to get output. +# -g option only creates the verbose parser. +# +# === Racc reported syntax error. +# +# Isn't there too many "end"? +# grammar of racc file is changed in v0.10. +# +# Racc does not use '%' mark, while yacc uses huge number of '%' marks.. +# +# === Racc reported "XXXX conflicts". +# +# Try "racc -v xxxx.y". +# It causes producing racc's internal log file, xxxx.output. +# +# === Generated parsers does not work correctly +# +# Try "racc -g xxxx.y". +# This command let racc generate "debugging parser". +# Then set @yydebug=true in your parser. +# It produces a working log of your parser. +# +# == Re-distributing Racc runtime +# +# A parser, which is created by Racc, requires the Racc runtime module; +# racc/parser.rb. +# +# Ruby 1.8.x comes with Racc runtime module, +# you need NOT distribute Racc runtime files. +# +# If you want to include the Racc runtime module with your parser. +# This can be done by using '-E' option: +# +# $ racc -E -omyparser.rb myparser.y +# +# This command creates myparser.rb which `includes' Racc runtime. +# Only you must do is to distribute your parser file (myparser.rb). +# +# Note: parser.rb is LGPL, but your parser is not. +# Your own parser is completely yours. +module Racc + + unless defined?(Racc_No_Extensions) + Racc_No_Extensions = false # :nodoc: + end + + class Parser + + Racc_Runtime_Version = '1.4.6' + Racc_Runtime_Revision = %w$originalRevision: 1.8 $[1] + + Racc_Runtime_Core_Version_R = '1.4.6' + Racc_Runtime_Core_Revision_R = %w$originalRevision: 1.8 $[1] + begin + require 'racc/cparse' + # Racc_Runtime_Core_Version_C = (defined in extension) + Racc_Runtime_Core_Revision_C = Racc_Runtime_Core_Id_C.split[2] + unless new.respond_to?(:_racc_do_parse_c, true) + raise LoadError, 'old cparse.so' + end + if Racc_No_Extensions + raise LoadError, 'selecting ruby version of racc runtime core' + end + + Racc_Main_Parsing_Routine = :_racc_do_parse_c # :nodoc: + Racc_YY_Parse_Method = :_racc_yyparse_c # :nodoc: + Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_C # :nodoc: + Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_C # :nodoc: + Racc_Runtime_Type = 'c' # :nodoc: + rescue LoadError + Racc_Main_Parsing_Routine = :_racc_do_parse_rb + Racc_YY_Parse_Method = :_racc_yyparse_rb + Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_R + Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_R + Racc_Runtime_Type = 'ruby' + end + + def Parser.racc_runtime_type # :nodoc: + Racc_Runtime_Type + end + + def _racc_setup + @yydebug = false unless self.class::Racc_debug_parser + @yydebug = false unless defined?(@yydebug) + if @yydebug + @racc_debug_out = $stderr unless defined?(@racc_debug_out) + @racc_debug_out ||= $stderr + end + arg = self.class::Racc_arg + arg[13] = true if arg.size < 14 + arg + end + + def _racc_init_sysvars + @racc_state = [0] + @racc_tstack = [] + @racc_vstack = [] + + @racc_t = nil + @racc_val = nil + + @racc_read_next = true + + @racc_user_yyerror = false + @racc_error_status = 0 + end + + # The entry point of the parser. This method is used with #next_token. + # If Racc wants to get token (and its value), calls next_token. + # + # Example: + # def parse + # @q = [[1,1], + # [2,2], + # [3,3], + # [false, '$']] + # do_parse + # end + # + # def next_token + # @q.shift + # end + def do_parse + __send__(Racc_Main_Parsing_Routine, _racc_setup(), false) + end + + # The method to fetch next token. + # If you use #do_parse method, you must implement #next_token. + # + # The format of return value is [TOKEN_SYMBOL, VALUE]. + # +token-symbol+ is represented by Ruby's symbol by default, e.g. :IDENT + # for 'IDENT'. ";" (String) for ';'. + # + # The final symbol (End of file) must be false. + def next_token + raise NotImplementedError, "#{self.class}\#next_token is not defined" + end + + def _racc_do_parse_rb(arg, in_debug) + action_table, action_check, action_default, action_pointer, + _, _, _, _, + _, _, token_table, _, + _, _, * = arg + + _racc_init_sysvars + tok = act = i = nil + + catch(:racc_end_parse) { + while true + if i = action_pointer[@racc_state[-1]] + if @racc_read_next + if @racc_t != 0 # not EOF + tok, @racc_val = next_token() + unless tok # EOF + @racc_t = 0 + else + @racc_t = (token_table[tok] or 1) # error token + end + racc_read_token(@racc_t, tok, @racc_val) if @yydebug + @racc_read_next = false + end + end + i += @racc_t + unless i >= 0 and + act = action_table[i] and + action_check[i] == @racc_state[-1] + act = action_default[@racc_state[-1]] + end + else + act = action_default[@racc_state[-1]] + end + while act = _racc_evalact(act, arg) + ; + end + end + } + end + + # Another entry point for the parser. + # If you use this method, you must implement RECEIVER#METHOD_ID method. + # + # RECEIVER#METHOD_ID is a method to get next token. + # It must 'yield' the token, which format is [TOKEN-SYMBOL, VALUE]. + def yyparse(recv, mid) + __send__(Racc_YY_Parse_Method, recv, mid, _racc_setup(), true) + end + + def _racc_yyparse_rb(recv, mid, arg, c_debug) + action_table, action_check, action_default, action_pointer, + _, _, _, _, + _, _, token_table, _, + _, _, * = arg + + _racc_init_sysvars + act = nil + i = nil + + catch(:racc_end_parse) { + until i = action_pointer[@racc_state[-1]] + while act = _racc_evalact(action_default[@racc_state[-1]], arg) + ; + end + end + recv.__send__(mid) do |tok, val| + unless tok + @racc_t = 0 + else + @racc_t = (token_table[tok] or 1) # error token + end + @racc_val = val + @racc_read_next = false + + i += @racc_t + unless i >= 0 and + act = action_table[i] and + action_check[i] == @racc_state[-1] + act = action_default[@racc_state[-1]] + end + while act = _racc_evalact(act, arg) + ; + end + + while not(i = action_pointer[@racc_state[-1]]) or + not @racc_read_next or + @racc_t == 0 # $ + unless i and i += @racc_t and + i >= 0 and + act = action_table[i] and + action_check[i] == @racc_state[-1] + act = action_default[@racc_state[-1]] + end + while act = _racc_evalact(act, arg) + ; + end + end + end + } + end + + ### + ### common + ### + + def _racc_evalact(act, arg) + action_table, action_check, _, action_pointer, + _, _, _, _, + _, _, _, shift_n, reduce_n, + _, _, * = arg + + if act > 0 and act < shift_n + # + # shift + # + if @racc_error_status > 0 + @racc_error_status -= 1 unless @racc_t == 1 # error token + end + @racc_vstack.push @racc_val + @racc_state.push act + @racc_read_next = true + if @yydebug + @racc_tstack.push @racc_t + racc_shift @racc_t, @racc_tstack, @racc_vstack + end + + elsif act < 0 and act > -reduce_n + # + # reduce + # + code = catch(:racc_jump) { + @racc_state.push _racc_do_reduce(arg, act) + false + } + if code + case code + when 1 # yyerror + @racc_user_yyerror = true # user_yyerror + return -reduce_n + when 2 # yyaccept + return shift_n + else + raise '[Racc Bug] unknown jump code' + end + end + + elsif act == shift_n + # + # accept + # + racc_accept if @yydebug + throw :racc_end_parse, @racc_vstack[0] + + elsif act == -reduce_n + # + # error + # + case @racc_error_status + when 0 + unless arg[21] # user_yyerror + on_error @racc_t, @racc_val, @racc_vstack + end + when 3 + if @racc_t == 0 # is $ + throw :racc_end_parse, nil + end + @racc_read_next = true + end + @racc_user_yyerror = false + @racc_error_status = 3 + while true + if i = action_pointer[@racc_state[-1]] + i += 1 # error token + if i >= 0 and + (act = action_table[i]) and + action_check[i] == @racc_state[-1] + break + end + end + throw :racc_end_parse, nil if @racc_state.size <= 1 + @racc_state.pop + @racc_vstack.pop + if @yydebug + @racc_tstack.pop + racc_e_pop @racc_state, @racc_tstack, @racc_vstack + end + end + return act + + else + raise "[Racc Bug] unknown action #{act.inspect}" + end + + racc_next_state(@racc_state[-1], @racc_state) if @yydebug + + nil + end + + def _racc_do_reduce(arg, act) + _, _, _, _, + goto_table, goto_check, goto_default, goto_pointer, + nt_base, reduce_table, _, _, + _, use_result, * = arg + state = @racc_state + vstack = @racc_vstack + tstack = @racc_tstack + + i = act * -3 + len = reduce_table[i] + reduce_to = reduce_table[i+1] + method_id = reduce_table[i+2] + void_array = [] + + tmp_t = tstack[-len, len] if @yydebug + tmp_v = vstack[-len, len] + tstack[-len, len] = void_array if @yydebug + vstack[-len, len] = void_array + state[-len, len] = void_array + + # tstack must be updated AFTER method call + if use_result + vstack.push __send__(method_id, tmp_v, vstack, tmp_v[0]) + else + vstack.push __send__(method_id, tmp_v, vstack) + end + tstack.push reduce_to + + racc_reduce(tmp_t, reduce_to, tstack, vstack) if @yydebug + + k1 = reduce_to - nt_base + if i = goto_pointer[k1] + i += state[-1] + if i >= 0 and (curstate = goto_table[i]) and goto_check[i] == k1 + return curstate + end + end + goto_default[k1] + end + + # This method is called when a parse error is found. + # + # ERROR_TOKEN_ID is an internal ID of token which caused error. + # You can get string representation of this ID by calling + # #token_to_str. + # + # ERROR_VALUE is a value of error token. + # + # value_stack is a stack of symbol values. + # DO NOT MODIFY this object. + # + # This method raises ParseError by default. + # + # If this method returns, parsers enter "error recovering mode". + def on_error(t, val, vstack) + raise ParseError, sprintf("\nparse error on value %s (%s)", + val.inspect, token_to_str(t) || '?') + end + + # Enter error recovering mode. + # This method does not call #on_error. + def yyerror + throw :racc_jump, 1 + end + + # Exit parser. + # Return value is Symbol_Value_Stack[0]. + def yyaccept + throw :racc_jump, 2 + end + + # Leave error recovering mode. + def yyerrok + @racc_error_status = 0 + end + + # For debugging output + def racc_read_token(t, tok, val) + @racc_debug_out.print 'read ' + @racc_debug_out.print tok.inspect, '(', racc_token2str(t), ') ' + @racc_debug_out.puts val.inspect + @racc_debug_out.puts + end + + def racc_shift(tok, tstack, vstack) + @racc_debug_out.puts "shift #{racc_token2str tok}" + racc_print_stacks tstack, vstack + @racc_debug_out.puts + end + + def racc_reduce(toks, sim, tstack, vstack) + out = @racc_debug_out + out.print 'reduce ' + if toks.empty? + out.print ' ' + else + toks.each {|t| out.print ' ', racc_token2str(t) } + end + out.puts " --> #{racc_token2str(sim)}" + + racc_print_stacks tstack, vstack + @racc_debug_out.puts + end + + def racc_accept + @racc_debug_out.puts 'accept' + @racc_debug_out.puts + end + + def racc_e_pop(state, tstack, vstack) + @racc_debug_out.puts 'error recovering mode: pop token' + racc_print_states state + racc_print_stacks tstack, vstack + @racc_debug_out.puts + end + + def racc_next_state(curstate, state) + @racc_debug_out.puts "goto #{curstate}" + racc_print_states state + @racc_debug_out.puts + end + + def racc_print_stacks(t, v) + out = @racc_debug_out + out.print ' [' + t.each_index do |i| + out.print ' (', racc_token2str(t[i]), ' ', v[i].inspect, ')' + end + out.puts ' ]' + end + + def racc_print_states(s) + out = @racc_debug_out + out.print ' [' + s.each {|st| out.print ' ', st } + out.puts ' ]' + end + + def racc_token2str(tok) + self.class::Racc_token_to_s_table[tok] or + raise "[Racc Bug] can't convert token #{tok} to string" + end + + # Convert internal ID of token symbol to the string. + def token_to_str(t) + self.class::Racc_token_to_s_table[t] + end + + end + +end diff --git a/gxg/standard/resolv-replace.rb b/gxg/standard/resolv-replace.rb new file mode 100644 index 0000000..a83e79d --- /dev/null +++ b/gxg/standard/resolv-replace.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'socket' +require 'resolv' + +class << IPSocket + # :stopdoc: + alias original_resolv_getaddress getaddress + # :startdoc: + def getaddress(host) + begin + return Resolv.getaddress(host).to_s + rescue Resolv::ResolvError + raise SocketError, "Hostname not known: #{host}" + end + end +end + +class TCPSocket < IPSocket + # :stopdoc: + alias original_resolv_initialize initialize + # :startdoc: + def initialize(host, serv, *rest) + rest[0] = IPSocket.getaddress(rest[0]) if rest[0] + original_resolv_initialize(IPSocket.getaddress(host), serv, *rest) + end +end + +class UDPSocket < IPSocket + # :stopdoc: + alias original_resolv_bind bind + # :startdoc: + def bind(host, port) + host = IPSocket.getaddress(host) if host != "" + original_resolv_bind(host, port) + end + + # :stopdoc: + alias original_resolv_connect connect + # :startdoc: + def connect(host, port) + original_resolv_connect(IPSocket.getaddress(host), port) + end + + # :stopdoc: + alias original_resolv_send send + # :startdoc: + def send(mesg, flags, *rest) + if rest.length == 2 + host, port = rest + begin + addrs = Resolv.getaddresses(host) + rescue Resolv::ResolvError + raise SocketError, "Hostname not known: #{host}" + end + addrs[0...-1].each {|addr| + begin + return original_resolv_send(mesg, flags, addr, port) + rescue SystemCallError + end + } + original_resolv_send(mesg, flags, addrs[-1], port) + else + original_resolv_send(mesg, flags, *rest) + end + end +end + +class SOCKSSocket < TCPSocket + # :stopdoc: + alias original_resolv_initialize initialize + # :startdoc: + def initialize(host, serv) + original_resolv_initialize(IPSocket.getaddress(host), port) + end +end if defined? SOCKSSocket diff --git a/gxg/standard/resolv.rb b/gxg/standard/resolv.rb new file mode 100644 index 0000000..48ee400 --- /dev/null +++ b/gxg/standard/resolv.rb @@ -0,0 +1,2856 @@ +# frozen_string_literal: true + +require 'socket' +require 'timeout' +require 'io/wait' + +begin + require 'securerandom' +rescue LoadError +end + +# Resolv is a thread-aware DNS resolver library written in Ruby. Resolv can +# handle multiple DNS requests concurrently without blocking the entire Ruby +# interpreter. +# +# See also resolv-replace.rb to replace the libc resolver with Resolv. +# +# Resolv can look up various DNS resources using the DNS module directly. +# +# Examples: +# +# p Resolv.getaddress "www.ruby-lang.org" +# p Resolv.getname "210.251.121.214" +# +# Resolv::DNS.open do |dns| +# ress = dns.getresources "www.ruby-lang.org", Resolv::DNS::Resource::IN::A +# p ress.map(&:address) +# ress = dns.getresources "ruby-lang.org", Resolv::DNS::Resource::IN::MX +# p ress.map { |r| [r.exchange.to_s, r.preference] } +# end +# +# +# == Bugs +# +# * NIS is not supported. +# * /etc/nsswitch.conf is not supported. + +class Resolv + + ## + # Looks up the first IP address for +name+. + + def self.getaddress(name) + DefaultResolver.getaddress(name) + end + + ## + # Looks up all IP address for +name+. + + def self.getaddresses(name) + DefaultResolver.getaddresses(name) + end + + ## + # Iterates over all IP addresses for +name+. + + def self.each_address(name, &block) + DefaultResolver.each_address(name, &block) + end + + ## + # Looks up the hostname of +address+. + + def self.getname(address) + DefaultResolver.getname(address) + end + + ## + # Looks up all hostnames for +address+. + + def self.getnames(address) + DefaultResolver.getnames(address) + end + + ## + # Iterates over all hostnames for +address+. + + def self.each_name(address, &proc) + DefaultResolver.each_name(address, &proc) + end + + ## + # Creates a new Resolv using +resolvers+. + + def initialize(resolvers=[Hosts.new, DNS.new]) + @resolvers = resolvers + end + + ## + # Looks up the first IP address for +name+. + + def getaddress(name) + each_address(name) {|address| return address} + raise ResolvError.new("no address for #{name}") + end + + ## + # Looks up all IP address for +name+. + + def getaddresses(name) + ret = [] + each_address(name) {|address| ret << address} + return ret + end + + ## + # Iterates over all IP addresses for +name+. + + def each_address(name) + if AddressRegex =~ name + yield name + return + end + yielded = false + @resolvers.each {|r| + r.each_address(name) {|address| + yield address.to_s + yielded = true + } + return if yielded + } + end + + ## + # Looks up the hostname of +address+. + + def getname(address) + each_name(address) {|name| return name} + raise ResolvError.new("no name for #{address}") + end + + ## + # Looks up all hostnames for +address+. + + def getnames(address) + ret = [] + each_name(address) {|name| ret << name} + return ret + end + + ## + # Iterates over all hostnames for +address+. + + def each_name(address) + yielded = false + @resolvers.each {|r| + r.each_name(address) {|name| + yield name.to_s + yielded = true + } + return if yielded + } + end + + ## + # Indicates a failure to resolve a name or address. + + class ResolvError < StandardError; end + + ## + # Indicates a timeout resolving a name or address. + + class ResolvTimeout < Timeout::Error; end + + ## + # Resolv::Hosts is a hostname resolver that uses the system hosts file. + + class Hosts + begin + raise LoadError unless /mswin|mingw|cygwin/ =~ RUBY_PLATFORM + require 'win32/resolv' + DefaultFileName = Win32::Resolv.get_hosts_path + rescue LoadError + DefaultFileName = '/etc/hosts' + end + + ## + # Creates a new Resolv::Hosts, using +filename+ for its data source. + + def initialize(filename = DefaultFileName) + @filename = filename + @mutex = Thread::Mutex.new + @initialized = nil + end + + def lazy_initialize # :nodoc: + @mutex.synchronize { + unless @initialized + @name2addr = {} + @addr2name = {} + File.open(@filename, 'rb') {|f| + f.each {|line| + line.sub!(/#.*/, '') + addr, hostname, *aliases = line.split(/\s+/) + next unless addr + addr.untaint + hostname.untaint + @addr2name[addr] = [] unless @addr2name.include? addr + @addr2name[addr] << hostname + @addr2name[addr] += aliases + @name2addr[hostname] = [] unless @name2addr.include? hostname + @name2addr[hostname] << addr + aliases.each {|n| + n.untaint + @name2addr[n] = [] unless @name2addr.include? n + @name2addr[n] << addr + } + } + } + @name2addr.each {|name, arr| arr.reverse!} + @initialized = true + end + } + self + end + + ## + # Gets the IP address of +name+ from the hosts file. + + def getaddress(name) + each_address(name) {|address| return address} + raise ResolvError.new("#{@filename} has no name: #{name}") + end + + ## + # Gets all IP addresses for +name+ from the hosts file. + + def getaddresses(name) + ret = [] + each_address(name) {|address| ret << address} + return ret + end + + ## + # Iterates over all IP addresses for +name+ retrieved from the hosts file. + + def each_address(name, &proc) + lazy_initialize + if @name2addr.include?(name) + @name2addr[name].each(&proc) + end + end + + ## + # Gets the hostname of +address+ from the hosts file. + + def getname(address) + each_name(address) {|name| return name} + raise ResolvError.new("#{@filename} has no address: #{address}") + end + + ## + # Gets all hostnames for +address+ from the hosts file. + + def getnames(address) + ret = [] + each_name(address) {|name| ret << name} + return ret + end + + ## + # Iterates over all hostnames for +address+ retrieved from the hosts file. + + def each_name(address, &proc) + lazy_initialize + @addr2name[address]&.each(&proc) + end + end + + ## + # Resolv::DNS is a DNS stub resolver. + # + # Information taken from the following places: + # + # * STD0013 + # * RFC 1035 + # * ftp://ftp.isi.edu/in-notes/iana/assignments/dns-parameters + # * etc. + + class DNS + + ## + # Default DNS Port + + Port = 53 + + ## + # Default DNS UDP packet size + + UDPSize = 512 + + ## + # Creates a new DNS resolver. See Resolv::DNS.new for argument details. + # + # Yields the created DNS resolver to the block, if given, otherwise + # returns it. + + def self.open(*args) + dns = new(*args) + return dns unless block_given? + begin + yield dns + ensure + dns.close + end + end + + ## + # Creates a new DNS resolver. + # + # +config_info+ can be: + # + # nil:: Uses /etc/resolv.conf. + # String:: Path to a file using /etc/resolv.conf's format. + # Hash:: Must contain :nameserver, :search and :ndots keys. + # :nameserver_port can be used to specify port number of nameserver address. + # + # The value of :nameserver should be an address string or + # an array of address strings. + # - :nameserver => '8.8.8.8' + # - :nameserver => ['8.8.8.8', '8.8.4.4'] + # + # The value of :nameserver_port should be an array of + # pair of nameserver address and port number. + # - :nameserver_port => [['8.8.8.8', 53], ['8.8.4.4', 53]] + # + # Example: + # + # Resolv::DNS.new(:nameserver => ['210.251.121.21'], + # :search => ['ruby-lang.org'], + # :ndots => 1) + + def initialize(config_info=nil) + @mutex = Thread::Mutex.new + @config = Config.new(config_info) + @initialized = nil + end + + # Sets the resolver timeouts. This may be a single positive number + # or an array of positive numbers representing timeouts in seconds. + # If an array is specified, a DNS request will retry and wait for + # each successive interval in the array until a successful response + # is received. Specifying +nil+ reverts to the default timeouts: + # [ 5, second = 5 * 2 / nameserver_count, 2 * second, 4 * second ] + # + # Example: + # + # dns.timeouts = 3 + # + def timeouts=(values) + @config.timeouts = values + end + + def lazy_initialize # :nodoc: + @mutex.synchronize { + unless @initialized + @config.lazy_initialize + @initialized = true + end + } + self + end + + ## + # Closes the DNS resolver. + + def close + @mutex.synchronize { + if @initialized + @initialized = false + end + } + end + + ## + # Gets the IP address of +name+ from the DNS resolver. + # + # +name+ can be a Resolv::DNS::Name or a String. Retrieved address will + # be a Resolv::IPv4 or Resolv::IPv6 + + def getaddress(name) + each_address(name) {|address| return address} + raise ResolvError.new("DNS result has no information for #{name}") + end + + ## + # Gets all IP addresses for +name+ from the DNS resolver. + # + # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will + # be a Resolv::IPv4 or Resolv::IPv6 + + def getaddresses(name) + ret = [] + each_address(name) {|address| ret << address} + return ret + end + + ## + # Iterates over all IP addresses for +name+ retrieved from the DNS + # resolver. + # + # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will + # be a Resolv::IPv4 or Resolv::IPv6 + + def each_address(name) + each_resource(name, Resource::IN::A) {|resource| yield resource.address} + if use_ipv6? + each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address} + end + end + + def use_ipv6? # :nodoc: + begin + list = Socket.ip_address_list + rescue NotImplementedError + return true + end + list.any? {|a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? } + end + private :use_ipv6? + + ## + # Gets the hostname for +address+ from the DNS resolver. + # + # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved + # name will be a Resolv::DNS::Name. + + def getname(address) + each_name(address) {|name| return name} + raise ResolvError.new("DNS result has no information for #{address}") + end + + ## + # Gets all hostnames for +address+ from the DNS resolver. + # + # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved + # names will be Resolv::DNS::Name instances. + + def getnames(address) + ret = [] + each_name(address) {|name| ret << name} + return ret + end + + ## + # Iterates over all hostnames for +address+ retrieved from the DNS + # resolver. + # + # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved + # names will be Resolv::DNS::Name instances. + + def each_name(address) + case address + when Name + ptr = address + when IPv4::Regex + ptr = IPv4.create(address).to_name + when IPv6::Regex + ptr = IPv6.create(address).to_name + else + raise ResolvError.new("cannot interpret as address: #{address}") + end + each_resource(ptr, Resource::IN::PTR) {|resource| yield resource.name} + end + + ## + # Look up the +typeclass+ DNS resource of +name+. + # + # +name+ must be a Resolv::DNS::Name or a String. + # + # +typeclass+ should be one of the following: + # + # * Resolv::DNS::Resource::IN::A + # * Resolv::DNS::Resource::IN::AAAA + # * Resolv::DNS::Resource::IN::ANY + # * Resolv::DNS::Resource::IN::CNAME + # * Resolv::DNS::Resource::IN::HINFO + # * Resolv::DNS::Resource::IN::MINFO + # * Resolv::DNS::Resource::IN::MX + # * Resolv::DNS::Resource::IN::NS + # * Resolv::DNS::Resource::IN::PTR + # * Resolv::DNS::Resource::IN::SOA + # * Resolv::DNS::Resource::IN::TXT + # * Resolv::DNS::Resource::IN::WKS + # + # Returned resource is represented as a Resolv::DNS::Resource instance, + # i.e. Resolv::DNS::Resource::IN::A. + + def getresource(name, typeclass) + each_resource(name, typeclass) {|resource| return resource} + raise ResolvError.new("DNS result has no information for #{name}") + end + + ## + # Looks up all +typeclass+ DNS resources for +name+. See #getresource for + # argument details. + + def getresources(name, typeclass) + ret = [] + each_resource(name, typeclass) {|resource| ret << resource} + return ret + end + + ## + # Iterates over all +typeclass+ DNS resources for +name+. See + # #getresource for argument details. + + def each_resource(name, typeclass, &proc) + fetch_resource(name, typeclass) {|reply, reply_name| + extract_resources(reply, reply_name, typeclass, &proc) + } + end + + def fetch_resource(name, typeclass) + lazy_initialize + requester = make_udp_requester + senders = {} + begin + @config.resolv(name) {|candidate, tout, nameserver, port| + msg = Message.new + msg.rd = 1 + msg.add_question(candidate, typeclass) + unless sender = senders[[candidate, nameserver, port]] + sender = requester.sender(msg, candidate, nameserver, port) + next if !sender + senders[[candidate, nameserver, port]] = sender + end + reply, reply_name = requester.request(sender, tout) + case reply.rcode + when RCode::NoError + if reply.tc == 1 and not Requester::TCP === requester + requester.close + # Retry via TCP: + requester = make_tcp_requester(nameserver, port) + senders = {} + # This will use TCP for all remaining candidates (assuming the + # current candidate does not already respond successfully via + # TCP). This makes sense because we already know the full + # response will not fit in an untruncated UDP packet. + redo + else + yield(reply, reply_name) + end + return + when RCode::NXDomain + raise Config::NXDomain.new(reply_name.to_s) + else + raise Config::OtherResolvError.new(reply_name.to_s) + end + } + ensure + requester.close + end + end + + def make_udp_requester # :nodoc: + nameserver_port = @config.nameserver_port + if nameserver_port.length == 1 + Requester::ConnectedUDP.new(*nameserver_port[0]) + else + Requester::UnconnectedUDP.new(*nameserver_port) + end + end + + def make_tcp_requester(host, port) # :nodoc: + return Requester::TCP.new(host, port) + end + + def extract_resources(msg, name, typeclass) # :nodoc: + if typeclass < Resource::ANY + n0 = Name.create(name) + msg.each_resource {|n, ttl, data| + yield data if n0 == n + } + end + yielded = false + n0 = Name.create(name) + msg.each_resource {|n, ttl, data| + if n0 == n + case data + when typeclass + yield data + yielded = true + when Resource::CNAME + n0 = data.name + end + end + } + return if yielded + msg.each_resource {|n, ttl, data| + if n0 == n + case data + when typeclass + yield data + end + end + } + end + + if defined? SecureRandom + def self.random(arg) # :nodoc: + begin + SecureRandom.random_number(arg) + rescue NotImplementedError + rand(arg) + end + end + else + def self.random(arg) # :nodoc: + rand(arg) + end + end + + + def self.rangerand(range) # :nodoc: + base = range.begin + len = range.end - range.begin + if !range.exclude_end? + len += 1 + end + base + random(len) + end + + RequestID = {} # :nodoc: + RequestIDMutex = Thread::Mutex.new # :nodoc: + + def self.allocate_request_id(host, port) # :nodoc: + id = nil + RequestIDMutex.synchronize { + h = (RequestID[[host, port]] ||= {}) + begin + id = rangerand(0x0000..0xffff) + end while h[id] + h[id] = true + } + id + end + + def self.free_request_id(host, port, id) # :nodoc: + RequestIDMutex.synchronize { + key = [host, port] + if h = RequestID[key] + h.delete id + if h.empty? + RequestID.delete key + end + end + } + end + + def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: + begin + port = rangerand(1024..65535) + udpsock.bind(bind_host, port) + rescue Errno::EADDRINUSE, # POSIX + Errno::EACCES, # SunOS: See PRIV_SYS_NFS in privileges(5) + Errno::EPERM # FreeBSD: security.mac.portacl.port_high is configurable. See mac_portacl(4). + retry + end + end + + class Requester # :nodoc: + def initialize + @senders = {} + @socks = nil + end + + def request(sender, tout) + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timelimit = start + tout + begin + sender.send + rescue Errno::EHOSTUNREACH, # multi-homed IPv6 may generate this + Errno::ENETUNREACH + raise ResolvTimeout + end + while true + before_select = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout = timelimit - before_select + if timeout <= 0 + raise ResolvTimeout + end + if @socks.size == 1 + select_result = @socks[0].wait_readable(timeout) ? [ @socks ] : nil + else + select_result = IO.select(@socks, nil, nil, timeout) + end + if !select_result + after_select = Process.clock_gettime(Process::CLOCK_MONOTONIC) + next if after_select < timelimit + raise ResolvTimeout + end + begin + reply, from = recv_reply(select_result[0]) + rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD + Errno::ECONNRESET # Windows + # No name server running on the server? + # Don't wait anymore. + raise ResolvTimeout + end + begin + msg = Message.decode(reply) + rescue DecodeError + next # broken DNS message ignored + end + if s = sender_for(from, msg) + break + else + # unexpected DNS message ignored + end + end + return msg, s.data + end + + def sender_for(addr, msg) + @senders[[addr,msg.id]] + end + + def close + socks = @socks + @socks = nil + socks&.each(&:close) + end + + class Sender # :nodoc: + def initialize(msg, data, sock) + @msg = msg + @data = data + @sock = sock + end + end + + class UnconnectedUDP < Requester # :nodoc: + def initialize(*nameserver_port) + super() + @nameserver_port = nameserver_port + @socks_hash = {} + @socks = [] + nameserver_port.each {|host, port| + if host.index(':') + bind_host = "::" + af = Socket::AF_INET6 + else + bind_host = "0.0.0.0" + af = Socket::AF_INET + end + next if @socks_hash[bind_host] + begin + sock = UDPSocket.new(af) + rescue Errno::EAFNOSUPPORT + next # The kernel doesn't support the address family. + end + sock.do_not_reverse_lookup = true + DNS.bind_random_port(sock, bind_host) + @socks << sock + @socks_hash[bind_host] = sock + } + end + + def recv_reply(readable_socks) + reply, from = readable_socks[0].recvfrom(UDPSize) + return reply, [from[3],from[1]] + end + + def sender(msg, data, host, port=Port) + sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"] + return nil if !sock + service = [host, port] + id = DNS.allocate_request_id(host, port) + request = msg.encode + request[0,2] = [id].pack('n') + return @senders[[service, id]] = + Sender.new(request, data, sock, host, port) + end + + def close + super + @senders.each_key {|service, id| + DNS.free_request_id(service[0], service[1], id) + } + end + + class Sender < Requester::Sender # :nodoc: + def initialize(msg, data, sock, host, port) + super(msg, data, sock) + @host = host + @port = port + end + attr_reader :data + + def send + raise "@sock is nil." if @sock.nil? + @sock.send(@msg, 0, @host, @port) + end + end + end + + class ConnectedUDP < Requester # :nodoc: + def initialize(host, port=Port) + super() + @host = host + @port = port + is_ipv6 = host.index(':') + sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET) + @socks = [sock] + sock.do_not_reverse_lookup = true + DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0") + sock.connect(host, port) + end + + def recv_reply(readable_socks) + reply = readable_socks[0].recv(UDPSize) + return reply, nil + end + + def sender(msg, data, host=@host, port=@port) + unless host == @host && port == @port + raise RequestError.new("host/port don't match: #{host}:#{port}") + end + id = DNS.allocate_request_id(@host, @port) + request = msg.encode + request[0,2] = [id].pack('n') + return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) + end + + def close + super + @senders.each_key {|from, id| + DNS.free_request_id(@host, @port, id) + } + end + + class Sender < Requester::Sender # :nodoc: + def send + raise "@sock is nil." if @sock.nil? + @sock.send(@msg, 0) + end + attr_reader :data + end + end + + class MDNSOneShot < UnconnectedUDP # :nodoc: + def sender(msg, data, host, port=Port) + id = DNS.allocate_request_id(host, port) + request = msg.encode + request[0,2] = [id].pack('n') + sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"] + return @senders[id] = + UnconnectedUDP::Sender.new(request, data, sock, host, port) + end + + def sender_for(addr, msg) + @senders[msg.id] + end + end + + class TCP < Requester # :nodoc: + def initialize(host, port=Port) + super() + @host = host + @port = port + sock = TCPSocket.new(@host, @port) + @socks = [sock] + @senders = {} + end + + def recv_reply(readable_socks) + len = readable_socks[0].read(2).unpack('n')[0] + reply = @socks[0].read(len) + return reply, nil + end + + def sender(msg, data, host=@host, port=@port) + unless host == @host && port == @port + raise RequestError.new("host/port don't match: #{host}:#{port}") + end + id = DNS.allocate_request_id(@host, @port) + request = msg.encode + request[0,2] = [request.length, id].pack('nn') + return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) + end + + class Sender < Requester::Sender # :nodoc: + def send + @sock.print(@msg) + @sock.flush + end + attr_reader :data + end + + def close + super + @senders.each_key {|from,id| + DNS.free_request_id(@host, @port, id) + } + end + end + + ## + # Indicates a problem with the DNS request. + + class RequestError < StandardError + end + end + + class Config # :nodoc: + def initialize(config_info=nil) + @mutex = Thread::Mutex.new + @config_info = config_info + @initialized = nil + @timeouts = nil + end + + def timeouts=(values) + if values + values = Array(values) + values.each do |t| + Numeric === t or raise ArgumentError, "#{t.inspect} is not numeric" + t > 0.0 or raise ArgumentError, "timeout=#{t} must be positive" + end + @timeouts = values + else + @timeouts = nil + end + end + + def Config.parse_resolv_conf(filename) + nameserver = [] + search = nil + ndots = 1 + File.open(filename, 'rb') {|f| + f.each {|line| + line.sub!(/[#;].*/, '') + keyword, *args = line.split(/\s+/) + args.each(&:untaint) + next unless keyword + case keyword + when 'nameserver' + nameserver += args + when 'domain' + next if args.empty? + search = [args[0]] + when 'search' + next if args.empty? + search = args + when 'options' + args.each {|arg| + case arg + when /\Andots:(\d+)\z/ + ndots = $1.to_i + end + } + end + } + } + return { :nameserver => nameserver, :search => search, :ndots => ndots } + end + + def Config.default_config_hash(filename="/etc/resolv.conf") + if File.exist? filename + config_hash = Config.parse_resolv_conf(filename) + else + if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM + require 'win32/resolv' + search, nameserver = Win32::Resolv.get_resolv_info + config_hash = {} + config_hash[:nameserver] = nameserver if nameserver + config_hash[:search] = [search].flatten if search + end + end + config_hash || {} + end + + def lazy_initialize + @mutex.synchronize { + unless @initialized + @nameserver_port = [] + @search = nil + @ndots = 1 + case @config_info + when nil + config_hash = Config.default_config_hash + when String + config_hash = Config.parse_resolv_conf(@config_info) + when Hash + config_hash = @config_info.dup + if String === config_hash[:nameserver] + config_hash[:nameserver] = [config_hash[:nameserver]] + end + if String === config_hash[:search] + config_hash[:search] = [config_hash[:search]] + end + else + raise ArgumentError.new("invalid resolv configuration: #{@config_info.inspect}") + end + if config_hash.include? :nameserver + @nameserver_port = config_hash[:nameserver].map {|ns| [ns, Port] } + end + if config_hash.include? :nameserver_port + @nameserver_port = config_hash[:nameserver_port].map {|ns, port| [ns, (port || Port)] } + end + @search = config_hash[:search] if config_hash.include? :search + @ndots = config_hash[:ndots] if config_hash.include? :ndots + + if @nameserver_port.empty? + @nameserver_port << ['0.0.0.0', Port] + end + if @search + @search = @search.map {|arg| Label.split(arg) } + else + hostname = Socket.gethostname + if /\./ =~ hostname + @search = [Label.split($')] + else + @search = [[]] + end + end + + if !@nameserver_port.kind_of?(Array) || + @nameserver_port.any? {|ns_port| + !(Array === ns_port) || + ns_port.length != 2 + !(String === ns_port[0]) || + !(Integer === ns_port[1]) + } + raise ArgumentError.new("invalid nameserver config: #{@nameserver_port.inspect}") + end + + if !@search.kind_of?(Array) || + !@search.all? {|ls| ls.all? {|l| Label::Str === l } } + raise ArgumentError.new("invalid search config: #{@search.inspect}") + end + + if !@ndots.kind_of?(Integer) + raise ArgumentError.new("invalid ndots config: #{@ndots.inspect}") + end + + @initialized = true + end + } + self + end + + def single? + lazy_initialize + if @nameserver_port.length == 1 + return @nameserver_port[0] + else + return nil + end + end + + def nameserver_port + @nameserver_port + end + + def generate_candidates(name) + candidates = nil + name = Name.create(name) + if name.absolute? + candidates = [name] + else + if @ndots <= name.length - 1 + candidates = [Name.new(name.to_a)] + else + candidates = [] + end + candidates.concat(@search.map {|domain| Name.new(name.to_a + domain)}) + fname = Name.create("#{name}.") + if !candidates.include?(fname) + candidates << fname + end + end + return candidates + end + + InitialTimeout = 5 + + def generate_timeouts + ts = [InitialTimeout] + ts << ts[-1] * 2 / @nameserver_port.length + ts << ts[-1] * 2 + ts << ts[-1] * 2 + return ts + end + + def resolv(name) + candidates = generate_candidates(name) + timeouts = @timeouts || generate_timeouts + begin + candidates.each {|candidate| + begin + timeouts.each {|tout| + @nameserver_port.each {|nameserver, port| + begin + yield candidate, tout, nameserver, port + rescue ResolvTimeout + end + } + } + raise ResolvError.new("DNS resolv timeout: #{name}") + rescue NXDomain + end + } + rescue ResolvError + end + end + + ## + # Indicates no such domain was found. + + class NXDomain < ResolvError + end + + ## + # Indicates some other unhandled resolver error was encountered. + + class OtherResolvError < ResolvError + end + end + + module OpCode # :nodoc: + Query = 0 + IQuery = 1 + Status = 2 + Notify = 4 + Update = 5 + end + + module RCode # :nodoc: + NoError = 0 + FormErr = 1 + ServFail = 2 + NXDomain = 3 + NotImp = 4 + Refused = 5 + YXDomain = 6 + YXRRSet = 7 + NXRRSet = 8 + NotAuth = 9 + NotZone = 10 + BADVERS = 16 + BADSIG = 16 + BADKEY = 17 + BADTIME = 18 + BADMODE = 19 + BADNAME = 20 + BADALG = 21 + end + + ## + # Indicates that the DNS response was unable to be decoded. + + class DecodeError < StandardError + end + + ## + # Indicates that the DNS request was unable to be encoded. + + class EncodeError < StandardError + end + + module Label # :nodoc: + def self.split(arg) + labels = [] + arg.scan(/[^\.]+/) {labels << Str.new($&)} + return labels + end + + class Str # :nodoc: + def initialize(string) + @string = string + # case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343] + # This assumes @string is given in ASCII compatible encoding. + @downcase = string.b.downcase + end + attr_reader :string, :downcase + + def to_s + return @string + end + + def inspect + return "#<#{self.class} #{self}>" + end + + def ==(other) + return self.class == other.class && @downcase == other.downcase + end + + def eql?(other) + return self == other + end + + def hash + return @downcase.hash + end + end + end + + ## + # A representation of a DNS name. + + class Name + + ## + # Creates a new DNS name from +arg+. +arg+ can be: + # + # Name:: returns +arg+. + # String:: Creates a new Name. + + def self.create(arg) + case arg + when Name + return arg + when String + return Name.new(Label.split(arg), /\.\z/ =~ arg ? true : false) + else + raise ArgumentError.new("cannot interpret as DNS name: #{arg.inspect}") + end + end + + def initialize(labels, absolute=true) # :nodoc: + labels = labels.map {|label| + case label + when String then Label::Str.new(label) + when Label::Str then label + else + raise ArgumentError, "unexpected label: #{label.inspect}" + end + } + @labels = labels + @absolute = absolute + end + + def inspect # :nodoc: + "#<#{self.class}: #{self}#{@absolute ? '.' : ''}>" + end + + ## + # True if this name is absolute. + + def absolute? + return @absolute + end + + def ==(other) # :nodoc: + return false unless Name === other + return false unless @absolute == other.absolute? + return @labels == other.to_a + end + + alias eql? == # :nodoc: + + ## + # Returns true if +other+ is a subdomain. + # + # Example: + # + # domain = Resolv::DNS::Name.create("y.z") + # p Resolv::DNS::Name.create("w.x.y.z").subdomain_of?(domain) #=> true + # p Resolv::DNS::Name.create("x.y.z").subdomain_of?(domain) #=> true + # p Resolv::DNS::Name.create("y.z").subdomain_of?(domain) #=> false + # p Resolv::DNS::Name.create("z").subdomain_of?(domain) #=> false + # p Resolv::DNS::Name.create("x.y.z.").subdomain_of?(domain) #=> false + # p Resolv::DNS::Name.create("w.z").subdomain_of?(domain) #=> false + # + + def subdomain_of?(other) + raise ArgumentError, "not a domain name: #{other.inspect}" unless Name === other + return false if @absolute != other.absolute? + other_len = other.length + return false if @labels.length <= other_len + return @labels[-other_len, other_len] == other.to_a + end + + def hash # :nodoc: + return @labels.hash ^ @absolute.hash + end + + def to_a # :nodoc: + return @labels + end + + def length # :nodoc: + return @labels.length + end + + def [](i) # :nodoc: + return @labels[i] + end + + ## + # returns the domain name as a string. + # + # The domain name doesn't have a trailing dot even if the name object is + # absolute. + # + # Example: + # + # p Resolv::DNS::Name.create("x.y.z.").to_s #=> "x.y.z" + # p Resolv::DNS::Name.create("x.y.z").to_s #=> "x.y.z" + + def to_s + return @labels.join('.') + end + end + + class Message # :nodoc: + @@identifier = -1 + + def initialize(id = (@@identifier += 1) & 0xffff) + @id = id + @qr = 0 + @opcode = 0 + @aa = 0 + @tc = 0 + @rd = 0 # recursion desired + @ra = 0 # recursion available + @rcode = 0 + @question = [] + @answer = [] + @authority = [] + @additional = [] + end + + attr_accessor :id, :qr, :opcode, :aa, :tc, :rd, :ra, :rcode + attr_reader :question, :answer, :authority, :additional + + def ==(other) + return @id == other.id && + @qr == other.qr && + @opcode == other.opcode && + @aa == other.aa && + @tc == other.tc && + @rd == other.rd && + @ra == other.ra && + @rcode == other.rcode && + @question == other.question && + @answer == other.answer && + @authority == other.authority && + @additional == other.additional + end + + def add_question(name, typeclass) + @question << [Name.create(name), typeclass] + end + + def each_question + @question.each {|name, typeclass| + yield name, typeclass + } + end + + def add_answer(name, ttl, data) + @answer << [Name.create(name), ttl, data] + end + + def each_answer + @answer.each {|name, ttl, data| + yield name, ttl, data + } + end + + def add_authority(name, ttl, data) + @authority << [Name.create(name), ttl, data] + end + + def each_authority + @authority.each {|name, ttl, data| + yield name, ttl, data + } + end + + def add_additional(name, ttl, data) + @additional << [Name.create(name), ttl, data] + end + + def each_additional + @additional.each {|name, ttl, data| + yield name, ttl, data + } + end + + def each_resource + each_answer {|name, ttl, data| yield name, ttl, data} + each_authority {|name, ttl, data| yield name, ttl, data} + each_additional {|name, ttl, data| yield name, ttl, data} + end + + def encode + return MessageEncoder.new {|msg| + msg.put_pack('nnnnnn', + @id, + (@qr & 1) << 15 | + (@opcode & 15) << 11 | + (@aa & 1) << 10 | + (@tc & 1) << 9 | + (@rd & 1) << 8 | + (@ra & 1) << 7 | + (@rcode & 15), + @question.length, + @answer.length, + @authority.length, + @additional.length) + @question.each {|q| + name, typeclass = q + msg.put_name(name) + msg.put_pack('nn', typeclass::TypeValue, typeclass::ClassValue) + } + [@answer, @authority, @additional].each {|rr| + rr.each {|r| + name, ttl, data = r + msg.put_name(name) + msg.put_pack('nnN', data.class::TypeValue, data.class::ClassValue, ttl) + msg.put_length16 {data.encode_rdata(msg)} + } + } + }.to_s + end + + class MessageEncoder # :nodoc: + def initialize + @data = ''.dup + @names = {} + yield self + end + + def to_s + return @data + end + + def put_bytes(d) + @data << d + end + + def put_pack(template, *d) + @data << d.pack(template) + end + + def put_length16 + length_index = @data.length + @data << "\0\0" + data_start = @data.length + yield + data_end = @data.length + @data[length_index, 2] = [data_end - data_start].pack("n") + end + + def put_string(d) + self.put_pack("C", d.length) + @data << d + end + + def put_string_list(ds) + ds.each {|d| + self.put_string(d) + } + end + + def put_name(d) + put_labels(d.to_a) + end + + def put_labels(d) + d.each_index {|i| + domain = d[i..-1] + if idx = @names[domain] + self.put_pack("n", 0xc000 | idx) + return + else + if @data.length < 0x4000 + @names[domain] = @data.length + end + self.put_label(d[i]) + end + } + @data << "\0" + end + + def put_label(d) + self.put_string(d.to_s) + end + end + + def Message.decode(m) + o = Message.new(0) + MessageDecoder.new(m) {|msg| + id, flag, qdcount, ancount, nscount, arcount = + msg.get_unpack('nnnnnn') + o.id = id + o.qr = (flag >> 15) & 1 + o.opcode = (flag >> 11) & 15 + o.aa = (flag >> 10) & 1 + o.tc = (flag >> 9) & 1 + o.rd = (flag >> 8) & 1 + o.ra = (flag >> 7) & 1 + o.rcode = flag & 15 + (1..qdcount).each { + name, typeclass = msg.get_question + o.add_question(name, typeclass) + } + (1..ancount).each { + name, ttl, data = msg.get_rr + o.add_answer(name, ttl, data) + } + (1..nscount).each { + name, ttl, data = msg.get_rr + o.add_authority(name, ttl, data) + } + (1..arcount).each { + name, ttl, data = msg.get_rr + o.add_additional(name, ttl, data) + } + } + return o + end + + class MessageDecoder # :nodoc: + def initialize(data) + @data = data + @index = 0 + @limit = data.bytesize + yield self + end + + def inspect + "\#<#{self.class}: #{@data.byteslice(0, @index).inspect} #{@data.byteslice(@index..-1).inspect}>" + end + + def get_length16 + len, = self.get_unpack('n') + save_limit = @limit + @limit = @index + len + d = yield(len) + if @index < @limit + raise DecodeError.new("junk exists") + elsif @limit < @index + raise DecodeError.new("limit exceeded") + end + @limit = save_limit + return d + end + + def get_bytes(len = @limit - @index) + raise DecodeError.new("limit exceeded") if @limit < @index + len + d = @data.byteslice(@index, len) + @index += len + return d + end + + def get_unpack(template) + len = 0 + template.each_byte {|byte| + byte = "%c" % byte + case byte + when ?c, ?C + len += 1 + when ?n + len += 2 + when ?N + len += 4 + else + raise StandardError.new("unsupported template: '#{byte.chr}' in '#{template}'") + end + } + raise DecodeError.new("limit exceeded") if @limit < @index + len + arr = @data.unpack("@#{@index}#{template}") + @index += len + return arr + end + + def get_string + raise DecodeError.new("limit exceeded") if @limit <= @index + len = @data.getbyte(@index) + raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len + d = @data.byteslice(@index + 1, len) + @index += 1 + len + return d + end + + def get_string_list + strings = [] + while @index < @limit + strings << self.get_string + end + strings + end + + def get_name + return Name.new(self.get_labels) + end + + def get_labels + prev_index = @index + save_index = nil + d = [] + while true + raise DecodeError.new("limit exceeded") if @limit <= @index + case @data.getbyte(@index) + when 0 + @index += 1 + if save_index + @index = save_index + end + return d + when 192..255 + idx = self.get_unpack('n')[0] & 0x3fff + if prev_index <= idx + raise DecodeError.new("non-backward name pointer") + end + prev_index = idx + if !save_index + save_index = @index + end + @index = idx + else + d << self.get_label + end + end + end + + def get_label + return Label::Str.new(self.get_string) + end + + def get_question + name = self.get_name + type, klass = self.get_unpack("nn") + return name, Resource.get_class(type, klass) + end + + def get_rr + name = self.get_name + type, klass, ttl = self.get_unpack('nnN') + typeclass = Resource.get_class(type, klass) + res = self.get_length16 do + begin + typeclass.decode_rdata self + rescue => e + raise DecodeError, e.message, e.backtrace + end + end + res.instance_variable_set :@ttl, ttl + return name, ttl, res + end + end + end + + ## + # A DNS query abstract class. + + class Query + def encode_rdata(msg) # :nodoc: + raise EncodeError.new("#{self.class} is query.") + end + + def self.decode_rdata(msg) # :nodoc: + raise DecodeError.new("#{self.class} is query.") + end + end + + ## + # A DNS resource abstract class. + + class Resource < Query + + ## + # Remaining Time To Live for this Resource. + + attr_reader :ttl + + ClassHash = {} # :nodoc: + + def encode_rdata(msg) # :nodoc: + raise NotImplementedError.new + end + + def self.decode_rdata(msg) # :nodoc: + raise NotImplementedError.new + end + + def ==(other) # :nodoc: + return false unless self.class == other.class + s_ivars = self.instance_variables + s_ivars.sort! + s_ivars.delete :@ttl + o_ivars = other.instance_variables + o_ivars.sort! + o_ivars.delete :@ttl + return s_ivars == o_ivars && + s_ivars.collect {|name| self.instance_variable_get name} == + o_ivars.collect {|name| other.instance_variable_get name} + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + h = 0 + vars = self.instance_variables + vars.delete :@ttl + vars.each {|name| + h ^= self.instance_variable_get(name).hash + } + return h + end + + def self.get_class(type_value, class_value) # :nodoc: + return ClassHash[[type_value, class_value]] || + Generic.create(type_value, class_value) + end + + ## + # A generic resource abstract class. + + class Generic < Resource + + ## + # Creates a new generic resource. + + def initialize(data) + @data = data + end + + ## + # Data for this generic resource. + + attr_reader :data + + def encode_rdata(msg) # :nodoc: + msg.put_bytes(data) + end + + def self.decode_rdata(msg) # :nodoc: + return self.new(msg.get_bytes) + end + + def self.create(type_value, class_value) # :nodoc: + c = Class.new(Generic) + c.const_set(:TypeValue, type_value) + c.const_set(:ClassValue, class_value) + Generic.const_set("Type#{type_value}_Class#{class_value}", c) + ClassHash[[type_value, class_value]] = c + return c + end + end + + ## + # Domain Name resource abstract class. + + class DomainName < Resource + + ## + # Creates a new DomainName from +name+. + + def initialize(name) + @name = name + end + + ## + # The name of this DomainName. + + attr_reader :name + + def encode_rdata(msg) # :nodoc: + msg.put_name(@name) + end + + def self.decode_rdata(msg) # :nodoc: + return self.new(msg.get_name) + end + end + + # Standard (class generic) RRs + + ClassValue = nil # :nodoc: + + ## + # An authoritative name server. + + class NS < DomainName + TypeValue = 2 # :nodoc: + end + + ## + # The canonical name for an alias. + + class CNAME < DomainName + TypeValue = 5 # :nodoc: + end + + ## + # Start Of Authority resource. + + class SOA < Resource + + TypeValue = 6 # :nodoc: + + ## + # Creates a new SOA record. See the attr documentation for the + # details of each argument. + + def initialize(mname, rname, serial, refresh, retry_, expire, minimum) + @mname = mname + @rname = rname + @serial = serial + @refresh = refresh + @retry = retry_ + @expire = expire + @minimum = minimum + end + + ## + # Name of the host where the master zone file for this zone resides. + + attr_reader :mname + + ## + # The person responsible for this domain name. + + attr_reader :rname + + ## + # The version number of the zone file. + + attr_reader :serial + + ## + # How often, in seconds, a secondary name server is to check for + # updates from the primary name server. + + attr_reader :refresh + + ## + # How often, in seconds, a secondary name server is to retry after a + # failure to check for a refresh. + + attr_reader :retry + + ## + # Time in seconds that a secondary name server is to use the data + # before refreshing from the primary name server. + + attr_reader :expire + + ## + # The minimum number of seconds to be used for TTL values in RRs. + + attr_reader :minimum + + def encode_rdata(msg) # :nodoc: + msg.put_name(@mname) + msg.put_name(@rname) + msg.put_pack('NNNNN', @serial, @refresh, @retry, @expire, @minimum) + end + + def self.decode_rdata(msg) # :nodoc: + mname = msg.get_name + rname = msg.get_name + serial, refresh, retry_, expire, minimum = msg.get_unpack('NNNNN') + return self.new( + mname, rname, serial, refresh, retry_, expire, minimum) + end + end + + ## + # A Pointer to another DNS name. + + class PTR < DomainName + TypeValue = 12 # :nodoc: + end + + ## + # Host Information resource. + + class HINFO < Resource + + TypeValue = 13 # :nodoc: + + ## + # Creates a new HINFO running +os+ on +cpu+. + + def initialize(cpu, os) + @cpu = cpu + @os = os + end + + ## + # CPU architecture for this resource. + + attr_reader :cpu + + ## + # Operating system for this resource. + + attr_reader :os + + def encode_rdata(msg) # :nodoc: + msg.put_string(@cpu) + msg.put_string(@os) + end + + def self.decode_rdata(msg) # :nodoc: + cpu = msg.get_string + os = msg.get_string + return self.new(cpu, os) + end + end + + ## + # Mailing list or mailbox information. + + class MINFO < Resource + + TypeValue = 14 # :nodoc: + + def initialize(rmailbx, emailbx) + @rmailbx = rmailbx + @emailbx = emailbx + end + + ## + # Domain name responsible for this mail list or mailbox. + + attr_reader :rmailbx + + ## + # Mailbox to use for error messages related to the mail list or mailbox. + + attr_reader :emailbx + + def encode_rdata(msg) # :nodoc: + msg.put_name(@rmailbx) + msg.put_name(@emailbx) + end + + def self.decode_rdata(msg) # :nodoc: + rmailbx = msg.get_string + emailbx = msg.get_string + return self.new(rmailbx, emailbx) + end + end + + ## + # Mail Exchanger resource. + + class MX < Resource + + TypeValue= 15 # :nodoc: + + ## + # Creates a new MX record with +preference+, accepting mail at + # +exchange+. + + def initialize(preference, exchange) + @preference = preference + @exchange = exchange + end + + ## + # The preference for this MX. + + attr_reader :preference + + ## + # The host of this MX. + + attr_reader :exchange + + def encode_rdata(msg) # :nodoc: + msg.put_pack('n', @preference) + msg.put_name(@exchange) + end + + def self.decode_rdata(msg) # :nodoc: + preference, = msg.get_unpack('n') + exchange = msg.get_name + return self.new(preference, exchange) + end + end + + ## + # Unstructured text resource. + + class TXT < Resource + + TypeValue = 16 # :nodoc: + + def initialize(first_string, *rest_strings) + @strings = [first_string, *rest_strings] + end + + ## + # Returns an Array of Strings for this TXT record. + + attr_reader :strings + + ## + # Returns the concatenated string from +strings+. + + def data + @strings.join("") + end + + def encode_rdata(msg) # :nodoc: + msg.put_string_list(@strings) + end + + def self.decode_rdata(msg) # :nodoc: + strings = msg.get_string_list + return self.new(*strings) + end + end + + ## + # Location resource + + class LOC < Resource + + TypeValue = 29 # :nodoc: + + def initialize(version, ssize, hprecision, vprecision, latitude, longitude, altitude) + @version = version + @ssize = Resolv::LOC::Size.create(ssize) + @hprecision = Resolv::LOC::Size.create(hprecision) + @vprecision = Resolv::LOC::Size.create(vprecision) + @latitude = Resolv::LOC::Coord.create(latitude) + @longitude = Resolv::LOC::Coord.create(longitude) + @altitude = Resolv::LOC::Alt.create(altitude) + end + + ## + # Returns the version value for this LOC record which should always be 00 + + attr_reader :version + + ## + # The spherical size of this LOC + # in meters using scientific notation as 2 integers of XeY + + attr_reader :ssize + + ## + # The horizontal precision using ssize type values + # in meters using scientific notation as 2 integers of XeY + # for precision use value/2 e.g. 2m = +/-1m + + attr_reader :hprecision + + ## + # The vertical precision using ssize type values + # in meters using scientific notation as 2 integers of XeY + # for precision use value/2 e.g. 2m = +/-1m + + attr_reader :vprecision + + ## + # The latitude for this LOC where 2**31 is the equator + # in thousandths of an arc second as an unsigned 32bit integer + + attr_reader :latitude + + ## + # The longitude for this LOC where 2**31 is the prime meridian + # in thousandths of an arc second as an unsigned 32bit integer + + attr_reader :longitude + + ## + # The altitude of the LOC above a reference sphere whose surface sits 100km below the WGS84 spheroid + # in centimeters as an unsigned 32bit integer + + attr_reader :altitude + + + def encode_rdata(msg) # :nodoc: + msg.put_bytes(@version) + msg.put_bytes(@ssize.scalar) + msg.put_bytes(@hprecision.scalar) + msg.put_bytes(@vprecision.scalar) + msg.put_bytes(@latitude.coordinates) + msg.put_bytes(@longitude.coordinates) + msg.put_bytes(@altitude.altitude) + end + + def self.decode_rdata(msg) # :nodoc: + version = msg.get_bytes(1) + ssize = msg.get_bytes(1) + hprecision = msg.get_bytes(1) + vprecision = msg.get_bytes(1) + latitude = msg.get_bytes(4) + longitude = msg.get_bytes(4) + altitude = msg.get_bytes(4) + return self.new( + version, + Resolv::LOC::Size.new(ssize), + Resolv::LOC::Size.new(hprecision), + Resolv::LOC::Size.new(vprecision), + Resolv::LOC::Coord.new(latitude,"lat"), + Resolv::LOC::Coord.new(longitude,"lon"), + Resolv::LOC::Alt.new(altitude) + ) + end + end + + ## + # A Query type requesting any RR. + + class ANY < Query + TypeValue = 255 # :nodoc: + end + + ClassInsensitiveTypes = [ # :nodoc: + NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY + ] + + ## + # module IN contains ARPA Internet specific RRs. + + module IN + + ClassValue = 1 # :nodoc: + + ClassInsensitiveTypes.each {|s| + c = Class.new(s) + c.const_set(:TypeValue, s::TypeValue) + c.const_set(:ClassValue, ClassValue) + ClassHash[[s::TypeValue, ClassValue]] = c + self.const_set(s.name.sub(/.*::/, ''), c) + } + + ## + # IPv4 Address resource + + class A < Resource + TypeValue = 1 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + + ## + # Creates a new A for +address+. + + def initialize(address) + @address = IPv4.create(address) + end + + ## + # The Resolv::IPv4 address for this A. + + attr_reader :address + + def encode_rdata(msg) # :nodoc: + msg.put_bytes(@address.address) + end + + def self.decode_rdata(msg) # :nodoc: + return self.new(IPv4.new(msg.get_bytes(4))) + end + end + + ## + # Well Known Service resource. + + class WKS < Resource + TypeValue = 11 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + + def initialize(address, protocol, bitmap) + @address = IPv4.create(address) + @protocol = protocol + @bitmap = bitmap + end + + ## + # The host these services run on. + + attr_reader :address + + ## + # IP protocol number for these services. + + attr_reader :protocol + + ## + # A bit map of enabled services on this host. + # + # If protocol is 6 (TCP) then the 26th bit corresponds to the SMTP + # service (port 25). If this bit is set, then an SMTP server should + # be listening on TCP port 25; if zero, SMTP service is not + # supported. + + attr_reader :bitmap + + def encode_rdata(msg) # :nodoc: + msg.put_bytes(@address.address) + msg.put_pack("n", @protocol) + msg.put_bytes(@bitmap) + end + + def self.decode_rdata(msg) # :nodoc: + address = IPv4.new(msg.get_bytes(4)) + protocol, = msg.get_unpack("n") + bitmap = msg.get_bytes + return self.new(address, protocol, bitmap) + end + end + + ## + # An IPv6 address record. + + class AAAA < Resource + TypeValue = 28 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + + ## + # Creates a new AAAA for +address+. + + def initialize(address) + @address = IPv6.create(address) + end + + ## + # The Resolv::IPv6 address for this AAAA. + + attr_reader :address + + def encode_rdata(msg) # :nodoc: + msg.put_bytes(@address.address) + end + + def self.decode_rdata(msg) # :nodoc: + return self.new(IPv6.new(msg.get_bytes(16))) + end + end + + ## + # SRV resource record defined in RFC 2782 + # + # These records identify the hostname and port that a service is + # available at. + + class SRV < Resource + TypeValue = 33 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + + # Create a SRV resource record. + # + # See the documentation for #priority, #weight, #port and #target + # for +priority+, +weight+, +port and +target+ respectively. + + def initialize(priority, weight, port, target) + @priority = priority.to_int + @weight = weight.to_int + @port = port.to_int + @target = Name.create(target) + end + + # The priority of this target host. + # + # A client MUST attempt to contact the target host with the + # lowest-numbered priority it can reach; target hosts with the same + # priority SHOULD be tried in an order defined by the weight field. + # The range is 0-65535. Note that it is not widely implemented and + # should be set to zero. + + attr_reader :priority + + # A server selection mechanism. + # + # The weight field specifies a relative weight for entries with the + # same priority. Larger weights SHOULD be given a proportionately + # higher probability of being selected. The range of this number is + # 0-65535. Domain administrators SHOULD use Weight 0 when there + # isn't any server selection to do, to make the RR easier to read + # for humans (less noisy). Note that it is not widely implemented + # and should be set to zero. + + attr_reader :weight + + # The port on this target host of this service. + # + # The range is 0-65535. + + attr_reader :port + + # The domain name of the target host. + # + # A target of "." means that the service is decidedly not available + # at this domain. + + attr_reader :target + + def encode_rdata(msg) # :nodoc: + msg.put_pack("n", @priority) + msg.put_pack("n", @weight) + msg.put_pack("n", @port) + msg.put_name(@target) + end + + def self.decode_rdata(msg) # :nodoc: + priority, = msg.get_unpack("n") + weight, = msg.get_unpack("n") + port, = msg.get_unpack("n") + target = msg.get_name + return self.new(priority, weight, port, target) + end + end + end + end + end + + ## + # A Resolv::DNS IPv4 address. + + class IPv4 + + ## + # Regular expression IPv4 addresses must match. + + Regex256 = /0 + |1(?:[0-9][0-9]?)? + |2(?:[0-4][0-9]?|5[0-5]?|[6-9])? + |[3-9][0-9]?/x + Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/ + + def self.create(arg) + case arg + when IPv4 + return arg + when Regex + if (0..255) === (a = $1.to_i) && + (0..255) === (b = $2.to_i) && + (0..255) === (c = $3.to_i) && + (0..255) === (d = $4.to_i) + return self.new([a, b, c, d].pack("CCCC")) + else + raise ArgumentError.new("IPv4 address with invalid value: " + arg) + end + else + raise ArgumentError.new("cannot interpret as IPv4 address: #{arg.inspect}") + end + end + + def initialize(address) # :nodoc: + unless address.kind_of?(String) + raise ArgumentError, 'IPv4 address must be a string' + end + unless address.length == 4 + raise ArgumentError, "IPv4 address expects 4 bytes but #{address.length} bytes" + end + @address = address + end + + ## + # A String representation of this IPv4 address. + + ## + # The raw IPv4 address as a String. + + attr_reader :address + + def to_s # :nodoc: + return sprintf("%d.%d.%d.%d", *@address.unpack("CCCC")) + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + ## + # Turns this IPv4 address into a Resolv::DNS::Name. + + def to_name + return DNS::Name.create( + '%d.%d.%d.%d.in-addr.arpa.' % @address.unpack('CCCC').reverse) + end + + def ==(other) # :nodoc: + return @address == other.address + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @address.hash + end + end + + ## + # A Resolv::DNS IPv6 address. + + class IPv6 + + ## + # IPv6 address format a:b:c:d:e:f:g:h + Regex_8Hex = /\A + (?:[0-9A-Fa-f]{1,4}:){7} + [0-9A-Fa-f]{1,4} + \z/x + + ## + # Compressed IPv6 address format a::b + + Regex_CompressedHex = /\A + ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: + ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) + \z/x + + ## + # IPv4 mapped IPv6 address format a:b:c:d:e:f:w.x.y.z + + Regex_6Hex4Dec = /\A + ((?:[0-9A-Fa-f]{1,4}:){6,6}) + (\d+)\.(\d+)\.(\d+)\.(\d+) + \z/x + + ## + # Compressed IPv4 mapped IPv6 address format a::b:w.x.y.z + + Regex_CompressedHex4Dec = /\A + ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: + ((?:[0-9A-Fa-f]{1,4}:)*) + (\d+)\.(\d+)\.(\d+)\.(\d+) + \z/x + + ## + # A composite IPv6 address Regexp. + + Regex = / + (?:#{Regex_8Hex}) | + (?:#{Regex_CompressedHex}) | + (?:#{Regex_6Hex4Dec}) | + (?:#{Regex_CompressedHex4Dec})/x + + ## + # Creates a new IPv6 address from +arg+ which may be: + # + # IPv6:: returns +arg+. + # String:: +arg+ must match one of the IPv6::Regex* constants + + def self.create(arg) + case arg + when IPv6 + return arg + when String + address = ''.b + if Regex_8Hex =~ arg + arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')} + elsif Regex_CompressedHex =~ arg + prefix = $1 + suffix = $2 + a1 = ''.b + a2 = ''.b + prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} + suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} + omitlen = 16 - a1.length - a2.length + address << a1 << "\0" * omitlen << a2 + elsif Regex_6Hex4Dec =~ arg + prefix, a, b, c, d = $1, $2.to_i, $3.to_i, $4.to_i, $5.to_i + if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d + prefix.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')} + address << [a, b, c, d].pack('CCCC') + else + raise ArgumentError.new("not numeric IPv6 address: " + arg) + end + elsif Regex_CompressedHex4Dec =~ arg + prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i + if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d + a1 = ''.b + a2 = ''.b + prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} + suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} + omitlen = 12 - a1.length - a2.length + address << a1 << "\0" * omitlen << a2 << [a, b, c, d].pack('CCCC') + else + raise ArgumentError.new("not numeric IPv6 address: " + arg) + end + else + raise ArgumentError.new("not numeric IPv6 address: " + arg) + end + return IPv6.new(address) + else + raise ArgumentError.new("cannot interpret as IPv6 address: #{arg.inspect}") + end + end + + def initialize(address) # :nodoc: + unless address.kind_of?(String) && address.length == 16 + raise ArgumentError.new('IPv6 address must be 16 bytes') + end + @address = address + end + + ## + # The raw IPv6 address as a String. + + attr_reader :address + + def to_s # :nodoc: + address = sprintf("%X:%X:%X:%X:%X:%X:%X:%X", *@address.unpack("nnnnnnnn")) + unless address.sub!(/(^|:)0(:0)+(:|$)/, '::') + address.sub!(/(^|:)0(:|$)/, '::') + end + return address + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + ## + # Turns this IPv6 address into a Resolv::DNS::Name. + #-- + # ip6.arpa should be searched too. [RFC3152] + + def to_name + return DNS::Name.new( + @address.unpack("H32")[0].split(//).reverse + ['ip6', 'arpa']) + end + + def ==(other) # :nodoc: + return @address == other.address + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @address.hash + end + end + + ## + # Resolv::MDNS is a one-shot Multicast DNS (mDNS) resolver. It blindly + # makes queries to the mDNS addresses without understanding anything about + # multicast ports. + # + # Information taken form the following places: + # + # * RFC 6762 + + class MDNS < DNS + + ## + # Default mDNS Port + + Port = 5353 + + ## + # Default IPv4 mDNS address + + AddressV4 = '224.0.0.251' + + ## + # Default IPv6 mDNS address + + AddressV6 = 'ff02::fb' + + ## + # Default mDNS addresses + + Addresses = [ + [AddressV4, Port], + [AddressV6, Port], + ] + + ## + # Creates a new one-shot Multicast DNS (mDNS) resolver. + # + # +config_info+ can be: + # + # nil:: + # Uses the default mDNS addresses + # + # Hash:: + # Must contain :nameserver or :nameserver_port like + # Resolv::DNS#initialize. + + def initialize(config_info=nil) + if config_info then + super({ nameserver_port: Addresses }.merge(config_info)) + else + super(nameserver_port: Addresses) + end + end + + ## + # Iterates over all IP addresses for +name+ retrieved from the mDNS + # resolver, provided name ends with "local". If the name does not end in + # "local" no records will be returned. + # + # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will + # be a Resolv::IPv4 or Resolv::IPv6 + + def each_address(name) + name = Resolv::DNS::Name.create(name) + + return unless name.to_a.last.to_s == 'local' + + super(name) + end + + def make_udp_requester # :nodoc: + nameserver_port = @config.nameserver_port + Requester::MDNSOneShot.new(*nameserver_port) + end + + end + + module LOC + + ## + # A Resolv::LOC::Size + + class Size + + Regex = /^(\d+\.*\d*)[m]$/ + + ## + # Creates a new LOC::Size from +arg+ which may be: + # + # LOC::Size:: returns +arg+. + # String:: +arg+ must match the LOC::Size::Regex constant + + def self.create(arg) + case arg + when Size + return arg + when String + scalar = '' + if Regex =~ arg + scalar = [(($1.to_f*(1e2)).to_i.to_s[0].to_i*(2**4)+(($1.to_f*(1e2)).to_i.to_s.length-1))].pack("C") + else + raise ArgumentError.new("not a properly formed Size string: " + arg) + end + return Size.new(scalar) + else + raise ArgumentError.new("cannot interpret as Size: #{arg.inspect}") + end + end + + def initialize(scalar) + @scalar = scalar + end + + ## + # The raw size + + attr_reader :scalar + + def to_s # :nodoc: + s = @scalar.unpack("H2").join.to_s + return ((s[0].to_i)*(10**(s[1].to_i-2))).to_s << "m" + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + def ==(other) # :nodoc: + return @scalar == other.scalar + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @scalar.hash + end + + end + + ## + # A Resolv::LOC::Coord + + class Coord + + Regex = /^(\d+)\s(\d+)\s(\d+\.\d+)\s([NESW])$/ + + ## + # Creates a new LOC::Coord from +arg+ which may be: + # + # LOC::Coord:: returns +arg+. + # String:: +arg+ must match the LOC::Coord::Regex constant + + def self.create(arg) + case arg + when Coord + return arg + when String + coordinates = '' + if Regex =~ arg && $1.to_f < 180 + m = $~ + hemi = (m[4][/[NE]/]) || (m[4][/[SW]/]) ? 1 : -1 + coordinates = [ ((m[1].to_i*(36e5)) + (m[2].to_i*(6e4)) + + (m[3].to_f*(1e3))) * hemi+(2**31) ].pack("N") + orientation = m[4][/[NS]/] ? 'lat' : 'lon' + else + raise ArgumentError.new("not a properly formed Coord string: " + arg) + end + return Coord.new(coordinates,orientation) + else + raise ArgumentError.new("cannot interpret as Coord: #{arg.inspect}") + end + end + + def initialize(coordinates,orientation) + unless coordinates.kind_of?(String) + raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}") + end + unless orientation.kind_of?(String) && orientation[/^lon$|^lat$/] + raise ArgumentError.new('Coord expects orientation to be a String argument of "lat" or "lon"') + end + @coordinates = coordinates + @orientation = orientation + end + + ## + # The raw coordinates + + attr_reader :coordinates + + ## The orientation of the hemisphere as 'lat' or 'lon' + + attr_reader :orientation + + def to_s # :nodoc: + c = @coordinates.unpack("N").join.to_i + val = (c - (2**31)).abs + fracsecs = (val % 1e3).to_i.to_s + val = val / 1e3 + secs = (val % 60).to_i.to_s + val = val / 60 + mins = (val % 60).to_i.to_s + degs = (val / 60).to_i.to_s + posi = (c >= 2**31) + case posi + when true + hemi = @orientation[/^lat$/] ? "N" : "E" + else + hemi = @orientation[/^lon$/] ? "W" : "S" + end + return degs << " " << mins << " " << secs << "." << fracsecs << " " << hemi + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + def ==(other) # :nodoc: + return @coordinates == other.coordinates + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @coordinates.hash + end + + end + + ## + # A Resolv::LOC::Alt + + class Alt + + Regex = /^([+-]*\d+\.*\d*)[m]$/ + + ## + # Creates a new LOC::Alt from +arg+ which may be: + # + # LOC::Alt:: returns +arg+. + # String:: +arg+ must match the LOC::Alt::Regex constant + + def self.create(arg) + case arg + when Alt + return arg + when String + altitude = '' + if Regex =~ arg + altitude = [($1.to_f*(1e2))+(1e7)].pack("N") + else + raise ArgumentError.new("not a properly formed Alt string: " + arg) + end + return Alt.new(altitude) + else + raise ArgumentError.new("cannot interpret as Alt: #{arg.inspect}") + end + end + + def initialize(altitude) + @altitude = altitude + end + + ## + # The raw altitude + + attr_reader :altitude + + def to_s # :nodoc: + a = @altitude.unpack("N").join.to_i + return ((a.to_f/1e2)-1e5).to_s + "m" + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + def ==(other) # :nodoc: + return @altitude == other.altitude + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @altitude.hash + end + + end + + end + + ## + # Default resolver to use for Resolv class methods. + + DefaultResolver = self.new + + ## + # Replaces the resolvers in the default resolver with +new_resolvers+. This + # allows resolvers to be changed for resolv-replace. + + def DefaultResolver.replace_resolvers new_resolvers + @resolvers = new_resolvers + end + + ## + # Address Regexp to use for matching IP addresses. + + AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/ + +end + diff --git a/gxg/standard/rinda/rinda.rb b/gxg/standard/rinda/rinda.rb new file mode 100644 index 0000000..e762286 --- /dev/null +++ b/gxg/standard/rinda/rinda.rb @@ -0,0 +1,327 @@ +# frozen_string_literal: false +require 'drb/drb' + +## +# A module to implement the Linda distributed computing paradigm in Ruby. +# +# Rinda is part of DRb (dRuby). +# +# == Example(s) +# +# See the sample/drb/ directory in the Ruby distribution, from 1.8.2 onwards. +# +#-- +# TODO +# == Introduction to Linda/rinda? +# +# == Why is this library separate from DRb? + +module Rinda + + ## + # Rinda error base class + + class RindaError < RuntimeError; end + + ## + # Raised when a hash-based tuple has an invalid key. + + class InvalidHashTupleKey < RindaError; end + + ## + # Raised when trying to use a canceled tuple. + + class RequestCanceledError < ThreadError; end + + ## + # Raised when trying to use an expired tuple. + + class RequestExpiredError < ThreadError; end + + ## + # A tuple is the elementary object in Rinda programming. + # Tuples may be matched against templates if the tuple and + # the template are the same size. + + class Tuple + + ## + # Creates a new Tuple from +ary_or_hash+ which must be an Array or Hash. + + def initialize(ary_or_hash) + if hash?(ary_or_hash) + init_with_hash(ary_or_hash) + else + init_with_ary(ary_or_hash) + end + end + + ## + # The number of elements in the tuple. + + def size + @tuple.size + end + + ## + # Accessor method for elements of the tuple. + + def [](k) + @tuple[k] + end + + ## + # Fetches item +k+ from the tuple. + + def fetch(k) + @tuple.fetch(k) + end + + ## + # Iterate through the tuple, yielding the index or key, and the + # value, thus ensuring arrays are iterated similarly to hashes. + + def each # FIXME + if Hash === @tuple + @tuple.each { |k, v| yield(k, v) } + else + @tuple.each_with_index { |v, k| yield(k, v) } + end + end + + ## + # Return the tuple itself + def value + @tuple + end + + private + + def hash?(ary_or_hash) + ary_or_hash.respond_to?(:keys) + end + + ## + # Munges +ary+ into a valid Tuple. + + def init_with_ary(ary) + @tuple = Array.new(ary.size) + @tuple.size.times do |i| + @tuple[i] = ary[i] + end + end + + ## + # Ensures +hash+ is a valid Tuple. + + def init_with_hash(hash) + @tuple = Hash.new + hash.each do |k, v| + raise InvalidHashTupleKey unless String === k + @tuple[k] = v + end + end + + end + + ## + # Templates are used to match tuples in Rinda. + + class Template < Tuple + + ## + # Matches this template against +tuple+. The +tuple+ must be the same + # size as the template. An element with a +nil+ value in a template acts + # as a wildcard, matching any value in the corresponding position in the + # tuple. Elements of the template match the +tuple+ if the are #== or + # #===. + # + # Template.new([:foo, 5]).match Tuple.new([:foo, 5]) # => true + # Template.new([:foo, nil]).match Tuple.new([:foo, 5]) # => true + # Template.new([String]).match Tuple.new(['hello']) # => true + # + # Template.new([:foo]).match Tuple.new([:foo, 5]) # => false + # Template.new([:foo, 6]).match Tuple.new([:foo, 5]) # => false + # Template.new([:foo, nil]).match Tuple.new([:foo]) # => false + # Template.new([:foo, 6]).match Tuple.new([:foo]) # => false + + def match(tuple) + return false unless tuple.respond_to?(:size) + return false unless tuple.respond_to?(:fetch) + return false unless self.size == tuple.size + each do |k, v| + begin + it = tuple.fetch(k) + rescue + return false + end + next if v.nil? + next if v == it + next if v === it + return false + end + return true + end + + ## + # Alias for #match. + + def ===(tuple) + match(tuple) + end + + end + + ## + # Documentation? + + class DRbObjectTemplate + + ## + # Creates a new DRbObjectTemplate that will match against +uri+ and +ref+. + + def initialize(uri=nil, ref=nil) + @drb_uri = uri + @drb_ref = ref + end + + ## + # This DRbObjectTemplate matches +ro+ if the remote object's drburi and + # drbref are the same. +nil+ is used as a wildcard. + + def ===(ro) + return true if super(ro) + unless @drb_uri.nil? + return false unless (@drb_uri === ro.__drburi rescue false) + end + unless @drb_ref.nil? + return false unless (@drb_ref === ro.__drbref rescue false) + end + true + end + + end + + ## + # TupleSpaceProxy allows a remote Tuplespace to appear as local. + + class TupleSpaceProxy + ## + # A Port ensures that a moved tuple arrives properly at its destination + # and does not get lost. + # + # See https://bugs.ruby-lang.org/issues/8125 + + class Port # :nodoc: + attr_reader :value + + def self.deliver + port = new + + begin + yield(port) + ensure + port.close + end + + port.value + end + + def initialize + @open = true + @value = nil + end + + ## + # Don't let the DRb thread push to it when remote sends tuple + + def close + @open = false + end + + ## + # Stores +value+ and ensure it does not get marshaled multiple times. + + def push value + raise 'port closed' unless @open + + @value = value + + nil # avoid Marshal + end + end + + ## + # Creates a new TupleSpaceProxy to wrap +ts+. + + def initialize(ts) + @ts = ts + end + + ## + # Adds +tuple+ to the proxied TupleSpace. See TupleSpace#write. + + def write(tuple, sec=nil) + @ts.write(tuple, sec) + end + + ## + # Takes +tuple+ from the proxied TupleSpace. See TupleSpace#take. + + def take(tuple, sec=nil, &block) + Port.deliver do |port| + @ts.move(DRbObject.new(port), tuple, sec, &block) + end + end + + ## + # Reads +tuple+ from the proxied TupleSpace. See TupleSpace#read. + + def read(tuple, sec=nil, &block) + @ts.read(tuple, sec, &block) + end + + ## + # Reads all tuples matching +tuple+ from the proxied TupleSpace. See + # TupleSpace#read_all. + + def read_all(tuple) + @ts.read_all(tuple) + end + + ## + # Registers for notifications of event +ev+ on the proxied TupleSpace. + # See TupleSpace#notify + + def notify(ev, tuple, sec=nil) + @ts.notify(ev, tuple, sec) + end + + end + + ## + # An SimpleRenewer allows a TupleSpace to check if a TupleEntry is still + # alive. + + class SimpleRenewer + + include DRbUndumped + + ## + # Creates a new SimpleRenewer that keeps an object alive for another +sec+ + # seconds. + + def initialize(sec=180) + @sec = sec + end + + ## + # Called by the TupleSpace to check if the object is still alive. + + def renew + @sec + end + end + +end + diff --git a/gxg/standard/rinda/ring.rb b/gxg/standard/rinda/ring.rb new file mode 100644 index 0000000..34dc245 --- /dev/null +++ b/gxg/standard/rinda/ring.rb @@ -0,0 +1,484 @@ +# frozen_string_literal: false +# +# Note: Rinda::Ring API is unstable. +# +require 'drb/drb' +require 'rinda/rinda' +require 'ipaddr' + +module Rinda + + ## + # The default port Ring discovery will use. + + Ring_PORT = 7647 + + ## + # A RingServer allows a Rinda::TupleSpace to be located via UDP broadcasts. + # Default service location uses the following steps: + # + # 1. A RingServer begins listening on the network broadcast UDP address. + # 2. A RingFinger sends a UDP packet containing the DRb URI where it will + # listen for a reply. + # 3. The RingServer receives the UDP packet and connects back to the + # provided DRb URI with the DRb service. + # + # A RingServer requires a TupleSpace: + # + # ts = Rinda::TupleSpace.new + # rs = Rinda::RingServer.new + # + # RingServer can also listen on multicast addresses for announcements. This + # allows multiple RingServers to run on the same host. To use network + # broadcast and multicast: + # + # ts = Rinda::TupleSpace.new + # rs = Rinda::RingServer.new ts, %w[Socket::INADDR_ANY, 239.0.0.1 ff02::1] + + class RingServer + + include DRbUndumped + + ## + # Special renewer for the RingServer to allow shutdown + + class Renewer # :nodoc: + include DRbUndumped + + ## + # Set to false to shutdown future requests using this Renewer + + attr_writer :renew + + def initialize # :nodoc: + @renew = true + end + + def renew # :nodoc: + @renew ? 1 : true + end + end + + ## + # Advertises +ts+ on the given +addresses+ at +port+. + # + # If +addresses+ is omitted only the UDP broadcast address is used. + # + # +addresses+ can contain multiple addresses. If a multicast address is + # given in +addresses+ then the RingServer will listen for multicast + # queries. + # + # If you use IPv4 multicast you may need to set an address of the inbound + # interface which joins a multicast group. + # + # ts = Rinda::TupleSpace.new + # rs = Rinda::RingServer.new(ts, [['239.0.0.1', '9.5.1.1']]) + # + # You can set addresses as an Array Object. The first element of the + # Array is a multicast address and the second is an inbound interface + # address. If the second is omitted then '0.0.0.0' is used. + # + # If you use IPv6 multicast you may need to set both the local interface + # address and the inbound interface index: + # + # rs = Rinda::RingServer.new(ts, [['ff02::1', '::1', 1]]) + # + # The first element is a multicast address and the second is an inbound + # interface address. The third is an inbound interface index. + # + # At this time there is no easy way to get an interface index by name. + # + # If the second is omitted then '::1' is used. + # If the third is omitted then 0 (default interface) is used. + + def initialize(ts, addresses=[Socket::INADDR_ANY], port=Ring_PORT) + @port = port + + if Integer === addresses then + addresses, @port = [Socket::INADDR_ANY], addresses + end + + @renewer = Renewer.new + + @ts = ts + @sockets = [] + addresses.each do |address| + if Array === address + make_socket(*address) + else + make_socket(address) + end + end + + @w_services = write_services + @r_service = reply_service + end + + ## + # Creates a socket at +address+ + # + # If +address+ is multicast address then +interface_address+ and + # +multicast_interface+ can be set as optional. + # + # A created socket is bound to +interface_address+. If you use IPv4 + # multicast then the interface of +interface_address+ is used as the + # inbound interface. If +interface_address+ is omitted or nil then + # '0.0.0.0' or '::1' is used. + # + # If you use IPv6 multicast then +multicast_interface+ is used as the + # inbound interface. +multicast_interface+ is a network interface index. + # If +multicast_interface+ is omitted then 0 (default interface) is used. + + def make_socket(address, interface_address=nil, multicast_interface=0) + addrinfo = Addrinfo.udp(address, @port) + + socket = Socket.new(addrinfo.pfamily, addrinfo.socktype, + addrinfo.protocol) + + if addrinfo.ipv4_multicast? or addrinfo.ipv6_multicast? then + if Socket.const_defined?(:SO_REUSEPORT) then + socket.setsockopt(:SOCKET, :SO_REUSEPORT, true) + else + socket.setsockopt(:SOCKET, :SO_REUSEADDR, true) + end + + if addrinfo.ipv4_multicast? then + interface_address = '0.0.0.0' if interface_address.nil? + socket.bind(Addrinfo.udp(interface_address, @port)) + + mreq = IPAddr.new(addrinfo.ip_address).hton + + IPAddr.new(interface_address).hton + + socket.setsockopt(:IPPROTO_IP, :IP_ADD_MEMBERSHIP, mreq) + else + interface_address = '::1' if interface_address.nil? + socket.bind(Addrinfo.udp(interface_address, @port)) + + mreq = IPAddr.new(addrinfo.ip_address).hton + + [multicast_interface].pack('I') + + socket.setsockopt(:IPPROTO_IPV6, :IPV6_JOIN_GROUP, mreq) + end + else + socket.bind(addrinfo) + end + + socket + rescue + socket = socket.close if socket + raise + ensure + @sockets << socket if socket + end + + ## + # Creates threads that pick up UDP packets and passes them to do_write for + # decoding. + + def write_services + @sockets.map do |s| + Thread.new(s) do |socket| + loop do + msg = socket.recv(1024) + do_write(msg) + end + end + end + end + + ## + # Extracts the response URI from +msg+ and adds it to TupleSpace where it + # will be picked up by +reply_service+ for notification. + + def do_write(msg) + Thread.new do + begin + tuple, sec = Marshal.load(msg) + @ts.write(tuple, sec) + rescue + end + end + end + + ## + # Creates a thread that notifies waiting clients from the TupleSpace. + + def reply_service + Thread.new do + loop do + do_reply + end + end + end + + ## + # Pulls lookup tuples out of the TupleSpace and sends their DRb object the + # address of the local TupleSpace. + + def do_reply + tuple = @ts.take([:lookup_ring, nil], @renewer) + Thread.new { tuple[1].call(@ts) rescue nil} + rescue + end + + ## + # Shuts down the RingServer + + def shutdown + @renewer.renew = false + + @w_services.each do |thread| + thread.kill + thread.join + end + + @sockets.each do |socket| + socket.close + end + + @r_service.kill + @r_service.join + end + + end + + ## + # RingFinger is used by RingServer clients to discover the RingServer's + # TupleSpace. Typically, all a client needs to do is call + # RingFinger.primary to retrieve the remote TupleSpace, which it can then + # begin using. + # + # To find the first available remote TupleSpace: + # + # Rinda::RingFinger.primary + # + # To create a RingFinger that broadcasts to a custom list: + # + # rf = Rinda::RingFinger.new ['localhost', '192.0.2.1'] + # rf.primary + # + # Rinda::RingFinger also understands multicast addresses and sets them up + # properly. This allows you to run multiple RingServers on the same host: + # + # rf = Rinda::RingFinger.new ['239.0.0.1'] + # rf.primary + # + # You can set the hop count (or TTL) for multicast searches using + # #multicast_hops. + # + # If you use IPv6 multicast you may need to set both an address and the + # outbound interface index: + # + # rf = Rinda::RingFinger.new ['ff02::1'] + # rf.multicast_interface = 1 + # rf.primary + # + # At this time there is no easy way to get an interface index by name. + + class RingFinger + + @@broadcast_list = ['', 'localhost'] + + @@finger = nil + + ## + # Creates a singleton RingFinger and looks for a RingServer. Returns the + # created RingFinger. + + def self.finger + unless @@finger + @@finger = self.new + @@finger.lookup_ring_any + end + @@finger + end + + ## + # Returns the first advertised TupleSpace. + + def self.primary + finger.primary + end + + ## + # Contains all discovered TupleSpaces except for the primary. + + def self.to_a + finger.to_a + end + + ## + # The list of addresses where RingFinger will send query packets. + + attr_accessor :broadcast_list + + ## + # Maximum number of hops for sent multicast packets (if using a multicast + # address in the broadcast list). The default is 1 (same as UDP + # broadcast). + + attr_accessor :multicast_hops + + ## + # The interface index to send IPv6 multicast packets from. + + attr_accessor :multicast_interface + + ## + # The port that RingFinger will send query packets to. + + attr_accessor :port + + ## + # Contain the first advertised TupleSpace after lookup_ring_any is called. + + attr_accessor :primary + + ## + # Creates a new RingFinger that will look for RingServers at +port+ on + # the addresses in +broadcast_list+. + # + # If +broadcast_list+ contains a multicast address then multicast queries + # will be made using the given multicast_hops and multicast_interface. + + def initialize(broadcast_list=@@broadcast_list, port=Ring_PORT) + @broadcast_list = broadcast_list || ['localhost'] + @port = port + @primary = nil + @rings = [] + + @multicast_hops = 1 + @multicast_interface = 0 + end + + ## + # Contains all discovered TupleSpaces except for the primary. + + def to_a + @rings + end + + ## + # Iterates over all discovered TupleSpaces starting with the primary. + + def each + lookup_ring_any unless @primary + return unless @primary + yield(@primary) + @rings.each { |x| yield(x) } + end + + ## + # Looks up RingServers waiting +timeout+ seconds. RingServers will be + # given +block+ as a callback, which will be called with the remote + # TupleSpace. + + def lookup_ring(timeout=5, &block) + return lookup_ring_any(timeout) unless block_given? + + msg = Marshal.dump([[:lookup_ring, DRbObject.new(block)], timeout]) + @broadcast_list.each do |it| + send_message(it, msg) + end + sleep(timeout) + end + + ## + # Returns the first found remote TupleSpace. Any further recovered + # TupleSpaces can be found by calling +to_a+. + + def lookup_ring_any(timeout=5) + queue = Thread::Queue.new + + Thread.new do + self.lookup_ring(timeout) do |ts| + queue.push(ts) + end + queue.push(nil) + end + + @primary = queue.pop + raise('RingNotFound') if @primary.nil? + + Thread.new do + while it = queue.pop + @rings.push(it) + end + end + + @primary + end + + ## + # Creates a socket for +address+ with the appropriate multicast options + # for multicast addresses. + + def make_socket(address) # :nodoc: + addrinfo = Addrinfo.udp(address, @port) + + soc = Socket.new(addrinfo.pfamily, addrinfo.socktype, addrinfo.protocol) + begin + if addrinfo.ipv4_multicast? then + soc.setsockopt(Socket::Option.ipv4_multicast_loop(1)) + soc.setsockopt(Socket::Option.ipv4_multicast_ttl(@multicast_hops)) + elsif addrinfo.ipv6_multicast? then + soc.setsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_LOOP, true) + soc.setsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_HOPS, + [@multicast_hops].pack('I')) + soc.setsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_IF, + [@multicast_interface].pack('I')) + else + soc.setsockopt(:SOL_SOCKET, :SO_BROADCAST, true) + end + + soc.connect(addrinfo) + rescue Exception + soc.close + raise + end + + soc + end + + def send_message(address, message) # :nodoc: + soc = make_socket(address) + + soc.send(message, 0) + rescue + nil + ensure + soc.close if soc + end + + end + + ## + # RingProvider uses a RingServer advertised TupleSpace as a name service. + # TupleSpace clients can register themselves with the remote TupleSpace and + # look up other provided services via the remote TupleSpace. + # + # Services are registered with a tuple of the format [:name, klass, + # DRbObject, description]. + + class RingProvider + + ## + # Creates a RingProvider that will provide a +klass+ service running on + # +front+, with a +description+. +renewer+ is optional. + + def initialize(klass, front, desc, renewer = nil) + @tuple = [:name, klass, front, desc] + @renewer = renewer || Rinda::SimpleRenewer.new + end + + ## + # Advertises this service on the primary remote TupleSpace. + + def provide + ts = Rinda::RingFinger.primary + ts.write(@tuple, @renewer) + end + + end + +end diff --git a/gxg/standard/rinda/tuplespace.rb b/gxg/standard/rinda/tuplespace.rb new file mode 100644 index 0000000..47860bc --- /dev/null +++ b/gxg/standard/rinda/tuplespace.rb @@ -0,0 +1,641 @@ +# frozen_string_literal: false +require 'monitor' +require 'drb/drb' +require 'rinda/rinda' +require 'forwardable' + +module Rinda + + ## + # A TupleEntry is a Tuple (i.e. a possible entry in some Tuplespace) + # together with expiry and cancellation data. + + class TupleEntry + + include DRbUndumped + + attr_accessor :expires + + ## + # Creates a TupleEntry based on +ary+ with an optional renewer or expiry + # time +sec+. + # + # A renewer must implement the +renew+ method which returns a Numeric, + # nil, or true to indicate when the tuple has expired. + + def initialize(ary, sec=nil) + @cancel = false + @expires = nil + @tuple = make_tuple(ary) + @renewer = nil + renew(sec) + end + + ## + # Marks this TupleEntry as canceled. + + def cancel + @cancel = true + end + + ## + # A TupleEntry is dead when it is canceled or expired. + + def alive? + !canceled? && !expired? + end + + ## + # Return the object which makes up the tuple itself: the Array + # or Hash. + + def value; @tuple.value; end + + ## + # Returns the canceled status. + + def canceled?; @cancel; end + + ## + # Has this tuple expired? (true/false). + # + # A tuple has expired when its expiry timer based on the +sec+ argument to + # #initialize runs out. + + def expired? + return true unless @expires + return false if @expires > Time.now + return true if @renewer.nil? + renew(@renewer) + return true unless @expires + return @expires < Time.now + end + + ## + # Reset the expiry time according to +sec_or_renewer+. + # + # +nil+:: it is set to expire in the far future. + # +true+:: it has expired. + # Numeric:: it will expire in that many seconds. + # + # Otherwise the argument refers to some kind of renewer object + # which will reset its expiry time. + + def renew(sec_or_renewer) + sec, @renewer = get_renewer(sec_or_renewer) + @expires = make_expires(sec) + end + + ## + # Returns an expiry Time based on +sec+ which can be one of: + # Numeric:: +sec+ seconds into the future + # +true+:: the expiry time is the start of 1970 (i.e. expired) + # +nil+:: it is Tue Jan 19 03:14:07 GMT Standard Time 2038 (i.e. when + # UNIX clocks will die) + + def make_expires(sec=nil) + case sec + when Numeric + Time.now + sec + when true + Time.at(1) + when nil + Time.at(2**31-1) + end + end + + ## + # Retrieves +key+ from the tuple. + + def [](key) + @tuple[key] + end + + ## + # Fetches +key+ from the tuple. + + def fetch(key) + @tuple.fetch(key) + end + + ## + # The size of the tuple. + + def size + @tuple.size + end + + ## + # Creates a Rinda::Tuple for +ary+. + + def make_tuple(ary) + Rinda::Tuple.new(ary) + end + + private + + ## + # Returns a valid argument to make_expires and the renewer or nil. + # + # Given +true+, +nil+, or Numeric, returns that value and +nil+ (no actual + # renewer). Otherwise it returns an expiry value from calling +it.renew+ + # and the renewer. + + def get_renewer(it) + case it + when Numeric, true, nil + return it, nil + else + begin + return it.renew, it + rescue Exception + return it, nil + end + end + end + + end + + ## + # A TemplateEntry is a Template together with expiry and cancellation data. + + class TemplateEntry < TupleEntry + ## + # Matches this TemplateEntry against +tuple+. See Template#match for + # details on how a Template matches a Tuple. + + def match(tuple) + @tuple.match(tuple) + end + + alias === match + + def make_tuple(ary) # :nodoc: + Rinda::Template.new(ary) + end + + end + + ## + # Documentation? + + class WaitTemplateEntry < TemplateEntry + + attr_reader :found + + def initialize(place, ary, expires=nil) + super(ary, expires) + @place = place + @cond = place.new_cond + @found = nil + end + + def cancel + super + signal + end + + def wait + @cond.wait + end + + def read(tuple) + @found = tuple + signal + end + + def signal + @place.synchronize do + @cond.signal + end + end + + end + + ## + # A NotifyTemplateEntry is returned by TupleSpace#notify and is notified of + # TupleSpace changes. You may receive either your subscribed event or the + # 'close' event when iterating over notifications. + # + # See TupleSpace#notify_event for valid notification types. + # + # == Example + # + # ts = Rinda::TupleSpace.new + # observer = ts.notify 'write', [nil] + # + # Thread.start do + # observer.each { |t| p t } + # end + # + # 3.times { |i| ts.write [i] } + # + # Outputs: + # + # ['write', [0]] + # ['write', [1]] + # ['write', [2]] + + class NotifyTemplateEntry < TemplateEntry + + ## + # Creates a new NotifyTemplateEntry that watches +place+ for +event+s that + # match +tuple+. + + def initialize(place, event, tuple, expires=nil) + ary = [event, Rinda::Template.new(tuple)] + super(ary, expires) + @queue = Thread::Queue.new + @done = false + end + + ## + # Called by TupleSpace to notify this NotifyTemplateEntry of a new event. + + def notify(ev) + @queue.push(ev) + end + + ## + # Retrieves a notification. Raises RequestExpiredError when this + # NotifyTemplateEntry expires. + + def pop + raise RequestExpiredError if @done + it = @queue.pop + @done = true if it[0] == 'close' + return it + end + + ## + # Yields event/tuple pairs until this NotifyTemplateEntry expires. + + def each # :yields: event, tuple + while !@done + it = pop + yield(it) + end + rescue + ensure + cancel + end + + end + + ## + # TupleBag is an unordered collection of tuples. It is the basis + # of Tuplespace. + + class TupleBag + class TupleBin + extend Forwardable + def_delegators '@bin', :find_all, :delete_if, :each, :empty? + + def initialize + @bin = [] + end + + def add(tuple) + @bin.push(tuple) + end + + def delete(tuple) + idx = @bin.rindex(tuple) + @bin.delete_at(idx) if idx + end + + def find + @bin.reverse_each do |x| + return x if yield(x) + end + nil + end + end + + def initialize # :nodoc: + @hash = {} + @enum = enum_for(:each_entry) + end + + ## + # +true+ if the TupleBag to see if it has any expired entries. + + def has_expires? + @enum.find do |tuple| + tuple.expires + end + end + + ## + # Add +tuple+ to the TupleBag. + + def push(tuple) + key = bin_key(tuple) + @hash[key] ||= TupleBin.new + @hash[key].add(tuple) + end + + ## + # Removes +tuple+ from the TupleBag. + + def delete(tuple) + key = bin_key(tuple) + bin = @hash[key] + return nil unless bin + bin.delete(tuple) + @hash.delete(key) if bin.empty? + tuple + end + + ## + # Finds all live tuples that match +template+. + def find_all(template) + bin_for_find(template).find_all do |tuple| + tuple.alive? && template.match(tuple) + end + end + + ## + # Finds a live tuple that matches +template+. + + def find(template) + bin_for_find(template).find do |tuple| + tuple.alive? && template.match(tuple) + end + end + + ## + # Finds all tuples in the TupleBag which when treated as templates, match + # +tuple+ and are alive. + + def find_all_template(tuple) + @enum.find_all do |template| + template.alive? && template.match(tuple) + end + end + + ## + # Delete tuples which dead tuples from the TupleBag, returning the deleted + # tuples. + + def delete_unless_alive + deleted = [] + @hash.each do |key, bin| + bin.delete_if do |tuple| + if tuple.alive? + false + else + deleted.push(tuple) + true + end + end + end + deleted + end + + private + def each_entry(&blk) + @hash.each do |k, v| + v.each(&blk) + end + end + + def bin_key(tuple) + head = tuple[0] + if head.class == Symbol + return head + else + false + end + end + + def bin_for_find(template) + key = bin_key(template) + key ? @hash.fetch(key, []) : @enum + end + end + + ## + # The Tuplespace manages access to the tuples it contains, + # ensuring mutual exclusion requirements are met. + # + # The +sec+ option for the write, take, move, read and notify methods may + # either be a number of seconds or a Renewer object. + + class TupleSpace + + include DRbUndumped + include MonitorMixin + + ## + # Creates a new TupleSpace. +period+ is used to control how often to look + # for dead tuples after modifications to the TupleSpace. + # + # If no dead tuples are found +period+ seconds after the last + # modification, the TupleSpace will stop looking for dead tuples. + + def initialize(period=60) + super() + @bag = TupleBag.new + @read_waiter = TupleBag.new + @take_waiter = TupleBag.new + @notify_waiter = TupleBag.new + @period = period + @keeper = nil + end + + ## + # Adds +tuple+ + + def write(tuple, sec=nil) + entry = create_entry(tuple, sec) + synchronize do + if entry.expired? + @read_waiter.find_all_template(entry).each do |template| + template.read(tuple) + end + notify_event('write', entry.value) + notify_event('delete', entry.value) + else + @bag.push(entry) + start_keeper if entry.expires + @read_waiter.find_all_template(entry).each do |template| + template.read(tuple) + end + @take_waiter.find_all_template(entry).each do |template| + template.signal + end + notify_event('write', entry.value) + end + end + entry + end + + ## + # Removes +tuple+ + + def take(tuple, sec=nil, &block) + move(nil, tuple, sec, &block) + end + + ## + # Moves +tuple+ to +port+. + + def move(port, tuple, sec=nil) + template = WaitTemplateEntry.new(self, tuple, sec) + yield(template) if block_given? + synchronize do + entry = @bag.find(template) + if entry + port.push(entry.value) if port + @bag.delete(entry) + notify_event('take', entry.value) + return port ? nil : entry.value + end + raise RequestExpiredError if template.expired? + + begin + @take_waiter.push(template) + start_keeper if template.expires + while true + raise RequestCanceledError if template.canceled? + raise RequestExpiredError if template.expired? + entry = @bag.find(template) + if entry + port.push(entry.value) if port + @bag.delete(entry) + notify_event('take', entry.value) + return port ? nil : entry.value + end + template.wait + end + ensure + @take_waiter.delete(template) + end + end + end + + ## + # Reads +tuple+, but does not remove it. + + def read(tuple, sec=nil) + template = WaitTemplateEntry.new(self, tuple, sec) + yield(template) if block_given? + synchronize do + entry = @bag.find(template) + return entry.value if entry + raise RequestExpiredError if template.expired? + + begin + @read_waiter.push(template) + start_keeper if template.expires + template.wait + raise RequestCanceledError if template.canceled? + raise RequestExpiredError if template.expired? + return template.found + ensure + @read_waiter.delete(template) + end + end + end + + ## + # Returns all tuples matching +tuple+. Does not remove the found tuples. + + def read_all(tuple) + template = WaitTemplateEntry.new(self, tuple, nil) + synchronize do + entry = @bag.find_all(template) + entry.collect do |e| + e.value + end + end + end + + ## + # Registers for notifications of +event+. Returns a NotifyTemplateEntry. + # See NotifyTemplateEntry for examples of how to listen for notifications. + # + # +event+ can be: + # 'write':: A tuple was added + # 'take':: A tuple was taken or moved + # 'delete':: A tuple was lost after being overwritten or expiring + # + # The TupleSpace will also notify you of the 'close' event when the + # NotifyTemplateEntry has expired. + + def notify(event, tuple, sec=nil) + template = NotifyTemplateEntry.new(self, event, tuple, sec) + synchronize do + @notify_waiter.push(template) + end + template + end + + private + + def create_entry(tuple, sec) + TupleEntry.new(tuple, sec) + end + + ## + # Removes dead tuples. + + def keep_clean + synchronize do + @read_waiter.delete_unless_alive.each do |e| + e.signal + end + @take_waiter.delete_unless_alive.each do |e| + e.signal + end + @notify_waiter.delete_unless_alive.each do |e| + e.notify(['close']) + end + @bag.delete_unless_alive.each do |e| + notify_event('delete', e.value) + end + end + end + + ## + # Notifies all registered listeners for +event+ of a status change of + # +tuple+. + + def notify_event(event, tuple) + ev = [event, tuple] + @notify_waiter.find_all_template(ev).each do |template| + template.notify(ev) + end + end + + ## + # Creates a thread that scans the tuplespace for expired tuples. + + def start_keeper + return if @keeper && @keeper.alive? + @keeper = Thread.new do + while true + sleep(@period) + synchronize do + break unless need_keeper? + keep_clean + end + end + end + end + + ## + # Checks the tuplespace to see if it needs cleaning. + + def need_keeper? + return true if @bag.has_expires? + return true if @read_waiter.has_expires? + return true if @take_waiter.has_expires? + return true if @notify_waiter.has_expires? + end + + end + +end + diff --git a/gxg/standard/scanf.rb b/gxg/standard/scanf.rb new file mode 100644 index 0000000..8379319 --- /dev/null +++ b/gxg/standard/scanf.rb @@ -0,0 +1,776 @@ +# frozen_string_literal: false +# scanf for Ruby +# +#-- +# $Release Version: 1.1.2 $ +# $Revision: 60261 $ +# $Id: scanf.rb 60261 2017-10-21 12:34:13Z nobu $ +# $Author: nobu $ +#++ +# +# == Description +# +# scanf is an implementation of the C function scanf(3), modified as necessary +# for Ruby compatibility. +# +# the methods provided are String#scanf, IO#scanf, and +# Kernel#scanf. Kernel#scanf is a wrapper around STDIN.scanf. IO#scanf +# can be used on any IO stream, including file handles and sockets. +# scanf can be called either with or without a block. +# +# Scanf scans an input string or stream according to a format, as +# described below in Conversions, and returns an array of matches between +# the format and the input. The format is defined in a string, and is +# similar (though not identical) to the formats used in Kernel#printf and +# Kernel#sprintf. +# +# The format may contain conversion specifiers, which tell scanf +# what form (type) each particular matched substring should be converted +# to (e.g., decimal integer, floating point number, literal string, +# etc.) The matches and conversions take place from left to right, and +# the conversions themselves are returned as an array. +# +# The format string may also contain characters other than those in the +# conversion specifiers. White space (blanks, tabs, or newlines) in the +# format string matches any amount of white space, including none, in +# the input. Everything else matches only itself. +# +# Scanning stops, and scanf returns, when any input character fails to +# match the specifications in the format string, or when input is +# exhausted, or when everything in the format string has been +# matched. All matches found up to the stopping point are returned in +# the return array (or yielded to the block, if a block was given). +# +# +# == Basic usage +# +# require 'scanf' +# +# # String#scanf and IO#scanf take a single argument, the format string +# array = a_string.scanf("%d%s") +# array = an_io.scanf("%d%s") +# +# # Kernel#scanf reads from STDIN +# array = scanf("%d%s") +# +# == Block usage +# +# When called with a block, scanf keeps scanning the input, cycling back +# to the beginning of the format string, and yields a new array of +# conversions to the block every time the format string is matched +# (including partial matches, but not including complete failures). The +# actual return value of scanf when called with a block is an array +# containing the results of all the executions of the block. +# +# str = "123 abc 456 def 789 ghi" +# str.scanf("%d%s") { |num,str| [ num * 2, str.upcase ] } +# # => [[246, "ABC"], [912, "DEF"], [1578, "GHI"]] +# +# == Conversions +# +# The single argument to scanf is a format string, which generally +# includes one or more conversion specifiers. Conversion specifiers +# begin with the percent character ('%') and include information about +# what scanf should next scan for (string, decimal number, single +# character, etc.). +# +# There may be an optional maximum field width, expressed as a decimal +# integer, between the % and the conversion. If no width is given, a +# default of `infinity' is used (with the exception of the %c specifier; +# see below). Otherwise, given a field width of n for a given +# conversion, at most n characters are scanned in processing +# that conversion. Before conversion begins, most conversions skip +# white space in the input string; this white space is not counted +# against the field width. +# +# The following conversions are available. +# +# [%] +# Matches a literal `%'. That is, `%%' in the format string matches a +# single input `%' character. No conversion is done, and the resulting +# '%' is not included in the return array. +# +# [d] +# Matches an optionally signed decimal integer. +# +# [u] +# Same as d. +# +# [i] +# Matches an optionally signed integer. The integer is read in base +# 16 if it begins with `0x' or `0X', in base 8 if it begins with `0', +# and in base 10 other- wise. Only characters that correspond to the +# base are recognized. +# +# [o] +# Matches an optionally signed octal integer. +# +# [x, X] +# Matches an optionally signed hexadecimal integer, +# +# [a, e, f, g, A, E, F, G] +# Matches an optionally signed floating-point number. +# +# [s] +# Matches a sequence of non-white-space character. The input string stops at +# white space or at the maximum field width, whichever occurs first. +# +# [c] +# Matches a single character, or a sequence of n characters if a +# field width of n is specified. The usual skip of leading white +# space is suppressed. To skip white space first, use an explicit space in +# the format. +# +# [[] +# Matches a nonempty sequence of characters from the specified set +# of accepted characters. The usual skip of leading white space is +# suppressed. This bracketed sub-expression is interpreted exactly like a +# character class in a Ruby regular expression. (In fact, it is placed as-is +# in a regular expression.) The matching against the input string ends with +# the appearance of a character not in (or, with a circumflex, in) the set, +# or when the field width runs out, whichever comes first. +# +# === Assignment suppression +# +# To require that a particular match occur, but without including the result +# in the return array, place the assignment suppression flag, which is +# the star character ('*'), immediately after the leading '%' of a format +# specifier (just before the field width, if any). +# +# == scanf for Ruby compared with scanf in C +# +# scanf for Ruby is based on the C function scanf(3), but with modifications, +# dictated mainly by the underlying differences between the languages. +# +# === Unimplemented flags and specifiers +# +# * The only flag implemented in scanf for Ruby is '*' (ignore +# upcoming conversion). Many of the flags available in C versions of +# scanf(3) have to do with the type of upcoming pointer arguments, and are +# meaningless in Ruby. +# +# * The n specifier (store number of characters consumed so far in +# next pointer) is not implemented. +# +# * The p specifier (match a pointer value) is not implemented. +# +# === Altered specifiers +# +# [o, u, x, X] +# In scanf for Ruby, all of these specifiers scan for an optionally signed +# integer, rather than for an unsigned integer like their C counterparts. +# +# === Return values +# +# scanf for Ruby returns an array of successful conversions, whereas +# scanf(3) returns the number of conversions successfully +# completed. (See below for more details on scanf for Ruby's return +# values.) +# +# == Return values +# +# Without a block, scanf returns an array containing all the conversions +# it has found. If none are found, scanf will return an empty array. An +# unsuccessful match is never ignored, but rather always signals the end +# of the scanning operation. If the first unsuccessful match takes place +# after one or more successful matches have already taken place, the +# returned array will contain the results of those successful matches. +# +# With a block scanf returns a 'map'-like array of transformations from +# the block -- that is, an array reflecting what the block did with each +# yielded result from the iterative scanf operation. (See "Block +# usage", above.) +# +# == Current limitations and bugs +# +# When using IO#scanf under Windows, make sure you open your files in +# binary mode: +# +# File.open("filename", "rb") +# +# so that scanf can keep track of characters correctly. +# +# Support for character classes is reasonably complete (since it +# essentially piggy-backs on Ruby's regular expression handling of +# character classes), but users are advised that character class testing +# has not been exhaustive, and that they should exercise some caution +# in using any of the more complex and/or arcane character class +# idioms. +# +# == License and copyright +# +# Copyright:: (c) 2002-2003 David Alan Black +# License:: Distributed on the same licensing terms as Ruby itself +# +# == Warranty disclaimer +# +# This software is provided "as is" and without any express or implied +# warranties, including, without limitation, the implied warranties of +# merchantability and fitness for a particular purpose. +# +# == Credits and acknowledgements +# +# scanf was developed as the major activity of the Austin Ruby Codefest +# (Austin, Texas, August 2002). +# +# Principal author:: David Alan Black (mailto:dblack@superlink.net) +# Co-author:: Hal Fulton (mailto:hal9000@hypermetrics.com) +# Project contributors:: Nolan Darilek, Jason Johnston +# +# Thanks to Hal Fulton for hosting the Codefest. +# +# Thanks to Matz for suggestions about the class design. +# +# Thanks to Gavin Sinclair for some feedback on the documentation. +# +# The text for parts of this document, especially the Description and +# Conversions sections, above, were adapted from the Linux Programmer's +# Manual manpage for scanf(3), dated 1995-11-01. +# +# == Bugs and bug reports +# +# scanf for Ruby is based on something of an amalgam of C scanf +# implementations and documentation, rather than on a single canonical +# description. Suggestions for features and behaviors which appear in +# other scanfs, and would be meaningful in Ruby, are welcome, as are +# reports of suspicious behaviors and/or bugs. (Please see "Credits and +# acknowledgements", above, for email addresses.) + +module Scanf + # :stopdoc: + + # ==Technical notes + # + # ===Rationale behind scanf for Ruby + # + # The impetus for a scanf implementation in Ruby comes chiefly from the fact + # that existing pattern matching operations, such as Regexp#match and + # String#scan, return all results as strings, which have to be converted to + # integers or floats explicitly in cases where what's ultimately wanted are + # integer or float values. + # + # ===Design of scanf for Ruby + # + # scanf for Ruby is essentially a -to- converter. + # + # When scanf is called, a FormatString object is generated from the + # format string ("%d%s...") argument. The FormatString object breaks the + # format string down into atoms ("%d", "%5f", "blah", etc.), and from + # each atom it creates a FormatSpecifier object, which it + # saves. + # + # Each FormatSpecifier has a regular expression fragment and a "handler" + # associated with it. For example, the regular expression fragment + # associated with the format "%d" is "([-+]?\d+)", and the handler + # associated with it is a wrapper around String#to_i. scanf itself calls + # FormatString#match, passing in the input string. FormatString#match + # iterates through its FormatSpecifiers; for each one, it matches the + # corresponding regular expression fragment against the string. If + # there's a match, it sends the matched string to the handler associated + # with the FormatSpecifier. + # + # Thus, to follow up the "%d" example: if "123" occurs in the input + # string when a FormatSpecifier consisting of "%d" is reached, the "123" + # will be matched against "([-+]?\d+)", and the matched string will be + # rendered into an integer by a call to to_i. + # + # The rendered match is then saved to an accumulator array, and the + # input string is reduced to the post-match substring. Thus the string + # is "eaten" from the left as the FormatSpecifiers are applied in + # sequence. (This is done to a duplicate string; the original string is + # not altered.) + # + # As soon as a regular expression fragment fails to match the string, or + # when the FormatString object runs out of FormatSpecifiers, scanning + # stops and results accumulated so far are returned in an array. + + class FormatSpecifier + + attr_reader :re_string, :matched_string, :conversion, :matched + + private + + def skip; /^\s*%\*/.match(@spec_string); end + + def extract_float(s) + return nil unless s &&! skip + if /\A(?[-+]?)0[xX](?\.\h+|\h+(?:\.\h*)?)[pP](?[-+]?\d+)/ =~ s + f1, f2 = frac.split('.') + f = f1.hex + if f2 + len = f2.length + if len > 0 + f += f2.hex / (16.0 ** len) + end + end + (sign == ?- ? -1 : 1) * Math.ldexp(f, exp.to_i) + elsif /\A([-+]?\d+)\.([eE][-+]\d+)/ =~ s + ($1 << $2).to_f + else + s.to_f + end + end + def extract_decimal(s); s.to_i if s &&! skip; end + def extract_hex(s); s.hex if s &&! skip; end + def extract_octal(s); s.oct if s &&! skip; end + def extract_integer(s); Integer(s) if s &&! skip; end + def extract_plain(s); s unless skip; end + + def nil_proc(s); nil; end + + public + + def to_s + @spec_string + end + + def count_space? + /(?:\A|\S)%\*?\d*c|%\d*\[/.match(@spec_string) + end + + def initialize(str) + @spec_string = str + h = '[A-Fa-f0-9]' + + @re_string, @handler = + case @spec_string + + # %[[:...:]] + when /%\*?(\[\[:[a-z]+:\]\])/ + [ "(#{$1}+)", :extract_plain ] + + # %5[[:...:]] + when /%\*?(\d+)(\[\[:[a-z]+:\]\])/ + [ "(#{$2}{1,#{$1}})", :extract_plain ] + + # %[...] + when /%\*?\[([^\]]*)\]/ + yes = $1 + if /^\^/.match(yes) then no = yes[1..-1] else no = '^' + yes end + [ "([#{yes}]+)(?=[#{no}]|\\z)", :extract_plain ] + + # %5[...] + when /%\*?(\d+)\[([^\]]*)\]/ + yes = $2 + w = $1 + [ "([#{yes}]{1,#{w}})", :extract_plain ] + + # %i + when /%\*?i/ + [ "([-+]?(?:(?:0[0-7]+)|(?:0[Xx]#{h}+)|(?:[1-9]\\d*)))", :extract_integer ] + + # %5i + when /%\*?(\d+)i/ + n = $1.to_i + s = "(" + if n > 1 then s += "[1-9]\\d{1,#{n-1}}|" end + if n > 1 then s += "0[0-7]{1,#{n-1}}|" end + if n > 2 then s += "[-+]0[0-7]{1,#{n-2}}|" end + if n > 2 then s += "[-+][1-9]\\d{1,#{n-2}}|" end + if n > 2 then s += "0[Xx]#{h}{1,#{n-2}}|" end + if n > 3 then s += "[-+]0[Xx]#{h}{1,#{n-3}}|" end + s += "\\d" + s += ")" + [ s, :extract_integer ] + + # %d, %u + when /%\*?[du]/ + [ '([-+]?\d+)', :extract_decimal ] + + # %5d, %5u + when /%\*?(\d+)[du]/ + n = $1.to_i + s = "(" + if n > 1 then s += "[-+]\\d{1,#{n-1}}|" end + s += "\\d{1,#{$1}})" + [ s, :extract_decimal ] + + # %x + when /%\*?[Xx]/ + [ "([-+]?(?:0[Xx])?#{h}+)", :extract_hex ] + + # %5x + when /%\*?(\d+)[Xx]/ + n = $1.to_i + s = "(" + if n > 3 then s += "[-+]0[Xx]#{h}{1,#{n-3}}|" end + if n > 2 then s += "0[Xx]#{h}{1,#{n-2}}|" end + if n > 1 then s += "[-+]#{h}{1,#{n-1}}|" end + s += "#{h}{1,#{n}}" + s += ")" + [ s, :extract_hex ] + + # %o + when /%\*?o/ + [ '([-+]?[0-7]+)', :extract_octal ] + + # %5o + when /%\*?(\d+)o/ + [ "([-+][0-7]{1,#{$1.to_i-1}}|[0-7]{1,#{$1}})", :extract_octal ] + + # %f + when /%\*?[aefgAEFG]/ + [ '([-+]?(?:0[xX](?:\.\h+|\h+(?:\.\h*)?)[pP][-+]?\d+|\d+(?![\d.])|\d*\.\d*(?:[eE][-+]?\d+)?))', :extract_float ] + + # %5f + when /%\*?(\d+)[aefgAEFG]/ + [ '(?=[-+]?(?:0[xX](?:\.\h+|\h+(?:\.\h*)?)[pP][-+]?\d+|\d+(?![\d.])|\d*\.\d*(?:[eE][-+]?\d+)?))' + + "(\\S{1,#{$1}})", :extract_float ] + + # %5s + when /%\*?(\d+)s/ + [ "(\\S{1,#{$1}})", :extract_plain ] + + # %s + when /%\*?s/ + [ '(\S+)', :extract_plain ] + + # %c + when /\s%\*?c/ + [ "\\s*(.)", :extract_plain ] + + # %c + when /%\*?c/ + [ "(.)", :extract_plain ] + + # %5c (whitespace issues are handled by the count_*_space? methods) + when /%\*?(\d+)c/ + [ "(.{1,#{$1}})", :extract_plain ] + + # %% + when /%%/ + [ '(\s*%)', :nil_proc ] + + # literal characters + else + [ "(#{Regexp.escape(@spec_string)})", :nil_proc ] + end + + @re_string = '\A' + @re_string + end + + def to_re + Regexp.new(@re_string,Regexp::MULTILINE) + end + + def match(str) + @matched = false + s = str.dup + s.sub!(/\A\s+/,'') unless count_space? + res = to_re.match(s) + if res + @conversion = send(@handler, res[1]) + @matched_string = @conversion.to_s + @matched = true + end + res + end + + def letter + @spec_string[/%\*?\d*([a-z\[])/, 1] + end + + def width + @spec_string[/%\*?(\d+)/, 1]&.to_i + end + + def mid_match? + return false unless @matched + cc_no_width = letter == '[' &&! width + c_or_cc_width = (letter == 'c' || letter == '[') && width + width_left = c_or_cc_width && (matched_string.size < width) + + return width_left || cc_no_width + end + + end + + class FormatString + + attr_reader :string_left, :last_spec_tried, + :last_match_tried, :matched_count, :space + + SPECIFIERS = 'diuXxofFeEgGscaA' + REGEX = / + # possible space, followed by... + (?:\s* + # percent sign, followed by... + % + # another percent sign, or... + (?:%| + # optional assignment suppression flag + \*? + # optional maximum field width + \d* + # named character class, ... + (?:\[\[:\w+:\]\]| + # traditional character class, or... + \[[^\]]*\]| + # specifier letter. + [#{SPECIFIERS}])))| + # or miscellaneous characters + [^%\s]+/ix + + def initialize(str) + @specs = [] + @i = 1 + s = str.to_s + return unless /\S/.match(s) + @space = true if /\s\z/.match(s) + @specs.replace s.scan(REGEX).map {|spec| FormatSpecifier.new(spec) } + end + + def to_s + @specs.join('') + end + + def prune(n=matched_count) + n.times { @specs.shift } + end + + def spec_count + @specs.size + end + + def last_spec + @i == spec_count - 1 + end + + def match(str) + accum = [] + @string_left = str + @matched_count = 0 + + @specs.each_with_index do |spec,i| + @i=i + @last_spec_tried = spec + @last_match_tried = spec.match(@string_left) + break unless @last_match_tried + @matched_count += 1 + + accum << spec.conversion + + @string_left = @last_match_tried.post_match + break if @string_left.empty? + end + return accum.compact + end + end + # :startdoc: +end + +class IO + + #:stopdoc: + # The trick here is doing a match where you grab one *line* + # of input at a time. The linebreak may or may not occur + # at the boundary where the string matches a format specifier. + # And if it does, some rule about whitespace may or may not + # be in effect... + # + # That's why this is much more elaborate than the string + # version. + # + # For each line: + # + # Match succeeds (non-emptily) + # and the last attempted spec/string sub-match succeeded: + # + # could the last spec keep matching? + # yes: save interim results and continue (next line) + # + # The last attempted spec/string did not match: + # + # are we on the next-to-last spec in the string? + # yes: + # is fmt_string.string_left all spaces? + # yes: does current spec care about input space? + # yes: fatal failure + # no: save interim results and continue + # no: continue [this state could be analyzed further] + # + #:startdoc: + + # Scans the current string until the match is exhausted, + # yielding each match as it is encountered in the string. + # A block is not necessary though, as the results will simply + # be aggregated into the final array. + # + # "123 456".block_scanf("%d") + # # => [123, 456] + # + # If a block is given, the value from that is returned from + # the yield is added to an output array. + # + # "123 456".block_scanf("%d") do |digit,| # the ',' unpacks the Array + # digit + 100 + # end + # # => [223, 556] + # + # See Scanf for details on creating a format string. + # + # You will need to require 'scanf' to use IO#scanf. + def scanf(str,&b) #:yield: current_match + return block_scanf(str,&b) if b + return [] unless str.size > 0 + + start_position = pos rescue 0 + matched_so_far = 0 + source_buffer = "" + result_buffer = [] + final_result = [] + + fstr = Scanf::FormatString.new(str) + + loop do + if eof || (tty? &&! fstr.match(source_buffer)) + final_result.concat(result_buffer) + break + end + + source_buffer << gets + + current_match = fstr.match(source_buffer) + + spec = fstr.last_spec_tried + + if spec.matched + if spec.mid_match? + result_buffer.replace(current_match) + next + end + + elsif (fstr.matched_count == fstr.spec_count - 1) + if /\A\s*\z/.match(fstr.string_left) + break if spec.count_space? + result_buffer.replace(current_match) + next + end + end + + final_result.concat(current_match) + + matched_so_far += source_buffer.size + source_buffer.replace(fstr.string_left) + matched_so_far -= source_buffer.size + break if fstr.last_spec + fstr.prune + end + + begin + seek(start_position + matched_so_far, IO::SEEK_SET) + rescue Errno::ESPIPE + end + + soak_up_spaces if fstr.last_spec && fstr.space + + return final_result + end + + private + + def soak_up_spaces + c = getc + ungetc(c) if c + until eof ||! c || /\S/.match(c.chr) + c = getc + end + ungetc(c) if (c && /\S/.match(c.chr)) + end + + def block_scanf(str) + final = [] +# Sub-ideal, since another FS gets created in scanf. +# But used here to determine the number of specifiers. + fstr = Scanf::FormatString.new(str) + last_spec = fstr.last_spec + begin + current = scanf(str) + break if current.empty? + final.push(yield(current)) + end until eof || fstr.last_spec_tried == last_spec + return final + end +end + +class String + + # :section: scanf + # + # You will need to require 'scanf' to use these methods + + # Scans the current string. If a block is given, it + # functions exactly like block_scanf. + # + # arr = "123 456".scanf("%d%d") + # # => [123, 456] + # + # require 'pp' + # + # "this 123 read that 456 other".scanf("%s%d%s") {|m| pp m} + # + # # ["this", 123, "read"] + # # ["that", 456, "other"] + # # => [["this", 123, "read"], ["that", 456, "other"]] + # + # See Scanf for details on creating a format string. + # + # You will need to require 'scanf' to use String#scanf + def scanf(fstr,&b) #:yield: current_match + if b + block_scanf(fstr,&b) + else + fs = + if fstr.is_a? Scanf::FormatString + fstr + else + Scanf::FormatString.new(fstr) + end + fs.match(self) + end + end + + # Scans the current string until the match is exhausted + # yielding each match as it is encountered in the string. + # A block is not necessary as the results will simply + # be aggregated into the final array. + # + # "123 456".block_scanf("%d") + # # => [123, 456] + # + # If a block is given, the value from that is returned from + # the yield is added to an output array. + # + # "123 456".block_scanf("%d) do |digit,| # the ',' unpacks the Array + # digit + 100 + # end + # # => [223, 556] + # + # See Scanf for details on creating a format string. + # + # You will need to require 'scanf' to use String#block_scanf + def block_scanf(fstr) #:yield: current_match + fs = Scanf::FormatString.new(fstr) + str = self.dup + final = [] + begin + current = str.scanf(fs) + final.push(yield(current)) unless current.empty? + str = fs.string_left + end until current.empty? || str.empty? + return final + end +end + +module Kernel + private + # Scans STDIN for data matching +format+. See IO#scanf for details. + # + # See Scanf for details on creating a format string. + # + # You will need to require 'scanf' to use Kernel#scanf. + def scanf(format, &b) #:doc: + STDIN.scanf(format ,&b) + end +end diff --git a/gxg/standard/shellwords.rb b/gxg/standard/shellwords.rb new file mode 100644 index 0000000..eb5fa2d --- /dev/null +++ b/gxg/standard/shellwords.rb @@ -0,0 +1,231 @@ +# frozen-string-literal: true +## +# == Manipulates strings like the UNIX Bourne shell +# +# This module manipulates strings according to the word parsing rules +# of the UNIX Bourne shell. +# +# The shellwords() function was originally a port of shellwords.pl, +# but modified to conform to the Shell & Utilities volume of the IEEE +# Std 1003.1-2008, 2016 Edition [1]. +# +# === Usage +# +# You can use Shellwords to parse a string into a Bourne shell friendly Array. +# +# require 'shellwords' +# +# argv = Shellwords.split('three blind "mice"') +# argv #=> ["three", "blind", "mice"] +# +# Once you've required Shellwords, you can use the #split alias +# String#shellsplit. +# +# argv = "see how they run".shellsplit +# argv #=> ["see", "how", "they", "run"] +# +# Be careful you don't leave a quote unmatched. +# +# argv = "they all ran after the farmer's wife".shellsplit +# #=> ArgumentError: Unmatched double quote: ... +# +# In this case, you might want to use Shellwords.escape, or its alias +# String#shellescape. +# +# This method will escape the String for you to safely use with a Bourne shell. +# +# argv = Shellwords.escape("special's.txt") +# argv #=> "special\\'s.txt" +# system("cat " + argv) +# +# Shellwords also comes with a core extension for Array, Array#shelljoin. +# +# argv = %w{ls -lta lib} +# system(argv.shelljoin) +# +# You can use this method to create an escaped string out of an array of tokens +# separated by a space. In this example we used the literal shortcut for +# Array.new. +# +# === Authors +# * Wakou Aoyama +# * Akinori MUSHA +# +# === Contact +# * Akinori MUSHA (current maintainer) +# +# === Resources +# +# 1: {IEEE Std 1003.1-2008, 2016 Edition, the Shell & Utilities volume}[http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html] + +module Shellwords + # Splits a string into an array of tokens in the same way the UNIX + # Bourne shell does. + # + # argv = Shellwords.split('here are "two words"') + # argv #=> ["here", "are", "two words"] + # + # Note, however, that this is not a command line parser. Shell + # metacharacters except for the single and double quotes and + # backslash are not treated as such. + # + # argv = Shellwords.split('ruby my_prog.rb | less') + # argv #=> ["ruby", "my_prog.rb", "|", "less"] + # + # String#shellsplit is a shortcut for this function. + # + # argv = 'here are "two words"'.shellsplit + # argv #=> ["here", "are", "two words"] + def shellsplit(line) + words = [] + field = String.new + line.scan(/\G\s*(?>([^\s\\\'\"]+)|'([^\']*)'|"((?:[^\"\\]|\\.)*)"|(\\.?)|(\S))(\s|\z)?/m) do + |word, sq, dq, esc, garbage, sep| + raise ArgumentError, "Unmatched double quote: #{line.inspect}" if garbage + # 2.2.3 Double-Quotes: + # + # The shall retain its special meaning as an + # escape character only when followed by one of the following + # characters when considered special: + # + # $ ` " \ + field << (word || sq || (dq && dq.gsub(/\\([$`"\\\n])/, '\\1')) || esc.gsub(/\\(.)/, '\\1')) + if sep + words << field + field = String.new + end + end + words + end + + alias shellwords shellsplit + + module_function :shellsplit, :shellwords + + class << self + alias split shellsplit + end + + # Escapes a string so that it can be safely used in a Bourne shell + # command line. +str+ can be a non-string object that responds to + # +to_s+. + # + # Note that a resulted string should be used unquoted and is not + # intended for use in double quotes nor in single quotes. + # + # argv = Shellwords.escape("It's better to give than to receive") + # argv #=> "It\\'s\\ better\\ to\\ give\\ than\\ to\\ receive" + # + # String#shellescape is a shorthand for this function. + # + # argv = "It's better to give than to receive".shellescape + # argv #=> "It\\'s\\ better\\ to\\ give\\ than\\ to\\ receive" + # + # # Search files in lib for method definitions + # pattern = "^[ \t]*def " + # open("| grep -Ern #{pattern.shellescape} lib") { |grep| + # grep.each_line { |line| + # file, lineno, matched_line = line.split(':', 3) + # # ... + # } + # } + # + # It is the caller's responsibility to encode the string in the right + # encoding for the shell environment where this string is used. + # + # Multibyte characters are treated as multibyte characters, not as bytes. + # + # Returns an empty quoted String if +str+ has a length of zero. + def shellescape(str) + str = str.to_s + + # An empty argument will be skipped, so return empty quotes. + return "''".dup if str.empty? + + str = str.dup + + # Treat multibyte characters as is. It is the caller's responsibility + # to encode the string in the right encoding for the shell + # environment. + str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1") + + # A LF cannot be escaped with a backslash because a backslash + LF + # combo is regarded as a line continuation and simply ignored. + str.gsub!(/\n/, "'\n'") + + return str + end + + module_function :shellescape + + class << self + alias escape shellescape + end + + # Builds a command line string from an argument list, +array+. + # + # All elements are joined into a single string with fields separated by a + # space, where each element is escaped for the Bourne shell and stringified + # using +to_s+. + # + # ary = ["There's", "a", "time", "and", "place", "for", "everything"] + # argv = Shellwords.join(ary) + # argv #=> "There\\'s a time and place for everything" + # + # Array#shelljoin is a shortcut for this function. + # + # ary = ["Don't", "rock", "the", "boat"] + # argv = ary.shelljoin + # argv #=> "Don\\'t rock the boat" + # + # You can also mix non-string objects in the elements as allowed in Array#join. + # + # output = `#{['ps', '-p', $$].shelljoin}` + # + def shelljoin(array) + array.map { |arg| shellescape(arg) }.join(' ') + end + + module_function :shelljoin + + class << self + alias join shelljoin + end +end + +class String + # call-seq: + # str.shellsplit => array + # + # Splits +str+ into an array of tokens in the same way the UNIX + # Bourne shell does. + # + # See Shellwords.shellsplit for details. + def shellsplit + Shellwords.split(self) + end + + # call-seq: + # str.shellescape => string + # + # Escapes +str+ so that it can be safely used in a Bourne shell + # command line. + # + # See Shellwords.shellescape for details. + def shellescape + Shellwords.escape(self) + end +end + +class Array + # call-seq: + # array.shelljoin => string + # + # Builds a command line string from an argument list +array+ joining + # all elements escaped for the Bourne shell and separated by a space. + # + # See Shellwords.shelljoin for details. + def shelljoin + Shellwords.join(self) + end +end diff --git a/gxg/standard/sync.rb b/gxg/standard/sync.rb new file mode 100644 index 0000000..8bbf4c1 --- /dev/null +++ b/gxg/standard/sync.rb @@ -0,0 +1,329 @@ +# frozen_string_literal: false +# +# sync.rb - 2 phase lock with counter +# $Release Version: 1.0$ +# $Revision: 56183 $ +# by Keiju ISHITSUKA(keiju@ishitsuka.com) +# +# -- +# Sync_m, Synchronizer_m +# Usage: +# obj.extend(Sync_m) +# or +# class Foo +# include Sync_m +# : +# end +# +# Sync_m#sync_mode +# Sync_m#sync_locked?, locked? +# Sync_m#sync_shared?, shared? +# Sync_m#sync_exclusive?, sync_exclusive? +# Sync_m#sync_try_lock, try_lock +# Sync_m#sync_lock, lock +# Sync_m#sync_unlock, unlock +# +# Sync, Synchronizer: +# Usage: +# sync = Sync.new +# +# Sync#mode +# Sync#locked? +# Sync#shared? +# Sync#exclusive? +# Sync#try_lock(mode) -- mode = :EX, :SH, :UN +# Sync#lock(mode) -- mode = :EX, :SH, :UN +# Sync#unlock +# Sync#synchronize(mode) {...} +# +# + +unless defined? Thread + raise "Thread not available for this ruby interpreter" +end + +## +# A module that provides a two-phase lock with a counter. + +module Sync_m + # lock mode + UN = :UN + SH = :SH + EX = :EX + + # exceptions + class Err < StandardError + def Err.Fail(*opt) + fail self, sprintf(self::Message, *opt) + end + + class UnknownLocker < Err + Message = "Thread(%s) not locked." + def UnknownLocker.Fail(th) + super(th.inspect) + end + end + + class LockModeFailer < Err + Message = "Unknown lock mode(%s)" + def LockModeFailer.Fail(mode) + if mode.id2name + mode = mode.id2name + end + super(mode) + end + end + end + + def Sync_m.define_aliases(cl) + cl.module_eval %q{ + alias locked? sync_locked? + alias shared? sync_shared? + alias exclusive? sync_exclusive? + alias lock sync_lock + alias unlock sync_unlock + alias try_lock sync_try_lock + alias synchronize sync_synchronize + } + end + + def Sync_m.append_features(cl) + super + # do nothing for Modules + # make aliases for Classes. + define_aliases(cl) unless cl.instance_of?(Module) + self + end + + def Sync_m.extend_object(obj) + super + obj.sync_extend + end + + def sync_extend + unless (defined? locked? and + defined? shared? and + defined? exclusive? and + defined? lock and + defined? unlock and + defined? try_lock and + defined? synchronize) + Sync_m.define_aliases(singleton_class) + end + sync_initialize + end + + # accessing + def sync_locked? + sync_mode != UN + end + + def sync_shared? + sync_mode == SH + end + + def sync_exclusive? + sync_mode == EX + end + + # locking methods. + def sync_try_lock(mode = EX) + return unlock if mode == UN + @sync_mutex.synchronize do + sync_try_lock_sub(mode) + end + end + + def sync_lock(m = EX) + return unlock if m == UN + Thread.handle_interrupt(StandardError => :on_blocking) do + while true + @sync_mutex.synchronize do + begin + if sync_try_lock_sub(m) + return self + else + if sync_sh_locker[Thread.current] + sync_upgrade_waiting.push [Thread.current, sync_sh_locker[Thread.current]] + sync_sh_locker.delete(Thread.current) + else + unless sync_waiting.include?(Thread.current) || sync_upgrade_waiting.reverse_each.any?{|w| w.first == Thread.current } + sync_waiting.push Thread.current + end + end + @sync_mutex.sleep + end + ensure + sync_waiting.delete(Thread.current) + end + end + end + end + self + end + + def sync_unlock(m = EX) + wakeup_threads = [] + @sync_mutex.synchronize do + if sync_mode == UN + Err::UnknownLocker.Fail(Thread.current) + end + + m = sync_mode if m == EX and sync_mode == SH + + runnable = false + case m + when UN + Err::UnknownLocker.Fail(Thread.current) + + when EX + if sync_ex_locker == Thread.current + if (self.sync_ex_count = sync_ex_count - 1) == 0 + self.sync_ex_locker = nil + if sync_sh_locker.include?(Thread.current) + self.sync_mode = SH + else + self.sync_mode = UN + end + runnable = true + end + else + Err::UnknownLocker.Fail(Thread.current) + end + + when SH + if (count = sync_sh_locker[Thread.current]).nil? + Err::UnknownLocker.Fail(Thread.current) + else + if (sync_sh_locker[Thread.current] = count - 1) == 0 + sync_sh_locker.delete(Thread.current) + if sync_sh_locker.empty? and sync_ex_count == 0 + self.sync_mode = UN + runnable = true + end + end + end + end + + if runnable + if sync_upgrade_waiting.size > 0 + th, count = sync_upgrade_waiting.shift + sync_sh_locker[th] = count + th.wakeup + wakeup_threads.push th + else + wait = sync_waiting + self.sync_waiting = [] + for th in wait + th.wakeup + wakeup_threads.push th + end + end + end + end + for th in wakeup_threads + th.run + end + self + end + + def sync_synchronize(mode = EX) + Thread.handle_interrupt(StandardError => :on_blocking) do + sync_lock(mode) + begin + yield + ensure + sync_unlock + end + end + end + + attr_accessor :sync_mode + + attr_accessor :sync_waiting + attr_accessor :sync_upgrade_waiting + attr_accessor :sync_sh_locker + attr_accessor :sync_ex_locker + attr_accessor :sync_ex_count + + def sync_inspect + sync_iv = instance_variables.select{|iv| /^@sync_/ =~ iv.id2name}.collect{|iv| iv.id2name + '=' + instance_eval(iv.id2name).inspect}.join(",") + print "<#{self.class}.extend Sync_m: #{inspect}, " + end + + private + + def sync_initialize + @sync_mode = UN + @sync_waiting = [] + @sync_upgrade_waiting = [] + @sync_sh_locker = Hash.new + @sync_ex_locker = nil + @sync_ex_count = 0 + + @sync_mutex = Thread::Mutex.new + end + + def initialize(*args) + super + sync_initialize + end + + def sync_try_lock_sub(m) + case m + when SH + case sync_mode + when UN + self.sync_mode = m + sync_sh_locker[Thread.current] = 1 + ret = true + when SH + count = 0 unless count = sync_sh_locker[Thread.current] + sync_sh_locker[Thread.current] = count + 1 + ret = true + when EX + # in EX mode, lock will upgrade to EX lock + if sync_ex_locker == Thread.current + self.sync_ex_count = sync_ex_count + 1 + ret = true + else + ret = false + end + end + when EX + if sync_mode == UN or + sync_mode == SH && sync_sh_locker.size == 1 && sync_sh_locker.include?(Thread.current) + self.sync_mode = m + self.sync_ex_locker = Thread.current + self.sync_ex_count = 1 + ret = true + elsif sync_mode == EX && sync_ex_locker == Thread.current + self.sync_ex_count = sync_ex_count + 1 + ret = true + else + ret = false + end + else + Err::LockModeFailer.Fail m + end + return ret + end +end + +## +# An alias for Sync_m from sync.rb + +Synchronizer_m = Sync_m + +## +# A class that provides two-phase lock with a counter. See Sync_m for +# details. + +class Sync + include Sync_m +end + +## +# An alias for Sync from sync.rb. See Sync_m for details. + +Synchronizer = Sync diff --git a/gxg/standard/syslog/logger.rb b/gxg/standard/syslog/logger.rb new file mode 100644 index 0000000..06cbe5b --- /dev/null +++ b/gxg/standard/syslog/logger.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: false +require 'syslog' +require 'logger' + +## +# Syslog::Logger is a Logger work-alike that logs via syslog instead of to a +# file. You can use Syslog::Logger to aggregate logs between multiple +# machines. +# +# By default, Syslog::Logger uses the program name 'ruby', but this can be +# changed via the first argument to Syslog::Logger.new. +# +# NOTE! You can only set the Syslog::Logger program name when you initialize +# Syslog::Logger for the first time. This is a limitation of the way +# Syslog::Logger uses syslog (and in some ways, a limitation of the way +# syslog(3) works). Attempts to change Syslog::Logger's program name after +# the first initialization will be ignored. +# +# === Example +# +# The following will log to syslogd on your local machine: +# +# require 'syslog/logger' +# +# log = Syslog::Logger.new 'my_program' +# log.info 'this line will be logged via syslog(3)' +# +# Also the facility may be set to specify the facility level which will be used: +# +# log.info 'this line will be logged using Syslog default facility level' +# +# log_local1 = Syslog::Logger.new 'my_program', Syslog::LOG_LOCAL1 +# log_local1.info 'this line will be logged using local1 facility level' +# +# +# You may need to perform some syslog.conf setup first. For a BSD machine add +# the following lines to /etc/syslog.conf: +# +# !my_program +# *.* /var/log/my_program.log +# +# Then touch /var/log/my_program.log and signal syslogd with a HUP +# (killall -HUP syslogd, on FreeBSD). +# +# If you wish to have logs automatically roll over and archive, see the +# newsyslog.conf(5) and newsyslog(8) man pages. + +class Syslog::Logger + # Default formatter for log messages. + class Formatter + def call severity, time, progname, msg + clean msg + end + + private + + ## + # Clean up messages so they're nice and pretty. + + def clean message + message = message.to_s.strip + message.gsub!(/\e\[[0-9;]*m/, '') # remove useless ansi color codes + return message + end + end + + ## + # The version of Syslog::Logger you are using. + + VERSION = '2.1.0' + + ## + # Maps Logger warning types to syslog(3) warning types. + # + # Messages from Ruby applications are not considered as critical as messages + # from other system daemons using syslog(3), so most messages are reduced by + # one level. For example, a fatal message for Ruby's Logger is considered + # an error for syslog(3). + + LEVEL_MAP = { + ::Logger::UNKNOWN => Syslog::LOG_ALERT, + ::Logger::FATAL => Syslog::LOG_ERR, + ::Logger::ERROR => Syslog::LOG_WARNING, + ::Logger::WARN => Syslog::LOG_NOTICE, + ::Logger::INFO => Syslog::LOG_INFO, + ::Logger::DEBUG => Syslog::LOG_DEBUG, + } + + ## + # Returns the internal Syslog object that is initialized when the + # first instance is created. + + def self.syslog + @@syslog + end + + ## + # Specifies the internal Syslog object to be used. + + def self.syslog= syslog + @@syslog = syslog + end + + ## + # Builds a methods for level +meth+. + + def self.make_methods meth + level = ::Logger.const_get(meth.upcase) + eval <<-EOM, nil, __FILE__, __LINE__ + 1 + def #{meth}(message = nil, &block) + add(#{level}, message, &block) + end + + def #{meth}? + @level <= #{level} + end + EOM + end + + ## + # :method: unknown + # + # Logs a +message+ at the unknown (syslog alert) log level, or logs the + # message returned from the block. + + ## + # :method: fatal + # + # Logs a +message+ at the fatal (syslog err) log level, or logs the message + # returned from the block. + + ## + # :method: error + # + # Logs a +message+ at the error (syslog warning) log level, or logs the + # message returned from the block. + + ## + # :method: warn + # + # Logs a +message+ at the warn (syslog notice) log level, or logs the + # message returned from the block. + + ## + # :method: info + # + # Logs a +message+ at the info (syslog info) log level, or logs the message + # returned from the block. + + ## + # :method: debug + # + # Logs a +message+ at the debug (syslog debug) log level, or logs the + # message returned from the block. + + Logger::Severity::constants.each do |severity| + make_methods severity.downcase + end + + ## + # Log level for Logger compatibility. + + attr_accessor :level + + # Logging formatter, as a +Proc+ that will take four arguments and + # return the formatted message. The arguments are: + # + # +severity+:: The Severity of the log message. + # +time+:: A Time instance representing when the message was logged. + # +progname+:: The #progname configured, or passed to the logger method. + # +msg+:: The _Object_ the user passed to the log message; not necessarily a + # String. + # + # The block should return an Object that can be written to the logging + # device via +write+. The default formatter is used when no formatter is + # set. + attr_accessor :formatter + + ## + # The facility argument is used to specify what type of program is logging the message. + + attr_accessor :facility + + ## + # Fills in variables for Logger compatibility. If this is the first + # instance of Syslog::Logger, +program_name+ may be set to change the logged + # program name. The +facility+ may be set to specify the facility level which will be used. + # + # Due to the way syslog works, only one program name may be chosen. + + def initialize program_name = 'ruby', facility = nil + @level = ::Logger::DEBUG + @formatter = Formatter.new + + @@syslog ||= Syslog.open(program_name) + + @facility = (facility || @@syslog.facility) + end + + ## + # Almost duplicates Logger#add. +progname+ is ignored. + + def add severity, message = nil, progname = nil, &block + severity ||= ::Logger::UNKNOWN + @level <= severity and + @@syslog.log( (LEVEL_MAP[severity] | @facility), '%s', formatter.call(severity, Time.now, progname, (message || block.call)) ) + true + end +end diff --git a/gxg/standard/thwait.rb b/gxg/standard/thwait.rb new file mode 100644 index 0000000..8f9e0c2 --- /dev/null +++ b/gxg/standard/thwait.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: false +# +# thwait.rb - thread synchronization class +# $Release Version: 0.9 $ +# $Revision: 1.3 $ +# by Keiju ISHITSUKA(Nihon Rational Software Co.,Ltd.) + +require "e2mmap.rb" + +# +# This class watches for termination of multiple threads. Basic functionality +# (wait until specified threads have terminated) can be accessed through the +# class method ThreadsWait::all_waits. Finer control can be gained using +# instance methods. +# +# Example: +# +# ThreadsWait.all_waits(thr1, thr2, ...) do |t| +# STDERR.puts "Thread #{t} has terminated." +# end +# +# +# th = ThreadsWait.new(thread1,...) +# th.next_wait # next one to be done +# +# +class ThreadsWait + extend Exception2MessageMapper + def_exception("ErrNoWaitingThread", "No threads for waiting.") + def_exception("ErrNoFinishedThread", "No finished threads.") + + # + # Waits until all specified threads have terminated. If a block is provided, + # it is executed for each thread as they terminate. + # + def ThreadsWait.all_waits(*threads) # :yield: thread + tw = ThreadsWait.new(*threads) + if block_given? + tw.all_waits do |th| + yield th + end + else + tw.all_waits + end + end + + # + # Creates a ThreadsWait object, specifying the threads to wait on. + # Non-blocking. + # + def initialize(*threads) + @threads = [] + @wait_queue = Thread::Queue.new + join_nowait(*threads) unless threads.empty? + end + + # Returns the array of threads that have not terminated yet. + attr_reader :threads + + # + # Returns +true+ if there are no threads in the pool still running. + # + def empty? + @threads.empty? + end + + # + # Returns +true+ if any thread has terminated and is ready to be collected. + # + def finished? + !@wait_queue.empty? + end + + # + # Waits for specified threads to terminate, and returns when one of + # the threads terminated. + # + def join(*threads) + join_nowait(*threads) + next_wait + end + + # + # Specifies the threads that this object will wait for, but does not actually + # wait. + # + def join_nowait(*threads) + threads.flatten! + @threads.concat threads + for th in threads + Thread.start(th) do |t| + begin + t.join + ensure + @wait_queue.push t + end + end + end + end + + # + # Waits until any of the specified threads has terminated, and returns the one + # that does. + # + # If there is no thread to wait, raises +ErrNoWaitingThread+. If +nonblock+ + # is true, and there is no terminated thread, raises +ErrNoFinishedThread+. + # + def next_wait(nonblock = nil) + ThreadsWait.fail ErrNoWaitingThread if @threads.empty? + begin + @threads.delete(th = @wait_queue.pop(nonblock)) + th + rescue ThreadError + ThreadsWait.fail ErrNoFinishedThread + end + end + + # + # Waits until all of the specified threads are terminated. If a block is + # supplied for the method, it is executed for each thread termination. + # + # Raises exceptions in the same manner as +next_wait+. + # + def all_waits + until @threads.empty? + th = next_wait + yield th if block_given? + end + end +end + +## +# An alias for ThreadsWait from thwait.rb + +ThWait = ThreadsWait + +# Documentation comments: +# - Source of documentation is evenly split between Nutshell, existing +# comments, and my own rephrasing. +# - I'm not particularly confident that the comments are all exactly correct. diff --git a/gxg/standard/timeout.rb b/gxg/standard/timeout.rb new file mode 100644 index 0000000..a33bb4c --- /dev/null +++ b/gxg/standard/timeout.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: false +# Timeout long-running blocks +# +# == Synopsis +# +# require 'timeout' +# status = Timeout::timeout(5) { +# # Something that should be interrupted if it takes more than 5 seconds... +# } +# +# == Description +# +# Timeout provides a way to auto-terminate a potentially long-running +# operation if it hasn't finished in a fixed amount of time. +# +# Previous versions didn't use a module for namespacing, however +# #timeout is provided for backwards compatibility. You +# should prefer Timeout.timeout instead. +# +# == Copyright +# +# Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc. +# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan + +module Timeout + # Raised by Timeout.timeout when the block times out. + class Error < RuntimeError + attr_reader :thread + + def self.catch(*args) + exc = new(*args) + exc.instance_variable_set(:@thread, Thread.current) + ::Kernel.catch(exc) {yield exc} + end + + def exception(*) + # TODO: use Fiber.current to see if self can be thrown + if self.thread == Thread.current + bt = caller + begin + throw(self, bt) + rescue UncaughtThrowError + end + end + self + end + end + + # :stopdoc: + THIS_FILE = /\A#{Regexp.quote(__FILE__)}:/o + CALLER_OFFSET = ((c = caller[0]) && THIS_FILE =~ c) ? 1 : 0 + private_constant :THIS_FILE, :CALLER_OFFSET + # :startdoc: + + # Perform an operation in a block, raising an error if it takes longer than + # +sec+ seconds to complete. + # + # +sec+:: Number of seconds to wait for the block to terminate. Any number + # may be used, including Floats to specify fractional seconds. A + # value of 0 or +nil+ will execute the block without any timeout. + # +klass+:: Exception Class to raise if the block fails to terminate + # in +sec+ seconds. Omitting will use the default, Timeout::Error + # +message+:: Error message to raise with Exception Class. + # Omitting will use the default, "execution expired" + # + # Returns the result of the block *if* the block completed before + # +sec+ seconds, otherwise throws an exception, based on the value of +klass+. + # + # The exception thrown to terminate the given block cannot be rescued inside + # the block unless +klass+ is given explicitly. + # + # Note that this is both a method of module Timeout, so you can include + # Timeout into your classes so they have a #timeout method, as well as + # a module method, so you can call it directly as Timeout.timeout(). + def timeout(sec, klass = nil, message = nil) #:yield: +sec+ + return yield(sec) if sec == nil or sec.zero? + message ||= "execution expired".freeze + from = "from #{caller_locations(1, 1)[0]}" if $DEBUG + e = Error + bl = proc do |exception| + begin + x = Thread.current + y = Thread.start { + Thread.current.name = from + begin + sleep sec + rescue => e + x.raise e + else + x.raise exception, message + end + } + return yield(sec) + ensure + if y + y.kill + y.join # make sure y is dead. + end + end + end + if klass + begin + bl.call(klass) + rescue klass => e + bt = e.backtrace + end + else + bt = Error.catch(message, &bl) + end + level = -caller(CALLER_OFFSET).size-2 + while THIS_FILE =~ bt[level] + bt.delete_at(level) + end + raise(e, message, bt) + end + + module_function :timeout +end + +def timeout(*args, &block) + warn "Object##{__method__} is deprecated, use Timeout.timeout instead.", uplevel: 1 + Timeout.timeout(*args, &block) +end + +# Another name for Timeout::Error, defined for backwards compatibility with +# earlier versions of timeout.rb. +TimeoutError = Timeout::Error +class Object + deprecate_constant :TimeoutError +end diff --git a/gxg/standard/tracer.rb b/gxg/standard/tracer.rb new file mode 100644 index 0000000..faafd80 --- /dev/null +++ b/gxg/standard/tracer.rb @@ -0,0 +1,286 @@ +# frozen_string_literal: false +#-- +# $Release Version: 0.3$ +# $Revision: 1.12 $ + +## +# Outputs a source level execution trace of a Ruby program. +# +# It does this by registering an event handler with Kernel#set_trace_func for +# processing incoming events. It also provides methods for filtering unwanted +# trace output (see Tracer.add_filter, Tracer.on, and Tracer.off). +# +# == Example +# +# Consider the following Ruby script +# +# class A +# def square(a) +# return a*a +# end +# end +# +# a = A.new +# a.square(5) +# +# Running the above script using ruby -r tracer example.rb will +# output the following trace to STDOUT (Note you can also explicitly +# require 'tracer') +# +# #0::38:Kernel:<: - +# #0:example.rb:3::-: class A +# #0:example.rb:3::C: class A +# #0:example.rb:4::-: def square(a) +# #0:example.rb:7::E: end +# #0:example.rb:9::-: a = A.new +# #0:example.rb:10::-: a.square(5) +# #0:example.rb:4:A:>: def square(a) +# #0:example.rb:5:A:-: return a*a +# #0:example.rb:6:A:<: end +# | | | | | +# | | | | ---------------------+ event +# | | | ------------------------+ class +# | | --------------------------+ line +# | ------------------------------------+ filename +# ---------------------------------------+ thread +# +# Symbol table used for displaying incoming events: +# +# +}+:: call a C-language routine +# +{+:: return from a C-language routine +# +>+:: call a Ruby method +# +C+:: start a class or module definition +# +E+:: finish a class or module definition +# +-+:: execute code on a new line +# +^+:: raise an exception +# +<+:: return from a Ruby method +# +# == Copyright +# +# by Keiju ISHITSUKA(keiju@ishitsuka.com) +# +class Tracer + class << self + # display additional debug information (defaults to false) + attr_accessor :verbose + alias verbose? verbose + + # output stream used to output trace (defaults to STDOUT) + attr_accessor :stdout + + # mutex lock used by tracer for displaying trace output + attr_reader :stdout_mutex + + # display process id in trace output (defaults to false) + attr_accessor :display_process_id + alias display_process_id? display_process_id + + # display thread id in trace output (defaults to true) + attr_accessor :display_thread_id + alias display_thread_id? display_thread_id + + # display C-routine calls in trace output (defaults to false) + attr_accessor :display_c_call + alias display_c_call? display_c_call + end + + Tracer::stdout = STDOUT + Tracer::verbose = false + Tracer::display_process_id = false + Tracer::display_thread_id = true + Tracer::display_c_call = false + + @stdout_mutex = Thread::Mutex.new + + # Symbol table used for displaying trace information + EVENT_SYMBOL = { + "line" => "-", + "call" => ">", + "return" => "<", + "class" => "C", + "end" => "E", + "raise" => "^", + "c-call" => "}", + "c-return" => "{", + "unknown" => "?" + } + + def initialize # :nodoc: + @threads = Hash.new + if defined? Thread.main + @threads[Thread.main.object_id] = 0 + else + @threads[Thread.current.object_id] = 0 + end + + @get_line_procs = {} + + @filters = [] + end + + def stdout # :nodoc: + Tracer.stdout + end + + def on # :nodoc: + if block_given? + on + begin + yield + ensure + off + end + else + set_trace_func method(:trace_func).to_proc + stdout.print "Trace on\n" if Tracer.verbose? + end + end + + def off # :nodoc: + set_trace_func nil + stdout.print "Trace off\n" if Tracer.verbose? + end + + def add_filter(p = proc) # :nodoc: + @filters.push p + end + + def set_get_line_procs(file, p = proc) # :nodoc: + @get_line_procs[file] = p + end + + def get_line(file, line) # :nodoc: + if p = @get_line_procs[file] + return p.call(line) + end + + unless list = SCRIPT_LINES__[file] + list = File.readlines(file) rescue [] + SCRIPT_LINES__[file] = list + end + + if l = list[line - 1] + l + else + "-\n" + end + end + + def get_thread_no # :nodoc: + if no = @threads[Thread.current.object_id] + no + else + @threads[Thread.current.object_id] = @threads.size + end + end + + def trace_func(event, file, line, id, binding, klass, *) # :nodoc: + return if file == __FILE__ + + for p in @filters + return unless p.call event, file, line, id, binding, klass + end + + return unless Tracer::display_c_call? or + event != "c-call" && event != "c-return" + + Tracer::stdout_mutex.synchronize do + if EVENT_SYMBOL[event] + stdout.printf("<%d>", $$) if Tracer::display_process_id? + stdout.printf("#%d:", get_thread_no) if Tracer::display_thread_id? + if line == 0 + source = "?\n" + else + source = get_line(file, line) + end + stdout.printf("%s:%d:%s:%s: %s", + file, + line, + klass || '', + EVENT_SYMBOL[event], + source) + end + end + + end + + # Reference to singleton instance of Tracer + Single = new + + ## + # Start tracing + # + # === Example + # + # Tracer.on + # # code to trace here + # Tracer.off + # + # You can also pass a block: + # + # Tracer.on { + # # trace everything in this block + # } + + def Tracer.on + if block_given? + Single.on{yield} + else + Single.on + end + end + + ## + # Disable tracing + + def Tracer.off + Single.off + end + + ## + # Register an event handler p which is called everytime a line + # in +file_name+ is executed. + # + # Example: + # + # Tracer.set_get_line_procs("example.rb", lambda { |line| + # puts "line number executed is #{line}" + # }) + + def Tracer.set_get_line_procs(file_name, p = proc) + Single.set_get_line_procs(file_name, p) + end + + ## + # Used to filter unwanted trace output + # + # Example which only outputs lines of code executed within the Kernel class: + # + # Tracer.add_filter do |event, file, line, id, binding, klass, *rest| + # "Kernel" == klass.to_s + # end + + def Tracer.add_filter(p = proc) + Single.add_filter(p) + end +end + +# :stopdoc: +SCRIPT_LINES__ = {} unless defined? SCRIPT_LINES__ + +if $0 == __FILE__ + # direct call + + $0 = ARGV[0] + ARGV.shift + Tracer.on + require $0 +else + # call Tracer.on only if required by -r command-line option + count = caller.count {|bt| %r%/rubygems/core_ext/kernel_require\.rb:% !~ bt} + if (defined?(Gem) and count == 0) or + (!defined?(Gem) and count <= 1) + Tracer.on + end +end +# :startdoc: diff --git a/gxg/standard/tsort.rb b/gxg/standard/tsort.rb new file mode 100644 index 0000000..2760b7d --- /dev/null +++ b/gxg/standard/tsort.rb @@ -0,0 +1,452 @@ +# frozen_string_literal: true + +#-- +# tsort.rb - provides a module for topological sorting and strongly connected components. +#++ +# + +# +# TSort implements topological sorting using Tarjan's algorithm for +# strongly connected components. +# +# TSort is designed to be able to be used with any object which can be +# interpreted as a directed graph. +# +# TSort requires two methods to interpret an object as a graph, +# tsort_each_node and tsort_each_child. +# +# * tsort_each_node is used to iterate for all nodes over a graph. +# * tsort_each_child is used to iterate for child nodes of a given node. +# +# The equality of nodes are defined by eql? and hash since +# TSort uses Hash internally. +# +# == A Simple Example +# +# The following example demonstrates how to mix the TSort module into an +# existing class (in this case, Hash). Here, we're treating each key in +# the hash as a node in the graph, and so we simply alias the required +# #tsort_each_node method to Hash's #each_key method. For each key in the +# hash, the associated value is an array of the node's child nodes. This +# choice in turn leads to our implementation of the required #tsort_each_child +# method, which fetches the array of child nodes and then iterates over that +# array using the user-supplied block. +# +# require 'tsort' +# +# class Hash +# include TSort +# alias tsort_each_node each_key +# def tsort_each_child(node, &block) +# fetch(node).each(&block) +# end +# end +# +# {1=>[2, 3], 2=>[3], 3=>[], 4=>[]}.tsort +# #=> [3, 2, 1, 4] +# +# {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}.strongly_connected_components +# #=> [[4], [2, 3], [1]] +# +# == A More Realistic Example +# +# A very simple `make' like tool can be implemented as follows: +# +# require 'tsort' +# +# class Make +# def initialize +# @dep = {} +# @dep.default = [] +# end +# +# def rule(outputs, inputs=[], &block) +# triple = [outputs, inputs, block] +# outputs.each {|f| @dep[f] = [triple]} +# @dep[triple] = inputs +# end +# +# def build(target) +# each_strongly_connected_component_from(target) {|ns| +# if ns.length != 1 +# fs = ns.delete_if {|n| Array === n} +# raise TSort::Cyclic.new("cyclic dependencies: #{fs.join ', '}") +# end +# n = ns.first +# if Array === n +# outputs, inputs, block = n +# inputs_time = inputs.map {|f| File.mtime f}.max +# begin +# outputs_time = outputs.map {|f| File.mtime f}.min +# rescue Errno::ENOENT +# outputs_time = nil +# end +# if outputs_time == nil || +# inputs_time != nil && outputs_time <= inputs_time +# sleep 1 if inputs_time != nil && inputs_time.to_i == Time.now.to_i +# block.call +# end +# end +# } +# end +# +# def tsort_each_child(node, &block) +# @dep[node].each(&block) +# end +# include TSort +# end +# +# def command(arg) +# print arg, "\n" +# system arg +# end +# +# m = Make.new +# m.rule(%w[t1]) { command 'date > t1' } +# m.rule(%w[t2]) { command 'date > t2' } +# m.rule(%w[t3]) { command 'date > t3' } +# m.rule(%w[t4], %w[t1 t3]) { command 'cat t1 t3 > t4' } +# m.rule(%w[t5], %w[t4 t2]) { command 'cat t4 t2 > t5' } +# m.build('t5') +# +# == Bugs +# +# * 'tsort.rb' is wrong name because this library uses +# Tarjan's algorithm for strongly connected components. +# Although 'strongly_connected_components.rb' is correct but too long. +# +# == References +# +# R. E. Tarjan, "Depth First Search and Linear Graph Algorithms", +# SIAM Journal on Computing, Vol. 1, No. 2, pp. 146-160, June 1972. +# + +module TSort + class Cyclic < StandardError + end + + # Returns a topologically sorted array of nodes. + # The array is sorted from children to parents, i.e. + # the first element has no child and the last node has no parent. + # + # If there is a cycle, TSort::Cyclic is raised. + # + # class G + # include TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # p graph.tsort #=> [4, 2, 3, 1] + # + # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}) + # p graph.tsort # raises TSort::Cyclic + # + def tsort + each_node = method(:tsort_each_node) + each_child = method(:tsort_each_child) + TSort.tsort(each_node, each_child) + end + + # Returns a topologically sorted array of nodes. + # The array is sorted from children to parents, i.e. + # the first element has no child and the last node has no parent. + # + # The graph is represented by _each_node_ and _each_child_. + # _each_node_ should have +call+ method which yields for each node in the graph. + # _each_child_ should have +call+ method which takes a node argument and yields for each child node. + # + # If there is a cycle, TSort::Cyclic is raised. + # + # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # p TSort.tsort(each_node, each_child) #=> [4, 2, 3, 1] + # + # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # p TSort.tsort(each_node, each_child) # raises TSort::Cyclic + # + def TSort.tsort(each_node, each_child) + TSort.tsort_each(each_node, each_child).to_a + end + + # The iterator version of the #tsort method. + # obj.tsort_each is similar to obj.tsort.each, but + # modification of _obj_ during the iteration may lead to unexpected results. + # + # #tsort_each returns +nil+. + # If there is a cycle, TSort::Cyclic is raised. + # + # class G + # include TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # graph.tsort_each {|n| p n } + # #=> 4 + # # 2 + # # 3 + # # 1 + # + def tsort_each(&block) # :yields: node + each_node = method(:tsort_each_node) + each_child = method(:tsort_each_child) + TSort.tsort_each(each_node, each_child, &block) + end + + # The iterator version of the TSort.tsort method. + # + # The graph is represented by _each_node_ and _each_child_. + # _each_node_ should have +call+ method which yields for each node in the graph. + # _each_child_ should have +call+ method which takes a node argument and yields for each child node. + # + # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # TSort.tsort_each(each_node, each_child) {|n| p n } + # #=> 4 + # # 2 + # # 3 + # # 1 + # + def TSort.tsort_each(each_node, each_child) # :yields: node + return to_enum(__method__, each_node, each_child) unless block_given? + + TSort.each_strongly_connected_component(each_node, each_child) {|component| + if component.size == 1 + yield component.first + else + raise Cyclic.new("topological sort failed: #{component.inspect}") + end + } + end + + # Returns strongly connected components as an array of arrays of nodes. + # The array is sorted from children to parents. + # Each elements of the array represents a strongly connected component. + # + # class G + # include TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # p graph.strongly_connected_components #=> [[4], [2], [3], [1]] + # + # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}) + # p graph.strongly_connected_components #=> [[4], [2, 3], [1]] + # + def strongly_connected_components + each_node = method(:tsort_each_node) + each_child = method(:tsort_each_child) + TSort.strongly_connected_components(each_node, each_child) + end + + # Returns strongly connected components as an array of arrays of nodes. + # The array is sorted from children to parents. + # Each elements of the array represents a strongly connected component. + # + # The graph is represented by _each_node_ and _each_child_. + # _each_node_ should have +call+ method which yields for each node in the graph. + # _each_child_ should have +call+ method which takes a node argument and yields for each child node. + # + # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # p TSort.strongly_connected_components(each_node, each_child) + # #=> [[4], [2], [3], [1]] + # + # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # p TSort.strongly_connected_components(each_node, each_child) + # #=> [[4], [2, 3], [1]] + # + def TSort.strongly_connected_components(each_node, each_child) + TSort.each_strongly_connected_component(each_node, each_child).to_a + end + + # The iterator version of the #strongly_connected_components method. + # obj.each_strongly_connected_component is similar to + # obj.strongly_connected_components.each, but + # modification of _obj_ during the iteration may lead to unexpected results. + # + # #each_strongly_connected_component returns +nil+. + # + # class G + # include TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # graph.each_strongly_connected_component {|scc| p scc } + # #=> [4] + # # [2] + # # [3] + # # [1] + # + # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}) + # graph.each_strongly_connected_component {|scc| p scc } + # #=> [4] + # # [2, 3] + # # [1] + # + def each_strongly_connected_component(&block) # :yields: nodes + each_node = method(:tsort_each_node) + each_child = method(:tsort_each_child) + TSort.each_strongly_connected_component(each_node, each_child, &block) + end + + # The iterator version of the TSort.strongly_connected_components method. + # + # The graph is represented by _each_node_ and _each_child_. + # _each_node_ should have +call+ method which yields for each node in the graph. + # _each_child_ should have +call+ method which takes a node argument and yields for each child node. + # + # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc } + # #=> [4] + # # [2] + # # [3] + # # [1] + # + # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc } + # #=> [4] + # # [2, 3] + # # [1] + # + def TSort.each_strongly_connected_component(each_node, each_child) # :yields: nodes + return to_enum(__method__, each_node, each_child) unless block_given? + + id_map = {} + stack = [] + each_node.call {|node| + unless id_map.include? node + TSort.each_strongly_connected_component_from(node, each_child, id_map, stack) {|c| + yield c + } + end + } + nil + end + + # Iterates over strongly connected component in the subgraph reachable from + # _node_. + # + # Return value is unspecified. + # + # #each_strongly_connected_component_from doesn't call #tsort_each_node. + # + # class G + # include TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # graph.each_strongly_connected_component_from(2) {|scc| p scc } + # #=> [4] + # # [2] + # + # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}) + # graph.each_strongly_connected_component_from(2) {|scc| p scc } + # #=> [4] + # # [2, 3] + # + def each_strongly_connected_component_from(node, id_map={}, stack=[], &block) # :yields: nodes + TSort.each_strongly_connected_component_from(node, method(:tsort_each_child), id_map, stack, &block) + end + + # Iterates over strongly connected components in a graph. + # The graph is represented by _node_ and _each_child_. + # + # _node_ is the first node. + # _each_child_ should have +call+ method which takes a node argument + # and yields for each child node. + # + # Return value is unspecified. + # + # #TSort.each_strongly_connected_component_from is a class method and + # it doesn't need a class to represent a graph which includes TSort. + # + # graph = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]} + # each_child = lambda {|n, &b| graph[n].each(&b) } + # TSort.each_strongly_connected_component_from(1, each_child) {|scc| + # p scc + # } + # #=> [4] + # # [2, 3] + # # [1] + # + def TSort.each_strongly_connected_component_from(node, each_child, id_map={}, stack=[]) # :yields: nodes + return to_enum(__method__, node, each_child, id_map, stack) unless block_given? + + minimum_id = node_id = id_map[node] = id_map.size + stack_length = stack.length + stack << node + + each_child.call(node) {|child| + if id_map.include? child + child_id = id_map[child] + minimum_id = child_id if child_id && child_id < minimum_id + else + sub_minimum_id = + TSort.each_strongly_connected_component_from(child, each_child, id_map, stack) {|c| + yield c + } + minimum_id = sub_minimum_id if sub_minimum_id < minimum_id + end + } + + if node_id == minimum_id + component = stack.slice!(stack_length .. -1) + component.each {|n| id_map[n] = nil} + yield component + end + + minimum_id + end + + # Should be implemented by a extended class. + # + # #tsort_each_node is used to iterate for all nodes over a graph. + # + def tsort_each_node # :yields: node + raise NotImplementedError.new + end + + # Should be implemented by a extended class. + # + # #tsort_each_child is used to iterate for child nodes of _node_. + # + def tsort_each_child(node) # :yields: child + raise NotImplementedError.new + end +end diff --git a/gxg/standard/unicode_normalize/normalize.rb b/gxg/standard/unicode_normalize/normalize.rb new file mode 100644 index 0000000..a2f7a29 --- /dev/null +++ b/gxg/standard/unicode_normalize/normalize.rb @@ -0,0 +1,175 @@ +# coding: utf-8 +# frozen_string_literal: false + +# Copyright Ayumu Nojima (野島 歩) and Martin J. Dürst (duerst@it.aoyama.ac.jp) + +# This file, the companion file tables.rb (autogenerated), and the module, +# constants, and method defined herein are part of the implementation of the +# built-in String class, not part of the standard library. They should +# therefore never be gemified. They implement the methods +# String#unicode_normalize, String#unicode_normalize!, and String#unicode_normalized?. +# +# They are placed here because they are written in Ruby. They are loaded on +# demand when any of the three methods mentioned above is executed for the +# first time. This reduces the memory footprint and startup time for scripts +# and applications that do not use those methods. +# +# The name and even the existence of the module UnicodeNormalize and all of its +# content are purely an implementation detail, and should not be exposed in +# any test or spec or otherwise. + +require 'unicode_normalize/tables.rb' + + +module UnicodeNormalize # :nodoc: + ## Constant for max hash capacity to avoid DoS attack + MAX_HASH_LENGTH = 18000 # enough for all test cases, otherwise tests get slow + + ## Regular Expressions and Hash Constants + REGEXP_D = Regexp.compile(REGEXP_D_STRING, Regexp::EXTENDED) + REGEXP_C = Regexp.compile(REGEXP_C_STRING, Regexp::EXTENDED) + REGEXP_K = Regexp.compile(REGEXP_K_STRING, Regexp::EXTENDED) + NF_HASH_D = Hash.new do |hash, key| + hash.shift if hash.length>MAX_HASH_LENGTH # prevent DoS attack + hash[key] = nfd_one(key) + end + NF_HASH_C = Hash.new do |hash, key| + hash.shift if hash.length>MAX_HASH_LENGTH # prevent DoS attack + hash[key] = nfc_one(key) + end + + ## Constants For Hangul + # for details such as the meaning of the identifiers below, please see + # http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf, pp. 144/145 + SBASE = 0xAC00 + LBASE = 0x1100 + VBASE = 0x1161 + TBASE = 0x11A7 + LCOUNT = 19 + VCOUNT = 21 + TCOUNT = 28 + NCOUNT = VCOUNT * TCOUNT + SCOUNT = LCOUNT * NCOUNT + + # Unicode-based encodings (except UTF-8) + UNICODE_ENCODINGS = [Encoding::UTF_16BE, Encoding::UTF_16LE, Encoding::UTF_32BE, Encoding::UTF_32LE, + Encoding::GB18030, Encoding::UCS_2BE, Encoding::UCS_4BE] + + ## Hangul Algorithm + def self.hangul_decomp_one(target) + syllable_index = target.ord - SBASE + return target if syllable_index < 0 || syllable_index >= SCOUNT + l = LBASE + syllable_index / NCOUNT + v = VBASE + (syllable_index % NCOUNT) / TCOUNT + t = TBASE + syllable_index % TCOUNT + (t==TBASE ? [l, v] : [l, v, t]).pack('U*') + target[1..-1] + end + + def self.hangul_comp_one(string) + length = string.length + if length>1 and 0 <= (lead =string[0].ord-LBASE) and lead < LCOUNT and + 0 <= (vowel=string[1].ord-VBASE) and vowel < VCOUNT + lead_vowel = SBASE + (lead * VCOUNT + vowel) * TCOUNT + if length>2 and 0 <= (trail=string[2].ord-TBASE) and trail < TCOUNT + (lead_vowel + trail).chr(Encoding::UTF_8) + string[3..-1] + else + lead_vowel.chr(Encoding::UTF_8) + string[2..-1] + end + else + string + end + end + + ## Canonical Ordering + def self.canonical_ordering_one(string) + sorting = string.each_char.collect { |c| [c, CLASS_TABLE[c]] } + (sorting.length-2).downto(0) do |i| # almost, but not exactly bubble sort + (0..i).each do |j| + later_class = sorting[j+1].last + if 0A-PR-Za-pr-z\u00A8\u00C6\u00D8" \ + "\u00E6\u00F8\u017F\u01B7\u0292\u0391\u0395\u0397" \ + "\u0399\u039F\u03A1\u03A5\u03A9\u03B1\u03B5\u03B7" \ + "\u03B9\u03BF\u03C1\u03C5\u03C9\u03D2\u0406\u0410" \ + "\u0413\u0415-\u0418\u041A\u041E\u0423\u0427\u042B\u042D" \ + "\u0430\u0433\u0435-\u0438\u043A\u043E\u0443\u0447\u044B" \ + "\u044D\u0456\u0474\u0475\u04D8\u04D9\u04E8\u04E9\u0627\u0648\u064A" \ + "\u06C1\u06D2\u06D5\u0928\u0930\u0933\u09C7\u0B47" \ + "\u0B92\u0BC6\u0BC7\u0C46\u0CBF\u0CC6\u0D46\u0D47\u0DD9\u1025" \ + "\u1B05\u1B07\u1B09\u1B0B\u1B0D\u1B11\u1B3A\u1B3C" \ + "\u1B3E\u1B3F\u1B42\u1FBF\u1FFE\u2190\u2192\u2194\u21D0" \ + "\u21D2\u21D4\u2203\u2208\u220B\u2223\u2225\u223C" \ + "\u2243\u2245\u2248\u224D\u2261\u2264\u2265\u2272\u2273\u2276\u2277" \ + "\u227A-\u227D\u2282\u2283\u2286\u2287\u2291\u2292\u22A2\u22A8\u22A9\u22AB\u22B2-\u22B5" \ + "\u3046\u304B\u304D\u304F\u3051\u3053\u3055\u3057" \ + "\u3059\u305B\u305D\u305F\u3061\u3064\u3066\u3068" \ + "\u306F\u3072\u3075\u3078\u307B\u309D\u30A6\u30AB" \ + "\u30AD\u30AF\u30B1\u30B3\u30B5\u30B7\u30B9\u30BB" \ + "\u30BD\u30BF\u30C1\u30C4\u30C6\u30C8\u30CF\u30D2" \ + "\u30D5\u30D8\u30DB\u30EF-\u30F2\u30FD\u{11099}\u{1109B}\u{110A5}" \ + "\u{11131}\u{11132}\u{11347}\u{114B9}\u{115B8}\u{115B9}" \ + "]?#{accents}+" \ + "|#{'' # precomposed Hangul syllables + }" \ + "[\u{AC00}-\u{D7A4}]" + REGEXP_C_STRING = "#{'' # composition exclusions + }" \ + "[\u0340\u0341\u0343\u0344\u0374\u037E\u0387\u0958-\u095F\u09DC\u09DD\u09DF" \ + "\u0A33\u0A36\u0A59-\u0A5B\u0A5E\u0B5C\u0B5D\u0F43\u0F4D\u0F52" \ + "\u0F57\u0F5C\u0F69\u0F73\u0F75\u0F76\u0F78\u0F81\u0F93" \ + "\u0F9D\u0FA2\u0FA7\u0FAC\u0FB9\u1F71\u1F73\u1F75" \ + "\u1F77\u1F79\u1F7B\u1F7D\u1FBB\u1FBE\u1FC9\u1FCB" \ + "\u1FD3\u1FDB\u1FE3\u1FEB\u1FEE\u1FEF\u1FF9\u1FFB\u1FFD" \ + "\u2000\u2001\u2126\u212A\u212B\u2329\u232A\u2ADC\uF900-\uFA0D\uFA10\uFA12" \ + "\uFA15-\uFA1E\uFA20\uFA22\uFA25\uFA26\uFA2A-\uFA6D\uFA70-\uFAD9\uFB1D\uFB1F" \ + "\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFB4E\u{1D15E}-\u{1D164}\u{1D1BB}-\u{1D1C0}" \ + "\u{2F800}-\u{2FA1D}" \ + "]#{accents}*" \ + "|#{'' # composition starters and characters that can be the result of a composition + }" \ + "[<->A-PR-Za-pr-z\u00A8\u00C0-\u00CF\u00D1-\u00D6" \ + "\u00D8-\u00DD\u00E0-\u00EF\u00F1-\u00F6\u00F8-\u00FD\u00FF-\u010F\u0112-\u0125\u0128-\u0130\u0134-\u0137" \ + "\u0139-\u013E\u0143-\u0148\u014C-\u0151\u0154-\u0165\u0168-\u017F\u01A0\u01A1\u01AF\u01B0\u01B7" \ + "\u01CD-\u01DC\u01DE-\u01E3\u01E6-\u01F0\u01F4\u01F5\u01F8-\u021B\u021E\u021F\u0226-\u0233\u0292" \ + "\u0385\u0386\u0388-\u038A\u038C\u038E-\u0391\u0395\u0397\u0399\u039F" \ + "\u03A1\u03A5\u03A9-\u03B1\u03B5\u03B7\u03B9\u03BF\u03C1" \ + "\u03C5\u03C9-\u03CE\u03D2-\u03D4\u0400\u0401\u0403\u0406\u0407\u040C-\u040E\u0410" \ + "\u0413\u0415-\u041A\u041E\u0423\u0427\u042B\u042D\u0430" \ + "\u0433\u0435-\u043A\u043E\u0443\u0447\u044B\u044D\u0450\u0451" \ + "\u0453\u0456\u0457\u045C-\u045E\u0474-\u0477\u04C1\u04C2\u04D0-\u04D3\u04D6-\u04DF\u04E2-\u04F5" \ + "\u04F8\u04F9\u0622-\u0627\u0648\u064A\u06C0-\u06C2\u06D2\u06D3\u06D5\u0928\u0929" \ + "\u0930\u0931\u0933\u0934\u09C7\u09CB\u09CC\u0B47\u0B48\u0B4B\u0B4C\u0B92\u0B94" \ + "\u0BC6\u0BC7\u0BCA-\u0BCC\u0C46\u0C48\u0CBF\u0CC0\u0CC6-\u0CC8\u0CCA\u0CCB\u0D46\u0D47" \ + "\u0D4A-\u0D4C\u0DD9\u0DDA\u0DDC-\u0DDE\u1025\u1026\u1B05-\u1B0E\u1B11\u1B12\u1B3A-\u1B43\u1E00-\u1E99" \ + "\u1E9B\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59" \ + "\u1F5B\u1F5D\u1F5F-\u1F70\u1F72\u1F74\u1F76\u1F78\u1F7A" \ + "\u1F7C\u1F80-\u1FB4\u1FB6-\u1FBA\u1FBC\u1FBF\u1FC1-\u1FC4\u1FC6-\u1FC8\u1FCA" \ + "\u1FCC-\u1FD2\u1FD6-\u1FDA\u1FDD-\u1FE2\u1FE4-\u1FEA\u1FEC\u1FED\u1FF2-\u1FF4\u1FF6-\u1FF8\u1FFA" \ + "\u1FFC\u1FFE\u2190\u2192\u2194\u219A\u219B\u21AE\u21CD-\u21D0" \ + "\u21D2\u21D4\u2203\u2204\u2208\u2209\u220B\u220C\u2223-\u2226\u223C\u2241" \ + "\u2243-\u2245\u2247-\u2249\u224D\u2260-\u2262\u2264\u2265\u226D-\u227D\u2280-\u2289\u2291\u2292" \ + "\u22A2\u22A8\u22A9\u22AB-\u22AF\u22B2-\u22B5\u22E0-\u22E3\u22EA-\u22ED\u3046\u304B-\u3062" \ + "\u3064-\u3069\u306F-\u307D\u3094\u309D\u309E\u30A6\u30AB-\u30C2\u30C4-\u30C9\u30CF-\u30DD" \ + "\u30EF-\u30F2\u30F4\u30F7-\u30FA\u30FD\u30FE\u{11099}-\u{1109C}\u{110A5}\u{110AB}\u{1112E}\u{1112F}" \ + "\u{11131}\u{11132}\u{11347}\u{1134B}\u{1134C}\u{114B9}\u{114BB}\u{114BC}\u{114BE}\u{115B8}-\u{115BB}" \ + "]?#{accents}+" \ + "|#{'' # Hangul syllables with separate trailer + }" \ + "[\uAC00\uAC1C\uAC38\uAC54\uAC70\uAC8C\uACA8\uACC4" \ + "\uACE0\uACFC\uAD18\uAD34\uAD50\uAD6C\uAD88\uADA4" \ + "\uADC0\uADDC\uADF8\uAE14\uAE30\uAE4C\uAE68\uAE84" \ + "\uAEA0\uAEBC\uAED8\uAEF4\uAF10\uAF2C\uAF48\uAF64" \ + "\uAF80\uAF9C\uAFB8\uAFD4\uAFF0\uB00C\uB028\uB044" \ + "\uB060\uB07C\uB098\uB0B4\uB0D0\uB0EC\uB108\uB124" \ + "\uB140\uB15C\uB178\uB194\uB1B0\uB1CC\uB1E8\uB204" \ + "\uB220\uB23C\uB258\uB274\uB290\uB2AC\uB2C8\uB2E4" \ + "\uB300\uB31C\uB338\uB354\uB370\uB38C\uB3A8\uB3C4" \ + "\uB3E0\uB3FC\uB418\uB434\uB450\uB46C\uB488\uB4A4" \ + "\uB4C0\uB4DC\uB4F8\uB514\uB530\uB54C\uB568\uB584" \ + "\uB5A0\uB5BC\uB5D8\uB5F4\uB610\uB62C\uB648\uB664" \ + "\uB680\uB69C\uB6B8\uB6D4\uB6F0\uB70C\uB728\uB744" \ + "\uB760\uB77C\uB798\uB7B4\uB7D0\uB7EC\uB808\uB824" \ + "\uB840\uB85C\uB878\uB894\uB8B0\uB8CC\uB8E8\uB904" \ + "\uB920\uB93C\uB958\uB974\uB990\uB9AC\uB9C8\uB9E4" \ + "\uBA00\uBA1C\uBA38\uBA54\uBA70\uBA8C\uBAA8\uBAC4" \ + "\uBAE0\uBAFC\uBB18\uBB34\uBB50\uBB6C\uBB88\uBBA4" \ + "\uBBC0\uBBDC\uBBF8\uBC14\uBC30\uBC4C\uBC68\uBC84" \ + "\uBCA0\uBCBC\uBCD8\uBCF4\uBD10\uBD2C\uBD48\uBD64" \ + "\uBD80\uBD9C\uBDB8\uBDD4\uBDF0\uBE0C\uBE28\uBE44" \ + "\uBE60\uBE7C\uBE98\uBEB4\uBED0\uBEEC\uBF08\uBF24" \ + "\uBF40\uBF5C\uBF78\uBF94\uBFB0\uBFCC\uBFE8\uC004" \ + "\uC020\uC03C\uC058\uC074\uC090\uC0AC\uC0C8\uC0E4" \ + "\uC100\uC11C\uC138\uC154\uC170\uC18C\uC1A8\uC1C4" \ + "\uC1E0\uC1FC\uC218\uC234\uC250\uC26C\uC288\uC2A4" \ + "\uC2C0\uC2DC\uC2F8\uC314\uC330\uC34C\uC368\uC384" \ + "\uC3A0\uC3BC\uC3D8\uC3F4\uC410\uC42C\uC448\uC464" \ + "\uC480\uC49C\uC4B8\uC4D4\uC4F0\uC50C\uC528\uC544" \ + "\uC560\uC57C\uC598\uC5B4\uC5D0\uC5EC\uC608\uC624" \ + "\uC640\uC65C\uC678\uC694\uC6B0\uC6CC\uC6E8\uC704" \ + "\uC720\uC73C\uC758\uC774\uC790\uC7AC\uC7C8\uC7E4" \ + "\uC800\uC81C\uC838\uC854\uC870\uC88C\uC8A8\uC8C4" \ + "\uC8E0\uC8FC\uC918\uC934\uC950\uC96C\uC988\uC9A4" \ + "\uC9C0\uC9DC\uC9F8\uCA14\uCA30\uCA4C\uCA68\uCA84" \ + "\uCAA0\uCABC\uCAD8\uCAF4\uCB10\uCB2C\uCB48\uCB64" \ + "\uCB80\uCB9C\uCBB8\uCBD4\uCBF0\uCC0C\uCC28\uCC44" \ + "\uCC60\uCC7C\uCC98\uCCB4\uCCD0\uCCEC\uCD08\uCD24" \ + "\uCD40\uCD5C\uCD78\uCD94\uCDB0\uCDCC\uCDE8\uCE04" \ + "\uCE20\uCE3C\uCE58\uCE74\uCE90\uCEAC\uCEC8\uCEE4" \ + "\uCF00\uCF1C\uCF38\uCF54\uCF70\uCF8C\uCFA8\uCFC4" \ + "\uCFE0\uCFFC\uD018\uD034\uD050\uD06C\uD088\uD0A4" \ + "\uD0C0\uD0DC\uD0F8\uD114\uD130\uD14C\uD168\uD184" \ + "\uD1A0\uD1BC\uD1D8\uD1F4\uD210\uD22C\uD248\uD264" \ + "\uD280\uD29C\uD2B8\uD2D4\uD2F0\uD30C\uD328\uD344" \ + "\uD360\uD37C\uD398\uD3B4\uD3D0\uD3EC\uD408\uD424" \ + "\uD440\uD45C\uD478\uD494\uD4B0\uD4CC\uD4E8\uD504" \ + "\uD520\uD53C\uD558\uD574\uD590\uD5AC\uD5C8\uD5E4" \ + "\uD600\uD61C\uD638\uD654\uD670\uD68C\uD6A8\uD6C4" \ + "\uD6E0\uD6FC\uD718\uD734\uD750\uD76C\uD788" \ + "][\u11A8-\u11C2]" \ + "|#{'' # decomposed Hangul syllables + }" \ + "[\u1100-\u1112][\u1161-\u1175][\u11A8-\u11C2]?" + REGEXP_K_STRING = "" \ + "[\u00A0\u00A8\u00AA\u00AF\u00B2-\u00B5\u00B8-\u00BA\u00BC-\u00BE\u0132\u0133" \ + "\u013F\u0140\u0149\u017F\u01C4-\u01CC\u01F1-\u01F3\u02B0-\u02B8\u02D8-\u02DD\u02E0-\u02E4" \ + "\u037A\u0384\u0385\u03D0-\u03D6\u03F0-\u03F2\u03F4\u03F5\u03F9\u0587\u0675-\u0678" \ + "\u0E33\u0EB3\u0EDC\u0EDD\u0F0C\u0F77\u0F79\u10FC\u1D2C-\u1D2E" \ + "\u1D30-\u1D3A\u1D3C-\u1D4D\u1D4F-\u1D6A\u1D78\u1D9B-\u1DBF\u1E9A\u1E9B\u1FBD\u1FBF-\u1FC1" \ + "\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED\u1FEE\u1FFD\u1FFE\u2000-\u200A\u2011\u2017\u2024-\u2026" \ + "\u202F\u2033\u2034\u2036\u2037\u203C\u203E\u2047-\u2049\u2057\u205F" \ + "\u2070\u2071\u2074-\u208E\u2090-\u209C\u20A8\u2100-\u2103\u2105-\u2107\u2109-\u2113\u2115\u2116" \ + "\u2119-\u211D\u2120-\u2122\u2124\u2128\u212C\u212D\u212F-\u2131\u2133-\u2139\u213B-\u2140" \ + "\u2145-\u2149\u2150-\u217F\u2189\u222C\u222D\u222F\u2230\u2460-\u24EA\u2A0C\u2A74-\u2A76" \ + "\u2C7C\u2C7D\u2D6F\u2E9F\u2EF3\u2F00-\u2FD5\u3000\u3036\u3038-\u303A" \ + "\u309B\u309C\u309F\u30FF\u3131-\u318E\u3192-\u319F\u3200-\u321E\u3220-\u3247\u3250-\u327E" \ + "\u3280-\u32FE\u3300-\u33FF\uA69C\uA69D\uA770\uA7F8\uA7F9\uAB5C-\uAB5F\uFB00-\uFB06\uFB13-\uFB17" \ + "\uFB20-\uFB29\uFB4F-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFC\uFE10-\uFE19\uFE30-\uFE44" \ + "\uFE47-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFE70-\uFE72\uFE74\uFE76-\uFEFC\uFF01-\uFFBE\uFFC2-\uFFC7" \ + "\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC\uFFE0-\uFFE6\uFFE8-\uFFEE\u{1D400}-\u{1D454}\u{1D456}-\u{1D49C}\u{1D49E}\u{1D49F}" \ + "\u{1D4A2}\u{1D4A5}\u{1D4A6}\u{1D4A9}-\u{1D4AC}\u{1D4AE}-\u{1D4B9}\u{1D4BB}\u{1D4BD}-\u{1D4C3}\u{1D4C5}-\u{1D505}\u{1D507}-\u{1D50A}" \ + "\u{1D50D}-\u{1D514}\u{1D516}-\u{1D51C}\u{1D51E}-\u{1D539}\u{1D53B}-\u{1D53E}\u{1D540}-\u{1D544}\u{1D546}\u{1D54A}-\u{1D550}\u{1D552}-\u{1D6A5}" \ + "\u{1D6A8}-\u{1D7CB}\u{1D7CE}-\u{1D7FF}\u{1EE00}-\u{1EE03}\u{1EE05}-\u{1EE1F}\u{1EE21}\u{1EE22}\u{1EE24}\u{1EE27}\u{1EE29}-\u{1EE32}" \ + "\u{1EE34}-\u{1EE37}\u{1EE39}\u{1EE3B}\u{1EE42}\u{1EE47}\u{1EE49}\u{1EE4B}\u{1EE4D}-\u{1EE4F}" \ + "\u{1EE51}\u{1EE52}\u{1EE54}\u{1EE57}\u{1EE59}\u{1EE5B}\u{1EE5D}\u{1EE5F}\u{1EE61}\u{1EE62}" \ + "\u{1EE64}\u{1EE67}-\u{1EE6A}\u{1EE6C}-\u{1EE72}\u{1EE74}-\u{1EE77}\u{1EE79}-\u{1EE7C}\u{1EE7E}\u{1EE80}-\u{1EE89}\u{1EE8B}-\u{1EE9B}" \ + "\u{1EEA1}-\u{1EEA3}\u{1EEA5}-\u{1EEA9}\u{1EEAB}-\u{1EEBB}\u{1F100}-\u{1F10A}\u{1F110}-\u{1F12E}\u{1F130}-\u{1F14F}\u{1F16A}\u{1F16B}\u{1F190}" \ + "\u{1F200}-\u{1F202}\u{1F210}-\u{1F23B}\u{1F240}-\u{1F248}\u{1F250}\u{1F251}" \ + "]" + + class_table = { + "\u0300"=>230, "\u0301"=>230, "\u0302"=>230, "\u0303"=>230, "\u0304"=>230, "\u0305"=>230, "\u0306"=>230, "\u0307"=>230, + "\u0308"=>230, "\u0309"=>230, "\u030A"=>230, "\u030B"=>230, "\u030C"=>230, "\u030D"=>230, "\u030E"=>230, "\u030F"=>230, + "\u0310"=>230, "\u0311"=>230, "\u0312"=>230, "\u0313"=>230, "\u0314"=>230, "\u0315"=>232, "\u0316"=>220, "\u0317"=>220, + "\u0318"=>220, "\u0319"=>220, "\u031A"=>232, "\u031B"=>216, "\u031C"=>220, "\u031D"=>220, "\u031E"=>220, "\u031F"=>220, + "\u0320"=>220, "\u0321"=>202, "\u0322"=>202, "\u0323"=>220, "\u0324"=>220, "\u0325"=>220, "\u0326"=>220, "\u0327"=>202, + "\u0328"=>202, "\u0329"=>220, "\u032A"=>220, "\u032B"=>220, "\u032C"=>220, "\u032D"=>220, "\u032E"=>220, "\u032F"=>220, + "\u0330"=>220, "\u0331"=>220, "\u0332"=>220, "\u0333"=>220, "\u0334"=>1, "\u0335"=>1, "\u0336"=>1, "\u0337"=>1, + "\u0338"=>1, "\u0339"=>220, "\u033A"=>220, "\u033B"=>220, "\u033C"=>220, "\u033D"=>230, "\u033E"=>230, "\u033F"=>230, + "\u0340"=>230, "\u0341"=>230, "\u0342"=>230, "\u0343"=>230, "\u0344"=>230, "\u0345"=>240, "\u0346"=>230, "\u0347"=>220, + "\u0348"=>220, "\u0349"=>220, "\u034A"=>230, "\u034B"=>230, "\u034C"=>230, "\u034D"=>220, "\u034E"=>220, "\u0350"=>230, + "\u0351"=>230, "\u0352"=>230, "\u0353"=>220, "\u0354"=>220, "\u0355"=>220, "\u0356"=>220, "\u0357"=>230, "\u0358"=>232, + "\u0359"=>220, "\u035A"=>220, "\u035B"=>230, "\u035C"=>233, "\u035D"=>234, "\u035E"=>234, "\u035F"=>233, "\u0360"=>234, + "\u0361"=>234, "\u0362"=>233, "\u0363"=>230, "\u0364"=>230, "\u0365"=>230, "\u0366"=>230, "\u0367"=>230, "\u0368"=>230, + "\u0369"=>230, "\u036A"=>230, "\u036B"=>230, "\u036C"=>230, "\u036D"=>230, "\u036E"=>230, "\u036F"=>230, "\u0483"=>230, + "\u0484"=>230, "\u0485"=>230, "\u0486"=>230, "\u0487"=>230, "\u0591"=>220, "\u0592"=>230, "\u0593"=>230, "\u0594"=>230, + "\u0595"=>230, "\u0596"=>220, "\u0597"=>230, "\u0598"=>230, "\u0599"=>230, "\u059A"=>222, "\u059B"=>220, "\u059C"=>230, + "\u059D"=>230, "\u059E"=>230, "\u059F"=>230, "\u05A0"=>230, "\u05A1"=>230, "\u05A2"=>220, "\u05A3"=>220, "\u05A4"=>220, + "\u05A5"=>220, "\u05A6"=>220, "\u05A7"=>220, "\u05A8"=>230, "\u05A9"=>230, "\u05AA"=>220, "\u05AB"=>230, "\u05AC"=>230, + "\u05AD"=>222, "\u05AE"=>228, "\u05AF"=>230, "\u05B0"=>10, "\u05B1"=>11, "\u05B2"=>12, "\u05B3"=>13, "\u05B4"=>14, + "\u05B5"=>15, "\u05B6"=>16, "\u05B7"=>17, "\u05B8"=>18, "\u05B9"=>19, "\u05BA"=>19, "\u05BB"=>20, "\u05BC"=>21, + "\u05BD"=>22, "\u05BF"=>23, "\u05C1"=>24, "\u05C2"=>25, "\u05C4"=>230, "\u05C5"=>220, "\u05C7"=>18, "\u0610"=>230, + "\u0611"=>230, "\u0612"=>230, "\u0613"=>230, "\u0614"=>230, "\u0615"=>230, "\u0616"=>230, "\u0617"=>230, "\u0618"=>30, + "\u0619"=>31, "\u061A"=>32, "\u064B"=>27, "\u064C"=>28, "\u064D"=>29, "\u064E"=>30, "\u064F"=>31, "\u0650"=>32, + "\u0651"=>33, "\u0652"=>34, "\u0653"=>230, "\u0654"=>230, "\u0655"=>220, "\u0656"=>220, "\u0657"=>230, "\u0658"=>230, + "\u0659"=>230, "\u065A"=>230, "\u065B"=>230, "\u065C"=>220, "\u065D"=>230, "\u065E"=>230, "\u065F"=>220, "\u0670"=>35, + "\u06D6"=>230, "\u06D7"=>230, "\u06D8"=>230, "\u06D9"=>230, "\u06DA"=>230, "\u06DB"=>230, "\u06DC"=>230, "\u06DF"=>230, + "\u06E0"=>230, "\u06E1"=>230, "\u06E2"=>230, "\u06E3"=>220, "\u06E4"=>230, "\u06E7"=>230, "\u06E8"=>230, "\u06EA"=>220, + "\u06EB"=>230, "\u06EC"=>230, "\u06ED"=>220, "\u0711"=>36, "\u0730"=>230, "\u0731"=>220, "\u0732"=>230, "\u0733"=>230, + "\u0734"=>220, "\u0735"=>230, "\u0736"=>230, "\u0737"=>220, "\u0738"=>220, "\u0739"=>220, "\u073A"=>230, "\u073B"=>220, + "\u073C"=>220, "\u073D"=>230, "\u073E"=>220, "\u073F"=>230, "\u0740"=>230, "\u0741"=>230, "\u0742"=>220, "\u0743"=>230, + "\u0744"=>220, "\u0745"=>230, "\u0746"=>220, "\u0747"=>230, "\u0748"=>220, "\u0749"=>230, "\u074A"=>230, "\u07EB"=>230, + "\u07EC"=>230, "\u07ED"=>230, "\u07EE"=>230, "\u07EF"=>230, "\u07F0"=>230, "\u07F1"=>230, "\u07F2"=>220, "\u07F3"=>230, + "\u0816"=>230, "\u0817"=>230, "\u0818"=>230, "\u0819"=>230, "\u081B"=>230, "\u081C"=>230, "\u081D"=>230, "\u081E"=>230, + "\u081F"=>230, "\u0820"=>230, "\u0821"=>230, "\u0822"=>230, "\u0823"=>230, "\u0825"=>230, "\u0826"=>230, "\u0827"=>230, + "\u0829"=>230, "\u082A"=>230, "\u082B"=>230, "\u082C"=>230, "\u082D"=>230, "\u0859"=>220, "\u085A"=>220, "\u085B"=>220, + "\u08D4"=>230, "\u08D5"=>230, "\u08D6"=>230, "\u08D7"=>230, "\u08D8"=>230, "\u08D9"=>230, "\u08DA"=>230, "\u08DB"=>230, + "\u08DC"=>230, "\u08DD"=>230, "\u08DE"=>230, "\u08DF"=>230, "\u08E0"=>230, "\u08E1"=>230, "\u08E3"=>220, "\u08E4"=>230, + "\u08E5"=>230, "\u08E6"=>220, "\u08E7"=>230, "\u08E8"=>230, "\u08E9"=>220, "\u08EA"=>230, "\u08EB"=>230, "\u08EC"=>230, + "\u08ED"=>220, "\u08EE"=>220, "\u08EF"=>220, "\u08F0"=>27, "\u08F1"=>28, "\u08F2"=>29, "\u08F3"=>230, "\u08F4"=>230, + "\u08F5"=>230, "\u08F6"=>220, "\u08F7"=>230, "\u08F8"=>230, "\u08F9"=>220, "\u08FA"=>220, "\u08FB"=>230, "\u08FC"=>230, + "\u08FD"=>230, "\u08FE"=>230, "\u08FF"=>230, "\u093C"=>7, "\u094D"=>9, "\u0951"=>230, "\u0952"=>220, "\u0953"=>230, + "\u0954"=>230, "\u09BC"=>7, "\u09CD"=>9, "\u0A3C"=>7, "\u0A4D"=>9, "\u0ABC"=>7, "\u0ACD"=>9, "\u0B3C"=>7, + "\u0B4D"=>9, "\u0BCD"=>9, "\u0C4D"=>9, "\u0C55"=>84, "\u0C56"=>91, "\u0CBC"=>7, "\u0CCD"=>9, "\u0D3B"=>9, + "\u0D3C"=>9, "\u0D4D"=>9, "\u0DCA"=>9, "\u0E38"=>103, "\u0E39"=>103, "\u0E3A"=>9, "\u0E48"=>107, "\u0E49"=>107, + "\u0E4A"=>107, "\u0E4B"=>107, "\u0EB8"=>118, "\u0EB9"=>118, "\u0EC8"=>122, "\u0EC9"=>122, "\u0ECA"=>122, "\u0ECB"=>122, + "\u0F18"=>220, "\u0F19"=>220, "\u0F35"=>220, "\u0F37"=>220, "\u0F39"=>216, "\u0F71"=>129, "\u0F72"=>130, "\u0F74"=>132, + "\u0F7A"=>130, "\u0F7B"=>130, "\u0F7C"=>130, "\u0F7D"=>130, "\u0F80"=>130, "\u0F82"=>230, "\u0F83"=>230, "\u0F84"=>9, + "\u0F86"=>230, "\u0F87"=>230, "\u0FC6"=>220, "\u1037"=>7, "\u1039"=>9, "\u103A"=>9, "\u108D"=>220, "\u135D"=>230, + "\u135E"=>230, "\u135F"=>230, "\u1714"=>9, "\u1734"=>9, "\u17D2"=>9, "\u17DD"=>230, "\u18A9"=>228, "\u1939"=>222, + "\u193A"=>230, "\u193B"=>220, "\u1A17"=>230, "\u1A18"=>220, "\u1A60"=>9, "\u1A75"=>230, "\u1A76"=>230, "\u1A77"=>230, + "\u1A78"=>230, "\u1A79"=>230, "\u1A7A"=>230, "\u1A7B"=>230, "\u1A7C"=>230, "\u1A7F"=>220, "\u1AB0"=>230, "\u1AB1"=>230, + "\u1AB2"=>230, "\u1AB3"=>230, "\u1AB4"=>230, "\u1AB5"=>220, "\u1AB6"=>220, "\u1AB7"=>220, "\u1AB8"=>220, "\u1AB9"=>220, + "\u1ABA"=>220, "\u1ABB"=>230, "\u1ABC"=>230, "\u1ABD"=>220, "\u1B34"=>7, "\u1B44"=>9, "\u1B6B"=>230, "\u1B6C"=>220, + "\u1B6D"=>230, "\u1B6E"=>230, "\u1B6F"=>230, "\u1B70"=>230, "\u1B71"=>230, "\u1B72"=>230, "\u1B73"=>230, "\u1BAA"=>9, + "\u1BAB"=>9, "\u1BE6"=>7, "\u1BF2"=>9, "\u1BF3"=>9, "\u1C37"=>7, "\u1CD0"=>230, "\u1CD1"=>230, "\u1CD2"=>230, + "\u1CD4"=>1, "\u1CD5"=>220, "\u1CD6"=>220, "\u1CD7"=>220, "\u1CD8"=>220, "\u1CD9"=>220, "\u1CDA"=>230, "\u1CDB"=>230, + "\u1CDC"=>220, "\u1CDD"=>220, "\u1CDE"=>220, "\u1CDF"=>220, "\u1CE0"=>230, "\u1CE2"=>1, "\u1CE3"=>1, "\u1CE4"=>1, + "\u1CE5"=>1, "\u1CE6"=>1, "\u1CE7"=>1, "\u1CE8"=>1, "\u1CED"=>220, "\u1CF4"=>230, "\u1CF8"=>230, "\u1CF9"=>230, + "\u1DC0"=>230, "\u1DC1"=>230, "\u1DC2"=>220, "\u1DC3"=>230, "\u1DC4"=>230, "\u1DC5"=>230, "\u1DC6"=>230, "\u1DC7"=>230, + "\u1DC8"=>230, "\u1DC9"=>230, "\u1DCA"=>220, "\u1DCB"=>230, "\u1DCC"=>230, "\u1DCD"=>234, "\u1DCE"=>214, "\u1DCF"=>220, + "\u1DD0"=>202, "\u1DD1"=>230, "\u1DD2"=>230, "\u1DD3"=>230, "\u1DD4"=>230, "\u1DD5"=>230, "\u1DD6"=>230, "\u1DD7"=>230, + "\u1DD8"=>230, "\u1DD9"=>230, "\u1DDA"=>230, "\u1DDB"=>230, "\u1DDC"=>230, "\u1DDD"=>230, "\u1DDE"=>230, "\u1DDF"=>230, + "\u1DE0"=>230, "\u1DE1"=>230, "\u1DE2"=>230, "\u1DE3"=>230, "\u1DE4"=>230, "\u1DE5"=>230, "\u1DE6"=>230, "\u1DE7"=>230, + "\u1DE8"=>230, "\u1DE9"=>230, "\u1DEA"=>230, "\u1DEB"=>230, "\u1DEC"=>230, "\u1DED"=>230, "\u1DEE"=>230, "\u1DEF"=>230, + "\u1DF0"=>230, "\u1DF1"=>230, "\u1DF2"=>230, "\u1DF3"=>230, "\u1DF4"=>230, "\u1DF5"=>230, "\u1DF6"=>232, "\u1DF7"=>228, + "\u1DF8"=>228, "\u1DF9"=>220, "\u1DFB"=>230, "\u1DFC"=>233, "\u1DFD"=>220, "\u1DFE"=>230, "\u1DFF"=>220, "\u20D0"=>230, + "\u20D1"=>230, "\u20D2"=>1, "\u20D3"=>1, "\u20D4"=>230, "\u20D5"=>230, "\u20D6"=>230, "\u20D7"=>230, "\u20D8"=>1, + "\u20D9"=>1, "\u20DA"=>1, "\u20DB"=>230, "\u20DC"=>230, "\u20E1"=>230, "\u20E5"=>1, "\u20E6"=>1, "\u20E7"=>230, + "\u20E8"=>220, "\u20E9"=>230, "\u20EA"=>1, "\u20EB"=>1, "\u20EC"=>220, "\u20ED"=>220, "\u20EE"=>220, "\u20EF"=>220, + "\u20F0"=>230, "\u2CEF"=>230, "\u2CF0"=>230, "\u2CF1"=>230, "\u2D7F"=>9, "\u2DE0"=>230, "\u2DE1"=>230, "\u2DE2"=>230, + "\u2DE3"=>230, "\u2DE4"=>230, "\u2DE5"=>230, "\u2DE6"=>230, "\u2DE7"=>230, "\u2DE8"=>230, "\u2DE9"=>230, "\u2DEA"=>230, + "\u2DEB"=>230, "\u2DEC"=>230, "\u2DED"=>230, "\u2DEE"=>230, "\u2DEF"=>230, "\u2DF0"=>230, "\u2DF1"=>230, "\u2DF2"=>230, + "\u2DF3"=>230, "\u2DF4"=>230, "\u2DF5"=>230, "\u2DF6"=>230, "\u2DF7"=>230, "\u2DF8"=>230, "\u2DF9"=>230, "\u2DFA"=>230, + "\u2DFB"=>230, "\u2DFC"=>230, "\u2DFD"=>230, "\u2DFE"=>230, "\u2DFF"=>230, "\u302A"=>218, "\u302B"=>228, "\u302C"=>232, + "\u302D"=>222, "\u302E"=>224, "\u302F"=>224, "\u3099"=>8, "\u309A"=>8, "\uA66F"=>230, "\uA674"=>230, "\uA675"=>230, + "\uA676"=>230, "\uA677"=>230, "\uA678"=>230, "\uA679"=>230, "\uA67A"=>230, "\uA67B"=>230, "\uA67C"=>230, "\uA67D"=>230, + "\uA69E"=>230, "\uA69F"=>230, "\uA6F0"=>230, "\uA6F1"=>230, "\uA806"=>9, "\uA8C4"=>9, "\uA8E0"=>230, "\uA8E1"=>230, + "\uA8E2"=>230, "\uA8E3"=>230, "\uA8E4"=>230, "\uA8E5"=>230, "\uA8E6"=>230, "\uA8E7"=>230, "\uA8E8"=>230, "\uA8E9"=>230, + "\uA8EA"=>230, "\uA8EB"=>230, "\uA8EC"=>230, "\uA8ED"=>230, "\uA8EE"=>230, "\uA8EF"=>230, "\uA8F0"=>230, "\uA8F1"=>230, + "\uA92B"=>220, "\uA92C"=>220, "\uA92D"=>220, "\uA953"=>9, "\uA9B3"=>7, "\uA9C0"=>9, "\uAAB0"=>230, "\uAAB2"=>230, + "\uAAB3"=>230, "\uAAB4"=>220, "\uAAB7"=>230, "\uAAB8"=>230, "\uAABE"=>230, "\uAABF"=>230, "\uAAC1"=>230, "\uAAF6"=>9, + "\uABED"=>9, "\uFB1E"=>26, "\uFE20"=>230, "\uFE21"=>230, "\uFE22"=>230, "\uFE23"=>230, "\uFE24"=>230, "\uFE25"=>230, + "\uFE26"=>230, "\uFE27"=>220, "\uFE28"=>220, "\uFE29"=>220, "\uFE2A"=>220, "\uFE2B"=>220, "\uFE2C"=>220, "\uFE2D"=>220, + "\uFE2E"=>230, "\uFE2F"=>230, "\u{101FD}"=>220, "\u{102E0}"=>220, "\u{10376}"=>230, "\u{10377}"=>230, "\u{10378}"=>230, "\u{10379}"=>230, + "\u{1037A}"=>230, "\u{10A0D}"=>220, "\u{10A0F}"=>230, "\u{10A38}"=>230, "\u{10A39}"=>1, "\u{10A3A}"=>220, "\u{10A3F}"=>9, "\u{10AE5}"=>230, + "\u{10AE6}"=>220, "\u{11046}"=>9, "\u{1107F}"=>9, "\u{110B9}"=>9, "\u{110BA}"=>7, "\u{11100}"=>230, "\u{11101}"=>230, "\u{11102}"=>230, + "\u{11133}"=>9, "\u{11134}"=>9, "\u{11173}"=>7, "\u{111C0}"=>9, "\u{111CA}"=>7, "\u{11235}"=>9, "\u{11236}"=>7, "\u{112E9}"=>7, + "\u{112EA}"=>9, "\u{1133C}"=>7, "\u{1134D}"=>9, "\u{11366}"=>230, "\u{11367}"=>230, "\u{11368}"=>230, "\u{11369}"=>230, "\u{1136A}"=>230, + "\u{1136B}"=>230, "\u{1136C}"=>230, "\u{11370}"=>230, "\u{11371}"=>230, "\u{11372}"=>230, "\u{11373}"=>230, "\u{11374}"=>230, "\u{11442}"=>9, + "\u{11446}"=>7, "\u{114C2}"=>9, "\u{114C3}"=>7, "\u{115BF}"=>9, "\u{115C0}"=>7, "\u{1163F}"=>9, "\u{116B6}"=>9, "\u{116B7}"=>7, + "\u{1172B}"=>9, "\u{11A34}"=>9, "\u{11A47}"=>9, "\u{11A99}"=>9, "\u{11C3F}"=>9, "\u{11D42}"=>7, "\u{11D44}"=>9, "\u{11D45}"=>9, + "\u{16AF0}"=>1, "\u{16AF1}"=>1, "\u{16AF2}"=>1, "\u{16AF3}"=>1, "\u{16AF4}"=>1, "\u{16B30}"=>230, "\u{16B31}"=>230, "\u{16B32}"=>230, + "\u{16B33}"=>230, "\u{16B34}"=>230, "\u{16B35}"=>230, "\u{16B36}"=>230, "\u{1BC9E}"=>1, "\u{1D165}"=>216, "\u{1D166}"=>216, "\u{1D167}"=>1, + "\u{1D168}"=>1, "\u{1D169}"=>1, "\u{1D16D}"=>226, "\u{1D16E}"=>216, "\u{1D16F}"=>216, "\u{1D170}"=>216, "\u{1D171}"=>216, "\u{1D172}"=>216, + "\u{1D17B}"=>220, "\u{1D17C}"=>220, "\u{1D17D}"=>220, "\u{1D17E}"=>220, "\u{1D17F}"=>220, "\u{1D180}"=>220, "\u{1D181}"=>220, "\u{1D182}"=>220, + "\u{1D185}"=>230, "\u{1D186}"=>230, "\u{1D187}"=>230, "\u{1D188}"=>230, "\u{1D189}"=>230, "\u{1D18A}"=>220, "\u{1D18B}"=>220, "\u{1D1AA}"=>230, + "\u{1D1AB}"=>230, "\u{1D1AC}"=>230, "\u{1D1AD}"=>230, "\u{1D242}"=>230, "\u{1D243}"=>230, "\u{1D244}"=>230, "\u{1E000}"=>230, "\u{1E001}"=>230, + "\u{1E002}"=>230, "\u{1E003}"=>230, "\u{1E004}"=>230, "\u{1E005}"=>230, "\u{1E006}"=>230, "\u{1E008}"=>230, "\u{1E009}"=>230, "\u{1E00A}"=>230, + "\u{1E00B}"=>230, "\u{1E00C}"=>230, "\u{1E00D}"=>230, "\u{1E00E}"=>230, "\u{1E00F}"=>230, "\u{1E010}"=>230, "\u{1E011}"=>230, "\u{1E012}"=>230, + "\u{1E013}"=>230, "\u{1E014}"=>230, "\u{1E015}"=>230, "\u{1E016}"=>230, "\u{1E017}"=>230, "\u{1E018}"=>230, "\u{1E01B}"=>230, "\u{1E01C}"=>230, + "\u{1E01D}"=>230, "\u{1E01E}"=>230, "\u{1E01F}"=>230, "\u{1E020}"=>230, "\u{1E021}"=>230, "\u{1E023}"=>230, "\u{1E024}"=>230, "\u{1E026}"=>230, + "\u{1E027}"=>230, "\u{1E028}"=>230, "\u{1E029}"=>230, "\u{1E02A}"=>230, "\u{1E8D0}"=>220, "\u{1E8D1}"=>220, "\u{1E8D2}"=>220, "\u{1E8D3}"=>220, + "\u{1E8D4}"=>220, "\u{1E8D5}"=>220, "\u{1E8D6}"=>220, "\u{1E944}"=>230, "\u{1E945}"=>230, "\u{1E946}"=>230, "\u{1E947}"=>230, "\u{1E948}"=>230, + "\u{1E949}"=>230, "\u{1E94A}"=>7, + } + class_table.default = 0 + CLASS_TABLE = class_table.freeze + + DECOMPOSITION_TABLE = { + "\u00C0"=>"A\u0300", "\u00C1"=>"A\u0301", "\u00C2"=>"A\u0302", "\u00C3"=>"A\u0303", "\u00C4"=>"A\u0308", "\u00C5"=>"A\u030A", "\u00C7"=>"C\u0327", "\u00C8"=>"E\u0300", + "\u00C9"=>"E\u0301", "\u00CA"=>"E\u0302", "\u00CB"=>"E\u0308", "\u00CC"=>"I\u0300", "\u00CD"=>"I\u0301", "\u00CE"=>"I\u0302", "\u00CF"=>"I\u0308", "\u00D1"=>"N\u0303", + "\u00D2"=>"O\u0300", "\u00D3"=>"O\u0301", "\u00D4"=>"O\u0302", "\u00D5"=>"O\u0303", "\u00D6"=>"O\u0308", "\u00D9"=>"U\u0300", "\u00DA"=>"U\u0301", "\u00DB"=>"U\u0302", + "\u00DC"=>"U\u0308", "\u00DD"=>"Y\u0301", "\u00E0"=>"a\u0300", "\u00E1"=>"a\u0301", "\u00E2"=>"a\u0302", "\u00E3"=>"a\u0303", "\u00E4"=>"a\u0308", "\u00E5"=>"a\u030A", + "\u00E7"=>"c\u0327", "\u00E8"=>"e\u0300", "\u00E9"=>"e\u0301", "\u00EA"=>"e\u0302", "\u00EB"=>"e\u0308", "\u00EC"=>"i\u0300", "\u00ED"=>"i\u0301", "\u00EE"=>"i\u0302", + "\u00EF"=>"i\u0308", "\u00F1"=>"n\u0303", "\u00F2"=>"o\u0300", "\u00F3"=>"o\u0301", "\u00F4"=>"o\u0302", "\u00F5"=>"o\u0303", "\u00F6"=>"o\u0308", "\u00F9"=>"u\u0300", + "\u00FA"=>"u\u0301", "\u00FB"=>"u\u0302", "\u00FC"=>"u\u0308", "\u00FD"=>"y\u0301", "\u00FF"=>"y\u0308", "\u0100"=>"A\u0304", "\u0101"=>"a\u0304", "\u0102"=>"A\u0306", + "\u0103"=>"a\u0306", "\u0104"=>"A\u0328", "\u0105"=>"a\u0328", "\u0106"=>"C\u0301", "\u0107"=>"c\u0301", "\u0108"=>"C\u0302", "\u0109"=>"c\u0302", "\u010A"=>"C\u0307", + "\u010B"=>"c\u0307", "\u010C"=>"C\u030C", "\u010D"=>"c\u030C", "\u010E"=>"D\u030C", "\u010F"=>"d\u030C", "\u0112"=>"E\u0304", "\u0113"=>"e\u0304", "\u0114"=>"E\u0306", + "\u0115"=>"e\u0306", "\u0116"=>"E\u0307", "\u0117"=>"e\u0307", "\u0118"=>"E\u0328", "\u0119"=>"e\u0328", "\u011A"=>"E\u030C", "\u011B"=>"e\u030C", "\u011C"=>"G\u0302", + "\u011D"=>"g\u0302", "\u011E"=>"G\u0306", "\u011F"=>"g\u0306", "\u0120"=>"G\u0307", "\u0121"=>"g\u0307", "\u0122"=>"G\u0327", "\u0123"=>"g\u0327", "\u0124"=>"H\u0302", + "\u0125"=>"h\u0302", "\u0128"=>"I\u0303", "\u0129"=>"i\u0303", "\u012A"=>"I\u0304", "\u012B"=>"i\u0304", "\u012C"=>"I\u0306", "\u012D"=>"i\u0306", "\u012E"=>"I\u0328", + "\u012F"=>"i\u0328", "\u0130"=>"I\u0307", "\u0134"=>"J\u0302", "\u0135"=>"j\u0302", "\u0136"=>"K\u0327", "\u0137"=>"k\u0327", "\u0139"=>"L\u0301", "\u013A"=>"l\u0301", + "\u013B"=>"L\u0327", "\u013C"=>"l\u0327", "\u013D"=>"L\u030C", "\u013E"=>"l\u030C", "\u0143"=>"N\u0301", "\u0144"=>"n\u0301", "\u0145"=>"N\u0327", "\u0146"=>"n\u0327", + "\u0147"=>"N\u030C", "\u0148"=>"n\u030C", "\u014C"=>"O\u0304", "\u014D"=>"o\u0304", "\u014E"=>"O\u0306", "\u014F"=>"o\u0306", "\u0150"=>"O\u030B", "\u0151"=>"o\u030B", + "\u0154"=>"R\u0301", "\u0155"=>"r\u0301", "\u0156"=>"R\u0327", "\u0157"=>"r\u0327", "\u0158"=>"R\u030C", "\u0159"=>"r\u030C", "\u015A"=>"S\u0301", "\u015B"=>"s\u0301", + "\u015C"=>"S\u0302", "\u015D"=>"s\u0302", "\u015E"=>"S\u0327", "\u015F"=>"s\u0327", "\u0160"=>"S\u030C", "\u0161"=>"s\u030C", "\u0162"=>"T\u0327", "\u0163"=>"t\u0327", + "\u0164"=>"T\u030C", "\u0165"=>"t\u030C", "\u0168"=>"U\u0303", "\u0169"=>"u\u0303", "\u016A"=>"U\u0304", "\u016B"=>"u\u0304", "\u016C"=>"U\u0306", "\u016D"=>"u\u0306", + "\u016E"=>"U\u030A", "\u016F"=>"u\u030A", "\u0170"=>"U\u030B", "\u0171"=>"u\u030B", "\u0172"=>"U\u0328", "\u0173"=>"u\u0328", "\u0174"=>"W\u0302", "\u0175"=>"w\u0302", + "\u0176"=>"Y\u0302", "\u0177"=>"y\u0302", "\u0178"=>"Y\u0308", "\u0179"=>"Z\u0301", "\u017A"=>"z\u0301", "\u017B"=>"Z\u0307", "\u017C"=>"z\u0307", "\u017D"=>"Z\u030C", + "\u017E"=>"z\u030C", "\u01A0"=>"O\u031B", "\u01A1"=>"o\u031B", "\u01AF"=>"U\u031B", "\u01B0"=>"u\u031B", "\u01CD"=>"A\u030C", "\u01CE"=>"a\u030C", "\u01CF"=>"I\u030C", + "\u01D0"=>"i\u030C", "\u01D1"=>"O\u030C", "\u01D2"=>"o\u030C", "\u01D3"=>"U\u030C", "\u01D4"=>"u\u030C", "\u01D5"=>"U\u0308\u0304", "\u01D6"=>"u\u0308\u0304", "\u01D7"=>"U\u0308\u0301", + "\u01D8"=>"u\u0308\u0301", "\u01D9"=>"U\u0308\u030C", "\u01DA"=>"u\u0308\u030C", "\u01DB"=>"U\u0308\u0300", "\u01DC"=>"u\u0308\u0300", "\u01DE"=>"A\u0308\u0304", "\u01DF"=>"a\u0308\u0304", "\u01E0"=>"A\u0307\u0304", + "\u01E1"=>"a\u0307\u0304", "\u01E2"=>"\u00C6\u0304", "\u01E3"=>"\u00E6\u0304", "\u01E6"=>"G\u030C", "\u01E7"=>"g\u030C", "\u01E8"=>"K\u030C", "\u01E9"=>"k\u030C", "\u01EA"=>"O\u0328", + "\u01EB"=>"o\u0328", "\u01EC"=>"O\u0328\u0304", "\u01ED"=>"o\u0328\u0304", "\u01EE"=>"\u01B7\u030C", "\u01EF"=>"\u0292\u030C", "\u01F0"=>"j\u030C", "\u01F4"=>"G\u0301", "\u01F5"=>"g\u0301", + "\u01F8"=>"N\u0300", "\u01F9"=>"n\u0300", "\u01FA"=>"A\u030A\u0301", "\u01FB"=>"a\u030A\u0301", "\u01FC"=>"\u00C6\u0301", "\u01FD"=>"\u00E6\u0301", "\u01FE"=>"\u00D8\u0301", "\u01FF"=>"\u00F8\u0301", + "\u0200"=>"A\u030F", "\u0201"=>"a\u030F", "\u0202"=>"A\u0311", "\u0203"=>"a\u0311", "\u0204"=>"E\u030F", "\u0205"=>"e\u030F", "\u0206"=>"E\u0311", "\u0207"=>"e\u0311", + "\u0208"=>"I\u030F", "\u0209"=>"i\u030F", "\u020A"=>"I\u0311", "\u020B"=>"i\u0311", "\u020C"=>"O\u030F", "\u020D"=>"o\u030F", "\u020E"=>"O\u0311", "\u020F"=>"o\u0311", + "\u0210"=>"R\u030F", "\u0211"=>"r\u030F", "\u0212"=>"R\u0311", "\u0213"=>"r\u0311", "\u0214"=>"U\u030F", "\u0215"=>"u\u030F", "\u0216"=>"U\u0311", "\u0217"=>"u\u0311", + "\u0218"=>"S\u0326", "\u0219"=>"s\u0326", "\u021A"=>"T\u0326", "\u021B"=>"t\u0326", "\u021E"=>"H\u030C", "\u021F"=>"h\u030C", "\u0226"=>"A\u0307", "\u0227"=>"a\u0307", + "\u0228"=>"E\u0327", "\u0229"=>"e\u0327", "\u022A"=>"O\u0308\u0304", "\u022B"=>"o\u0308\u0304", "\u022C"=>"O\u0303\u0304", "\u022D"=>"o\u0303\u0304", "\u022E"=>"O\u0307", "\u022F"=>"o\u0307", + "\u0230"=>"O\u0307\u0304", "\u0231"=>"o\u0307\u0304", "\u0232"=>"Y\u0304", "\u0233"=>"y\u0304", "\u0340"=>"\u0300", "\u0341"=>"\u0301", "\u0343"=>"\u0313", "\u0344"=>"\u0308\u0301", + "\u0374"=>"\u02B9", "\u037E"=>";", "\u0385"=>"\u00A8\u0301", "\u0386"=>"\u0391\u0301", "\u0387"=>"\u00B7", "\u0388"=>"\u0395\u0301", "\u0389"=>"\u0397\u0301", "\u038A"=>"\u0399\u0301", + "\u038C"=>"\u039F\u0301", "\u038E"=>"\u03A5\u0301", "\u038F"=>"\u03A9\u0301", "\u0390"=>"\u03B9\u0308\u0301", "\u03AA"=>"\u0399\u0308", "\u03AB"=>"\u03A5\u0308", "\u03AC"=>"\u03B1\u0301", "\u03AD"=>"\u03B5\u0301", + "\u03AE"=>"\u03B7\u0301", "\u03AF"=>"\u03B9\u0301", "\u03B0"=>"\u03C5\u0308\u0301", "\u03CA"=>"\u03B9\u0308", "\u03CB"=>"\u03C5\u0308", "\u03CC"=>"\u03BF\u0301", "\u03CD"=>"\u03C5\u0301", "\u03CE"=>"\u03C9\u0301", + "\u03D3"=>"\u03D2\u0301", "\u03D4"=>"\u03D2\u0308", "\u0400"=>"\u0415\u0300", "\u0401"=>"\u0415\u0308", "\u0403"=>"\u0413\u0301", "\u0407"=>"\u0406\u0308", "\u040C"=>"\u041A\u0301", "\u040D"=>"\u0418\u0300", + "\u040E"=>"\u0423\u0306", "\u0419"=>"\u0418\u0306", "\u0439"=>"\u0438\u0306", "\u0450"=>"\u0435\u0300", "\u0451"=>"\u0435\u0308", "\u0453"=>"\u0433\u0301", "\u0457"=>"\u0456\u0308", "\u045C"=>"\u043A\u0301", + "\u045D"=>"\u0438\u0300", "\u045E"=>"\u0443\u0306", "\u0476"=>"\u0474\u030F", "\u0477"=>"\u0475\u030F", "\u04C1"=>"\u0416\u0306", "\u04C2"=>"\u0436\u0306", "\u04D0"=>"\u0410\u0306", "\u04D1"=>"\u0430\u0306", + "\u04D2"=>"\u0410\u0308", "\u04D3"=>"\u0430\u0308", "\u04D6"=>"\u0415\u0306", "\u04D7"=>"\u0435\u0306", "\u04DA"=>"\u04D8\u0308", "\u04DB"=>"\u04D9\u0308", "\u04DC"=>"\u0416\u0308", "\u04DD"=>"\u0436\u0308", + "\u04DE"=>"\u0417\u0308", "\u04DF"=>"\u0437\u0308", "\u04E2"=>"\u0418\u0304", "\u04E3"=>"\u0438\u0304", "\u04E4"=>"\u0418\u0308", "\u04E5"=>"\u0438\u0308", "\u04E6"=>"\u041E\u0308", "\u04E7"=>"\u043E\u0308", + "\u04EA"=>"\u04E8\u0308", "\u04EB"=>"\u04E9\u0308", "\u04EC"=>"\u042D\u0308", "\u04ED"=>"\u044D\u0308", "\u04EE"=>"\u0423\u0304", "\u04EF"=>"\u0443\u0304", "\u04F0"=>"\u0423\u0308", "\u04F1"=>"\u0443\u0308", + "\u04F2"=>"\u0423\u030B", "\u04F3"=>"\u0443\u030B", "\u04F4"=>"\u0427\u0308", "\u04F5"=>"\u0447\u0308", "\u04F8"=>"\u042B\u0308", "\u04F9"=>"\u044B\u0308", "\u0622"=>"\u0627\u0653", "\u0623"=>"\u0627\u0654", + "\u0624"=>"\u0648\u0654", "\u0625"=>"\u0627\u0655", "\u0626"=>"\u064A\u0654", "\u06C0"=>"\u06D5\u0654", "\u06C2"=>"\u06C1\u0654", "\u06D3"=>"\u06D2\u0654", "\u0929"=>"\u0928\u093C", "\u0931"=>"\u0930\u093C", + "\u0934"=>"\u0933\u093C", "\u0958"=>"\u0915\u093C", "\u0959"=>"\u0916\u093C", "\u095A"=>"\u0917\u093C", "\u095B"=>"\u091C\u093C", "\u095C"=>"\u0921\u093C", "\u095D"=>"\u0922\u093C", "\u095E"=>"\u092B\u093C", + "\u095F"=>"\u092F\u093C", "\u09CB"=>"\u09C7\u09BE", "\u09CC"=>"\u09C7\u09D7", "\u09DC"=>"\u09A1\u09BC", "\u09DD"=>"\u09A2\u09BC", "\u09DF"=>"\u09AF\u09BC", "\u0A33"=>"\u0A32\u0A3C", "\u0A36"=>"\u0A38\u0A3C", + "\u0A59"=>"\u0A16\u0A3C", "\u0A5A"=>"\u0A17\u0A3C", "\u0A5B"=>"\u0A1C\u0A3C", "\u0A5E"=>"\u0A2B\u0A3C", "\u0B48"=>"\u0B47\u0B56", "\u0B4B"=>"\u0B47\u0B3E", "\u0B4C"=>"\u0B47\u0B57", "\u0B5C"=>"\u0B21\u0B3C", + "\u0B5D"=>"\u0B22\u0B3C", "\u0B94"=>"\u0B92\u0BD7", "\u0BCA"=>"\u0BC6\u0BBE", "\u0BCB"=>"\u0BC7\u0BBE", "\u0BCC"=>"\u0BC6\u0BD7", "\u0C48"=>"\u0C46\u0C56", "\u0CC0"=>"\u0CBF\u0CD5", "\u0CC7"=>"\u0CC6\u0CD5", + "\u0CC8"=>"\u0CC6\u0CD6", "\u0CCA"=>"\u0CC6\u0CC2", "\u0CCB"=>"\u0CC6\u0CC2\u0CD5", "\u0D4A"=>"\u0D46\u0D3E", "\u0D4B"=>"\u0D47\u0D3E", "\u0D4C"=>"\u0D46\u0D57", "\u0DDA"=>"\u0DD9\u0DCA", "\u0DDC"=>"\u0DD9\u0DCF", + "\u0DDD"=>"\u0DD9\u0DCF\u0DCA", "\u0DDE"=>"\u0DD9\u0DDF", "\u0F43"=>"\u0F42\u0FB7", "\u0F4D"=>"\u0F4C\u0FB7", "\u0F52"=>"\u0F51\u0FB7", "\u0F57"=>"\u0F56\u0FB7", "\u0F5C"=>"\u0F5B\u0FB7", "\u0F69"=>"\u0F40\u0FB5", + "\u0F73"=>"\u0F71\u0F72", "\u0F75"=>"\u0F71\u0F74", "\u0F76"=>"\u0FB2\u0F80", "\u0F78"=>"\u0FB3\u0F80", "\u0F81"=>"\u0F71\u0F80", "\u0F93"=>"\u0F92\u0FB7", "\u0F9D"=>"\u0F9C\u0FB7", "\u0FA2"=>"\u0FA1\u0FB7", + "\u0FA7"=>"\u0FA6\u0FB7", "\u0FAC"=>"\u0FAB\u0FB7", "\u0FB9"=>"\u0F90\u0FB5", "\u1026"=>"\u1025\u102E", "\u1B06"=>"\u1B05\u1B35", "\u1B08"=>"\u1B07\u1B35", "\u1B0A"=>"\u1B09\u1B35", "\u1B0C"=>"\u1B0B\u1B35", + "\u1B0E"=>"\u1B0D\u1B35", "\u1B12"=>"\u1B11\u1B35", "\u1B3B"=>"\u1B3A\u1B35", "\u1B3D"=>"\u1B3C\u1B35", "\u1B40"=>"\u1B3E\u1B35", "\u1B41"=>"\u1B3F\u1B35", "\u1B43"=>"\u1B42\u1B35", "\u1E00"=>"A\u0325", + "\u1E01"=>"a\u0325", "\u1E02"=>"B\u0307", "\u1E03"=>"b\u0307", "\u1E04"=>"B\u0323", "\u1E05"=>"b\u0323", "\u1E06"=>"B\u0331", "\u1E07"=>"b\u0331", "\u1E08"=>"C\u0327\u0301", + "\u1E09"=>"c\u0327\u0301", "\u1E0A"=>"D\u0307", "\u1E0B"=>"d\u0307", "\u1E0C"=>"D\u0323", "\u1E0D"=>"d\u0323", "\u1E0E"=>"D\u0331", "\u1E0F"=>"d\u0331", "\u1E10"=>"D\u0327", + "\u1E11"=>"d\u0327", "\u1E12"=>"D\u032D", "\u1E13"=>"d\u032D", "\u1E14"=>"E\u0304\u0300", "\u1E15"=>"e\u0304\u0300", "\u1E16"=>"E\u0304\u0301", "\u1E17"=>"e\u0304\u0301", "\u1E18"=>"E\u032D", + "\u1E19"=>"e\u032D", "\u1E1A"=>"E\u0330", "\u1E1B"=>"e\u0330", "\u1E1C"=>"E\u0327\u0306", "\u1E1D"=>"e\u0327\u0306", "\u1E1E"=>"F\u0307", "\u1E1F"=>"f\u0307", "\u1E20"=>"G\u0304", + "\u1E21"=>"g\u0304", "\u1E22"=>"H\u0307", "\u1E23"=>"h\u0307", "\u1E24"=>"H\u0323", "\u1E25"=>"h\u0323", "\u1E26"=>"H\u0308", "\u1E27"=>"h\u0308", "\u1E28"=>"H\u0327", + "\u1E29"=>"h\u0327", "\u1E2A"=>"H\u032E", "\u1E2B"=>"h\u032E", "\u1E2C"=>"I\u0330", "\u1E2D"=>"i\u0330", "\u1E2E"=>"I\u0308\u0301", "\u1E2F"=>"i\u0308\u0301", "\u1E30"=>"K\u0301", + "\u1E31"=>"k\u0301", "\u1E32"=>"K\u0323", "\u1E33"=>"k\u0323", "\u1E34"=>"K\u0331", "\u1E35"=>"k\u0331", "\u1E36"=>"L\u0323", "\u1E37"=>"l\u0323", "\u1E38"=>"L\u0323\u0304", + "\u1E39"=>"l\u0323\u0304", "\u1E3A"=>"L\u0331", "\u1E3B"=>"l\u0331", "\u1E3C"=>"L\u032D", "\u1E3D"=>"l\u032D", "\u1E3E"=>"M\u0301", "\u1E3F"=>"m\u0301", "\u1E40"=>"M\u0307", + "\u1E41"=>"m\u0307", "\u1E42"=>"M\u0323", "\u1E43"=>"m\u0323", "\u1E44"=>"N\u0307", "\u1E45"=>"n\u0307", "\u1E46"=>"N\u0323", "\u1E47"=>"n\u0323", "\u1E48"=>"N\u0331", + "\u1E49"=>"n\u0331", "\u1E4A"=>"N\u032D", "\u1E4B"=>"n\u032D", "\u1E4C"=>"O\u0303\u0301", "\u1E4D"=>"o\u0303\u0301", "\u1E4E"=>"O\u0303\u0308", "\u1E4F"=>"o\u0303\u0308", "\u1E50"=>"O\u0304\u0300", + "\u1E51"=>"o\u0304\u0300", "\u1E52"=>"O\u0304\u0301", "\u1E53"=>"o\u0304\u0301", "\u1E54"=>"P\u0301", "\u1E55"=>"p\u0301", "\u1E56"=>"P\u0307", "\u1E57"=>"p\u0307", "\u1E58"=>"R\u0307", + "\u1E59"=>"r\u0307", "\u1E5A"=>"R\u0323", "\u1E5B"=>"r\u0323", "\u1E5C"=>"R\u0323\u0304", "\u1E5D"=>"r\u0323\u0304", "\u1E5E"=>"R\u0331", "\u1E5F"=>"r\u0331", "\u1E60"=>"S\u0307", + "\u1E61"=>"s\u0307", "\u1E62"=>"S\u0323", "\u1E63"=>"s\u0323", "\u1E64"=>"S\u0301\u0307", "\u1E65"=>"s\u0301\u0307", "\u1E66"=>"S\u030C\u0307", "\u1E67"=>"s\u030C\u0307", "\u1E68"=>"S\u0323\u0307", + "\u1E69"=>"s\u0323\u0307", "\u1E6A"=>"T\u0307", "\u1E6B"=>"t\u0307", "\u1E6C"=>"T\u0323", "\u1E6D"=>"t\u0323", "\u1E6E"=>"T\u0331", "\u1E6F"=>"t\u0331", "\u1E70"=>"T\u032D", + "\u1E71"=>"t\u032D", "\u1E72"=>"U\u0324", "\u1E73"=>"u\u0324", "\u1E74"=>"U\u0330", "\u1E75"=>"u\u0330", "\u1E76"=>"U\u032D", "\u1E77"=>"u\u032D", "\u1E78"=>"U\u0303\u0301", + "\u1E79"=>"u\u0303\u0301", "\u1E7A"=>"U\u0304\u0308", "\u1E7B"=>"u\u0304\u0308", "\u1E7C"=>"V\u0303", "\u1E7D"=>"v\u0303", "\u1E7E"=>"V\u0323", "\u1E7F"=>"v\u0323", "\u1E80"=>"W\u0300", + "\u1E81"=>"w\u0300", "\u1E82"=>"W\u0301", "\u1E83"=>"w\u0301", "\u1E84"=>"W\u0308", "\u1E85"=>"w\u0308", "\u1E86"=>"W\u0307", "\u1E87"=>"w\u0307", "\u1E88"=>"W\u0323", + "\u1E89"=>"w\u0323", "\u1E8A"=>"X\u0307", "\u1E8B"=>"x\u0307", "\u1E8C"=>"X\u0308", "\u1E8D"=>"x\u0308", "\u1E8E"=>"Y\u0307", "\u1E8F"=>"y\u0307", "\u1E90"=>"Z\u0302", + "\u1E91"=>"z\u0302", "\u1E92"=>"Z\u0323", "\u1E93"=>"z\u0323", "\u1E94"=>"Z\u0331", "\u1E95"=>"z\u0331", "\u1E96"=>"h\u0331", "\u1E97"=>"t\u0308", "\u1E98"=>"w\u030A", + "\u1E99"=>"y\u030A", "\u1E9B"=>"\u017F\u0307", "\u1EA0"=>"A\u0323", "\u1EA1"=>"a\u0323", "\u1EA2"=>"A\u0309", "\u1EA3"=>"a\u0309", "\u1EA4"=>"A\u0302\u0301", "\u1EA5"=>"a\u0302\u0301", + "\u1EA6"=>"A\u0302\u0300", "\u1EA7"=>"a\u0302\u0300", "\u1EA8"=>"A\u0302\u0309", "\u1EA9"=>"a\u0302\u0309", "\u1EAA"=>"A\u0302\u0303", "\u1EAB"=>"a\u0302\u0303", "\u1EAC"=>"A\u0323\u0302", "\u1EAD"=>"a\u0323\u0302", + "\u1EAE"=>"A\u0306\u0301", "\u1EAF"=>"a\u0306\u0301", "\u1EB0"=>"A\u0306\u0300", "\u1EB1"=>"a\u0306\u0300", "\u1EB2"=>"A\u0306\u0309", "\u1EB3"=>"a\u0306\u0309", "\u1EB4"=>"A\u0306\u0303", "\u1EB5"=>"a\u0306\u0303", + "\u1EB6"=>"A\u0323\u0306", "\u1EB7"=>"a\u0323\u0306", "\u1EB8"=>"E\u0323", "\u1EB9"=>"e\u0323", "\u1EBA"=>"E\u0309", "\u1EBB"=>"e\u0309", "\u1EBC"=>"E\u0303", "\u1EBD"=>"e\u0303", + "\u1EBE"=>"E\u0302\u0301", "\u1EBF"=>"e\u0302\u0301", "\u1EC0"=>"E\u0302\u0300", "\u1EC1"=>"e\u0302\u0300", "\u1EC2"=>"E\u0302\u0309", "\u1EC3"=>"e\u0302\u0309", "\u1EC4"=>"E\u0302\u0303", "\u1EC5"=>"e\u0302\u0303", + "\u1EC6"=>"E\u0323\u0302", "\u1EC7"=>"e\u0323\u0302", "\u1EC8"=>"I\u0309", "\u1EC9"=>"i\u0309", "\u1ECA"=>"I\u0323", "\u1ECB"=>"i\u0323", "\u1ECC"=>"O\u0323", "\u1ECD"=>"o\u0323", + "\u1ECE"=>"O\u0309", "\u1ECF"=>"o\u0309", "\u1ED0"=>"O\u0302\u0301", "\u1ED1"=>"o\u0302\u0301", "\u1ED2"=>"O\u0302\u0300", "\u1ED3"=>"o\u0302\u0300", "\u1ED4"=>"O\u0302\u0309", "\u1ED5"=>"o\u0302\u0309", + "\u1ED6"=>"O\u0302\u0303", "\u1ED7"=>"o\u0302\u0303", "\u1ED8"=>"O\u0323\u0302", "\u1ED9"=>"o\u0323\u0302", "\u1EDA"=>"O\u031B\u0301", "\u1EDB"=>"o\u031B\u0301", "\u1EDC"=>"O\u031B\u0300", "\u1EDD"=>"o\u031B\u0300", + "\u1EDE"=>"O\u031B\u0309", "\u1EDF"=>"o\u031B\u0309", "\u1EE0"=>"O\u031B\u0303", "\u1EE1"=>"o\u031B\u0303", "\u1EE2"=>"O\u031B\u0323", "\u1EE3"=>"o\u031B\u0323", "\u1EE4"=>"U\u0323", "\u1EE5"=>"u\u0323", + "\u1EE6"=>"U\u0309", "\u1EE7"=>"u\u0309", "\u1EE8"=>"U\u031B\u0301", "\u1EE9"=>"u\u031B\u0301", "\u1EEA"=>"U\u031B\u0300", "\u1EEB"=>"u\u031B\u0300", "\u1EEC"=>"U\u031B\u0309", "\u1EED"=>"u\u031B\u0309", + "\u1EEE"=>"U\u031B\u0303", "\u1EEF"=>"u\u031B\u0303", "\u1EF0"=>"U\u031B\u0323", "\u1EF1"=>"u\u031B\u0323", "\u1EF2"=>"Y\u0300", "\u1EF3"=>"y\u0300", "\u1EF4"=>"Y\u0323", "\u1EF5"=>"y\u0323", + "\u1EF6"=>"Y\u0309", "\u1EF7"=>"y\u0309", "\u1EF8"=>"Y\u0303", "\u1EF9"=>"y\u0303", "\u1F00"=>"\u03B1\u0313", "\u1F01"=>"\u03B1\u0314", "\u1F02"=>"\u03B1\u0313\u0300", "\u1F03"=>"\u03B1\u0314\u0300", + "\u1F04"=>"\u03B1\u0313\u0301", "\u1F05"=>"\u03B1\u0314\u0301", "\u1F06"=>"\u03B1\u0313\u0342", "\u1F07"=>"\u03B1\u0314\u0342", "\u1F08"=>"\u0391\u0313", "\u1F09"=>"\u0391\u0314", "\u1F0A"=>"\u0391\u0313\u0300", "\u1F0B"=>"\u0391\u0314\u0300", + "\u1F0C"=>"\u0391\u0313\u0301", "\u1F0D"=>"\u0391\u0314\u0301", "\u1F0E"=>"\u0391\u0313\u0342", "\u1F0F"=>"\u0391\u0314\u0342", "\u1F10"=>"\u03B5\u0313", "\u1F11"=>"\u03B5\u0314", "\u1F12"=>"\u03B5\u0313\u0300", "\u1F13"=>"\u03B5\u0314\u0300", + "\u1F14"=>"\u03B5\u0313\u0301", "\u1F15"=>"\u03B5\u0314\u0301", "\u1F18"=>"\u0395\u0313", "\u1F19"=>"\u0395\u0314", "\u1F1A"=>"\u0395\u0313\u0300", "\u1F1B"=>"\u0395\u0314\u0300", "\u1F1C"=>"\u0395\u0313\u0301", "\u1F1D"=>"\u0395\u0314\u0301", + "\u1F20"=>"\u03B7\u0313", "\u1F21"=>"\u03B7\u0314", "\u1F22"=>"\u03B7\u0313\u0300", "\u1F23"=>"\u03B7\u0314\u0300", "\u1F24"=>"\u03B7\u0313\u0301", "\u1F25"=>"\u03B7\u0314\u0301", "\u1F26"=>"\u03B7\u0313\u0342", "\u1F27"=>"\u03B7\u0314\u0342", + "\u1F28"=>"\u0397\u0313", "\u1F29"=>"\u0397\u0314", "\u1F2A"=>"\u0397\u0313\u0300", "\u1F2B"=>"\u0397\u0314\u0300", "\u1F2C"=>"\u0397\u0313\u0301", "\u1F2D"=>"\u0397\u0314\u0301", "\u1F2E"=>"\u0397\u0313\u0342", "\u1F2F"=>"\u0397\u0314\u0342", + "\u1F30"=>"\u03B9\u0313", "\u1F31"=>"\u03B9\u0314", "\u1F32"=>"\u03B9\u0313\u0300", "\u1F33"=>"\u03B9\u0314\u0300", "\u1F34"=>"\u03B9\u0313\u0301", "\u1F35"=>"\u03B9\u0314\u0301", "\u1F36"=>"\u03B9\u0313\u0342", "\u1F37"=>"\u03B9\u0314\u0342", + "\u1F38"=>"\u0399\u0313", "\u1F39"=>"\u0399\u0314", "\u1F3A"=>"\u0399\u0313\u0300", "\u1F3B"=>"\u0399\u0314\u0300", "\u1F3C"=>"\u0399\u0313\u0301", "\u1F3D"=>"\u0399\u0314\u0301", "\u1F3E"=>"\u0399\u0313\u0342", "\u1F3F"=>"\u0399\u0314\u0342", + "\u1F40"=>"\u03BF\u0313", "\u1F41"=>"\u03BF\u0314", "\u1F42"=>"\u03BF\u0313\u0300", "\u1F43"=>"\u03BF\u0314\u0300", "\u1F44"=>"\u03BF\u0313\u0301", "\u1F45"=>"\u03BF\u0314\u0301", "\u1F48"=>"\u039F\u0313", "\u1F49"=>"\u039F\u0314", + "\u1F4A"=>"\u039F\u0313\u0300", "\u1F4B"=>"\u039F\u0314\u0300", "\u1F4C"=>"\u039F\u0313\u0301", "\u1F4D"=>"\u039F\u0314\u0301", "\u1F50"=>"\u03C5\u0313", "\u1F51"=>"\u03C5\u0314", "\u1F52"=>"\u03C5\u0313\u0300", "\u1F53"=>"\u03C5\u0314\u0300", + "\u1F54"=>"\u03C5\u0313\u0301", "\u1F55"=>"\u03C5\u0314\u0301", "\u1F56"=>"\u03C5\u0313\u0342", "\u1F57"=>"\u03C5\u0314\u0342", "\u1F59"=>"\u03A5\u0314", "\u1F5B"=>"\u03A5\u0314\u0300", "\u1F5D"=>"\u03A5\u0314\u0301", "\u1F5F"=>"\u03A5\u0314\u0342", + "\u1F60"=>"\u03C9\u0313", "\u1F61"=>"\u03C9\u0314", "\u1F62"=>"\u03C9\u0313\u0300", "\u1F63"=>"\u03C9\u0314\u0300", "\u1F64"=>"\u03C9\u0313\u0301", "\u1F65"=>"\u03C9\u0314\u0301", "\u1F66"=>"\u03C9\u0313\u0342", "\u1F67"=>"\u03C9\u0314\u0342", + "\u1F68"=>"\u03A9\u0313", "\u1F69"=>"\u03A9\u0314", "\u1F6A"=>"\u03A9\u0313\u0300", "\u1F6B"=>"\u03A9\u0314\u0300", "\u1F6C"=>"\u03A9\u0313\u0301", "\u1F6D"=>"\u03A9\u0314\u0301", "\u1F6E"=>"\u03A9\u0313\u0342", "\u1F6F"=>"\u03A9\u0314\u0342", + "\u1F70"=>"\u03B1\u0300", "\u1F71"=>"\u03B1\u0301", "\u1F72"=>"\u03B5\u0300", "\u1F73"=>"\u03B5\u0301", "\u1F74"=>"\u03B7\u0300", "\u1F75"=>"\u03B7\u0301", "\u1F76"=>"\u03B9\u0300", "\u1F77"=>"\u03B9\u0301", + "\u1F78"=>"\u03BF\u0300", "\u1F79"=>"\u03BF\u0301", "\u1F7A"=>"\u03C5\u0300", "\u1F7B"=>"\u03C5\u0301", "\u1F7C"=>"\u03C9\u0300", "\u1F7D"=>"\u03C9\u0301", "\u1F80"=>"\u03B1\u0313\u0345", "\u1F81"=>"\u03B1\u0314\u0345", + "\u1F82"=>"\u03B1\u0313\u0300\u0345", "\u1F83"=>"\u03B1\u0314\u0300\u0345", "\u1F84"=>"\u03B1\u0313\u0301\u0345", "\u1F85"=>"\u03B1\u0314\u0301\u0345", "\u1F86"=>"\u03B1\u0313\u0342\u0345", "\u1F87"=>"\u03B1\u0314\u0342\u0345", "\u1F88"=>"\u0391\u0313\u0345", "\u1F89"=>"\u0391\u0314\u0345", + "\u1F8A"=>"\u0391\u0313\u0300\u0345", "\u1F8B"=>"\u0391\u0314\u0300\u0345", "\u1F8C"=>"\u0391\u0313\u0301\u0345", "\u1F8D"=>"\u0391\u0314\u0301\u0345", "\u1F8E"=>"\u0391\u0313\u0342\u0345", "\u1F8F"=>"\u0391\u0314\u0342\u0345", "\u1F90"=>"\u03B7\u0313\u0345", "\u1F91"=>"\u03B7\u0314\u0345", + "\u1F92"=>"\u03B7\u0313\u0300\u0345", "\u1F93"=>"\u03B7\u0314\u0300\u0345", "\u1F94"=>"\u03B7\u0313\u0301\u0345", "\u1F95"=>"\u03B7\u0314\u0301\u0345", "\u1F96"=>"\u03B7\u0313\u0342\u0345", "\u1F97"=>"\u03B7\u0314\u0342\u0345", "\u1F98"=>"\u0397\u0313\u0345", "\u1F99"=>"\u0397\u0314\u0345", + "\u1F9A"=>"\u0397\u0313\u0300\u0345", "\u1F9B"=>"\u0397\u0314\u0300\u0345", "\u1F9C"=>"\u0397\u0313\u0301\u0345", "\u1F9D"=>"\u0397\u0314\u0301\u0345", "\u1F9E"=>"\u0397\u0313\u0342\u0345", "\u1F9F"=>"\u0397\u0314\u0342\u0345", "\u1FA0"=>"\u03C9\u0313\u0345", "\u1FA1"=>"\u03C9\u0314\u0345", + "\u1FA2"=>"\u03C9\u0313\u0300\u0345", "\u1FA3"=>"\u03C9\u0314\u0300\u0345", "\u1FA4"=>"\u03C9\u0313\u0301\u0345", "\u1FA5"=>"\u03C9\u0314\u0301\u0345", "\u1FA6"=>"\u03C9\u0313\u0342\u0345", "\u1FA7"=>"\u03C9\u0314\u0342\u0345", "\u1FA8"=>"\u03A9\u0313\u0345", "\u1FA9"=>"\u03A9\u0314\u0345", + "\u1FAA"=>"\u03A9\u0313\u0300\u0345", "\u1FAB"=>"\u03A9\u0314\u0300\u0345", "\u1FAC"=>"\u03A9\u0313\u0301\u0345", "\u1FAD"=>"\u03A9\u0314\u0301\u0345", "\u1FAE"=>"\u03A9\u0313\u0342\u0345", "\u1FAF"=>"\u03A9\u0314\u0342\u0345", "\u1FB0"=>"\u03B1\u0306", "\u1FB1"=>"\u03B1\u0304", + "\u1FB2"=>"\u03B1\u0300\u0345", "\u1FB3"=>"\u03B1\u0345", "\u1FB4"=>"\u03B1\u0301\u0345", "\u1FB6"=>"\u03B1\u0342", "\u1FB7"=>"\u03B1\u0342\u0345", "\u1FB8"=>"\u0391\u0306", "\u1FB9"=>"\u0391\u0304", "\u1FBA"=>"\u0391\u0300", + "\u1FBB"=>"\u0391\u0301", "\u1FBC"=>"\u0391\u0345", "\u1FBE"=>"\u03B9", "\u1FC1"=>"\u00A8\u0342", "\u1FC2"=>"\u03B7\u0300\u0345", "\u1FC3"=>"\u03B7\u0345", "\u1FC4"=>"\u03B7\u0301\u0345", "\u1FC6"=>"\u03B7\u0342", + "\u1FC7"=>"\u03B7\u0342\u0345", "\u1FC8"=>"\u0395\u0300", "\u1FC9"=>"\u0395\u0301", "\u1FCA"=>"\u0397\u0300", "\u1FCB"=>"\u0397\u0301", "\u1FCC"=>"\u0397\u0345", "\u1FCD"=>"\u1FBF\u0300", "\u1FCE"=>"\u1FBF\u0301", + "\u1FCF"=>"\u1FBF\u0342", "\u1FD0"=>"\u03B9\u0306", "\u1FD1"=>"\u03B9\u0304", "\u1FD2"=>"\u03B9\u0308\u0300", "\u1FD3"=>"\u03B9\u0308\u0301", "\u1FD6"=>"\u03B9\u0342", "\u1FD7"=>"\u03B9\u0308\u0342", "\u1FD8"=>"\u0399\u0306", + "\u1FD9"=>"\u0399\u0304", "\u1FDA"=>"\u0399\u0300", "\u1FDB"=>"\u0399\u0301", "\u1FDD"=>"\u1FFE\u0300", "\u1FDE"=>"\u1FFE\u0301", "\u1FDF"=>"\u1FFE\u0342", "\u1FE0"=>"\u03C5\u0306", "\u1FE1"=>"\u03C5\u0304", + "\u1FE2"=>"\u03C5\u0308\u0300", "\u1FE3"=>"\u03C5\u0308\u0301", "\u1FE4"=>"\u03C1\u0313", "\u1FE5"=>"\u03C1\u0314", "\u1FE6"=>"\u03C5\u0342", "\u1FE7"=>"\u03C5\u0308\u0342", "\u1FE8"=>"\u03A5\u0306", "\u1FE9"=>"\u03A5\u0304", + "\u1FEA"=>"\u03A5\u0300", "\u1FEB"=>"\u03A5\u0301", "\u1FEC"=>"\u03A1\u0314", "\u1FED"=>"\u00A8\u0300", "\u1FEE"=>"\u00A8\u0301", "\u1FEF"=>"`", "\u1FF2"=>"\u03C9\u0300\u0345", "\u1FF3"=>"\u03C9\u0345", + "\u1FF4"=>"\u03C9\u0301\u0345", "\u1FF6"=>"\u03C9\u0342", "\u1FF7"=>"\u03C9\u0342\u0345", "\u1FF8"=>"\u039F\u0300", "\u1FF9"=>"\u039F\u0301", "\u1FFA"=>"\u03A9\u0300", "\u1FFB"=>"\u03A9\u0301", "\u1FFC"=>"\u03A9\u0345", + "\u1FFD"=>"\u00B4", "\u2000"=>"\u2002", "\u2001"=>"\u2003", "\u2126"=>"\u03A9", "\u212A"=>"K", "\u212B"=>"A\u030A", "\u219A"=>"\u2190\u0338", "\u219B"=>"\u2192\u0338", + "\u21AE"=>"\u2194\u0338", "\u21CD"=>"\u21D0\u0338", "\u21CE"=>"\u21D4\u0338", "\u21CF"=>"\u21D2\u0338", "\u2204"=>"\u2203\u0338", "\u2209"=>"\u2208\u0338", "\u220C"=>"\u220B\u0338", "\u2224"=>"\u2223\u0338", + "\u2226"=>"\u2225\u0338", "\u2241"=>"\u223C\u0338", "\u2244"=>"\u2243\u0338", "\u2247"=>"\u2245\u0338", "\u2249"=>"\u2248\u0338", "\u2260"=>"=\u0338", "\u2262"=>"\u2261\u0338", "\u226D"=>"\u224D\u0338", + "\u226E"=>"<\u0338", "\u226F"=>">\u0338", "\u2270"=>"\u2264\u0338", "\u2271"=>"\u2265\u0338", "\u2274"=>"\u2272\u0338", "\u2275"=>"\u2273\u0338", "\u2278"=>"\u2276\u0338", "\u2279"=>"\u2277\u0338", + "\u2280"=>"\u227A\u0338", "\u2281"=>"\u227B\u0338", "\u2284"=>"\u2282\u0338", "\u2285"=>"\u2283\u0338", "\u2288"=>"\u2286\u0338", "\u2289"=>"\u2287\u0338", "\u22AC"=>"\u22A2\u0338", "\u22AD"=>"\u22A8\u0338", + "\u22AE"=>"\u22A9\u0338", "\u22AF"=>"\u22AB\u0338", "\u22E0"=>"\u227C\u0338", "\u22E1"=>"\u227D\u0338", "\u22E2"=>"\u2291\u0338", "\u22E3"=>"\u2292\u0338", "\u22EA"=>"\u22B2\u0338", "\u22EB"=>"\u22B3\u0338", + "\u22EC"=>"\u22B4\u0338", "\u22ED"=>"\u22B5\u0338", "\u2329"=>"\u3008", "\u232A"=>"\u3009", "\u2ADC"=>"\u2ADD\u0338", "\u304C"=>"\u304B\u3099", "\u304E"=>"\u304D\u3099", "\u3050"=>"\u304F\u3099", + "\u3052"=>"\u3051\u3099", "\u3054"=>"\u3053\u3099", "\u3056"=>"\u3055\u3099", "\u3058"=>"\u3057\u3099", "\u305A"=>"\u3059\u3099", "\u305C"=>"\u305B\u3099", "\u305E"=>"\u305D\u3099", "\u3060"=>"\u305F\u3099", + "\u3062"=>"\u3061\u3099", "\u3065"=>"\u3064\u3099", "\u3067"=>"\u3066\u3099", "\u3069"=>"\u3068\u3099", "\u3070"=>"\u306F\u3099", "\u3071"=>"\u306F\u309A", "\u3073"=>"\u3072\u3099", "\u3074"=>"\u3072\u309A", + "\u3076"=>"\u3075\u3099", "\u3077"=>"\u3075\u309A", "\u3079"=>"\u3078\u3099", "\u307A"=>"\u3078\u309A", "\u307C"=>"\u307B\u3099", "\u307D"=>"\u307B\u309A", "\u3094"=>"\u3046\u3099", "\u309E"=>"\u309D\u3099", + "\u30AC"=>"\u30AB\u3099", "\u30AE"=>"\u30AD\u3099", "\u30B0"=>"\u30AF\u3099", "\u30B2"=>"\u30B1\u3099", "\u30B4"=>"\u30B3\u3099", "\u30B6"=>"\u30B5\u3099", "\u30B8"=>"\u30B7\u3099", "\u30BA"=>"\u30B9\u3099", + "\u30BC"=>"\u30BB\u3099", "\u30BE"=>"\u30BD\u3099", "\u30C0"=>"\u30BF\u3099", "\u30C2"=>"\u30C1\u3099", "\u30C5"=>"\u30C4\u3099", "\u30C7"=>"\u30C6\u3099", "\u30C9"=>"\u30C8\u3099", "\u30D0"=>"\u30CF\u3099", + "\u30D1"=>"\u30CF\u309A", "\u30D3"=>"\u30D2\u3099", "\u30D4"=>"\u30D2\u309A", "\u30D6"=>"\u30D5\u3099", "\u30D7"=>"\u30D5\u309A", "\u30D9"=>"\u30D8\u3099", "\u30DA"=>"\u30D8\u309A", "\u30DC"=>"\u30DB\u3099", + "\u30DD"=>"\u30DB\u309A", "\u30F4"=>"\u30A6\u3099", "\u30F7"=>"\u30EF\u3099", "\u30F8"=>"\u30F0\u3099", "\u30F9"=>"\u30F1\u3099", "\u30FA"=>"\u30F2\u3099", "\u30FE"=>"\u30FD\u3099", "\uF900"=>"\u8C48", + "\uF901"=>"\u66F4", "\uF902"=>"\u8ECA", "\uF903"=>"\u8CC8", "\uF904"=>"\u6ED1", "\uF905"=>"\u4E32", "\uF906"=>"\u53E5", "\uF907"=>"\u9F9C", "\uF908"=>"\u9F9C", + "\uF909"=>"\u5951", "\uF90A"=>"\u91D1", "\uF90B"=>"\u5587", "\uF90C"=>"\u5948", "\uF90D"=>"\u61F6", "\uF90E"=>"\u7669", "\uF90F"=>"\u7F85", "\uF910"=>"\u863F", + "\uF911"=>"\u87BA", "\uF912"=>"\u88F8", "\uF913"=>"\u908F", "\uF914"=>"\u6A02", "\uF915"=>"\u6D1B", "\uF916"=>"\u70D9", "\uF917"=>"\u73DE", "\uF918"=>"\u843D", + "\uF919"=>"\u916A", "\uF91A"=>"\u99F1", "\uF91B"=>"\u4E82", "\uF91C"=>"\u5375", "\uF91D"=>"\u6B04", "\uF91E"=>"\u721B", "\uF91F"=>"\u862D", "\uF920"=>"\u9E1E", + "\uF921"=>"\u5D50", "\uF922"=>"\u6FEB", "\uF923"=>"\u85CD", "\uF924"=>"\u8964", "\uF925"=>"\u62C9", "\uF926"=>"\u81D8", "\uF927"=>"\u881F", "\uF928"=>"\u5ECA", + "\uF929"=>"\u6717", "\uF92A"=>"\u6D6A", "\uF92B"=>"\u72FC", "\uF92C"=>"\u90CE", "\uF92D"=>"\u4F86", "\uF92E"=>"\u51B7", "\uF92F"=>"\u52DE", "\uF930"=>"\u64C4", + "\uF931"=>"\u6AD3", "\uF932"=>"\u7210", "\uF933"=>"\u76E7", "\uF934"=>"\u8001", "\uF935"=>"\u8606", "\uF936"=>"\u865C", "\uF937"=>"\u8DEF", "\uF938"=>"\u9732", + "\uF939"=>"\u9B6F", "\uF93A"=>"\u9DFA", "\uF93B"=>"\u788C", "\uF93C"=>"\u797F", "\uF93D"=>"\u7DA0", "\uF93E"=>"\u83C9", "\uF93F"=>"\u9304", "\uF940"=>"\u9E7F", + "\uF941"=>"\u8AD6", "\uF942"=>"\u58DF", "\uF943"=>"\u5F04", "\uF944"=>"\u7C60", "\uF945"=>"\u807E", "\uF946"=>"\u7262", "\uF947"=>"\u78CA", "\uF948"=>"\u8CC2", + "\uF949"=>"\u96F7", "\uF94A"=>"\u58D8", "\uF94B"=>"\u5C62", "\uF94C"=>"\u6A13", "\uF94D"=>"\u6DDA", "\uF94E"=>"\u6F0F", "\uF94F"=>"\u7D2F", "\uF950"=>"\u7E37", + "\uF951"=>"\u964B", "\uF952"=>"\u52D2", "\uF953"=>"\u808B", "\uF954"=>"\u51DC", "\uF955"=>"\u51CC", "\uF956"=>"\u7A1C", "\uF957"=>"\u7DBE", "\uF958"=>"\u83F1", + "\uF959"=>"\u9675", "\uF95A"=>"\u8B80", "\uF95B"=>"\u62CF", "\uF95C"=>"\u6A02", "\uF95D"=>"\u8AFE", "\uF95E"=>"\u4E39", "\uF95F"=>"\u5BE7", "\uF960"=>"\u6012", + "\uF961"=>"\u7387", "\uF962"=>"\u7570", "\uF963"=>"\u5317", "\uF964"=>"\u78FB", "\uF965"=>"\u4FBF", "\uF966"=>"\u5FA9", "\uF967"=>"\u4E0D", "\uF968"=>"\u6CCC", + "\uF969"=>"\u6578", "\uF96A"=>"\u7D22", "\uF96B"=>"\u53C3", "\uF96C"=>"\u585E", "\uF96D"=>"\u7701", "\uF96E"=>"\u8449", "\uF96F"=>"\u8AAA", "\uF970"=>"\u6BBA", + "\uF971"=>"\u8FB0", "\uF972"=>"\u6C88", "\uF973"=>"\u62FE", "\uF974"=>"\u82E5", "\uF975"=>"\u63A0", "\uF976"=>"\u7565", "\uF977"=>"\u4EAE", "\uF978"=>"\u5169", + "\uF979"=>"\u51C9", "\uF97A"=>"\u6881", "\uF97B"=>"\u7CE7", "\uF97C"=>"\u826F", "\uF97D"=>"\u8AD2", "\uF97E"=>"\u91CF", "\uF97F"=>"\u52F5", "\uF980"=>"\u5442", + "\uF981"=>"\u5973", "\uF982"=>"\u5EEC", "\uF983"=>"\u65C5", "\uF984"=>"\u6FFE", "\uF985"=>"\u792A", "\uF986"=>"\u95AD", "\uF987"=>"\u9A6A", "\uF988"=>"\u9E97", + "\uF989"=>"\u9ECE", "\uF98A"=>"\u529B", "\uF98B"=>"\u66C6", "\uF98C"=>"\u6B77", "\uF98D"=>"\u8F62", "\uF98E"=>"\u5E74", "\uF98F"=>"\u6190", "\uF990"=>"\u6200", + "\uF991"=>"\u649A", "\uF992"=>"\u6F23", "\uF993"=>"\u7149", "\uF994"=>"\u7489", "\uF995"=>"\u79CA", "\uF996"=>"\u7DF4", "\uF997"=>"\u806F", "\uF998"=>"\u8F26", + "\uF999"=>"\u84EE", "\uF99A"=>"\u9023", "\uF99B"=>"\u934A", "\uF99C"=>"\u5217", "\uF99D"=>"\u52A3", "\uF99E"=>"\u54BD", "\uF99F"=>"\u70C8", "\uF9A0"=>"\u88C2", + "\uF9A1"=>"\u8AAA", "\uF9A2"=>"\u5EC9", "\uF9A3"=>"\u5FF5", "\uF9A4"=>"\u637B", "\uF9A5"=>"\u6BAE", "\uF9A6"=>"\u7C3E", "\uF9A7"=>"\u7375", "\uF9A8"=>"\u4EE4", + "\uF9A9"=>"\u56F9", "\uF9AA"=>"\u5BE7", "\uF9AB"=>"\u5DBA", "\uF9AC"=>"\u601C", "\uF9AD"=>"\u73B2", "\uF9AE"=>"\u7469", "\uF9AF"=>"\u7F9A", "\uF9B0"=>"\u8046", + "\uF9B1"=>"\u9234", "\uF9B2"=>"\u96F6", "\uF9B3"=>"\u9748", "\uF9B4"=>"\u9818", "\uF9B5"=>"\u4F8B", "\uF9B6"=>"\u79AE", "\uF9B7"=>"\u91B4", "\uF9B8"=>"\u96B8", + "\uF9B9"=>"\u60E1", "\uF9BA"=>"\u4E86", "\uF9BB"=>"\u50DA", "\uF9BC"=>"\u5BEE", "\uF9BD"=>"\u5C3F", "\uF9BE"=>"\u6599", "\uF9BF"=>"\u6A02", "\uF9C0"=>"\u71CE", + "\uF9C1"=>"\u7642", "\uF9C2"=>"\u84FC", "\uF9C3"=>"\u907C", "\uF9C4"=>"\u9F8D", "\uF9C5"=>"\u6688", "\uF9C6"=>"\u962E", "\uF9C7"=>"\u5289", "\uF9C8"=>"\u677B", + "\uF9C9"=>"\u67F3", "\uF9CA"=>"\u6D41", "\uF9CB"=>"\u6E9C", "\uF9CC"=>"\u7409", "\uF9CD"=>"\u7559", "\uF9CE"=>"\u786B", "\uF9CF"=>"\u7D10", "\uF9D0"=>"\u985E", + "\uF9D1"=>"\u516D", "\uF9D2"=>"\u622E", "\uF9D3"=>"\u9678", "\uF9D4"=>"\u502B", "\uF9D5"=>"\u5D19", "\uF9D6"=>"\u6DEA", "\uF9D7"=>"\u8F2A", "\uF9D8"=>"\u5F8B", + "\uF9D9"=>"\u6144", "\uF9DA"=>"\u6817", "\uF9DB"=>"\u7387", "\uF9DC"=>"\u9686", "\uF9DD"=>"\u5229", "\uF9DE"=>"\u540F", "\uF9DF"=>"\u5C65", "\uF9E0"=>"\u6613", + "\uF9E1"=>"\u674E", "\uF9E2"=>"\u68A8", "\uF9E3"=>"\u6CE5", "\uF9E4"=>"\u7406", "\uF9E5"=>"\u75E2", "\uF9E6"=>"\u7F79", "\uF9E7"=>"\u88CF", "\uF9E8"=>"\u88E1", + "\uF9E9"=>"\u91CC", "\uF9EA"=>"\u96E2", "\uF9EB"=>"\u533F", "\uF9EC"=>"\u6EBA", "\uF9ED"=>"\u541D", "\uF9EE"=>"\u71D0", "\uF9EF"=>"\u7498", "\uF9F0"=>"\u85FA", + "\uF9F1"=>"\u96A3", "\uF9F2"=>"\u9C57", "\uF9F3"=>"\u9E9F", "\uF9F4"=>"\u6797", "\uF9F5"=>"\u6DCB", "\uF9F6"=>"\u81E8", "\uF9F7"=>"\u7ACB", "\uF9F8"=>"\u7B20", + "\uF9F9"=>"\u7C92", "\uF9FA"=>"\u72C0", "\uF9FB"=>"\u7099", "\uF9FC"=>"\u8B58", "\uF9FD"=>"\u4EC0", "\uF9FE"=>"\u8336", "\uF9FF"=>"\u523A", "\uFA00"=>"\u5207", + "\uFA01"=>"\u5EA6", "\uFA02"=>"\u62D3", "\uFA03"=>"\u7CD6", "\uFA04"=>"\u5B85", "\uFA05"=>"\u6D1E", "\uFA06"=>"\u66B4", "\uFA07"=>"\u8F3B", "\uFA08"=>"\u884C", + "\uFA09"=>"\u964D", "\uFA0A"=>"\u898B", "\uFA0B"=>"\u5ED3", "\uFA0C"=>"\u5140", "\uFA0D"=>"\u55C0", "\uFA10"=>"\u585A", "\uFA12"=>"\u6674", "\uFA15"=>"\u51DE", + "\uFA16"=>"\u732A", "\uFA17"=>"\u76CA", "\uFA18"=>"\u793C", "\uFA19"=>"\u795E", "\uFA1A"=>"\u7965", "\uFA1B"=>"\u798F", "\uFA1C"=>"\u9756", "\uFA1D"=>"\u7CBE", + "\uFA1E"=>"\u7FBD", "\uFA20"=>"\u8612", "\uFA22"=>"\u8AF8", "\uFA25"=>"\u9038", "\uFA26"=>"\u90FD", "\uFA2A"=>"\u98EF", "\uFA2B"=>"\u98FC", "\uFA2C"=>"\u9928", + "\uFA2D"=>"\u9DB4", "\uFA2E"=>"\u90DE", "\uFA2F"=>"\u96B7", "\uFA30"=>"\u4FAE", "\uFA31"=>"\u50E7", "\uFA32"=>"\u514D", "\uFA33"=>"\u52C9", "\uFA34"=>"\u52E4", + "\uFA35"=>"\u5351", "\uFA36"=>"\u559D", "\uFA37"=>"\u5606", "\uFA38"=>"\u5668", "\uFA39"=>"\u5840", "\uFA3A"=>"\u58A8", "\uFA3B"=>"\u5C64", "\uFA3C"=>"\u5C6E", + "\uFA3D"=>"\u6094", "\uFA3E"=>"\u6168", "\uFA3F"=>"\u618E", "\uFA40"=>"\u61F2", "\uFA41"=>"\u654F", "\uFA42"=>"\u65E2", "\uFA43"=>"\u6691", "\uFA44"=>"\u6885", + "\uFA45"=>"\u6D77", "\uFA46"=>"\u6E1A", "\uFA47"=>"\u6F22", "\uFA48"=>"\u716E", "\uFA49"=>"\u722B", "\uFA4A"=>"\u7422", "\uFA4B"=>"\u7891", "\uFA4C"=>"\u793E", + "\uFA4D"=>"\u7949", "\uFA4E"=>"\u7948", "\uFA4F"=>"\u7950", "\uFA50"=>"\u7956", "\uFA51"=>"\u795D", "\uFA52"=>"\u798D", "\uFA53"=>"\u798E", "\uFA54"=>"\u7A40", + "\uFA55"=>"\u7A81", "\uFA56"=>"\u7BC0", "\uFA57"=>"\u7DF4", "\uFA58"=>"\u7E09", "\uFA59"=>"\u7E41", "\uFA5A"=>"\u7F72", "\uFA5B"=>"\u8005", "\uFA5C"=>"\u81ED", + "\uFA5D"=>"\u8279", "\uFA5E"=>"\u8279", "\uFA5F"=>"\u8457", "\uFA60"=>"\u8910", "\uFA61"=>"\u8996", "\uFA62"=>"\u8B01", "\uFA63"=>"\u8B39", "\uFA64"=>"\u8CD3", + "\uFA65"=>"\u8D08", "\uFA66"=>"\u8FB6", "\uFA67"=>"\u9038", "\uFA68"=>"\u96E3", "\uFA69"=>"\u97FF", "\uFA6A"=>"\u983B", "\uFA6B"=>"\u6075", "\uFA6C"=>"\u{242EE}", + "\uFA6D"=>"\u8218", "\uFA70"=>"\u4E26", "\uFA71"=>"\u51B5", "\uFA72"=>"\u5168", "\uFA73"=>"\u4F80", "\uFA74"=>"\u5145", "\uFA75"=>"\u5180", "\uFA76"=>"\u52C7", + "\uFA77"=>"\u52FA", "\uFA78"=>"\u559D", "\uFA79"=>"\u5555", "\uFA7A"=>"\u5599", "\uFA7B"=>"\u55E2", "\uFA7C"=>"\u585A", "\uFA7D"=>"\u58B3", "\uFA7E"=>"\u5944", + "\uFA7F"=>"\u5954", "\uFA80"=>"\u5A62", "\uFA81"=>"\u5B28", "\uFA82"=>"\u5ED2", "\uFA83"=>"\u5ED9", "\uFA84"=>"\u5F69", "\uFA85"=>"\u5FAD", "\uFA86"=>"\u60D8", + "\uFA87"=>"\u614E", "\uFA88"=>"\u6108", "\uFA89"=>"\u618E", "\uFA8A"=>"\u6160", "\uFA8B"=>"\u61F2", "\uFA8C"=>"\u6234", "\uFA8D"=>"\u63C4", "\uFA8E"=>"\u641C", + "\uFA8F"=>"\u6452", "\uFA90"=>"\u6556", "\uFA91"=>"\u6674", "\uFA92"=>"\u6717", "\uFA93"=>"\u671B", "\uFA94"=>"\u6756", "\uFA95"=>"\u6B79", "\uFA96"=>"\u6BBA", + "\uFA97"=>"\u6D41", "\uFA98"=>"\u6EDB", "\uFA99"=>"\u6ECB", "\uFA9A"=>"\u6F22", "\uFA9B"=>"\u701E", "\uFA9C"=>"\u716E", "\uFA9D"=>"\u77A7", "\uFA9E"=>"\u7235", + "\uFA9F"=>"\u72AF", "\uFAA0"=>"\u732A", "\uFAA1"=>"\u7471", "\uFAA2"=>"\u7506", "\uFAA3"=>"\u753B", "\uFAA4"=>"\u761D", "\uFAA5"=>"\u761F", "\uFAA6"=>"\u76CA", + "\uFAA7"=>"\u76DB", "\uFAA8"=>"\u76F4", "\uFAA9"=>"\u774A", "\uFAAA"=>"\u7740", "\uFAAB"=>"\u78CC", "\uFAAC"=>"\u7AB1", "\uFAAD"=>"\u7BC0", "\uFAAE"=>"\u7C7B", + "\uFAAF"=>"\u7D5B", "\uFAB0"=>"\u7DF4", "\uFAB1"=>"\u7F3E", "\uFAB2"=>"\u8005", "\uFAB3"=>"\u8352", "\uFAB4"=>"\u83EF", "\uFAB5"=>"\u8779", "\uFAB6"=>"\u8941", + "\uFAB7"=>"\u8986", "\uFAB8"=>"\u8996", "\uFAB9"=>"\u8ABF", "\uFABA"=>"\u8AF8", "\uFABB"=>"\u8ACB", "\uFABC"=>"\u8B01", "\uFABD"=>"\u8AFE", "\uFABE"=>"\u8AED", + "\uFABF"=>"\u8B39", "\uFAC0"=>"\u8B8A", "\uFAC1"=>"\u8D08", "\uFAC2"=>"\u8F38", "\uFAC3"=>"\u9072", "\uFAC4"=>"\u9199", "\uFAC5"=>"\u9276", "\uFAC6"=>"\u967C", + "\uFAC7"=>"\u96E3", "\uFAC8"=>"\u9756", "\uFAC9"=>"\u97DB", "\uFACA"=>"\u97FF", "\uFACB"=>"\u980B", "\uFACC"=>"\u983B", "\uFACD"=>"\u9B12", "\uFACE"=>"\u9F9C", + "\uFACF"=>"\u{2284A}", "\uFAD0"=>"\u{22844}", "\uFAD1"=>"\u{233D5}", "\uFAD2"=>"\u3B9D", "\uFAD3"=>"\u4018", "\uFAD4"=>"\u4039", "\uFAD5"=>"\u{25249}", "\uFAD6"=>"\u{25CD0}", + "\uFAD7"=>"\u{27ED3}", "\uFAD8"=>"\u9F43", "\uFAD9"=>"\u9F8E", "\uFB1D"=>"\u05D9\u05B4", "\uFB1F"=>"\u05F2\u05B7", "\uFB2A"=>"\u05E9\u05C1", "\uFB2B"=>"\u05E9\u05C2", "\uFB2C"=>"\u05E9\u05BC\u05C1", + "\uFB2D"=>"\u05E9\u05BC\u05C2", "\uFB2E"=>"\u05D0\u05B7", "\uFB2F"=>"\u05D0\u05B8", "\uFB30"=>"\u05D0\u05BC", "\uFB31"=>"\u05D1\u05BC", "\uFB32"=>"\u05D2\u05BC", "\uFB33"=>"\u05D3\u05BC", "\uFB34"=>"\u05D4\u05BC", + "\uFB35"=>"\u05D5\u05BC", "\uFB36"=>"\u05D6\u05BC", "\uFB38"=>"\u05D8\u05BC", "\uFB39"=>"\u05D9\u05BC", "\uFB3A"=>"\u05DA\u05BC", "\uFB3B"=>"\u05DB\u05BC", "\uFB3C"=>"\u05DC\u05BC", "\uFB3E"=>"\u05DE\u05BC", + "\uFB40"=>"\u05E0\u05BC", "\uFB41"=>"\u05E1\u05BC", "\uFB43"=>"\u05E3\u05BC", "\uFB44"=>"\u05E4\u05BC", "\uFB46"=>"\u05E6\u05BC", "\uFB47"=>"\u05E7\u05BC", "\uFB48"=>"\u05E8\u05BC", "\uFB49"=>"\u05E9\u05BC", + "\uFB4A"=>"\u05EA\u05BC", "\uFB4B"=>"\u05D5\u05B9", "\uFB4C"=>"\u05D1\u05BF", "\uFB4D"=>"\u05DB\u05BF", "\uFB4E"=>"\u05E4\u05BF", "\u{1109A}"=>"\u{11099}\u{110BA}", "\u{1109C}"=>"\u{1109B}\u{110BA}", "\u{110AB}"=>"\u{110A5}\u{110BA}", + "\u{1112E}"=>"\u{11131}\u{11127}", "\u{1112F}"=>"\u{11132}\u{11127}", "\u{1134B}"=>"\u{11347}\u{1133E}", "\u{1134C}"=>"\u{11347}\u{11357}", "\u{114BB}"=>"\u{114B9}\u{114BA}", "\u{114BC}"=>"\u{114B9}\u{114B0}", "\u{114BE}"=>"\u{114B9}\u{114BD}", "\u{115BA}"=>"\u{115B8}\u{115AF}", + "\u{115BB}"=>"\u{115B9}\u{115AF}", "\u{1D15E}"=>"\u{1D157}\u{1D165}", "\u{1D15F}"=>"\u{1D158}\u{1D165}", "\u{1D160}"=>"\u{1D158}\u{1D165}\u{1D16E}", "\u{1D161}"=>"\u{1D158}\u{1D165}\u{1D16F}", "\u{1D162}"=>"\u{1D158}\u{1D165}\u{1D170}", "\u{1D163}"=>"\u{1D158}\u{1D165}\u{1D171}", "\u{1D164}"=>"\u{1D158}\u{1D165}\u{1D172}", + "\u{1D1BB}"=>"\u{1D1B9}\u{1D165}", "\u{1D1BC}"=>"\u{1D1BA}\u{1D165}", "\u{1D1BD}"=>"\u{1D1B9}\u{1D165}\u{1D16E}", "\u{1D1BE}"=>"\u{1D1BA}\u{1D165}\u{1D16E}", "\u{1D1BF}"=>"\u{1D1B9}\u{1D165}\u{1D16F}", "\u{1D1C0}"=>"\u{1D1BA}\u{1D165}\u{1D16F}", "\u{2F800}"=>"\u4E3D", "\u{2F801}"=>"\u4E38", + "\u{2F802}"=>"\u4E41", "\u{2F803}"=>"\u{20122}", "\u{2F804}"=>"\u4F60", "\u{2F805}"=>"\u4FAE", "\u{2F806}"=>"\u4FBB", "\u{2F807}"=>"\u5002", "\u{2F808}"=>"\u507A", "\u{2F809}"=>"\u5099", + "\u{2F80A}"=>"\u50E7", "\u{2F80B}"=>"\u50CF", "\u{2F80C}"=>"\u349E", "\u{2F80D}"=>"\u{2063A}", "\u{2F80E}"=>"\u514D", "\u{2F80F}"=>"\u5154", "\u{2F810}"=>"\u5164", "\u{2F811}"=>"\u5177", + "\u{2F812}"=>"\u{2051C}", "\u{2F813}"=>"\u34B9", "\u{2F814}"=>"\u5167", "\u{2F815}"=>"\u518D", "\u{2F816}"=>"\u{2054B}", "\u{2F817}"=>"\u5197", "\u{2F818}"=>"\u51A4", "\u{2F819}"=>"\u4ECC", + "\u{2F81A}"=>"\u51AC", "\u{2F81B}"=>"\u51B5", "\u{2F81C}"=>"\u{291DF}", "\u{2F81D}"=>"\u51F5", "\u{2F81E}"=>"\u5203", "\u{2F81F}"=>"\u34DF", "\u{2F820}"=>"\u523B", "\u{2F821}"=>"\u5246", + "\u{2F822}"=>"\u5272", "\u{2F823}"=>"\u5277", "\u{2F824}"=>"\u3515", "\u{2F825}"=>"\u52C7", "\u{2F826}"=>"\u52C9", "\u{2F827}"=>"\u52E4", "\u{2F828}"=>"\u52FA", "\u{2F829}"=>"\u5305", + "\u{2F82A}"=>"\u5306", "\u{2F82B}"=>"\u5317", "\u{2F82C}"=>"\u5349", "\u{2F82D}"=>"\u5351", "\u{2F82E}"=>"\u535A", "\u{2F82F}"=>"\u5373", "\u{2F830}"=>"\u537D", "\u{2F831}"=>"\u537F", + "\u{2F832}"=>"\u537F", "\u{2F833}"=>"\u537F", "\u{2F834}"=>"\u{20A2C}", "\u{2F835}"=>"\u7070", "\u{2F836}"=>"\u53CA", "\u{2F837}"=>"\u53DF", "\u{2F838}"=>"\u{20B63}", "\u{2F839}"=>"\u53EB", + "\u{2F83A}"=>"\u53F1", "\u{2F83B}"=>"\u5406", "\u{2F83C}"=>"\u549E", "\u{2F83D}"=>"\u5438", "\u{2F83E}"=>"\u5448", "\u{2F83F}"=>"\u5468", "\u{2F840}"=>"\u54A2", "\u{2F841}"=>"\u54F6", + "\u{2F842}"=>"\u5510", "\u{2F843}"=>"\u5553", "\u{2F844}"=>"\u5563", "\u{2F845}"=>"\u5584", "\u{2F846}"=>"\u5584", "\u{2F847}"=>"\u5599", "\u{2F848}"=>"\u55AB", "\u{2F849}"=>"\u55B3", + "\u{2F84A}"=>"\u55C2", "\u{2F84B}"=>"\u5716", "\u{2F84C}"=>"\u5606", "\u{2F84D}"=>"\u5717", "\u{2F84E}"=>"\u5651", "\u{2F84F}"=>"\u5674", "\u{2F850}"=>"\u5207", "\u{2F851}"=>"\u58EE", + "\u{2F852}"=>"\u57CE", "\u{2F853}"=>"\u57F4", "\u{2F854}"=>"\u580D", "\u{2F855}"=>"\u578B", "\u{2F856}"=>"\u5832", "\u{2F857}"=>"\u5831", "\u{2F858}"=>"\u58AC", "\u{2F859}"=>"\u{214E4}", + "\u{2F85A}"=>"\u58F2", "\u{2F85B}"=>"\u58F7", "\u{2F85C}"=>"\u5906", "\u{2F85D}"=>"\u591A", "\u{2F85E}"=>"\u5922", "\u{2F85F}"=>"\u5962", "\u{2F860}"=>"\u{216A8}", "\u{2F861}"=>"\u{216EA}", + "\u{2F862}"=>"\u59EC", "\u{2F863}"=>"\u5A1B", "\u{2F864}"=>"\u5A27", "\u{2F865}"=>"\u59D8", "\u{2F866}"=>"\u5A66", "\u{2F867}"=>"\u36EE", "\u{2F868}"=>"\u36FC", "\u{2F869}"=>"\u5B08", + "\u{2F86A}"=>"\u5B3E", "\u{2F86B}"=>"\u5B3E", "\u{2F86C}"=>"\u{219C8}", "\u{2F86D}"=>"\u5BC3", "\u{2F86E}"=>"\u5BD8", "\u{2F86F}"=>"\u5BE7", "\u{2F870}"=>"\u5BF3", "\u{2F871}"=>"\u{21B18}", + "\u{2F872}"=>"\u5BFF", "\u{2F873}"=>"\u5C06", "\u{2F874}"=>"\u5F53", "\u{2F875}"=>"\u5C22", "\u{2F876}"=>"\u3781", "\u{2F877}"=>"\u5C60", "\u{2F878}"=>"\u5C6E", "\u{2F879}"=>"\u5CC0", + "\u{2F87A}"=>"\u5C8D", "\u{2F87B}"=>"\u{21DE4}", "\u{2F87C}"=>"\u5D43", "\u{2F87D}"=>"\u{21DE6}", "\u{2F87E}"=>"\u5D6E", "\u{2F87F}"=>"\u5D6B", "\u{2F880}"=>"\u5D7C", "\u{2F881}"=>"\u5DE1", + "\u{2F882}"=>"\u5DE2", "\u{2F883}"=>"\u382F", "\u{2F884}"=>"\u5DFD", "\u{2F885}"=>"\u5E28", "\u{2F886}"=>"\u5E3D", "\u{2F887}"=>"\u5E69", "\u{2F888}"=>"\u3862", "\u{2F889}"=>"\u{22183}", + "\u{2F88A}"=>"\u387C", "\u{2F88B}"=>"\u5EB0", "\u{2F88C}"=>"\u5EB3", "\u{2F88D}"=>"\u5EB6", "\u{2F88E}"=>"\u5ECA", "\u{2F88F}"=>"\u{2A392}", "\u{2F890}"=>"\u5EFE", "\u{2F891}"=>"\u{22331}", + "\u{2F892}"=>"\u{22331}", "\u{2F893}"=>"\u8201", "\u{2F894}"=>"\u5F22", "\u{2F895}"=>"\u5F22", "\u{2F896}"=>"\u38C7", "\u{2F897}"=>"\u{232B8}", "\u{2F898}"=>"\u{261DA}", "\u{2F899}"=>"\u5F62", + "\u{2F89A}"=>"\u5F6B", "\u{2F89B}"=>"\u38E3", "\u{2F89C}"=>"\u5F9A", "\u{2F89D}"=>"\u5FCD", "\u{2F89E}"=>"\u5FD7", "\u{2F89F}"=>"\u5FF9", "\u{2F8A0}"=>"\u6081", "\u{2F8A1}"=>"\u393A", + "\u{2F8A2}"=>"\u391C", "\u{2F8A3}"=>"\u6094", "\u{2F8A4}"=>"\u{226D4}", "\u{2F8A5}"=>"\u60C7", "\u{2F8A6}"=>"\u6148", "\u{2F8A7}"=>"\u614C", "\u{2F8A8}"=>"\u614E", "\u{2F8A9}"=>"\u614C", + "\u{2F8AA}"=>"\u617A", "\u{2F8AB}"=>"\u618E", "\u{2F8AC}"=>"\u61B2", "\u{2F8AD}"=>"\u61A4", "\u{2F8AE}"=>"\u61AF", "\u{2F8AF}"=>"\u61DE", "\u{2F8B0}"=>"\u61F2", "\u{2F8B1}"=>"\u61F6", + "\u{2F8B2}"=>"\u6210", "\u{2F8B3}"=>"\u621B", "\u{2F8B4}"=>"\u625D", "\u{2F8B5}"=>"\u62B1", "\u{2F8B6}"=>"\u62D4", "\u{2F8B7}"=>"\u6350", "\u{2F8B8}"=>"\u{22B0C}", "\u{2F8B9}"=>"\u633D", + "\u{2F8BA}"=>"\u62FC", "\u{2F8BB}"=>"\u6368", "\u{2F8BC}"=>"\u6383", "\u{2F8BD}"=>"\u63E4", "\u{2F8BE}"=>"\u{22BF1}", "\u{2F8BF}"=>"\u6422", "\u{2F8C0}"=>"\u63C5", "\u{2F8C1}"=>"\u63A9", + "\u{2F8C2}"=>"\u3A2E", "\u{2F8C3}"=>"\u6469", "\u{2F8C4}"=>"\u647E", "\u{2F8C5}"=>"\u649D", "\u{2F8C6}"=>"\u6477", "\u{2F8C7}"=>"\u3A6C", "\u{2F8C8}"=>"\u654F", "\u{2F8C9}"=>"\u656C", + "\u{2F8CA}"=>"\u{2300A}", "\u{2F8CB}"=>"\u65E3", "\u{2F8CC}"=>"\u66F8", "\u{2F8CD}"=>"\u6649", "\u{2F8CE}"=>"\u3B19", "\u{2F8CF}"=>"\u6691", "\u{2F8D0}"=>"\u3B08", "\u{2F8D1}"=>"\u3AE4", + "\u{2F8D2}"=>"\u5192", "\u{2F8D3}"=>"\u5195", "\u{2F8D4}"=>"\u6700", "\u{2F8D5}"=>"\u669C", "\u{2F8D6}"=>"\u80AD", "\u{2F8D7}"=>"\u43D9", "\u{2F8D8}"=>"\u6717", "\u{2F8D9}"=>"\u671B", + "\u{2F8DA}"=>"\u6721", "\u{2F8DB}"=>"\u675E", "\u{2F8DC}"=>"\u6753", "\u{2F8DD}"=>"\u{233C3}", "\u{2F8DE}"=>"\u3B49", "\u{2F8DF}"=>"\u67FA", "\u{2F8E0}"=>"\u6785", "\u{2F8E1}"=>"\u6852", + "\u{2F8E2}"=>"\u6885", "\u{2F8E3}"=>"\u{2346D}", "\u{2F8E4}"=>"\u688E", "\u{2F8E5}"=>"\u681F", "\u{2F8E6}"=>"\u6914", "\u{2F8E7}"=>"\u3B9D", "\u{2F8E8}"=>"\u6942", "\u{2F8E9}"=>"\u69A3", + "\u{2F8EA}"=>"\u69EA", "\u{2F8EB}"=>"\u6AA8", "\u{2F8EC}"=>"\u{236A3}", "\u{2F8ED}"=>"\u6ADB", "\u{2F8EE}"=>"\u3C18", "\u{2F8EF}"=>"\u6B21", "\u{2F8F0}"=>"\u{238A7}", "\u{2F8F1}"=>"\u6B54", + "\u{2F8F2}"=>"\u3C4E", "\u{2F8F3}"=>"\u6B72", "\u{2F8F4}"=>"\u6B9F", "\u{2F8F5}"=>"\u6BBA", "\u{2F8F6}"=>"\u6BBB", "\u{2F8F7}"=>"\u{23A8D}", "\u{2F8F8}"=>"\u{21D0B}", "\u{2F8F9}"=>"\u{23AFA}", + "\u{2F8FA}"=>"\u6C4E", "\u{2F8FB}"=>"\u{23CBC}", "\u{2F8FC}"=>"\u6CBF", "\u{2F8FD}"=>"\u6CCD", "\u{2F8FE}"=>"\u6C67", "\u{2F8FF}"=>"\u6D16", "\u{2F900}"=>"\u6D3E", "\u{2F901}"=>"\u6D77", + "\u{2F902}"=>"\u6D41", "\u{2F903}"=>"\u6D69", "\u{2F904}"=>"\u6D78", "\u{2F905}"=>"\u6D85", "\u{2F906}"=>"\u{23D1E}", "\u{2F907}"=>"\u6D34", "\u{2F908}"=>"\u6E2F", "\u{2F909}"=>"\u6E6E", + "\u{2F90A}"=>"\u3D33", "\u{2F90B}"=>"\u6ECB", "\u{2F90C}"=>"\u6EC7", "\u{2F90D}"=>"\u{23ED1}", "\u{2F90E}"=>"\u6DF9", "\u{2F90F}"=>"\u6F6E", "\u{2F910}"=>"\u{23F5E}", "\u{2F911}"=>"\u{23F8E}", + "\u{2F912}"=>"\u6FC6", "\u{2F913}"=>"\u7039", "\u{2F914}"=>"\u701E", "\u{2F915}"=>"\u701B", "\u{2F916}"=>"\u3D96", "\u{2F917}"=>"\u704A", "\u{2F918}"=>"\u707D", "\u{2F919}"=>"\u7077", + "\u{2F91A}"=>"\u70AD", "\u{2F91B}"=>"\u{20525}", "\u{2F91C}"=>"\u7145", "\u{2F91D}"=>"\u{24263}", "\u{2F91E}"=>"\u719C", "\u{2F91F}"=>"\u{243AB}", "\u{2F920}"=>"\u7228", "\u{2F921}"=>"\u7235", + "\u{2F922}"=>"\u7250", "\u{2F923}"=>"\u{24608}", "\u{2F924}"=>"\u7280", "\u{2F925}"=>"\u7295", "\u{2F926}"=>"\u{24735}", "\u{2F927}"=>"\u{24814}", "\u{2F928}"=>"\u737A", "\u{2F929}"=>"\u738B", + "\u{2F92A}"=>"\u3EAC", "\u{2F92B}"=>"\u73A5", "\u{2F92C}"=>"\u3EB8", "\u{2F92D}"=>"\u3EB8", "\u{2F92E}"=>"\u7447", "\u{2F92F}"=>"\u745C", "\u{2F930}"=>"\u7471", "\u{2F931}"=>"\u7485", + "\u{2F932}"=>"\u74CA", "\u{2F933}"=>"\u3F1B", "\u{2F934}"=>"\u7524", "\u{2F935}"=>"\u{24C36}", "\u{2F936}"=>"\u753E", "\u{2F937}"=>"\u{24C92}", "\u{2F938}"=>"\u7570", "\u{2F939}"=>"\u{2219F}", + "\u{2F93A}"=>"\u7610", "\u{2F93B}"=>"\u{24FA1}", "\u{2F93C}"=>"\u{24FB8}", "\u{2F93D}"=>"\u{25044}", "\u{2F93E}"=>"\u3FFC", "\u{2F93F}"=>"\u4008", "\u{2F940}"=>"\u76F4", "\u{2F941}"=>"\u{250F3}", + "\u{2F942}"=>"\u{250F2}", "\u{2F943}"=>"\u{25119}", "\u{2F944}"=>"\u{25133}", "\u{2F945}"=>"\u771E", "\u{2F946}"=>"\u771F", "\u{2F947}"=>"\u771F", "\u{2F948}"=>"\u774A", "\u{2F949}"=>"\u4039", + "\u{2F94A}"=>"\u778B", "\u{2F94B}"=>"\u4046", "\u{2F94C}"=>"\u4096", "\u{2F94D}"=>"\u{2541D}", "\u{2F94E}"=>"\u784E", "\u{2F94F}"=>"\u788C", "\u{2F950}"=>"\u78CC", "\u{2F951}"=>"\u40E3", + "\u{2F952}"=>"\u{25626}", "\u{2F953}"=>"\u7956", "\u{2F954}"=>"\u{2569A}", "\u{2F955}"=>"\u{256C5}", "\u{2F956}"=>"\u798F", "\u{2F957}"=>"\u79EB", "\u{2F958}"=>"\u412F", "\u{2F959}"=>"\u7A40", + "\u{2F95A}"=>"\u7A4A", "\u{2F95B}"=>"\u7A4F", "\u{2F95C}"=>"\u{2597C}", "\u{2F95D}"=>"\u{25AA7}", "\u{2F95E}"=>"\u{25AA7}", "\u{2F95F}"=>"\u7AEE", "\u{2F960}"=>"\u4202", "\u{2F961}"=>"\u{25BAB}", + "\u{2F962}"=>"\u7BC6", "\u{2F963}"=>"\u7BC9", "\u{2F964}"=>"\u4227", "\u{2F965}"=>"\u{25C80}", "\u{2F966}"=>"\u7CD2", "\u{2F967}"=>"\u42A0", "\u{2F968}"=>"\u7CE8", "\u{2F969}"=>"\u7CE3", + "\u{2F96A}"=>"\u7D00", "\u{2F96B}"=>"\u{25F86}", "\u{2F96C}"=>"\u7D63", "\u{2F96D}"=>"\u4301", "\u{2F96E}"=>"\u7DC7", "\u{2F96F}"=>"\u7E02", "\u{2F970}"=>"\u7E45", "\u{2F971}"=>"\u4334", + "\u{2F972}"=>"\u{26228}", "\u{2F973}"=>"\u{26247}", "\u{2F974}"=>"\u4359", "\u{2F975}"=>"\u{262D9}", "\u{2F976}"=>"\u7F7A", "\u{2F977}"=>"\u{2633E}", "\u{2F978}"=>"\u7F95", "\u{2F979}"=>"\u7FFA", + "\u{2F97A}"=>"\u8005", "\u{2F97B}"=>"\u{264DA}", "\u{2F97C}"=>"\u{26523}", "\u{2F97D}"=>"\u8060", "\u{2F97E}"=>"\u{265A8}", "\u{2F97F}"=>"\u8070", "\u{2F980}"=>"\u{2335F}", "\u{2F981}"=>"\u43D5", + "\u{2F982}"=>"\u80B2", "\u{2F983}"=>"\u8103", "\u{2F984}"=>"\u440B", "\u{2F985}"=>"\u813E", "\u{2F986}"=>"\u5AB5", "\u{2F987}"=>"\u{267A7}", "\u{2F988}"=>"\u{267B5}", "\u{2F989}"=>"\u{23393}", + "\u{2F98A}"=>"\u{2339C}", "\u{2F98B}"=>"\u8201", "\u{2F98C}"=>"\u8204", "\u{2F98D}"=>"\u8F9E", "\u{2F98E}"=>"\u446B", "\u{2F98F}"=>"\u8291", "\u{2F990}"=>"\u828B", "\u{2F991}"=>"\u829D", + "\u{2F992}"=>"\u52B3", "\u{2F993}"=>"\u82B1", "\u{2F994}"=>"\u82B3", "\u{2F995}"=>"\u82BD", "\u{2F996}"=>"\u82E6", "\u{2F997}"=>"\u{26B3C}", "\u{2F998}"=>"\u82E5", "\u{2F999}"=>"\u831D", + "\u{2F99A}"=>"\u8363", "\u{2F99B}"=>"\u83AD", "\u{2F99C}"=>"\u8323", "\u{2F99D}"=>"\u83BD", "\u{2F99E}"=>"\u83E7", "\u{2F99F}"=>"\u8457", "\u{2F9A0}"=>"\u8353", "\u{2F9A1}"=>"\u83CA", + "\u{2F9A2}"=>"\u83CC", "\u{2F9A3}"=>"\u83DC", "\u{2F9A4}"=>"\u{26C36}", "\u{2F9A5}"=>"\u{26D6B}", "\u{2F9A6}"=>"\u{26CD5}", "\u{2F9A7}"=>"\u452B", "\u{2F9A8}"=>"\u84F1", "\u{2F9A9}"=>"\u84F3", + "\u{2F9AA}"=>"\u8516", "\u{2F9AB}"=>"\u{273CA}", "\u{2F9AC}"=>"\u8564", "\u{2F9AD}"=>"\u{26F2C}", "\u{2F9AE}"=>"\u455D", "\u{2F9AF}"=>"\u4561", "\u{2F9B0}"=>"\u{26FB1}", "\u{2F9B1}"=>"\u{270D2}", + "\u{2F9B2}"=>"\u456B", "\u{2F9B3}"=>"\u8650", "\u{2F9B4}"=>"\u865C", "\u{2F9B5}"=>"\u8667", "\u{2F9B6}"=>"\u8669", "\u{2F9B7}"=>"\u86A9", "\u{2F9B8}"=>"\u8688", "\u{2F9B9}"=>"\u870E", + "\u{2F9BA}"=>"\u86E2", "\u{2F9BB}"=>"\u8779", "\u{2F9BC}"=>"\u8728", "\u{2F9BD}"=>"\u876B", "\u{2F9BE}"=>"\u8786", "\u{2F9BF}"=>"\u45D7", "\u{2F9C0}"=>"\u87E1", "\u{2F9C1}"=>"\u8801", + "\u{2F9C2}"=>"\u45F9", "\u{2F9C3}"=>"\u8860", "\u{2F9C4}"=>"\u8863", "\u{2F9C5}"=>"\u{27667}", "\u{2F9C6}"=>"\u88D7", "\u{2F9C7}"=>"\u88DE", "\u{2F9C8}"=>"\u4635", "\u{2F9C9}"=>"\u88FA", + "\u{2F9CA}"=>"\u34BB", "\u{2F9CB}"=>"\u{278AE}", "\u{2F9CC}"=>"\u{27966}", "\u{2F9CD}"=>"\u46BE", "\u{2F9CE}"=>"\u46C7", "\u{2F9CF}"=>"\u8AA0", "\u{2F9D0}"=>"\u8AED", "\u{2F9D1}"=>"\u8B8A", + "\u{2F9D2}"=>"\u8C55", "\u{2F9D3}"=>"\u{27CA8}", "\u{2F9D4}"=>"\u8CAB", "\u{2F9D5}"=>"\u8CC1", "\u{2F9D6}"=>"\u8D1B", "\u{2F9D7}"=>"\u8D77", "\u{2F9D8}"=>"\u{27F2F}", "\u{2F9D9}"=>"\u{20804}", + "\u{2F9DA}"=>"\u8DCB", "\u{2F9DB}"=>"\u8DBC", "\u{2F9DC}"=>"\u8DF0", "\u{2F9DD}"=>"\u{208DE}", "\u{2F9DE}"=>"\u8ED4", "\u{2F9DF}"=>"\u8F38", "\u{2F9E0}"=>"\u{285D2}", "\u{2F9E1}"=>"\u{285ED}", + "\u{2F9E2}"=>"\u9094", "\u{2F9E3}"=>"\u90F1", "\u{2F9E4}"=>"\u9111", "\u{2F9E5}"=>"\u{2872E}", "\u{2F9E6}"=>"\u911B", "\u{2F9E7}"=>"\u9238", "\u{2F9E8}"=>"\u92D7", "\u{2F9E9}"=>"\u92D8", + "\u{2F9EA}"=>"\u927C", "\u{2F9EB}"=>"\u93F9", "\u{2F9EC}"=>"\u9415", "\u{2F9ED}"=>"\u{28BFA}", "\u{2F9EE}"=>"\u958B", "\u{2F9EF}"=>"\u4995", "\u{2F9F0}"=>"\u95B7", "\u{2F9F1}"=>"\u{28D77}", + "\u{2F9F2}"=>"\u49E6", "\u{2F9F3}"=>"\u96C3", "\u{2F9F4}"=>"\u5DB2", "\u{2F9F5}"=>"\u9723", "\u{2F9F6}"=>"\u{29145}", "\u{2F9F7}"=>"\u{2921A}", "\u{2F9F8}"=>"\u4A6E", "\u{2F9F9}"=>"\u4A76", + "\u{2F9FA}"=>"\u97E0", "\u{2F9FB}"=>"\u{2940A}", "\u{2F9FC}"=>"\u4AB2", "\u{2F9FD}"=>"\u{29496}", "\u{2F9FE}"=>"\u980B", "\u{2F9FF}"=>"\u980B", "\u{2FA00}"=>"\u9829", "\u{2FA01}"=>"\u{295B6}", + "\u{2FA02}"=>"\u98E2", "\u{2FA03}"=>"\u4B33", "\u{2FA04}"=>"\u9929", "\u{2FA05}"=>"\u99A7", "\u{2FA06}"=>"\u99C2", "\u{2FA07}"=>"\u99FE", "\u{2FA08}"=>"\u4BCE", "\u{2FA09}"=>"\u{29B30}", + "\u{2FA0A}"=>"\u9B12", "\u{2FA0B}"=>"\u9C40", "\u{2FA0C}"=>"\u9CFD", "\u{2FA0D}"=>"\u4CCE", "\u{2FA0E}"=>"\u4CED", "\u{2FA0F}"=>"\u9D67", "\u{2FA10}"=>"\u{2A0CE}", "\u{2FA11}"=>"\u4CF8", + "\u{2FA12}"=>"\u{2A105}", "\u{2FA13}"=>"\u{2A20E}", "\u{2FA14}"=>"\u{2A291}", "\u{2FA15}"=>"\u9EBB", "\u{2FA16}"=>"\u4D56", "\u{2FA17}"=>"\u9EF9", "\u{2FA18}"=>"\u9EFE", "\u{2FA19}"=>"\u9F05", + "\u{2FA1A}"=>"\u9F0F", "\u{2FA1B}"=>"\u9F16", "\u{2FA1C}"=>"\u9F3B", "\u{2FA1D}"=>"\u{2A600}", + }.freeze + + KOMPATIBLE_TABLE = { + "\u00A0"=>" ", "\u00A8"=>" \u0308", "\u00AA"=>"a", "\u00AF"=>" \u0304", "\u00B2"=>"2", "\u00B3"=>"3", "\u00B4"=>" \u0301", "\u00B5"=>"\u03BC", + "\u00B8"=>" \u0327", "\u00B9"=>"1", "\u00BA"=>"o", "\u00BC"=>"1\u20444", "\u00BD"=>"1\u20442", "\u00BE"=>"3\u20444", "\u0132"=>"IJ", "\u0133"=>"ij", + "\u013F"=>"L\u00B7", "\u0140"=>"l\u00B7", "\u0149"=>"\u02BCn", "\u017F"=>"s", "\u01C4"=>"D\u017D", "\u01C5"=>"D\u017E", "\u01C6"=>"d\u017E", "\u01C7"=>"LJ", + "\u01C8"=>"Lj", "\u01C9"=>"lj", "\u01CA"=>"NJ", "\u01CB"=>"Nj", "\u01CC"=>"nj", "\u01F1"=>"DZ", "\u01F2"=>"Dz", "\u01F3"=>"dz", + "\u02B0"=>"h", "\u02B1"=>"\u0266", "\u02B2"=>"j", "\u02B3"=>"r", "\u02B4"=>"\u0279", "\u02B5"=>"\u027B", "\u02B6"=>"\u0281", "\u02B7"=>"w", + "\u02B8"=>"y", "\u02D8"=>" \u0306", "\u02D9"=>" \u0307", "\u02DA"=>" \u030A", "\u02DB"=>" \u0328", "\u02DC"=>" \u0303", "\u02DD"=>" \u030B", "\u02E0"=>"\u0263", + "\u02E1"=>"l", "\u02E2"=>"s", "\u02E3"=>"x", "\u02E4"=>"\u0295", "\u037A"=>" \u0345", "\u0384"=>" \u0301", "\u03D0"=>"\u03B2", "\u03D1"=>"\u03B8", + "\u03D2"=>"\u03A5", "\u03D5"=>"\u03C6", "\u03D6"=>"\u03C0", "\u03F0"=>"\u03BA", "\u03F1"=>"\u03C1", "\u03F2"=>"\u03C2", "\u03F4"=>"\u0398", "\u03F5"=>"\u03B5", + "\u03F9"=>"\u03A3", "\u0587"=>"\u0565\u0582", "\u0675"=>"\u0627\u0674", "\u0676"=>"\u0648\u0674", "\u0677"=>"\u06C7\u0674", "\u0678"=>"\u064A\u0674", "\u0E33"=>"\u0E4D\u0E32", "\u0EB3"=>"\u0ECD\u0EB2", + "\u0EDC"=>"\u0EAB\u0E99", "\u0EDD"=>"\u0EAB\u0EA1", "\u0F0C"=>"\u0F0B", "\u0F77"=>"\u0FB2\u0F81", "\u0F79"=>"\u0FB3\u0F81", "\u10FC"=>"\u10DC", "\u1D2C"=>"A", "\u1D2D"=>"\u00C6", + "\u1D2E"=>"B", "\u1D30"=>"D", "\u1D31"=>"E", "\u1D32"=>"\u018E", "\u1D33"=>"G", "\u1D34"=>"H", "\u1D35"=>"I", "\u1D36"=>"J", + "\u1D37"=>"K", "\u1D38"=>"L", "\u1D39"=>"M", "\u1D3A"=>"N", "\u1D3C"=>"O", "\u1D3D"=>"\u0222", "\u1D3E"=>"P", "\u1D3F"=>"R", + "\u1D40"=>"T", "\u1D41"=>"U", "\u1D42"=>"W", "\u1D43"=>"a", "\u1D44"=>"\u0250", "\u1D45"=>"\u0251", "\u1D46"=>"\u1D02", "\u1D47"=>"b", + "\u1D48"=>"d", "\u1D49"=>"e", "\u1D4A"=>"\u0259", "\u1D4B"=>"\u025B", "\u1D4C"=>"\u025C", "\u1D4D"=>"g", "\u1D4F"=>"k", "\u1D50"=>"m", + "\u1D51"=>"\u014B", "\u1D52"=>"o", "\u1D53"=>"\u0254", "\u1D54"=>"\u1D16", "\u1D55"=>"\u1D17", "\u1D56"=>"p", "\u1D57"=>"t", "\u1D58"=>"u", + "\u1D59"=>"\u1D1D", "\u1D5A"=>"\u026F", "\u1D5B"=>"v", "\u1D5C"=>"\u1D25", "\u1D5D"=>"\u03B2", "\u1D5E"=>"\u03B3", "\u1D5F"=>"\u03B4", "\u1D60"=>"\u03C6", + "\u1D61"=>"\u03C7", "\u1D62"=>"i", "\u1D63"=>"r", "\u1D64"=>"u", "\u1D65"=>"v", "\u1D66"=>"\u03B2", "\u1D67"=>"\u03B3", "\u1D68"=>"\u03C1", + "\u1D69"=>"\u03C6", "\u1D6A"=>"\u03C7", "\u1D78"=>"\u043D", "\u1D9B"=>"\u0252", "\u1D9C"=>"c", "\u1D9D"=>"\u0255", "\u1D9E"=>"\u00F0", "\u1D9F"=>"\u025C", + "\u1DA0"=>"f", "\u1DA1"=>"\u025F", "\u1DA2"=>"\u0261", "\u1DA3"=>"\u0265", "\u1DA4"=>"\u0268", "\u1DA5"=>"\u0269", "\u1DA6"=>"\u026A", "\u1DA7"=>"\u1D7B", + "\u1DA8"=>"\u029D", "\u1DA9"=>"\u026D", "\u1DAA"=>"\u1D85", "\u1DAB"=>"\u029F", "\u1DAC"=>"\u0271", "\u1DAD"=>"\u0270", "\u1DAE"=>"\u0272", "\u1DAF"=>"\u0273", + "\u1DB0"=>"\u0274", "\u1DB1"=>"\u0275", "\u1DB2"=>"\u0278", "\u1DB3"=>"\u0282", "\u1DB4"=>"\u0283", "\u1DB5"=>"\u01AB", "\u1DB6"=>"\u0289", "\u1DB7"=>"\u028A", + "\u1DB8"=>"\u1D1C", "\u1DB9"=>"\u028B", "\u1DBA"=>"\u028C", "\u1DBB"=>"z", "\u1DBC"=>"\u0290", "\u1DBD"=>"\u0291", "\u1DBE"=>"\u0292", "\u1DBF"=>"\u03B8", + "\u1E9A"=>"a\u02BE", "\u1FBD"=>" \u0313", "\u1FBF"=>" \u0313", "\u1FC0"=>" \u0342", "\u1FFE"=>" \u0314", "\u2002"=>" ", "\u2003"=>" ", "\u2004"=>" ", + "\u2005"=>" ", "\u2006"=>" ", "\u2007"=>" ", "\u2008"=>" ", "\u2009"=>" ", "\u200A"=>" ", "\u2011"=>"\u2010", "\u2017"=>" \u0333", + "\u2024"=>".", "\u2025"=>"..", "\u2026"=>"...", "\u202F"=>" ", "\u2033"=>"\u2032\u2032", "\u2034"=>"\u2032\u2032\u2032", "\u2036"=>"\u2035\u2035", "\u2037"=>"\u2035\u2035\u2035", + "\u203C"=>"!!", "\u203E"=>" \u0305", "\u2047"=>"??", "\u2048"=>"?!", "\u2049"=>"!?", "\u2057"=>"\u2032\u2032\u2032\u2032", "\u205F"=>" ", "\u2070"=>"0", + "\u2071"=>"i", "\u2074"=>"4", "\u2075"=>"5", "\u2076"=>"6", "\u2077"=>"7", "\u2078"=>"8", "\u2079"=>"9", "\u207A"=>"+", + "\u207B"=>"\u2212", "\u207C"=>"=", "\u207D"=>"(", "\u207E"=>")", "\u207F"=>"n", "\u2080"=>"0", "\u2081"=>"1", "\u2082"=>"2", + "\u2083"=>"3", "\u2084"=>"4", "\u2085"=>"5", "\u2086"=>"6", "\u2087"=>"7", "\u2088"=>"8", "\u2089"=>"9", "\u208A"=>"+", + "\u208B"=>"\u2212", "\u208C"=>"=", "\u208D"=>"(", "\u208E"=>")", "\u2090"=>"a", "\u2091"=>"e", "\u2092"=>"o", "\u2093"=>"x", + "\u2094"=>"\u0259", "\u2095"=>"h", "\u2096"=>"k", "\u2097"=>"l", "\u2098"=>"m", "\u2099"=>"n", "\u209A"=>"p", "\u209B"=>"s", + "\u209C"=>"t", "\u20A8"=>"Rs", "\u2100"=>"a/c", "\u2101"=>"a/s", "\u2102"=>"C", "\u2103"=>"\u00B0C", "\u2105"=>"c/o", "\u2106"=>"c/u", + "\u2107"=>"\u0190", "\u2109"=>"\u00B0F", "\u210A"=>"g", "\u210B"=>"H", "\u210C"=>"H", "\u210D"=>"H", "\u210E"=>"h", "\u210F"=>"\u0127", + "\u2110"=>"I", "\u2111"=>"I", "\u2112"=>"L", "\u2113"=>"l", "\u2115"=>"N", "\u2116"=>"No", "\u2119"=>"P", "\u211A"=>"Q", + "\u211B"=>"R", "\u211C"=>"R", "\u211D"=>"R", "\u2120"=>"SM", "\u2121"=>"TEL", "\u2122"=>"TM", "\u2124"=>"Z", "\u2128"=>"Z", + "\u212C"=>"B", "\u212D"=>"C", "\u212F"=>"e", "\u2130"=>"E", "\u2131"=>"F", "\u2133"=>"M", "\u2134"=>"o", "\u2135"=>"\u05D0", + "\u2136"=>"\u05D1", "\u2137"=>"\u05D2", "\u2138"=>"\u05D3", "\u2139"=>"i", "\u213B"=>"FAX", "\u213C"=>"\u03C0", "\u213D"=>"\u03B3", "\u213E"=>"\u0393", + "\u213F"=>"\u03A0", "\u2140"=>"\u2211", "\u2145"=>"D", "\u2146"=>"d", "\u2147"=>"e", "\u2148"=>"i", "\u2149"=>"j", "\u2150"=>"1\u20447", + "\u2151"=>"1\u20449", "\u2152"=>"1\u204410", "\u2153"=>"1\u20443", "\u2154"=>"2\u20443", "\u2155"=>"1\u20445", "\u2156"=>"2\u20445", "\u2157"=>"3\u20445", "\u2158"=>"4\u20445", + "\u2159"=>"1\u20446", "\u215A"=>"5\u20446", "\u215B"=>"1\u20448", "\u215C"=>"3\u20448", "\u215D"=>"5\u20448", "\u215E"=>"7\u20448", "\u215F"=>"1\u2044", "\u2160"=>"I", + "\u2161"=>"II", "\u2162"=>"III", "\u2163"=>"IV", "\u2164"=>"V", "\u2165"=>"VI", "\u2166"=>"VII", "\u2167"=>"VIII", "\u2168"=>"IX", + "\u2169"=>"X", "\u216A"=>"XI", "\u216B"=>"XII", "\u216C"=>"L", "\u216D"=>"C", "\u216E"=>"D", "\u216F"=>"M", "\u2170"=>"i", + "\u2171"=>"ii", "\u2172"=>"iii", "\u2173"=>"iv", "\u2174"=>"v", "\u2175"=>"vi", "\u2176"=>"vii", "\u2177"=>"viii", "\u2178"=>"ix", + "\u2179"=>"x", "\u217A"=>"xi", "\u217B"=>"xii", "\u217C"=>"l", "\u217D"=>"c", "\u217E"=>"d", "\u217F"=>"m", "\u2189"=>"0\u20443", + "\u222C"=>"\u222B\u222B", "\u222D"=>"\u222B\u222B\u222B", "\u222F"=>"\u222E\u222E", "\u2230"=>"\u222E\u222E\u222E", "\u2460"=>"1", "\u2461"=>"2", "\u2462"=>"3", "\u2463"=>"4", + "\u2464"=>"5", "\u2465"=>"6", "\u2466"=>"7", "\u2467"=>"8", "\u2468"=>"9", "\u2469"=>"10", "\u246A"=>"11", "\u246B"=>"12", + "\u246C"=>"13", "\u246D"=>"14", "\u246E"=>"15", "\u246F"=>"16", "\u2470"=>"17", "\u2471"=>"18", "\u2472"=>"19", "\u2473"=>"20", + "\u2474"=>"(1)", "\u2475"=>"(2)", "\u2476"=>"(3)", "\u2477"=>"(4)", "\u2478"=>"(5)", "\u2479"=>"(6)", "\u247A"=>"(7)", "\u247B"=>"(8)", + "\u247C"=>"(9)", "\u247D"=>"(10)", "\u247E"=>"(11)", "\u247F"=>"(12)", "\u2480"=>"(13)", "\u2481"=>"(14)", "\u2482"=>"(15)", "\u2483"=>"(16)", + "\u2484"=>"(17)", "\u2485"=>"(18)", "\u2486"=>"(19)", "\u2487"=>"(20)", "\u2488"=>"1.", "\u2489"=>"2.", "\u248A"=>"3.", "\u248B"=>"4.", + "\u248C"=>"5.", "\u248D"=>"6.", "\u248E"=>"7.", "\u248F"=>"8.", "\u2490"=>"9.", "\u2491"=>"10.", "\u2492"=>"11.", "\u2493"=>"12.", + "\u2494"=>"13.", "\u2495"=>"14.", "\u2496"=>"15.", "\u2497"=>"16.", "\u2498"=>"17.", "\u2499"=>"18.", "\u249A"=>"19.", "\u249B"=>"20.", + "\u249C"=>"(a)", "\u249D"=>"(b)", "\u249E"=>"(c)", "\u249F"=>"(d)", "\u24A0"=>"(e)", "\u24A1"=>"(f)", "\u24A2"=>"(g)", "\u24A3"=>"(h)", + "\u24A4"=>"(i)", "\u24A5"=>"(j)", "\u24A6"=>"(k)", "\u24A7"=>"(l)", "\u24A8"=>"(m)", "\u24A9"=>"(n)", "\u24AA"=>"(o)", "\u24AB"=>"(p)", + "\u24AC"=>"(q)", "\u24AD"=>"(r)", "\u24AE"=>"(s)", "\u24AF"=>"(t)", "\u24B0"=>"(u)", "\u24B1"=>"(v)", "\u24B2"=>"(w)", "\u24B3"=>"(x)", + "\u24B4"=>"(y)", "\u24B5"=>"(z)", "\u24B6"=>"A", "\u24B7"=>"B", "\u24B8"=>"C", "\u24B9"=>"D", "\u24BA"=>"E", "\u24BB"=>"F", + "\u24BC"=>"G", "\u24BD"=>"H", "\u24BE"=>"I", "\u24BF"=>"J", "\u24C0"=>"K", "\u24C1"=>"L", "\u24C2"=>"M", "\u24C3"=>"N", + "\u24C4"=>"O", "\u24C5"=>"P", "\u24C6"=>"Q", "\u24C7"=>"R", "\u24C8"=>"S", "\u24C9"=>"T", "\u24CA"=>"U", "\u24CB"=>"V", + "\u24CC"=>"W", "\u24CD"=>"X", "\u24CE"=>"Y", "\u24CF"=>"Z", "\u24D0"=>"a", "\u24D1"=>"b", "\u24D2"=>"c", "\u24D3"=>"d", + "\u24D4"=>"e", "\u24D5"=>"f", "\u24D6"=>"g", "\u24D7"=>"h", "\u24D8"=>"i", "\u24D9"=>"j", "\u24DA"=>"k", "\u24DB"=>"l", + "\u24DC"=>"m", "\u24DD"=>"n", "\u24DE"=>"o", "\u24DF"=>"p", "\u24E0"=>"q", "\u24E1"=>"r", "\u24E2"=>"s", "\u24E3"=>"t", + "\u24E4"=>"u", "\u24E5"=>"v", "\u24E6"=>"w", "\u24E7"=>"x", "\u24E8"=>"y", "\u24E9"=>"z", "\u24EA"=>"0", "\u2A0C"=>"\u222B\u222B\u222B\u222B", + "\u2A74"=>"::=", "\u2A75"=>"==", "\u2A76"=>"===", "\u2C7C"=>"j", "\u2C7D"=>"V", "\u2D6F"=>"\u2D61", "\u2E9F"=>"\u6BCD", "\u2EF3"=>"\u9F9F", + "\u2F00"=>"\u4E00", "\u2F01"=>"\u4E28", "\u2F02"=>"\u4E36", "\u2F03"=>"\u4E3F", "\u2F04"=>"\u4E59", "\u2F05"=>"\u4E85", "\u2F06"=>"\u4E8C", "\u2F07"=>"\u4EA0", + "\u2F08"=>"\u4EBA", "\u2F09"=>"\u513F", "\u2F0A"=>"\u5165", "\u2F0B"=>"\u516B", "\u2F0C"=>"\u5182", "\u2F0D"=>"\u5196", "\u2F0E"=>"\u51AB", "\u2F0F"=>"\u51E0", + "\u2F10"=>"\u51F5", "\u2F11"=>"\u5200", "\u2F12"=>"\u529B", "\u2F13"=>"\u52F9", "\u2F14"=>"\u5315", "\u2F15"=>"\u531A", "\u2F16"=>"\u5338", "\u2F17"=>"\u5341", + "\u2F18"=>"\u535C", "\u2F19"=>"\u5369", "\u2F1A"=>"\u5382", "\u2F1B"=>"\u53B6", "\u2F1C"=>"\u53C8", "\u2F1D"=>"\u53E3", "\u2F1E"=>"\u56D7", "\u2F1F"=>"\u571F", + "\u2F20"=>"\u58EB", "\u2F21"=>"\u5902", "\u2F22"=>"\u590A", "\u2F23"=>"\u5915", "\u2F24"=>"\u5927", "\u2F25"=>"\u5973", "\u2F26"=>"\u5B50", "\u2F27"=>"\u5B80", + "\u2F28"=>"\u5BF8", "\u2F29"=>"\u5C0F", "\u2F2A"=>"\u5C22", "\u2F2B"=>"\u5C38", "\u2F2C"=>"\u5C6E", "\u2F2D"=>"\u5C71", "\u2F2E"=>"\u5DDB", "\u2F2F"=>"\u5DE5", + "\u2F30"=>"\u5DF1", "\u2F31"=>"\u5DFE", "\u2F32"=>"\u5E72", "\u2F33"=>"\u5E7A", "\u2F34"=>"\u5E7F", "\u2F35"=>"\u5EF4", "\u2F36"=>"\u5EFE", "\u2F37"=>"\u5F0B", + "\u2F38"=>"\u5F13", "\u2F39"=>"\u5F50", "\u2F3A"=>"\u5F61", "\u2F3B"=>"\u5F73", "\u2F3C"=>"\u5FC3", "\u2F3D"=>"\u6208", "\u2F3E"=>"\u6236", "\u2F3F"=>"\u624B", + "\u2F40"=>"\u652F", "\u2F41"=>"\u6534", "\u2F42"=>"\u6587", "\u2F43"=>"\u6597", "\u2F44"=>"\u65A4", "\u2F45"=>"\u65B9", "\u2F46"=>"\u65E0", "\u2F47"=>"\u65E5", + "\u2F48"=>"\u66F0", "\u2F49"=>"\u6708", "\u2F4A"=>"\u6728", "\u2F4B"=>"\u6B20", "\u2F4C"=>"\u6B62", "\u2F4D"=>"\u6B79", "\u2F4E"=>"\u6BB3", "\u2F4F"=>"\u6BCB", + "\u2F50"=>"\u6BD4", "\u2F51"=>"\u6BDB", "\u2F52"=>"\u6C0F", "\u2F53"=>"\u6C14", "\u2F54"=>"\u6C34", "\u2F55"=>"\u706B", "\u2F56"=>"\u722A", "\u2F57"=>"\u7236", + "\u2F58"=>"\u723B", "\u2F59"=>"\u723F", "\u2F5A"=>"\u7247", "\u2F5B"=>"\u7259", "\u2F5C"=>"\u725B", "\u2F5D"=>"\u72AC", "\u2F5E"=>"\u7384", "\u2F5F"=>"\u7389", + "\u2F60"=>"\u74DC", "\u2F61"=>"\u74E6", "\u2F62"=>"\u7518", "\u2F63"=>"\u751F", "\u2F64"=>"\u7528", "\u2F65"=>"\u7530", "\u2F66"=>"\u758B", "\u2F67"=>"\u7592", + "\u2F68"=>"\u7676", "\u2F69"=>"\u767D", "\u2F6A"=>"\u76AE", "\u2F6B"=>"\u76BF", "\u2F6C"=>"\u76EE", "\u2F6D"=>"\u77DB", "\u2F6E"=>"\u77E2", "\u2F6F"=>"\u77F3", + "\u2F70"=>"\u793A", "\u2F71"=>"\u79B8", "\u2F72"=>"\u79BE", "\u2F73"=>"\u7A74", "\u2F74"=>"\u7ACB", "\u2F75"=>"\u7AF9", "\u2F76"=>"\u7C73", "\u2F77"=>"\u7CF8", + "\u2F78"=>"\u7F36", "\u2F79"=>"\u7F51", "\u2F7A"=>"\u7F8A", "\u2F7B"=>"\u7FBD", "\u2F7C"=>"\u8001", "\u2F7D"=>"\u800C", "\u2F7E"=>"\u8012", "\u2F7F"=>"\u8033", + "\u2F80"=>"\u807F", "\u2F81"=>"\u8089", "\u2F82"=>"\u81E3", "\u2F83"=>"\u81EA", "\u2F84"=>"\u81F3", "\u2F85"=>"\u81FC", "\u2F86"=>"\u820C", "\u2F87"=>"\u821B", + "\u2F88"=>"\u821F", "\u2F89"=>"\u826E", "\u2F8A"=>"\u8272", "\u2F8B"=>"\u8278", "\u2F8C"=>"\u864D", "\u2F8D"=>"\u866B", "\u2F8E"=>"\u8840", "\u2F8F"=>"\u884C", + "\u2F90"=>"\u8863", "\u2F91"=>"\u897E", "\u2F92"=>"\u898B", "\u2F93"=>"\u89D2", "\u2F94"=>"\u8A00", "\u2F95"=>"\u8C37", "\u2F96"=>"\u8C46", "\u2F97"=>"\u8C55", + "\u2F98"=>"\u8C78", "\u2F99"=>"\u8C9D", "\u2F9A"=>"\u8D64", "\u2F9B"=>"\u8D70", "\u2F9C"=>"\u8DB3", "\u2F9D"=>"\u8EAB", "\u2F9E"=>"\u8ECA", "\u2F9F"=>"\u8F9B", + "\u2FA0"=>"\u8FB0", "\u2FA1"=>"\u8FB5", "\u2FA2"=>"\u9091", "\u2FA3"=>"\u9149", "\u2FA4"=>"\u91C6", "\u2FA5"=>"\u91CC", "\u2FA6"=>"\u91D1", "\u2FA7"=>"\u9577", + "\u2FA8"=>"\u9580", "\u2FA9"=>"\u961C", "\u2FAA"=>"\u96B6", "\u2FAB"=>"\u96B9", "\u2FAC"=>"\u96E8", "\u2FAD"=>"\u9751", "\u2FAE"=>"\u975E", "\u2FAF"=>"\u9762", + "\u2FB0"=>"\u9769", "\u2FB1"=>"\u97CB", "\u2FB2"=>"\u97ED", "\u2FB3"=>"\u97F3", "\u2FB4"=>"\u9801", "\u2FB5"=>"\u98A8", "\u2FB6"=>"\u98DB", "\u2FB7"=>"\u98DF", + "\u2FB8"=>"\u9996", "\u2FB9"=>"\u9999", "\u2FBA"=>"\u99AC", "\u2FBB"=>"\u9AA8", "\u2FBC"=>"\u9AD8", "\u2FBD"=>"\u9ADF", "\u2FBE"=>"\u9B25", "\u2FBF"=>"\u9B2F", + "\u2FC0"=>"\u9B32", "\u2FC1"=>"\u9B3C", "\u2FC2"=>"\u9B5A", "\u2FC3"=>"\u9CE5", "\u2FC4"=>"\u9E75", "\u2FC5"=>"\u9E7F", "\u2FC6"=>"\u9EA5", "\u2FC7"=>"\u9EBB", + "\u2FC8"=>"\u9EC3", "\u2FC9"=>"\u9ECD", "\u2FCA"=>"\u9ED1", "\u2FCB"=>"\u9EF9", "\u2FCC"=>"\u9EFD", "\u2FCD"=>"\u9F0E", "\u2FCE"=>"\u9F13", "\u2FCF"=>"\u9F20", + "\u2FD0"=>"\u9F3B", "\u2FD1"=>"\u9F4A", "\u2FD2"=>"\u9F52", "\u2FD3"=>"\u9F8D", "\u2FD4"=>"\u9F9C", "\u2FD5"=>"\u9FA0", "\u3000"=>" ", "\u3036"=>"\u3012", + "\u3038"=>"\u5341", "\u3039"=>"\u5344", "\u303A"=>"\u5345", "\u309B"=>" \u3099", "\u309C"=>" \u309A", "\u309F"=>"\u3088\u308A", "\u30FF"=>"\u30B3\u30C8", "\u3131"=>"\u1100", + "\u3132"=>"\u1101", "\u3133"=>"\u11AA", "\u3134"=>"\u1102", "\u3135"=>"\u11AC", "\u3136"=>"\u11AD", "\u3137"=>"\u1103", "\u3138"=>"\u1104", "\u3139"=>"\u1105", + "\u313A"=>"\u11B0", "\u313B"=>"\u11B1", "\u313C"=>"\u11B2", "\u313D"=>"\u11B3", "\u313E"=>"\u11B4", "\u313F"=>"\u11B5", "\u3140"=>"\u111A", "\u3141"=>"\u1106", + "\u3142"=>"\u1107", "\u3143"=>"\u1108", "\u3144"=>"\u1121", "\u3145"=>"\u1109", "\u3146"=>"\u110A", "\u3147"=>"\u110B", "\u3148"=>"\u110C", "\u3149"=>"\u110D", + "\u314A"=>"\u110E", "\u314B"=>"\u110F", "\u314C"=>"\u1110", "\u314D"=>"\u1111", "\u314E"=>"\u1112", "\u314F"=>"\u1161", "\u3150"=>"\u1162", "\u3151"=>"\u1163", + "\u3152"=>"\u1164", "\u3153"=>"\u1165", "\u3154"=>"\u1166", "\u3155"=>"\u1167", "\u3156"=>"\u1168", "\u3157"=>"\u1169", "\u3158"=>"\u116A", "\u3159"=>"\u116B", + "\u315A"=>"\u116C", "\u315B"=>"\u116D", "\u315C"=>"\u116E", "\u315D"=>"\u116F", "\u315E"=>"\u1170", "\u315F"=>"\u1171", "\u3160"=>"\u1172", "\u3161"=>"\u1173", + "\u3162"=>"\u1174", "\u3163"=>"\u1175", "\u3164"=>"\u1160", "\u3165"=>"\u1114", "\u3166"=>"\u1115", "\u3167"=>"\u11C7", "\u3168"=>"\u11C8", "\u3169"=>"\u11CC", + "\u316A"=>"\u11CE", "\u316B"=>"\u11D3", "\u316C"=>"\u11D7", "\u316D"=>"\u11D9", "\u316E"=>"\u111C", "\u316F"=>"\u11DD", "\u3170"=>"\u11DF", "\u3171"=>"\u111D", + "\u3172"=>"\u111E", "\u3173"=>"\u1120", "\u3174"=>"\u1122", "\u3175"=>"\u1123", "\u3176"=>"\u1127", "\u3177"=>"\u1129", "\u3178"=>"\u112B", "\u3179"=>"\u112C", + "\u317A"=>"\u112D", "\u317B"=>"\u112E", "\u317C"=>"\u112F", "\u317D"=>"\u1132", "\u317E"=>"\u1136", "\u317F"=>"\u1140", "\u3180"=>"\u1147", "\u3181"=>"\u114C", + "\u3182"=>"\u11F1", "\u3183"=>"\u11F2", "\u3184"=>"\u1157", "\u3185"=>"\u1158", "\u3186"=>"\u1159", "\u3187"=>"\u1184", "\u3188"=>"\u1185", "\u3189"=>"\u1188", + "\u318A"=>"\u1191", "\u318B"=>"\u1192", "\u318C"=>"\u1194", "\u318D"=>"\u119E", "\u318E"=>"\u11A1", "\u3192"=>"\u4E00", "\u3193"=>"\u4E8C", "\u3194"=>"\u4E09", + "\u3195"=>"\u56DB", "\u3196"=>"\u4E0A", "\u3197"=>"\u4E2D", "\u3198"=>"\u4E0B", "\u3199"=>"\u7532", "\u319A"=>"\u4E59", "\u319B"=>"\u4E19", "\u319C"=>"\u4E01", + "\u319D"=>"\u5929", "\u319E"=>"\u5730", "\u319F"=>"\u4EBA", "\u3200"=>"(\u1100)", "\u3201"=>"(\u1102)", "\u3202"=>"(\u1103)", "\u3203"=>"(\u1105)", "\u3204"=>"(\u1106)", + "\u3205"=>"(\u1107)", "\u3206"=>"(\u1109)", "\u3207"=>"(\u110B)", "\u3208"=>"(\u110C)", "\u3209"=>"(\u110E)", "\u320A"=>"(\u110F)", "\u320B"=>"(\u1110)", "\u320C"=>"(\u1111)", + "\u320D"=>"(\u1112)", "\u320E"=>"(\u1100\u1161)", "\u320F"=>"(\u1102\u1161)", "\u3210"=>"(\u1103\u1161)", "\u3211"=>"(\u1105\u1161)", "\u3212"=>"(\u1106\u1161)", "\u3213"=>"(\u1107\u1161)", "\u3214"=>"(\u1109\u1161)", + "\u3215"=>"(\u110B\u1161)", "\u3216"=>"(\u110C\u1161)", "\u3217"=>"(\u110E\u1161)", "\u3218"=>"(\u110F\u1161)", "\u3219"=>"(\u1110\u1161)", "\u321A"=>"(\u1111\u1161)", "\u321B"=>"(\u1112\u1161)", "\u321C"=>"(\u110C\u116E)", + "\u321D"=>"(\u110B\u1169\u110C\u1165\u11AB)", "\u321E"=>"(\u110B\u1169\u1112\u116E)", "\u3220"=>"(\u4E00)", "\u3221"=>"(\u4E8C)", "\u3222"=>"(\u4E09)", "\u3223"=>"(\u56DB)", "\u3224"=>"(\u4E94)", "\u3225"=>"(\u516D)", + "\u3226"=>"(\u4E03)", "\u3227"=>"(\u516B)", "\u3228"=>"(\u4E5D)", "\u3229"=>"(\u5341)", "\u322A"=>"(\u6708)", "\u322B"=>"(\u706B)", "\u322C"=>"(\u6C34)", "\u322D"=>"(\u6728)", + "\u322E"=>"(\u91D1)", "\u322F"=>"(\u571F)", "\u3230"=>"(\u65E5)", "\u3231"=>"(\u682A)", "\u3232"=>"(\u6709)", "\u3233"=>"(\u793E)", "\u3234"=>"(\u540D)", "\u3235"=>"(\u7279)", + "\u3236"=>"(\u8CA1)", "\u3237"=>"(\u795D)", "\u3238"=>"(\u52B4)", "\u3239"=>"(\u4EE3)", "\u323A"=>"(\u547C)", "\u323B"=>"(\u5B66)", "\u323C"=>"(\u76E3)", "\u323D"=>"(\u4F01)", + "\u323E"=>"(\u8CC7)", "\u323F"=>"(\u5354)", "\u3240"=>"(\u796D)", "\u3241"=>"(\u4F11)", "\u3242"=>"(\u81EA)", "\u3243"=>"(\u81F3)", "\u3244"=>"\u554F", "\u3245"=>"\u5E7C", + "\u3246"=>"\u6587", "\u3247"=>"\u7B8F", "\u3250"=>"PTE", "\u3251"=>"21", "\u3252"=>"22", "\u3253"=>"23", "\u3254"=>"24", "\u3255"=>"25", + "\u3256"=>"26", "\u3257"=>"27", "\u3258"=>"28", "\u3259"=>"29", "\u325A"=>"30", "\u325B"=>"31", "\u325C"=>"32", "\u325D"=>"33", + "\u325E"=>"34", "\u325F"=>"35", "\u3260"=>"\u1100", "\u3261"=>"\u1102", "\u3262"=>"\u1103", "\u3263"=>"\u1105", "\u3264"=>"\u1106", "\u3265"=>"\u1107", + "\u3266"=>"\u1109", "\u3267"=>"\u110B", "\u3268"=>"\u110C", "\u3269"=>"\u110E", "\u326A"=>"\u110F", "\u326B"=>"\u1110", "\u326C"=>"\u1111", "\u326D"=>"\u1112", + "\u326E"=>"\u1100\u1161", "\u326F"=>"\u1102\u1161", "\u3270"=>"\u1103\u1161", "\u3271"=>"\u1105\u1161", "\u3272"=>"\u1106\u1161", "\u3273"=>"\u1107\u1161", "\u3274"=>"\u1109\u1161", "\u3275"=>"\u110B\u1161", + "\u3276"=>"\u110C\u1161", "\u3277"=>"\u110E\u1161", "\u3278"=>"\u110F\u1161", "\u3279"=>"\u1110\u1161", "\u327A"=>"\u1111\u1161", "\u327B"=>"\u1112\u1161", "\u327C"=>"\u110E\u1161\u11B7\u1100\u1169", "\u327D"=>"\u110C\u116E\u110B\u1174", + "\u327E"=>"\u110B\u116E", "\u3280"=>"\u4E00", "\u3281"=>"\u4E8C", "\u3282"=>"\u4E09", "\u3283"=>"\u56DB", "\u3284"=>"\u4E94", "\u3285"=>"\u516D", "\u3286"=>"\u4E03", + "\u3287"=>"\u516B", "\u3288"=>"\u4E5D", "\u3289"=>"\u5341", "\u328A"=>"\u6708", "\u328B"=>"\u706B", "\u328C"=>"\u6C34", "\u328D"=>"\u6728", "\u328E"=>"\u91D1", + "\u328F"=>"\u571F", "\u3290"=>"\u65E5", "\u3291"=>"\u682A", "\u3292"=>"\u6709", "\u3293"=>"\u793E", "\u3294"=>"\u540D", "\u3295"=>"\u7279", "\u3296"=>"\u8CA1", + "\u3297"=>"\u795D", "\u3298"=>"\u52B4", "\u3299"=>"\u79D8", "\u329A"=>"\u7537", "\u329B"=>"\u5973", "\u329C"=>"\u9069", "\u329D"=>"\u512A", "\u329E"=>"\u5370", + "\u329F"=>"\u6CE8", "\u32A0"=>"\u9805", "\u32A1"=>"\u4F11", "\u32A2"=>"\u5199", "\u32A3"=>"\u6B63", "\u32A4"=>"\u4E0A", "\u32A5"=>"\u4E2D", "\u32A6"=>"\u4E0B", + "\u32A7"=>"\u5DE6", "\u32A8"=>"\u53F3", "\u32A9"=>"\u533B", "\u32AA"=>"\u5B97", "\u32AB"=>"\u5B66", "\u32AC"=>"\u76E3", "\u32AD"=>"\u4F01", "\u32AE"=>"\u8CC7", + "\u32AF"=>"\u5354", "\u32B0"=>"\u591C", "\u32B1"=>"36", "\u32B2"=>"37", "\u32B3"=>"38", "\u32B4"=>"39", "\u32B5"=>"40", "\u32B6"=>"41", + "\u32B7"=>"42", "\u32B8"=>"43", "\u32B9"=>"44", "\u32BA"=>"45", "\u32BB"=>"46", "\u32BC"=>"47", "\u32BD"=>"48", "\u32BE"=>"49", + "\u32BF"=>"50", "\u32C0"=>"1\u6708", "\u32C1"=>"2\u6708", "\u32C2"=>"3\u6708", "\u32C3"=>"4\u6708", "\u32C4"=>"5\u6708", "\u32C5"=>"6\u6708", "\u32C6"=>"7\u6708", + "\u32C7"=>"8\u6708", "\u32C8"=>"9\u6708", "\u32C9"=>"10\u6708", "\u32CA"=>"11\u6708", "\u32CB"=>"12\u6708", "\u32CC"=>"Hg", "\u32CD"=>"erg", "\u32CE"=>"eV", + "\u32CF"=>"LTD", "\u32D0"=>"\u30A2", "\u32D1"=>"\u30A4", "\u32D2"=>"\u30A6", "\u32D3"=>"\u30A8", "\u32D4"=>"\u30AA", "\u32D5"=>"\u30AB", "\u32D6"=>"\u30AD", + "\u32D7"=>"\u30AF", "\u32D8"=>"\u30B1", "\u32D9"=>"\u30B3", "\u32DA"=>"\u30B5", "\u32DB"=>"\u30B7", "\u32DC"=>"\u30B9", "\u32DD"=>"\u30BB", "\u32DE"=>"\u30BD", + "\u32DF"=>"\u30BF", "\u32E0"=>"\u30C1", "\u32E1"=>"\u30C4", "\u32E2"=>"\u30C6", "\u32E3"=>"\u30C8", "\u32E4"=>"\u30CA", "\u32E5"=>"\u30CB", "\u32E6"=>"\u30CC", + "\u32E7"=>"\u30CD", "\u32E8"=>"\u30CE", "\u32E9"=>"\u30CF", "\u32EA"=>"\u30D2", "\u32EB"=>"\u30D5", "\u32EC"=>"\u30D8", "\u32ED"=>"\u30DB", "\u32EE"=>"\u30DE", + "\u32EF"=>"\u30DF", "\u32F0"=>"\u30E0", "\u32F1"=>"\u30E1", "\u32F2"=>"\u30E2", "\u32F3"=>"\u30E4", "\u32F4"=>"\u30E6", "\u32F5"=>"\u30E8", "\u32F6"=>"\u30E9", + "\u32F7"=>"\u30EA", "\u32F8"=>"\u30EB", "\u32F9"=>"\u30EC", "\u32FA"=>"\u30ED", "\u32FB"=>"\u30EF", "\u32FC"=>"\u30F0", "\u32FD"=>"\u30F1", "\u32FE"=>"\u30F2", + "\u3300"=>"\u30A2\u30D1\u30FC\u30C8", "\u3301"=>"\u30A2\u30EB\u30D5\u30A1", "\u3302"=>"\u30A2\u30F3\u30DA\u30A2", "\u3303"=>"\u30A2\u30FC\u30EB", "\u3304"=>"\u30A4\u30CB\u30F3\u30B0", "\u3305"=>"\u30A4\u30F3\u30C1", "\u3306"=>"\u30A6\u30A9\u30F3", "\u3307"=>"\u30A8\u30B9\u30AF\u30FC\u30C9", + "\u3308"=>"\u30A8\u30FC\u30AB\u30FC", "\u3309"=>"\u30AA\u30F3\u30B9", "\u330A"=>"\u30AA\u30FC\u30E0", "\u330B"=>"\u30AB\u30A4\u30EA", "\u330C"=>"\u30AB\u30E9\u30C3\u30C8", "\u330D"=>"\u30AB\u30ED\u30EA\u30FC", "\u330E"=>"\u30AC\u30ED\u30F3", "\u330F"=>"\u30AC\u30F3\u30DE", + "\u3310"=>"\u30AE\u30AC", "\u3311"=>"\u30AE\u30CB\u30FC", "\u3312"=>"\u30AD\u30E5\u30EA\u30FC", "\u3313"=>"\u30AE\u30EB\u30C0\u30FC", "\u3314"=>"\u30AD\u30ED", "\u3315"=>"\u30AD\u30ED\u30B0\u30E9\u30E0", "\u3316"=>"\u30AD\u30ED\u30E1\u30FC\u30C8\u30EB", "\u3317"=>"\u30AD\u30ED\u30EF\u30C3\u30C8", + "\u3318"=>"\u30B0\u30E9\u30E0", "\u3319"=>"\u30B0\u30E9\u30E0\u30C8\u30F3", "\u331A"=>"\u30AF\u30EB\u30BC\u30A4\u30ED", "\u331B"=>"\u30AF\u30ED\u30FC\u30CD", "\u331C"=>"\u30B1\u30FC\u30B9", "\u331D"=>"\u30B3\u30EB\u30CA", "\u331E"=>"\u30B3\u30FC\u30DD", "\u331F"=>"\u30B5\u30A4\u30AF\u30EB", + "\u3320"=>"\u30B5\u30F3\u30C1\u30FC\u30E0", "\u3321"=>"\u30B7\u30EA\u30F3\u30B0", "\u3322"=>"\u30BB\u30F3\u30C1", "\u3323"=>"\u30BB\u30F3\u30C8", "\u3324"=>"\u30C0\u30FC\u30B9", "\u3325"=>"\u30C7\u30B7", "\u3326"=>"\u30C9\u30EB", "\u3327"=>"\u30C8\u30F3", + "\u3328"=>"\u30CA\u30CE", "\u3329"=>"\u30CE\u30C3\u30C8", "\u332A"=>"\u30CF\u30A4\u30C4", "\u332B"=>"\u30D1\u30FC\u30BB\u30F3\u30C8", "\u332C"=>"\u30D1\u30FC\u30C4", "\u332D"=>"\u30D0\u30FC\u30EC\u30EB", "\u332E"=>"\u30D4\u30A2\u30B9\u30C8\u30EB", "\u332F"=>"\u30D4\u30AF\u30EB", + "\u3330"=>"\u30D4\u30B3", "\u3331"=>"\u30D3\u30EB", "\u3332"=>"\u30D5\u30A1\u30E9\u30C3\u30C9", "\u3333"=>"\u30D5\u30A3\u30FC\u30C8", "\u3334"=>"\u30D6\u30C3\u30B7\u30A7\u30EB", "\u3335"=>"\u30D5\u30E9\u30F3", "\u3336"=>"\u30D8\u30AF\u30BF\u30FC\u30EB", "\u3337"=>"\u30DA\u30BD", + "\u3338"=>"\u30DA\u30CB\u30D2", "\u3339"=>"\u30D8\u30EB\u30C4", "\u333A"=>"\u30DA\u30F3\u30B9", "\u333B"=>"\u30DA\u30FC\u30B8", "\u333C"=>"\u30D9\u30FC\u30BF", "\u333D"=>"\u30DD\u30A4\u30F3\u30C8", "\u333E"=>"\u30DC\u30EB\u30C8", "\u333F"=>"\u30DB\u30F3", + "\u3340"=>"\u30DD\u30F3\u30C9", "\u3341"=>"\u30DB\u30FC\u30EB", "\u3342"=>"\u30DB\u30FC\u30F3", "\u3343"=>"\u30DE\u30A4\u30AF\u30ED", "\u3344"=>"\u30DE\u30A4\u30EB", "\u3345"=>"\u30DE\u30C3\u30CF", "\u3346"=>"\u30DE\u30EB\u30AF", "\u3347"=>"\u30DE\u30F3\u30B7\u30E7\u30F3", + "\u3348"=>"\u30DF\u30AF\u30ED\u30F3", "\u3349"=>"\u30DF\u30EA", "\u334A"=>"\u30DF\u30EA\u30D0\u30FC\u30EB", "\u334B"=>"\u30E1\u30AC", "\u334C"=>"\u30E1\u30AC\u30C8\u30F3", "\u334D"=>"\u30E1\u30FC\u30C8\u30EB", "\u334E"=>"\u30E4\u30FC\u30C9", "\u334F"=>"\u30E4\u30FC\u30EB", + "\u3350"=>"\u30E6\u30A2\u30F3", "\u3351"=>"\u30EA\u30C3\u30C8\u30EB", "\u3352"=>"\u30EA\u30E9", "\u3353"=>"\u30EB\u30D4\u30FC", "\u3354"=>"\u30EB\u30FC\u30D6\u30EB", "\u3355"=>"\u30EC\u30E0", "\u3356"=>"\u30EC\u30F3\u30C8\u30B2\u30F3", "\u3357"=>"\u30EF\u30C3\u30C8", + "\u3358"=>"0\u70B9", "\u3359"=>"1\u70B9", "\u335A"=>"2\u70B9", "\u335B"=>"3\u70B9", "\u335C"=>"4\u70B9", "\u335D"=>"5\u70B9", "\u335E"=>"6\u70B9", "\u335F"=>"7\u70B9", + "\u3360"=>"8\u70B9", "\u3361"=>"9\u70B9", "\u3362"=>"10\u70B9", "\u3363"=>"11\u70B9", "\u3364"=>"12\u70B9", "\u3365"=>"13\u70B9", "\u3366"=>"14\u70B9", "\u3367"=>"15\u70B9", + "\u3368"=>"16\u70B9", "\u3369"=>"17\u70B9", "\u336A"=>"18\u70B9", "\u336B"=>"19\u70B9", "\u336C"=>"20\u70B9", "\u336D"=>"21\u70B9", "\u336E"=>"22\u70B9", "\u336F"=>"23\u70B9", + "\u3370"=>"24\u70B9", "\u3371"=>"hPa", "\u3372"=>"da", "\u3373"=>"AU", "\u3374"=>"bar", "\u3375"=>"oV", "\u3376"=>"pc", "\u3377"=>"dm", + "\u3378"=>"dm2", "\u3379"=>"dm3", "\u337A"=>"IU", "\u337B"=>"\u5E73\u6210", "\u337C"=>"\u662D\u548C", "\u337D"=>"\u5927\u6B63", "\u337E"=>"\u660E\u6CBB", "\u337F"=>"\u682A\u5F0F\u4F1A\u793E", + "\u3380"=>"pA", "\u3381"=>"nA", "\u3382"=>"\u03BCA", "\u3383"=>"mA", "\u3384"=>"kA", "\u3385"=>"KB", "\u3386"=>"MB", "\u3387"=>"GB", + "\u3388"=>"cal", "\u3389"=>"kcal", "\u338A"=>"pF", "\u338B"=>"nF", "\u338C"=>"\u03BCF", "\u338D"=>"\u03BCg", "\u338E"=>"mg", "\u338F"=>"kg", + "\u3390"=>"Hz", "\u3391"=>"kHz", "\u3392"=>"MHz", "\u3393"=>"GHz", "\u3394"=>"THz", "\u3395"=>"\u03BCl", "\u3396"=>"ml", "\u3397"=>"dl", + "\u3398"=>"kl", "\u3399"=>"fm", "\u339A"=>"nm", "\u339B"=>"\u03BCm", "\u339C"=>"mm", "\u339D"=>"cm", "\u339E"=>"km", "\u339F"=>"mm2", + "\u33A0"=>"cm2", "\u33A1"=>"m2", "\u33A2"=>"km2", "\u33A3"=>"mm3", "\u33A4"=>"cm3", "\u33A5"=>"m3", "\u33A6"=>"km3", "\u33A7"=>"m\u2215s", + "\u33A8"=>"m\u2215s2", "\u33A9"=>"Pa", "\u33AA"=>"kPa", "\u33AB"=>"MPa", "\u33AC"=>"GPa", "\u33AD"=>"rad", "\u33AE"=>"rad\u2215s", "\u33AF"=>"rad\u2215s2", + "\u33B0"=>"ps", "\u33B1"=>"ns", "\u33B2"=>"\u03BCs", "\u33B3"=>"ms", "\u33B4"=>"pV", "\u33B5"=>"nV", "\u33B6"=>"\u03BCV", "\u33B7"=>"mV", + "\u33B8"=>"kV", "\u33B9"=>"MV", "\u33BA"=>"pW", "\u33BB"=>"nW", "\u33BC"=>"\u03BCW", "\u33BD"=>"mW", "\u33BE"=>"kW", "\u33BF"=>"MW", + "\u33C0"=>"k\u03A9", "\u33C1"=>"M\u03A9", "\u33C2"=>"a.m.", "\u33C3"=>"Bq", "\u33C4"=>"cc", "\u33C5"=>"cd", "\u33C6"=>"C\u2215kg", "\u33C7"=>"Co.", + "\u33C8"=>"dB", "\u33C9"=>"Gy", "\u33CA"=>"ha", "\u33CB"=>"HP", "\u33CC"=>"in", "\u33CD"=>"KK", "\u33CE"=>"KM", "\u33CF"=>"kt", + "\u33D0"=>"lm", "\u33D1"=>"ln", "\u33D2"=>"log", "\u33D3"=>"lx", "\u33D4"=>"mb", "\u33D5"=>"mil", "\u33D6"=>"mol", "\u33D7"=>"PH", + "\u33D8"=>"p.m.", "\u33D9"=>"PPM", "\u33DA"=>"PR", "\u33DB"=>"sr", "\u33DC"=>"Sv", "\u33DD"=>"Wb", "\u33DE"=>"V\u2215m", "\u33DF"=>"A\u2215m", + "\u33E0"=>"1\u65E5", "\u33E1"=>"2\u65E5", "\u33E2"=>"3\u65E5", "\u33E3"=>"4\u65E5", "\u33E4"=>"5\u65E5", "\u33E5"=>"6\u65E5", "\u33E6"=>"7\u65E5", "\u33E7"=>"8\u65E5", + "\u33E8"=>"9\u65E5", "\u33E9"=>"10\u65E5", "\u33EA"=>"11\u65E5", "\u33EB"=>"12\u65E5", "\u33EC"=>"13\u65E5", "\u33ED"=>"14\u65E5", "\u33EE"=>"15\u65E5", "\u33EF"=>"16\u65E5", + "\u33F0"=>"17\u65E5", "\u33F1"=>"18\u65E5", "\u33F2"=>"19\u65E5", "\u33F3"=>"20\u65E5", "\u33F4"=>"21\u65E5", "\u33F5"=>"22\u65E5", "\u33F6"=>"23\u65E5", "\u33F7"=>"24\u65E5", + "\u33F8"=>"25\u65E5", "\u33F9"=>"26\u65E5", "\u33FA"=>"27\u65E5", "\u33FB"=>"28\u65E5", "\u33FC"=>"29\u65E5", "\u33FD"=>"30\u65E5", "\u33FE"=>"31\u65E5", "\u33FF"=>"gal", + "\uA69C"=>"\u044A", "\uA69D"=>"\u044C", "\uA770"=>"\uA76F", "\uA7F8"=>"\u0126", "\uA7F9"=>"\u0153", "\uAB5C"=>"\uA727", "\uAB5D"=>"\uAB37", "\uAB5E"=>"\u026B", + "\uAB5F"=>"\uAB52", "\uFB00"=>"ff", "\uFB01"=>"fi", "\uFB02"=>"fl", "\uFB03"=>"ffi", "\uFB04"=>"ffl", "\uFB05"=>"st", "\uFB06"=>"st", + "\uFB13"=>"\u0574\u0576", "\uFB14"=>"\u0574\u0565", "\uFB15"=>"\u0574\u056B", "\uFB16"=>"\u057E\u0576", "\uFB17"=>"\u0574\u056D", "\uFB20"=>"\u05E2", "\uFB21"=>"\u05D0", "\uFB22"=>"\u05D3", + "\uFB23"=>"\u05D4", "\uFB24"=>"\u05DB", "\uFB25"=>"\u05DC", "\uFB26"=>"\u05DD", "\uFB27"=>"\u05E8", "\uFB28"=>"\u05EA", "\uFB29"=>"+", "\uFB4F"=>"\u05D0\u05DC", + "\uFB50"=>"\u0671", "\uFB51"=>"\u0671", "\uFB52"=>"\u067B", "\uFB53"=>"\u067B", "\uFB54"=>"\u067B", "\uFB55"=>"\u067B", "\uFB56"=>"\u067E", "\uFB57"=>"\u067E", + "\uFB58"=>"\u067E", "\uFB59"=>"\u067E", "\uFB5A"=>"\u0680", "\uFB5B"=>"\u0680", "\uFB5C"=>"\u0680", "\uFB5D"=>"\u0680", "\uFB5E"=>"\u067A", "\uFB5F"=>"\u067A", + "\uFB60"=>"\u067A", "\uFB61"=>"\u067A", "\uFB62"=>"\u067F", "\uFB63"=>"\u067F", "\uFB64"=>"\u067F", "\uFB65"=>"\u067F", "\uFB66"=>"\u0679", "\uFB67"=>"\u0679", + "\uFB68"=>"\u0679", "\uFB69"=>"\u0679", "\uFB6A"=>"\u06A4", "\uFB6B"=>"\u06A4", "\uFB6C"=>"\u06A4", "\uFB6D"=>"\u06A4", "\uFB6E"=>"\u06A6", "\uFB6F"=>"\u06A6", + "\uFB70"=>"\u06A6", "\uFB71"=>"\u06A6", "\uFB72"=>"\u0684", "\uFB73"=>"\u0684", "\uFB74"=>"\u0684", "\uFB75"=>"\u0684", "\uFB76"=>"\u0683", "\uFB77"=>"\u0683", + "\uFB78"=>"\u0683", "\uFB79"=>"\u0683", "\uFB7A"=>"\u0686", "\uFB7B"=>"\u0686", "\uFB7C"=>"\u0686", "\uFB7D"=>"\u0686", "\uFB7E"=>"\u0687", "\uFB7F"=>"\u0687", + "\uFB80"=>"\u0687", "\uFB81"=>"\u0687", "\uFB82"=>"\u068D", "\uFB83"=>"\u068D", "\uFB84"=>"\u068C", "\uFB85"=>"\u068C", "\uFB86"=>"\u068E", "\uFB87"=>"\u068E", + "\uFB88"=>"\u0688", "\uFB89"=>"\u0688", "\uFB8A"=>"\u0698", "\uFB8B"=>"\u0698", "\uFB8C"=>"\u0691", "\uFB8D"=>"\u0691", "\uFB8E"=>"\u06A9", "\uFB8F"=>"\u06A9", + "\uFB90"=>"\u06A9", "\uFB91"=>"\u06A9", "\uFB92"=>"\u06AF", "\uFB93"=>"\u06AF", "\uFB94"=>"\u06AF", "\uFB95"=>"\u06AF", "\uFB96"=>"\u06B3", "\uFB97"=>"\u06B3", + "\uFB98"=>"\u06B3", "\uFB99"=>"\u06B3", "\uFB9A"=>"\u06B1", "\uFB9B"=>"\u06B1", "\uFB9C"=>"\u06B1", "\uFB9D"=>"\u06B1", "\uFB9E"=>"\u06BA", "\uFB9F"=>"\u06BA", + "\uFBA0"=>"\u06BB", "\uFBA1"=>"\u06BB", "\uFBA2"=>"\u06BB", "\uFBA3"=>"\u06BB", "\uFBA4"=>"\u06C0", "\uFBA5"=>"\u06C0", "\uFBA6"=>"\u06C1", "\uFBA7"=>"\u06C1", + "\uFBA8"=>"\u06C1", "\uFBA9"=>"\u06C1", "\uFBAA"=>"\u06BE", "\uFBAB"=>"\u06BE", "\uFBAC"=>"\u06BE", "\uFBAD"=>"\u06BE", "\uFBAE"=>"\u06D2", "\uFBAF"=>"\u06D2", + "\uFBB0"=>"\u06D3", "\uFBB1"=>"\u06D3", "\uFBD3"=>"\u06AD", "\uFBD4"=>"\u06AD", "\uFBD5"=>"\u06AD", "\uFBD6"=>"\u06AD", "\uFBD7"=>"\u06C7", "\uFBD8"=>"\u06C7", + "\uFBD9"=>"\u06C6", "\uFBDA"=>"\u06C6", "\uFBDB"=>"\u06C8", "\uFBDC"=>"\u06C8", "\uFBDD"=>"\u06C7\u0674", "\uFBDE"=>"\u06CB", "\uFBDF"=>"\u06CB", "\uFBE0"=>"\u06C5", + "\uFBE1"=>"\u06C5", "\uFBE2"=>"\u06C9", "\uFBE3"=>"\u06C9", "\uFBE4"=>"\u06D0", "\uFBE5"=>"\u06D0", "\uFBE6"=>"\u06D0", "\uFBE7"=>"\u06D0", "\uFBE8"=>"\u0649", + "\uFBE9"=>"\u0649", "\uFBEA"=>"\u0626\u0627", "\uFBEB"=>"\u0626\u0627", "\uFBEC"=>"\u0626\u06D5", "\uFBED"=>"\u0626\u06D5", "\uFBEE"=>"\u0626\u0648", "\uFBEF"=>"\u0626\u0648", "\uFBF0"=>"\u0626\u06C7", + "\uFBF1"=>"\u0626\u06C7", "\uFBF2"=>"\u0626\u06C6", "\uFBF3"=>"\u0626\u06C6", "\uFBF4"=>"\u0626\u06C8", "\uFBF5"=>"\u0626\u06C8", "\uFBF6"=>"\u0626\u06D0", "\uFBF7"=>"\u0626\u06D0", "\uFBF8"=>"\u0626\u06D0", + "\uFBF9"=>"\u0626\u0649", "\uFBFA"=>"\u0626\u0649", "\uFBFB"=>"\u0626\u0649", "\uFBFC"=>"\u06CC", "\uFBFD"=>"\u06CC", "\uFBFE"=>"\u06CC", "\uFBFF"=>"\u06CC", "\uFC00"=>"\u0626\u062C", + "\uFC01"=>"\u0626\u062D", "\uFC02"=>"\u0626\u0645", "\uFC03"=>"\u0626\u0649", "\uFC04"=>"\u0626\u064A", "\uFC05"=>"\u0628\u062C", "\uFC06"=>"\u0628\u062D", "\uFC07"=>"\u0628\u062E", "\uFC08"=>"\u0628\u0645", + "\uFC09"=>"\u0628\u0649", "\uFC0A"=>"\u0628\u064A", "\uFC0B"=>"\u062A\u062C", "\uFC0C"=>"\u062A\u062D", "\uFC0D"=>"\u062A\u062E", "\uFC0E"=>"\u062A\u0645", "\uFC0F"=>"\u062A\u0649", "\uFC10"=>"\u062A\u064A", + "\uFC11"=>"\u062B\u062C", "\uFC12"=>"\u062B\u0645", "\uFC13"=>"\u062B\u0649", "\uFC14"=>"\u062B\u064A", "\uFC15"=>"\u062C\u062D", "\uFC16"=>"\u062C\u0645", "\uFC17"=>"\u062D\u062C", "\uFC18"=>"\u062D\u0645", + "\uFC19"=>"\u062E\u062C", "\uFC1A"=>"\u062E\u062D", "\uFC1B"=>"\u062E\u0645", "\uFC1C"=>"\u0633\u062C", "\uFC1D"=>"\u0633\u062D", "\uFC1E"=>"\u0633\u062E", "\uFC1F"=>"\u0633\u0645", "\uFC20"=>"\u0635\u062D", + "\uFC21"=>"\u0635\u0645", "\uFC22"=>"\u0636\u062C", "\uFC23"=>"\u0636\u062D", "\uFC24"=>"\u0636\u062E", "\uFC25"=>"\u0636\u0645", "\uFC26"=>"\u0637\u062D", "\uFC27"=>"\u0637\u0645", "\uFC28"=>"\u0638\u0645", + "\uFC29"=>"\u0639\u062C", "\uFC2A"=>"\u0639\u0645", "\uFC2B"=>"\u063A\u062C", "\uFC2C"=>"\u063A\u0645", "\uFC2D"=>"\u0641\u062C", "\uFC2E"=>"\u0641\u062D", "\uFC2F"=>"\u0641\u062E", "\uFC30"=>"\u0641\u0645", + "\uFC31"=>"\u0641\u0649", "\uFC32"=>"\u0641\u064A", "\uFC33"=>"\u0642\u062D", "\uFC34"=>"\u0642\u0645", "\uFC35"=>"\u0642\u0649", "\uFC36"=>"\u0642\u064A", "\uFC37"=>"\u0643\u0627", "\uFC38"=>"\u0643\u062C", + "\uFC39"=>"\u0643\u062D", "\uFC3A"=>"\u0643\u062E", "\uFC3B"=>"\u0643\u0644", "\uFC3C"=>"\u0643\u0645", "\uFC3D"=>"\u0643\u0649", "\uFC3E"=>"\u0643\u064A", "\uFC3F"=>"\u0644\u062C", "\uFC40"=>"\u0644\u062D", + "\uFC41"=>"\u0644\u062E", "\uFC42"=>"\u0644\u0645", "\uFC43"=>"\u0644\u0649", "\uFC44"=>"\u0644\u064A", "\uFC45"=>"\u0645\u062C", "\uFC46"=>"\u0645\u062D", "\uFC47"=>"\u0645\u062E", "\uFC48"=>"\u0645\u0645", + "\uFC49"=>"\u0645\u0649", "\uFC4A"=>"\u0645\u064A", "\uFC4B"=>"\u0646\u062C", "\uFC4C"=>"\u0646\u062D", "\uFC4D"=>"\u0646\u062E", "\uFC4E"=>"\u0646\u0645", "\uFC4F"=>"\u0646\u0649", "\uFC50"=>"\u0646\u064A", + "\uFC51"=>"\u0647\u062C", "\uFC52"=>"\u0647\u0645", "\uFC53"=>"\u0647\u0649", "\uFC54"=>"\u0647\u064A", "\uFC55"=>"\u064A\u062C", "\uFC56"=>"\u064A\u062D", "\uFC57"=>"\u064A\u062E", "\uFC58"=>"\u064A\u0645", + "\uFC59"=>"\u064A\u0649", "\uFC5A"=>"\u064A\u064A", "\uFC5B"=>"\u0630\u0670", "\uFC5C"=>"\u0631\u0670", "\uFC5D"=>"\u0649\u0670", "\uFC5E"=>" \u064C\u0651", "\uFC5F"=>" \u064D\u0651", "\uFC60"=>" \u064E\u0651", + "\uFC61"=>" \u064F\u0651", "\uFC62"=>" \u0650\u0651", "\uFC63"=>" \u0651\u0670", "\uFC64"=>"\u0626\u0631", "\uFC65"=>"\u0626\u0632", "\uFC66"=>"\u0626\u0645", "\uFC67"=>"\u0626\u0646", "\uFC68"=>"\u0626\u0649", + "\uFC69"=>"\u0626\u064A", "\uFC6A"=>"\u0628\u0631", "\uFC6B"=>"\u0628\u0632", "\uFC6C"=>"\u0628\u0645", "\uFC6D"=>"\u0628\u0646", "\uFC6E"=>"\u0628\u0649", "\uFC6F"=>"\u0628\u064A", "\uFC70"=>"\u062A\u0631", + "\uFC71"=>"\u062A\u0632", "\uFC72"=>"\u062A\u0645", "\uFC73"=>"\u062A\u0646", "\uFC74"=>"\u062A\u0649", "\uFC75"=>"\u062A\u064A", "\uFC76"=>"\u062B\u0631", "\uFC77"=>"\u062B\u0632", "\uFC78"=>"\u062B\u0645", + "\uFC79"=>"\u062B\u0646", "\uFC7A"=>"\u062B\u0649", "\uFC7B"=>"\u062B\u064A", "\uFC7C"=>"\u0641\u0649", "\uFC7D"=>"\u0641\u064A", "\uFC7E"=>"\u0642\u0649", "\uFC7F"=>"\u0642\u064A", "\uFC80"=>"\u0643\u0627", + "\uFC81"=>"\u0643\u0644", "\uFC82"=>"\u0643\u0645", "\uFC83"=>"\u0643\u0649", "\uFC84"=>"\u0643\u064A", "\uFC85"=>"\u0644\u0645", "\uFC86"=>"\u0644\u0649", "\uFC87"=>"\u0644\u064A", "\uFC88"=>"\u0645\u0627", + "\uFC89"=>"\u0645\u0645", "\uFC8A"=>"\u0646\u0631", "\uFC8B"=>"\u0646\u0632", "\uFC8C"=>"\u0646\u0645", "\uFC8D"=>"\u0646\u0646", "\uFC8E"=>"\u0646\u0649", "\uFC8F"=>"\u0646\u064A", "\uFC90"=>"\u0649\u0670", + "\uFC91"=>"\u064A\u0631", "\uFC92"=>"\u064A\u0632", "\uFC93"=>"\u064A\u0645", "\uFC94"=>"\u064A\u0646", "\uFC95"=>"\u064A\u0649", "\uFC96"=>"\u064A\u064A", "\uFC97"=>"\u0626\u062C", "\uFC98"=>"\u0626\u062D", + "\uFC99"=>"\u0626\u062E", "\uFC9A"=>"\u0626\u0645", "\uFC9B"=>"\u0626\u0647", "\uFC9C"=>"\u0628\u062C", "\uFC9D"=>"\u0628\u062D", "\uFC9E"=>"\u0628\u062E", "\uFC9F"=>"\u0628\u0645", "\uFCA0"=>"\u0628\u0647", + "\uFCA1"=>"\u062A\u062C", "\uFCA2"=>"\u062A\u062D", "\uFCA3"=>"\u062A\u062E", "\uFCA4"=>"\u062A\u0645", "\uFCA5"=>"\u062A\u0647", "\uFCA6"=>"\u062B\u0645", "\uFCA7"=>"\u062C\u062D", "\uFCA8"=>"\u062C\u0645", + "\uFCA9"=>"\u062D\u062C", "\uFCAA"=>"\u062D\u0645", "\uFCAB"=>"\u062E\u062C", "\uFCAC"=>"\u062E\u0645", "\uFCAD"=>"\u0633\u062C", "\uFCAE"=>"\u0633\u062D", "\uFCAF"=>"\u0633\u062E", "\uFCB0"=>"\u0633\u0645", + "\uFCB1"=>"\u0635\u062D", "\uFCB2"=>"\u0635\u062E", "\uFCB3"=>"\u0635\u0645", "\uFCB4"=>"\u0636\u062C", "\uFCB5"=>"\u0636\u062D", "\uFCB6"=>"\u0636\u062E", "\uFCB7"=>"\u0636\u0645", "\uFCB8"=>"\u0637\u062D", + "\uFCB9"=>"\u0638\u0645", "\uFCBA"=>"\u0639\u062C", "\uFCBB"=>"\u0639\u0645", "\uFCBC"=>"\u063A\u062C", "\uFCBD"=>"\u063A\u0645", "\uFCBE"=>"\u0641\u062C", "\uFCBF"=>"\u0641\u062D", "\uFCC0"=>"\u0641\u062E", + "\uFCC1"=>"\u0641\u0645", "\uFCC2"=>"\u0642\u062D", "\uFCC3"=>"\u0642\u0645", "\uFCC4"=>"\u0643\u062C", "\uFCC5"=>"\u0643\u062D", "\uFCC6"=>"\u0643\u062E", "\uFCC7"=>"\u0643\u0644", "\uFCC8"=>"\u0643\u0645", + "\uFCC9"=>"\u0644\u062C", "\uFCCA"=>"\u0644\u062D", "\uFCCB"=>"\u0644\u062E", "\uFCCC"=>"\u0644\u0645", "\uFCCD"=>"\u0644\u0647", "\uFCCE"=>"\u0645\u062C", "\uFCCF"=>"\u0645\u062D", "\uFCD0"=>"\u0645\u062E", + "\uFCD1"=>"\u0645\u0645", "\uFCD2"=>"\u0646\u062C", "\uFCD3"=>"\u0646\u062D", "\uFCD4"=>"\u0646\u062E", "\uFCD5"=>"\u0646\u0645", "\uFCD6"=>"\u0646\u0647", "\uFCD7"=>"\u0647\u062C", "\uFCD8"=>"\u0647\u0645", + "\uFCD9"=>"\u0647\u0670", "\uFCDA"=>"\u064A\u062C", "\uFCDB"=>"\u064A\u062D", "\uFCDC"=>"\u064A\u062E", "\uFCDD"=>"\u064A\u0645", "\uFCDE"=>"\u064A\u0647", "\uFCDF"=>"\u0626\u0645", "\uFCE0"=>"\u0626\u0647", + "\uFCE1"=>"\u0628\u0645", "\uFCE2"=>"\u0628\u0647", "\uFCE3"=>"\u062A\u0645", "\uFCE4"=>"\u062A\u0647", "\uFCE5"=>"\u062B\u0645", "\uFCE6"=>"\u062B\u0647", "\uFCE7"=>"\u0633\u0645", "\uFCE8"=>"\u0633\u0647", + "\uFCE9"=>"\u0634\u0645", "\uFCEA"=>"\u0634\u0647", "\uFCEB"=>"\u0643\u0644", "\uFCEC"=>"\u0643\u0645", "\uFCED"=>"\u0644\u0645", "\uFCEE"=>"\u0646\u0645", "\uFCEF"=>"\u0646\u0647", "\uFCF0"=>"\u064A\u0645", + "\uFCF1"=>"\u064A\u0647", "\uFCF2"=>"\u0640\u064E\u0651", "\uFCF3"=>"\u0640\u064F\u0651", "\uFCF4"=>"\u0640\u0650\u0651", "\uFCF5"=>"\u0637\u0649", "\uFCF6"=>"\u0637\u064A", "\uFCF7"=>"\u0639\u0649", "\uFCF8"=>"\u0639\u064A", + "\uFCF9"=>"\u063A\u0649", "\uFCFA"=>"\u063A\u064A", "\uFCFB"=>"\u0633\u0649", "\uFCFC"=>"\u0633\u064A", "\uFCFD"=>"\u0634\u0649", "\uFCFE"=>"\u0634\u064A", "\uFCFF"=>"\u062D\u0649", "\uFD00"=>"\u062D\u064A", + "\uFD01"=>"\u062C\u0649", "\uFD02"=>"\u062C\u064A", "\uFD03"=>"\u062E\u0649", "\uFD04"=>"\u062E\u064A", "\uFD05"=>"\u0635\u0649", "\uFD06"=>"\u0635\u064A", "\uFD07"=>"\u0636\u0649", "\uFD08"=>"\u0636\u064A", + "\uFD09"=>"\u0634\u062C", "\uFD0A"=>"\u0634\u062D", "\uFD0B"=>"\u0634\u062E", "\uFD0C"=>"\u0634\u0645", "\uFD0D"=>"\u0634\u0631", "\uFD0E"=>"\u0633\u0631", "\uFD0F"=>"\u0635\u0631", "\uFD10"=>"\u0636\u0631", + "\uFD11"=>"\u0637\u0649", "\uFD12"=>"\u0637\u064A", "\uFD13"=>"\u0639\u0649", "\uFD14"=>"\u0639\u064A", "\uFD15"=>"\u063A\u0649", "\uFD16"=>"\u063A\u064A", "\uFD17"=>"\u0633\u0649", "\uFD18"=>"\u0633\u064A", + "\uFD19"=>"\u0634\u0649", "\uFD1A"=>"\u0634\u064A", "\uFD1B"=>"\u062D\u0649", "\uFD1C"=>"\u062D\u064A", "\uFD1D"=>"\u062C\u0649", "\uFD1E"=>"\u062C\u064A", "\uFD1F"=>"\u062E\u0649", "\uFD20"=>"\u062E\u064A", + "\uFD21"=>"\u0635\u0649", "\uFD22"=>"\u0635\u064A", "\uFD23"=>"\u0636\u0649", "\uFD24"=>"\u0636\u064A", "\uFD25"=>"\u0634\u062C", "\uFD26"=>"\u0634\u062D", "\uFD27"=>"\u0634\u062E", "\uFD28"=>"\u0634\u0645", + "\uFD29"=>"\u0634\u0631", "\uFD2A"=>"\u0633\u0631", "\uFD2B"=>"\u0635\u0631", "\uFD2C"=>"\u0636\u0631", "\uFD2D"=>"\u0634\u062C", "\uFD2E"=>"\u0634\u062D", "\uFD2F"=>"\u0634\u062E", "\uFD30"=>"\u0634\u0645", + "\uFD31"=>"\u0633\u0647", "\uFD32"=>"\u0634\u0647", "\uFD33"=>"\u0637\u0645", "\uFD34"=>"\u0633\u062C", "\uFD35"=>"\u0633\u062D", "\uFD36"=>"\u0633\u062E", "\uFD37"=>"\u0634\u062C", "\uFD38"=>"\u0634\u062D", + "\uFD39"=>"\u0634\u062E", "\uFD3A"=>"\u0637\u0645", "\uFD3B"=>"\u0638\u0645", "\uFD3C"=>"\u0627\u064B", "\uFD3D"=>"\u0627\u064B", "\uFD50"=>"\u062A\u062C\u0645", "\uFD51"=>"\u062A\u062D\u062C", "\uFD52"=>"\u062A\u062D\u062C", + "\uFD53"=>"\u062A\u062D\u0645", "\uFD54"=>"\u062A\u062E\u0645", "\uFD55"=>"\u062A\u0645\u062C", "\uFD56"=>"\u062A\u0645\u062D", "\uFD57"=>"\u062A\u0645\u062E", "\uFD58"=>"\u062C\u0645\u062D", "\uFD59"=>"\u062C\u0645\u062D", "\uFD5A"=>"\u062D\u0645\u064A", + "\uFD5B"=>"\u062D\u0645\u0649", "\uFD5C"=>"\u0633\u062D\u062C", "\uFD5D"=>"\u0633\u062C\u062D", "\uFD5E"=>"\u0633\u062C\u0649", "\uFD5F"=>"\u0633\u0645\u062D", "\uFD60"=>"\u0633\u0645\u062D", "\uFD61"=>"\u0633\u0645\u062C", "\uFD62"=>"\u0633\u0645\u0645", + "\uFD63"=>"\u0633\u0645\u0645", "\uFD64"=>"\u0635\u062D\u062D", "\uFD65"=>"\u0635\u062D\u062D", "\uFD66"=>"\u0635\u0645\u0645", "\uFD67"=>"\u0634\u062D\u0645", "\uFD68"=>"\u0634\u062D\u0645", "\uFD69"=>"\u0634\u062C\u064A", "\uFD6A"=>"\u0634\u0645\u062E", + "\uFD6B"=>"\u0634\u0645\u062E", "\uFD6C"=>"\u0634\u0645\u0645", "\uFD6D"=>"\u0634\u0645\u0645", "\uFD6E"=>"\u0636\u062D\u0649", "\uFD6F"=>"\u0636\u062E\u0645", "\uFD70"=>"\u0636\u062E\u0645", "\uFD71"=>"\u0637\u0645\u062D", "\uFD72"=>"\u0637\u0645\u062D", + "\uFD73"=>"\u0637\u0645\u0645", "\uFD74"=>"\u0637\u0645\u064A", "\uFD75"=>"\u0639\u062C\u0645", "\uFD76"=>"\u0639\u0645\u0645", "\uFD77"=>"\u0639\u0645\u0645", "\uFD78"=>"\u0639\u0645\u0649", "\uFD79"=>"\u063A\u0645\u0645", "\uFD7A"=>"\u063A\u0645\u064A", + "\uFD7B"=>"\u063A\u0645\u0649", "\uFD7C"=>"\u0641\u062E\u0645", "\uFD7D"=>"\u0641\u062E\u0645", "\uFD7E"=>"\u0642\u0645\u062D", "\uFD7F"=>"\u0642\u0645\u0645", "\uFD80"=>"\u0644\u062D\u0645", "\uFD81"=>"\u0644\u062D\u064A", "\uFD82"=>"\u0644\u062D\u0649", + "\uFD83"=>"\u0644\u062C\u062C", "\uFD84"=>"\u0644\u062C\u062C", "\uFD85"=>"\u0644\u062E\u0645", "\uFD86"=>"\u0644\u062E\u0645", "\uFD87"=>"\u0644\u0645\u062D", "\uFD88"=>"\u0644\u0645\u062D", "\uFD89"=>"\u0645\u062D\u062C", "\uFD8A"=>"\u0645\u062D\u0645", + "\uFD8B"=>"\u0645\u062D\u064A", "\uFD8C"=>"\u0645\u062C\u062D", "\uFD8D"=>"\u0645\u062C\u0645", "\uFD8E"=>"\u0645\u062E\u062C", "\uFD8F"=>"\u0645\u062E\u0645", "\uFD92"=>"\u0645\u062C\u062E", "\uFD93"=>"\u0647\u0645\u062C", "\uFD94"=>"\u0647\u0645\u0645", + "\uFD95"=>"\u0646\u062D\u0645", "\uFD96"=>"\u0646\u062D\u0649", "\uFD97"=>"\u0646\u062C\u0645", "\uFD98"=>"\u0646\u062C\u0645", "\uFD99"=>"\u0646\u062C\u0649", "\uFD9A"=>"\u0646\u0645\u064A", "\uFD9B"=>"\u0646\u0645\u0649", "\uFD9C"=>"\u064A\u0645\u0645", + "\uFD9D"=>"\u064A\u0645\u0645", "\uFD9E"=>"\u0628\u062E\u064A", "\uFD9F"=>"\u062A\u062C\u064A", "\uFDA0"=>"\u062A\u062C\u0649", "\uFDA1"=>"\u062A\u062E\u064A", "\uFDA2"=>"\u062A\u062E\u0649", "\uFDA3"=>"\u062A\u0645\u064A", "\uFDA4"=>"\u062A\u0645\u0649", + "\uFDA5"=>"\u062C\u0645\u064A", "\uFDA6"=>"\u062C\u062D\u0649", "\uFDA7"=>"\u062C\u0645\u0649", "\uFDA8"=>"\u0633\u062E\u0649", "\uFDA9"=>"\u0635\u062D\u064A", "\uFDAA"=>"\u0634\u062D\u064A", "\uFDAB"=>"\u0636\u062D\u064A", "\uFDAC"=>"\u0644\u062C\u064A", + "\uFDAD"=>"\u0644\u0645\u064A", "\uFDAE"=>"\u064A\u062D\u064A", "\uFDAF"=>"\u064A\u062C\u064A", "\uFDB0"=>"\u064A\u0645\u064A", "\uFDB1"=>"\u0645\u0645\u064A", "\uFDB2"=>"\u0642\u0645\u064A", "\uFDB3"=>"\u0646\u062D\u064A", "\uFDB4"=>"\u0642\u0645\u062D", + "\uFDB5"=>"\u0644\u062D\u0645", "\uFDB6"=>"\u0639\u0645\u064A", "\uFDB7"=>"\u0643\u0645\u064A", "\uFDB8"=>"\u0646\u062C\u062D", "\uFDB9"=>"\u0645\u062E\u064A", "\uFDBA"=>"\u0644\u062C\u0645", "\uFDBB"=>"\u0643\u0645\u0645", "\uFDBC"=>"\u0644\u062C\u0645", + "\uFDBD"=>"\u0646\u062C\u062D", "\uFDBE"=>"\u062C\u062D\u064A", "\uFDBF"=>"\u062D\u062C\u064A", "\uFDC0"=>"\u0645\u062C\u064A", "\uFDC1"=>"\u0641\u0645\u064A", "\uFDC2"=>"\u0628\u062D\u064A", "\uFDC3"=>"\u0643\u0645\u0645", "\uFDC4"=>"\u0639\u062C\u0645", + "\uFDC5"=>"\u0635\u0645\u0645", "\uFDC6"=>"\u0633\u062E\u064A", "\uFDC7"=>"\u0646\u062C\u064A", "\uFDF0"=>"\u0635\u0644\u06D2", "\uFDF1"=>"\u0642\u0644\u06D2", "\uFDF2"=>"\u0627\u0644\u0644\u0647", "\uFDF3"=>"\u0627\u0643\u0628\u0631", "\uFDF4"=>"\u0645\u062D\u0645\u062F", + "\uFDF5"=>"\u0635\u0644\u0639\u0645", "\uFDF6"=>"\u0631\u0633\u0648\u0644", "\uFDF7"=>"\u0639\u0644\u064A\u0647", "\uFDF8"=>"\u0648\u0633\u0644\u0645", "\uFDF9"=>"\u0635\u0644\u0649", "\uFDFA"=>"\u0635\u0644\u0649 \u0627\u0644\u0644\u0647 \u0639\u0644\u064A\u0647 \u0648\u0633\u0644\u0645", "\uFDFB"=>"\u062C\u0644 \u062C\u0644\u0627\u0644\u0647", "\uFDFC"=>"\u0631\u06CC\u0627\u0644", + "\uFE10"=>",", "\uFE11"=>"\u3001", "\uFE12"=>"\u3002", "\uFE13"=>":", "\uFE14"=>";", "\uFE15"=>"!", "\uFE16"=>"?", "\uFE17"=>"\u3016", + "\uFE18"=>"\u3017", "\uFE19"=>"...", "\uFE30"=>"..", "\uFE31"=>"\u2014", "\uFE32"=>"\u2013", "\uFE33"=>"_", "\uFE34"=>"_", "\uFE35"=>"(", + "\uFE36"=>")", "\uFE37"=>"{", "\uFE38"=>"}", "\uFE39"=>"\u3014", "\uFE3A"=>"\u3015", "\uFE3B"=>"\u3010", "\uFE3C"=>"\u3011", "\uFE3D"=>"\u300A", + "\uFE3E"=>"\u300B", "\uFE3F"=>"\u3008", "\uFE40"=>"\u3009", "\uFE41"=>"\u300C", "\uFE42"=>"\u300D", "\uFE43"=>"\u300E", "\uFE44"=>"\u300F", "\uFE47"=>"[", + "\uFE48"=>"]", "\uFE49"=>" \u0305", "\uFE4A"=>" \u0305", "\uFE4B"=>" \u0305", "\uFE4C"=>" \u0305", "\uFE4D"=>"_", "\uFE4E"=>"_", "\uFE4F"=>"_", + "\uFE50"=>",", "\uFE51"=>"\u3001", "\uFE52"=>".", "\uFE54"=>";", "\uFE55"=>":", "\uFE56"=>"?", "\uFE57"=>"!", "\uFE58"=>"\u2014", + "\uFE59"=>"(", "\uFE5A"=>")", "\uFE5B"=>"{", "\uFE5C"=>"}", "\uFE5D"=>"\u3014", "\uFE5E"=>"\u3015", "\uFE5F"=>"#", "\uFE60"=>"&", + "\uFE61"=>"*", "\uFE62"=>"+", "\uFE63"=>"-", "\uFE64"=>"<", "\uFE65"=>">", "\uFE66"=>"=", "\uFE68"=>"\\", "\uFE69"=>"$", + "\uFE6A"=>"%", "\uFE6B"=>"@", "\uFE70"=>" \u064B", "\uFE71"=>"\u0640\u064B", "\uFE72"=>" \u064C", "\uFE74"=>" \u064D", "\uFE76"=>" \u064E", "\uFE77"=>"\u0640\u064E", + "\uFE78"=>" \u064F", "\uFE79"=>"\u0640\u064F", "\uFE7A"=>" \u0650", "\uFE7B"=>"\u0640\u0650", "\uFE7C"=>" \u0651", "\uFE7D"=>"\u0640\u0651", "\uFE7E"=>" \u0652", "\uFE7F"=>"\u0640\u0652", + "\uFE80"=>"\u0621", "\uFE81"=>"\u0622", "\uFE82"=>"\u0622", "\uFE83"=>"\u0623", "\uFE84"=>"\u0623", "\uFE85"=>"\u0624", "\uFE86"=>"\u0624", "\uFE87"=>"\u0625", + "\uFE88"=>"\u0625", "\uFE89"=>"\u0626", "\uFE8A"=>"\u0626", "\uFE8B"=>"\u0626", "\uFE8C"=>"\u0626", "\uFE8D"=>"\u0627", "\uFE8E"=>"\u0627", "\uFE8F"=>"\u0628", + "\uFE90"=>"\u0628", "\uFE91"=>"\u0628", "\uFE92"=>"\u0628", "\uFE93"=>"\u0629", "\uFE94"=>"\u0629", "\uFE95"=>"\u062A", "\uFE96"=>"\u062A", "\uFE97"=>"\u062A", + "\uFE98"=>"\u062A", "\uFE99"=>"\u062B", "\uFE9A"=>"\u062B", "\uFE9B"=>"\u062B", "\uFE9C"=>"\u062B", "\uFE9D"=>"\u062C", "\uFE9E"=>"\u062C", "\uFE9F"=>"\u062C", + "\uFEA0"=>"\u062C", "\uFEA1"=>"\u062D", "\uFEA2"=>"\u062D", "\uFEA3"=>"\u062D", "\uFEA4"=>"\u062D", "\uFEA5"=>"\u062E", "\uFEA6"=>"\u062E", "\uFEA7"=>"\u062E", + "\uFEA8"=>"\u062E", "\uFEA9"=>"\u062F", "\uFEAA"=>"\u062F", "\uFEAB"=>"\u0630", "\uFEAC"=>"\u0630", "\uFEAD"=>"\u0631", "\uFEAE"=>"\u0631", "\uFEAF"=>"\u0632", + "\uFEB0"=>"\u0632", "\uFEB1"=>"\u0633", "\uFEB2"=>"\u0633", "\uFEB3"=>"\u0633", "\uFEB4"=>"\u0633", "\uFEB5"=>"\u0634", "\uFEB6"=>"\u0634", "\uFEB7"=>"\u0634", + "\uFEB8"=>"\u0634", "\uFEB9"=>"\u0635", "\uFEBA"=>"\u0635", "\uFEBB"=>"\u0635", "\uFEBC"=>"\u0635", "\uFEBD"=>"\u0636", "\uFEBE"=>"\u0636", "\uFEBF"=>"\u0636", + "\uFEC0"=>"\u0636", "\uFEC1"=>"\u0637", "\uFEC2"=>"\u0637", "\uFEC3"=>"\u0637", "\uFEC4"=>"\u0637", "\uFEC5"=>"\u0638", "\uFEC6"=>"\u0638", "\uFEC7"=>"\u0638", + "\uFEC8"=>"\u0638", "\uFEC9"=>"\u0639", "\uFECA"=>"\u0639", "\uFECB"=>"\u0639", "\uFECC"=>"\u0639", "\uFECD"=>"\u063A", "\uFECE"=>"\u063A", "\uFECF"=>"\u063A", + "\uFED0"=>"\u063A", "\uFED1"=>"\u0641", "\uFED2"=>"\u0641", "\uFED3"=>"\u0641", "\uFED4"=>"\u0641", "\uFED5"=>"\u0642", "\uFED6"=>"\u0642", "\uFED7"=>"\u0642", + "\uFED8"=>"\u0642", "\uFED9"=>"\u0643", "\uFEDA"=>"\u0643", "\uFEDB"=>"\u0643", "\uFEDC"=>"\u0643", "\uFEDD"=>"\u0644", "\uFEDE"=>"\u0644", "\uFEDF"=>"\u0644", + "\uFEE0"=>"\u0644", "\uFEE1"=>"\u0645", "\uFEE2"=>"\u0645", "\uFEE3"=>"\u0645", "\uFEE4"=>"\u0645", "\uFEE5"=>"\u0646", "\uFEE6"=>"\u0646", "\uFEE7"=>"\u0646", + "\uFEE8"=>"\u0646", "\uFEE9"=>"\u0647", "\uFEEA"=>"\u0647", "\uFEEB"=>"\u0647", "\uFEEC"=>"\u0647", "\uFEED"=>"\u0648", "\uFEEE"=>"\u0648", "\uFEEF"=>"\u0649", + "\uFEF0"=>"\u0649", "\uFEF1"=>"\u064A", "\uFEF2"=>"\u064A", "\uFEF3"=>"\u064A", "\uFEF4"=>"\u064A", "\uFEF5"=>"\u0644\u0622", "\uFEF6"=>"\u0644\u0622", "\uFEF7"=>"\u0644\u0623", + "\uFEF8"=>"\u0644\u0623", "\uFEF9"=>"\u0644\u0625", "\uFEFA"=>"\u0644\u0625", "\uFEFB"=>"\u0644\u0627", "\uFEFC"=>"\u0644\u0627", "\uFF01"=>"!", "\uFF02"=>"\"", "\uFF03"=>"#", + "\uFF04"=>"$", "\uFF05"=>"%", "\uFF06"=>"&", "\uFF07"=>"'", "\uFF08"=>"(", "\uFF09"=>")", "\uFF0A"=>"*", "\uFF0B"=>"+", + "\uFF0C"=>",", "\uFF0D"=>"-", "\uFF0E"=>".", "\uFF0F"=>"/", "\uFF10"=>"0", "\uFF11"=>"1", "\uFF12"=>"2", "\uFF13"=>"3", + "\uFF14"=>"4", "\uFF15"=>"5", "\uFF16"=>"6", "\uFF17"=>"7", "\uFF18"=>"8", "\uFF19"=>"9", "\uFF1A"=>":", "\uFF1B"=>";", + "\uFF1C"=>"<", "\uFF1D"=>"=", "\uFF1E"=>">", "\uFF1F"=>"?", "\uFF20"=>"@", "\uFF21"=>"A", "\uFF22"=>"B", "\uFF23"=>"C", + "\uFF24"=>"D", "\uFF25"=>"E", "\uFF26"=>"F", "\uFF27"=>"G", "\uFF28"=>"H", "\uFF29"=>"I", "\uFF2A"=>"J", "\uFF2B"=>"K", + "\uFF2C"=>"L", "\uFF2D"=>"M", "\uFF2E"=>"N", "\uFF2F"=>"O", "\uFF30"=>"P", "\uFF31"=>"Q", "\uFF32"=>"R", "\uFF33"=>"S", + "\uFF34"=>"T", "\uFF35"=>"U", "\uFF36"=>"V", "\uFF37"=>"W", "\uFF38"=>"X", "\uFF39"=>"Y", "\uFF3A"=>"Z", "\uFF3B"=>"[", + "\uFF3C"=>"\\", "\uFF3D"=>"]", "\uFF3E"=>"^", "\uFF3F"=>"_", "\uFF40"=>"`", "\uFF41"=>"a", "\uFF42"=>"b", "\uFF43"=>"c", + "\uFF44"=>"d", "\uFF45"=>"e", "\uFF46"=>"f", "\uFF47"=>"g", "\uFF48"=>"h", "\uFF49"=>"i", "\uFF4A"=>"j", "\uFF4B"=>"k", + "\uFF4C"=>"l", "\uFF4D"=>"m", "\uFF4E"=>"n", "\uFF4F"=>"o", "\uFF50"=>"p", "\uFF51"=>"q", "\uFF52"=>"r", "\uFF53"=>"s", + "\uFF54"=>"t", "\uFF55"=>"u", "\uFF56"=>"v", "\uFF57"=>"w", "\uFF58"=>"x", "\uFF59"=>"y", "\uFF5A"=>"z", "\uFF5B"=>"{", + "\uFF5C"=>"|", "\uFF5D"=>"}", "\uFF5E"=>"~", "\uFF5F"=>"\u2985", "\uFF60"=>"\u2986", "\uFF61"=>"\u3002", "\uFF62"=>"\u300C", "\uFF63"=>"\u300D", + "\uFF64"=>"\u3001", "\uFF65"=>"\u30FB", "\uFF66"=>"\u30F2", "\uFF67"=>"\u30A1", "\uFF68"=>"\u30A3", "\uFF69"=>"\u30A5", "\uFF6A"=>"\u30A7", "\uFF6B"=>"\u30A9", + "\uFF6C"=>"\u30E3", "\uFF6D"=>"\u30E5", "\uFF6E"=>"\u30E7", "\uFF6F"=>"\u30C3", "\uFF70"=>"\u30FC", "\uFF71"=>"\u30A2", "\uFF72"=>"\u30A4", "\uFF73"=>"\u30A6", + "\uFF74"=>"\u30A8", "\uFF75"=>"\u30AA", "\uFF76"=>"\u30AB", "\uFF77"=>"\u30AD", "\uFF78"=>"\u30AF", "\uFF79"=>"\u30B1", "\uFF7A"=>"\u30B3", "\uFF7B"=>"\u30B5", + "\uFF7C"=>"\u30B7", "\uFF7D"=>"\u30B9", "\uFF7E"=>"\u30BB", "\uFF7F"=>"\u30BD", "\uFF80"=>"\u30BF", "\uFF81"=>"\u30C1", "\uFF82"=>"\u30C4", "\uFF83"=>"\u30C6", + "\uFF84"=>"\u30C8", "\uFF85"=>"\u30CA", "\uFF86"=>"\u30CB", "\uFF87"=>"\u30CC", "\uFF88"=>"\u30CD", "\uFF89"=>"\u30CE", "\uFF8A"=>"\u30CF", "\uFF8B"=>"\u30D2", + "\uFF8C"=>"\u30D5", "\uFF8D"=>"\u30D8", "\uFF8E"=>"\u30DB", "\uFF8F"=>"\u30DE", "\uFF90"=>"\u30DF", "\uFF91"=>"\u30E0", "\uFF92"=>"\u30E1", "\uFF93"=>"\u30E2", + "\uFF94"=>"\u30E4", "\uFF95"=>"\u30E6", "\uFF96"=>"\u30E8", "\uFF97"=>"\u30E9", "\uFF98"=>"\u30EA", "\uFF99"=>"\u30EB", "\uFF9A"=>"\u30EC", "\uFF9B"=>"\u30ED", + "\uFF9C"=>"\u30EF", "\uFF9D"=>"\u30F3", "\uFF9E"=>"\u3099", "\uFF9F"=>"\u309A", "\uFFA0"=>"\u1160", "\uFFA1"=>"\u1100", "\uFFA2"=>"\u1101", "\uFFA3"=>"\u11AA", + "\uFFA4"=>"\u1102", "\uFFA5"=>"\u11AC", "\uFFA6"=>"\u11AD", "\uFFA7"=>"\u1103", "\uFFA8"=>"\u1104", "\uFFA9"=>"\u1105", "\uFFAA"=>"\u11B0", "\uFFAB"=>"\u11B1", + "\uFFAC"=>"\u11B2", "\uFFAD"=>"\u11B3", "\uFFAE"=>"\u11B4", "\uFFAF"=>"\u11B5", "\uFFB0"=>"\u111A", "\uFFB1"=>"\u1106", "\uFFB2"=>"\u1107", "\uFFB3"=>"\u1108", + "\uFFB4"=>"\u1121", "\uFFB5"=>"\u1109", "\uFFB6"=>"\u110A", "\uFFB7"=>"\u110B", "\uFFB8"=>"\u110C", "\uFFB9"=>"\u110D", "\uFFBA"=>"\u110E", "\uFFBB"=>"\u110F", + "\uFFBC"=>"\u1110", "\uFFBD"=>"\u1111", "\uFFBE"=>"\u1112", "\uFFC2"=>"\u1161", "\uFFC3"=>"\u1162", "\uFFC4"=>"\u1163", "\uFFC5"=>"\u1164", "\uFFC6"=>"\u1165", + "\uFFC7"=>"\u1166", "\uFFCA"=>"\u1167", "\uFFCB"=>"\u1168", "\uFFCC"=>"\u1169", "\uFFCD"=>"\u116A", "\uFFCE"=>"\u116B", "\uFFCF"=>"\u116C", "\uFFD2"=>"\u116D", + "\uFFD3"=>"\u116E", "\uFFD4"=>"\u116F", "\uFFD5"=>"\u1170", "\uFFD6"=>"\u1171", "\uFFD7"=>"\u1172", "\uFFDA"=>"\u1173", "\uFFDB"=>"\u1174", "\uFFDC"=>"\u1175", + "\uFFE0"=>"\u00A2", "\uFFE1"=>"\u00A3", "\uFFE2"=>"\u00AC", "\uFFE3"=>" \u0304", "\uFFE4"=>"\u00A6", "\uFFE5"=>"\u00A5", "\uFFE6"=>"\u20A9", "\uFFE8"=>"\u2502", + "\uFFE9"=>"\u2190", "\uFFEA"=>"\u2191", "\uFFEB"=>"\u2192", "\uFFEC"=>"\u2193", "\uFFED"=>"\u25A0", "\uFFEE"=>"\u25CB", "\u{1D400}"=>"A", "\u{1D401}"=>"B", + "\u{1D402}"=>"C", "\u{1D403}"=>"D", "\u{1D404}"=>"E", "\u{1D405}"=>"F", "\u{1D406}"=>"G", "\u{1D407}"=>"H", "\u{1D408}"=>"I", "\u{1D409}"=>"J", + "\u{1D40A}"=>"K", "\u{1D40B}"=>"L", "\u{1D40C}"=>"M", "\u{1D40D}"=>"N", "\u{1D40E}"=>"O", "\u{1D40F}"=>"P", "\u{1D410}"=>"Q", "\u{1D411}"=>"R", + "\u{1D412}"=>"S", "\u{1D413}"=>"T", "\u{1D414}"=>"U", "\u{1D415}"=>"V", "\u{1D416}"=>"W", "\u{1D417}"=>"X", "\u{1D418}"=>"Y", "\u{1D419}"=>"Z", + "\u{1D41A}"=>"a", "\u{1D41B}"=>"b", "\u{1D41C}"=>"c", "\u{1D41D}"=>"d", "\u{1D41E}"=>"e", "\u{1D41F}"=>"f", "\u{1D420}"=>"g", "\u{1D421}"=>"h", + "\u{1D422}"=>"i", "\u{1D423}"=>"j", "\u{1D424}"=>"k", "\u{1D425}"=>"l", "\u{1D426}"=>"m", "\u{1D427}"=>"n", "\u{1D428}"=>"o", "\u{1D429}"=>"p", + "\u{1D42A}"=>"q", "\u{1D42B}"=>"r", "\u{1D42C}"=>"s", "\u{1D42D}"=>"t", "\u{1D42E}"=>"u", "\u{1D42F}"=>"v", "\u{1D430}"=>"w", "\u{1D431}"=>"x", + "\u{1D432}"=>"y", "\u{1D433}"=>"z", "\u{1D434}"=>"A", "\u{1D435}"=>"B", "\u{1D436}"=>"C", "\u{1D437}"=>"D", "\u{1D438}"=>"E", "\u{1D439}"=>"F", + "\u{1D43A}"=>"G", "\u{1D43B}"=>"H", "\u{1D43C}"=>"I", "\u{1D43D}"=>"J", "\u{1D43E}"=>"K", "\u{1D43F}"=>"L", "\u{1D440}"=>"M", "\u{1D441}"=>"N", + "\u{1D442}"=>"O", "\u{1D443}"=>"P", "\u{1D444}"=>"Q", "\u{1D445}"=>"R", "\u{1D446}"=>"S", "\u{1D447}"=>"T", "\u{1D448}"=>"U", "\u{1D449}"=>"V", + "\u{1D44A}"=>"W", "\u{1D44B}"=>"X", "\u{1D44C}"=>"Y", "\u{1D44D}"=>"Z", "\u{1D44E}"=>"a", "\u{1D44F}"=>"b", "\u{1D450}"=>"c", "\u{1D451}"=>"d", + "\u{1D452}"=>"e", "\u{1D453}"=>"f", "\u{1D454}"=>"g", "\u{1D456}"=>"i", "\u{1D457}"=>"j", "\u{1D458}"=>"k", "\u{1D459}"=>"l", "\u{1D45A}"=>"m", + "\u{1D45B}"=>"n", "\u{1D45C}"=>"o", "\u{1D45D}"=>"p", "\u{1D45E}"=>"q", "\u{1D45F}"=>"r", "\u{1D460}"=>"s", "\u{1D461}"=>"t", "\u{1D462}"=>"u", + "\u{1D463}"=>"v", "\u{1D464}"=>"w", "\u{1D465}"=>"x", "\u{1D466}"=>"y", "\u{1D467}"=>"z", "\u{1D468}"=>"A", "\u{1D469}"=>"B", "\u{1D46A}"=>"C", + "\u{1D46B}"=>"D", "\u{1D46C}"=>"E", "\u{1D46D}"=>"F", "\u{1D46E}"=>"G", "\u{1D46F}"=>"H", "\u{1D470}"=>"I", "\u{1D471}"=>"J", "\u{1D472}"=>"K", + "\u{1D473}"=>"L", "\u{1D474}"=>"M", "\u{1D475}"=>"N", "\u{1D476}"=>"O", "\u{1D477}"=>"P", "\u{1D478}"=>"Q", "\u{1D479}"=>"R", "\u{1D47A}"=>"S", + "\u{1D47B}"=>"T", "\u{1D47C}"=>"U", "\u{1D47D}"=>"V", "\u{1D47E}"=>"W", "\u{1D47F}"=>"X", "\u{1D480}"=>"Y", "\u{1D481}"=>"Z", "\u{1D482}"=>"a", + "\u{1D483}"=>"b", "\u{1D484}"=>"c", "\u{1D485}"=>"d", "\u{1D486}"=>"e", "\u{1D487}"=>"f", "\u{1D488}"=>"g", "\u{1D489}"=>"h", "\u{1D48A}"=>"i", + "\u{1D48B}"=>"j", "\u{1D48C}"=>"k", "\u{1D48D}"=>"l", "\u{1D48E}"=>"m", "\u{1D48F}"=>"n", "\u{1D490}"=>"o", "\u{1D491}"=>"p", "\u{1D492}"=>"q", + "\u{1D493}"=>"r", "\u{1D494}"=>"s", "\u{1D495}"=>"t", "\u{1D496}"=>"u", "\u{1D497}"=>"v", "\u{1D498}"=>"w", "\u{1D499}"=>"x", "\u{1D49A}"=>"y", + "\u{1D49B}"=>"z", "\u{1D49C}"=>"A", "\u{1D49E}"=>"C", "\u{1D49F}"=>"D", "\u{1D4A2}"=>"G", "\u{1D4A5}"=>"J", "\u{1D4A6}"=>"K", "\u{1D4A9}"=>"N", + "\u{1D4AA}"=>"O", "\u{1D4AB}"=>"P", "\u{1D4AC}"=>"Q", "\u{1D4AE}"=>"S", "\u{1D4AF}"=>"T", "\u{1D4B0}"=>"U", "\u{1D4B1}"=>"V", "\u{1D4B2}"=>"W", + "\u{1D4B3}"=>"X", "\u{1D4B4}"=>"Y", "\u{1D4B5}"=>"Z", "\u{1D4B6}"=>"a", "\u{1D4B7}"=>"b", "\u{1D4B8}"=>"c", "\u{1D4B9}"=>"d", "\u{1D4BB}"=>"f", + "\u{1D4BD}"=>"h", "\u{1D4BE}"=>"i", "\u{1D4BF}"=>"j", "\u{1D4C0}"=>"k", "\u{1D4C1}"=>"l", "\u{1D4C2}"=>"m", "\u{1D4C3}"=>"n", "\u{1D4C5}"=>"p", + "\u{1D4C6}"=>"q", "\u{1D4C7}"=>"r", "\u{1D4C8}"=>"s", "\u{1D4C9}"=>"t", "\u{1D4CA}"=>"u", "\u{1D4CB}"=>"v", "\u{1D4CC}"=>"w", "\u{1D4CD}"=>"x", + "\u{1D4CE}"=>"y", "\u{1D4CF}"=>"z", "\u{1D4D0}"=>"A", "\u{1D4D1}"=>"B", "\u{1D4D2}"=>"C", "\u{1D4D3}"=>"D", "\u{1D4D4}"=>"E", "\u{1D4D5}"=>"F", + "\u{1D4D6}"=>"G", "\u{1D4D7}"=>"H", "\u{1D4D8}"=>"I", "\u{1D4D9}"=>"J", "\u{1D4DA}"=>"K", "\u{1D4DB}"=>"L", "\u{1D4DC}"=>"M", "\u{1D4DD}"=>"N", + "\u{1D4DE}"=>"O", "\u{1D4DF}"=>"P", "\u{1D4E0}"=>"Q", "\u{1D4E1}"=>"R", "\u{1D4E2}"=>"S", "\u{1D4E3}"=>"T", "\u{1D4E4}"=>"U", "\u{1D4E5}"=>"V", + "\u{1D4E6}"=>"W", "\u{1D4E7}"=>"X", "\u{1D4E8}"=>"Y", "\u{1D4E9}"=>"Z", "\u{1D4EA}"=>"a", "\u{1D4EB}"=>"b", "\u{1D4EC}"=>"c", "\u{1D4ED}"=>"d", + "\u{1D4EE}"=>"e", "\u{1D4EF}"=>"f", "\u{1D4F0}"=>"g", "\u{1D4F1}"=>"h", "\u{1D4F2}"=>"i", "\u{1D4F3}"=>"j", "\u{1D4F4}"=>"k", "\u{1D4F5}"=>"l", + "\u{1D4F6}"=>"m", "\u{1D4F7}"=>"n", "\u{1D4F8}"=>"o", "\u{1D4F9}"=>"p", "\u{1D4FA}"=>"q", "\u{1D4FB}"=>"r", "\u{1D4FC}"=>"s", "\u{1D4FD}"=>"t", + "\u{1D4FE}"=>"u", "\u{1D4FF}"=>"v", "\u{1D500}"=>"w", "\u{1D501}"=>"x", "\u{1D502}"=>"y", "\u{1D503}"=>"z", "\u{1D504}"=>"A", "\u{1D505}"=>"B", + "\u{1D507}"=>"D", "\u{1D508}"=>"E", "\u{1D509}"=>"F", "\u{1D50A}"=>"G", "\u{1D50D}"=>"J", "\u{1D50E}"=>"K", "\u{1D50F}"=>"L", "\u{1D510}"=>"M", + "\u{1D511}"=>"N", "\u{1D512}"=>"O", "\u{1D513}"=>"P", "\u{1D514}"=>"Q", "\u{1D516}"=>"S", "\u{1D517}"=>"T", "\u{1D518}"=>"U", "\u{1D519}"=>"V", + "\u{1D51A}"=>"W", "\u{1D51B}"=>"X", "\u{1D51C}"=>"Y", "\u{1D51E}"=>"a", "\u{1D51F}"=>"b", "\u{1D520}"=>"c", "\u{1D521}"=>"d", "\u{1D522}"=>"e", + "\u{1D523}"=>"f", "\u{1D524}"=>"g", "\u{1D525}"=>"h", "\u{1D526}"=>"i", "\u{1D527}"=>"j", "\u{1D528}"=>"k", "\u{1D529}"=>"l", "\u{1D52A}"=>"m", + "\u{1D52B}"=>"n", "\u{1D52C}"=>"o", "\u{1D52D}"=>"p", "\u{1D52E}"=>"q", "\u{1D52F}"=>"r", "\u{1D530}"=>"s", "\u{1D531}"=>"t", "\u{1D532}"=>"u", + "\u{1D533}"=>"v", "\u{1D534}"=>"w", "\u{1D535}"=>"x", "\u{1D536}"=>"y", "\u{1D537}"=>"z", "\u{1D538}"=>"A", "\u{1D539}"=>"B", "\u{1D53B}"=>"D", + "\u{1D53C}"=>"E", "\u{1D53D}"=>"F", "\u{1D53E}"=>"G", "\u{1D540}"=>"I", "\u{1D541}"=>"J", "\u{1D542}"=>"K", "\u{1D543}"=>"L", "\u{1D544}"=>"M", + "\u{1D546}"=>"O", "\u{1D54A}"=>"S", "\u{1D54B}"=>"T", "\u{1D54C}"=>"U", "\u{1D54D}"=>"V", "\u{1D54E}"=>"W", "\u{1D54F}"=>"X", "\u{1D550}"=>"Y", + "\u{1D552}"=>"a", "\u{1D553}"=>"b", "\u{1D554}"=>"c", "\u{1D555}"=>"d", "\u{1D556}"=>"e", "\u{1D557}"=>"f", "\u{1D558}"=>"g", "\u{1D559}"=>"h", + "\u{1D55A}"=>"i", "\u{1D55B}"=>"j", "\u{1D55C}"=>"k", "\u{1D55D}"=>"l", "\u{1D55E}"=>"m", "\u{1D55F}"=>"n", "\u{1D560}"=>"o", "\u{1D561}"=>"p", + "\u{1D562}"=>"q", "\u{1D563}"=>"r", "\u{1D564}"=>"s", "\u{1D565}"=>"t", "\u{1D566}"=>"u", "\u{1D567}"=>"v", "\u{1D568}"=>"w", "\u{1D569}"=>"x", + "\u{1D56A}"=>"y", "\u{1D56B}"=>"z", "\u{1D56C}"=>"A", "\u{1D56D}"=>"B", "\u{1D56E}"=>"C", "\u{1D56F}"=>"D", "\u{1D570}"=>"E", "\u{1D571}"=>"F", + "\u{1D572}"=>"G", "\u{1D573}"=>"H", "\u{1D574}"=>"I", "\u{1D575}"=>"J", "\u{1D576}"=>"K", "\u{1D577}"=>"L", "\u{1D578}"=>"M", "\u{1D579}"=>"N", + "\u{1D57A}"=>"O", "\u{1D57B}"=>"P", "\u{1D57C}"=>"Q", "\u{1D57D}"=>"R", "\u{1D57E}"=>"S", "\u{1D57F}"=>"T", "\u{1D580}"=>"U", "\u{1D581}"=>"V", + "\u{1D582}"=>"W", "\u{1D583}"=>"X", "\u{1D584}"=>"Y", "\u{1D585}"=>"Z", "\u{1D586}"=>"a", "\u{1D587}"=>"b", "\u{1D588}"=>"c", "\u{1D589}"=>"d", + "\u{1D58A}"=>"e", "\u{1D58B}"=>"f", "\u{1D58C}"=>"g", "\u{1D58D}"=>"h", "\u{1D58E}"=>"i", "\u{1D58F}"=>"j", "\u{1D590}"=>"k", "\u{1D591}"=>"l", + "\u{1D592}"=>"m", "\u{1D593}"=>"n", "\u{1D594}"=>"o", "\u{1D595}"=>"p", "\u{1D596}"=>"q", "\u{1D597}"=>"r", "\u{1D598}"=>"s", "\u{1D599}"=>"t", + "\u{1D59A}"=>"u", "\u{1D59B}"=>"v", "\u{1D59C}"=>"w", "\u{1D59D}"=>"x", "\u{1D59E}"=>"y", "\u{1D59F}"=>"z", "\u{1D5A0}"=>"A", "\u{1D5A1}"=>"B", + "\u{1D5A2}"=>"C", "\u{1D5A3}"=>"D", "\u{1D5A4}"=>"E", "\u{1D5A5}"=>"F", "\u{1D5A6}"=>"G", "\u{1D5A7}"=>"H", "\u{1D5A8}"=>"I", "\u{1D5A9}"=>"J", + "\u{1D5AA}"=>"K", "\u{1D5AB}"=>"L", "\u{1D5AC}"=>"M", "\u{1D5AD}"=>"N", "\u{1D5AE}"=>"O", "\u{1D5AF}"=>"P", "\u{1D5B0}"=>"Q", "\u{1D5B1}"=>"R", + "\u{1D5B2}"=>"S", "\u{1D5B3}"=>"T", "\u{1D5B4}"=>"U", "\u{1D5B5}"=>"V", "\u{1D5B6}"=>"W", "\u{1D5B7}"=>"X", "\u{1D5B8}"=>"Y", "\u{1D5B9}"=>"Z", + "\u{1D5BA}"=>"a", "\u{1D5BB}"=>"b", "\u{1D5BC}"=>"c", "\u{1D5BD}"=>"d", "\u{1D5BE}"=>"e", "\u{1D5BF}"=>"f", "\u{1D5C0}"=>"g", "\u{1D5C1}"=>"h", + "\u{1D5C2}"=>"i", "\u{1D5C3}"=>"j", "\u{1D5C4}"=>"k", "\u{1D5C5}"=>"l", "\u{1D5C6}"=>"m", "\u{1D5C7}"=>"n", "\u{1D5C8}"=>"o", "\u{1D5C9}"=>"p", + "\u{1D5CA}"=>"q", "\u{1D5CB}"=>"r", "\u{1D5CC}"=>"s", "\u{1D5CD}"=>"t", "\u{1D5CE}"=>"u", "\u{1D5CF}"=>"v", "\u{1D5D0}"=>"w", "\u{1D5D1}"=>"x", + "\u{1D5D2}"=>"y", "\u{1D5D3}"=>"z", "\u{1D5D4}"=>"A", "\u{1D5D5}"=>"B", "\u{1D5D6}"=>"C", "\u{1D5D7}"=>"D", "\u{1D5D8}"=>"E", "\u{1D5D9}"=>"F", + "\u{1D5DA}"=>"G", "\u{1D5DB}"=>"H", "\u{1D5DC}"=>"I", "\u{1D5DD}"=>"J", "\u{1D5DE}"=>"K", "\u{1D5DF}"=>"L", "\u{1D5E0}"=>"M", "\u{1D5E1}"=>"N", + "\u{1D5E2}"=>"O", "\u{1D5E3}"=>"P", "\u{1D5E4}"=>"Q", "\u{1D5E5}"=>"R", "\u{1D5E6}"=>"S", "\u{1D5E7}"=>"T", "\u{1D5E8}"=>"U", "\u{1D5E9}"=>"V", + "\u{1D5EA}"=>"W", "\u{1D5EB}"=>"X", "\u{1D5EC}"=>"Y", "\u{1D5ED}"=>"Z", "\u{1D5EE}"=>"a", "\u{1D5EF}"=>"b", "\u{1D5F0}"=>"c", "\u{1D5F1}"=>"d", + "\u{1D5F2}"=>"e", "\u{1D5F3}"=>"f", "\u{1D5F4}"=>"g", "\u{1D5F5}"=>"h", "\u{1D5F6}"=>"i", "\u{1D5F7}"=>"j", "\u{1D5F8}"=>"k", "\u{1D5F9}"=>"l", + "\u{1D5FA}"=>"m", "\u{1D5FB}"=>"n", "\u{1D5FC}"=>"o", "\u{1D5FD}"=>"p", "\u{1D5FE}"=>"q", "\u{1D5FF}"=>"r", "\u{1D600}"=>"s", "\u{1D601}"=>"t", + "\u{1D602}"=>"u", "\u{1D603}"=>"v", "\u{1D604}"=>"w", "\u{1D605}"=>"x", "\u{1D606}"=>"y", "\u{1D607}"=>"z", "\u{1D608}"=>"A", "\u{1D609}"=>"B", + "\u{1D60A}"=>"C", "\u{1D60B}"=>"D", "\u{1D60C}"=>"E", "\u{1D60D}"=>"F", "\u{1D60E}"=>"G", "\u{1D60F}"=>"H", "\u{1D610}"=>"I", "\u{1D611}"=>"J", + "\u{1D612}"=>"K", "\u{1D613}"=>"L", "\u{1D614}"=>"M", "\u{1D615}"=>"N", "\u{1D616}"=>"O", "\u{1D617}"=>"P", "\u{1D618}"=>"Q", "\u{1D619}"=>"R", + "\u{1D61A}"=>"S", "\u{1D61B}"=>"T", "\u{1D61C}"=>"U", "\u{1D61D}"=>"V", "\u{1D61E}"=>"W", "\u{1D61F}"=>"X", "\u{1D620}"=>"Y", "\u{1D621}"=>"Z", + "\u{1D622}"=>"a", "\u{1D623}"=>"b", "\u{1D624}"=>"c", "\u{1D625}"=>"d", "\u{1D626}"=>"e", "\u{1D627}"=>"f", "\u{1D628}"=>"g", "\u{1D629}"=>"h", + "\u{1D62A}"=>"i", "\u{1D62B}"=>"j", "\u{1D62C}"=>"k", "\u{1D62D}"=>"l", "\u{1D62E}"=>"m", "\u{1D62F}"=>"n", "\u{1D630}"=>"o", "\u{1D631}"=>"p", + "\u{1D632}"=>"q", "\u{1D633}"=>"r", "\u{1D634}"=>"s", "\u{1D635}"=>"t", "\u{1D636}"=>"u", "\u{1D637}"=>"v", "\u{1D638}"=>"w", "\u{1D639}"=>"x", + "\u{1D63A}"=>"y", "\u{1D63B}"=>"z", "\u{1D63C}"=>"A", "\u{1D63D}"=>"B", "\u{1D63E}"=>"C", "\u{1D63F}"=>"D", "\u{1D640}"=>"E", "\u{1D641}"=>"F", + "\u{1D642}"=>"G", "\u{1D643}"=>"H", "\u{1D644}"=>"I", "\u{1D645}"=>"J", "\u{1D646}"=>"K", "\u{1D647}"=>"L", "\u{1D648}"=>"M", "\u{1D649}"=>"N", + "\u{1D64A}"=>"O", "\u{1D64B}"=>"P", "\u{1D64C}"=>"Q", "\u{1D64D}"=>"R", "\u{1D64E}"=>"S", "\u{1D64F}"=>"T", "\u{1D650}"=>"U", "\u{1D651}"=>"V", + "\u{1D652}"=>"W", "\u{1D653}"=>"X", "\u{1D654}"=>"Y", "\u{1D655}"=>"Z", "\u{1D656}"=>"a", "\u{1D657}"=>"b", "\u{1D658}"=>"c", "\u{1D659}"=>"d", + "\u{1D65A}"=>"e", "\u{1D65B}"=>"f", "\u{1D65C}"=>"g", "\u{1D65D}"=>"h", "\u{1D65E}"=>"i", "\u{1D65F}"=>"j", "\u{1D660}"=>"k", "\u{1D661}"=>"l", + "\u{1D662}"=>"m", "\u{1D663}"=>"n", "\u{1D664}"=>"o", "\u{1D665}"=>"p", "\u{1D666}"=>"q", "\u{1D667}"=>"r", "\u{1D668}"=>"s", "\u{1D669}"=>"t", + "\u{1D66A}"=>"u", "\u{1D66B}"=>"v", "\u{1D66C}"=>"w", "\u{1D66D}"=>"x", "\u{1D66E}"=>"y", "\u{1D66F}"=>"z", "\u{1D670}"=>"A", "\u{1D671}"=>"B", + "\u{1D672}"=>"C", "\u{1D673}"=>"D", "\u{1D674}"=>"E", "\u{1D675}"=>"F", "\u{1D676}"=>"G", "\u{1D677}"=>"H", "\u{1D678}"=>"I", "\u{1D679}"=>"J", + "\u{1D67A}"=>"K", "\u{1D67B}"=>"L", "\u{1D67C}"=>"M", "\u{1D67D}"=>"N", "\u{1D67E}"=>"O", "\u{1D67F}"=>"P", "\u{1D680}"=>"Q", "\u{1D681}"=>"R", + "\u{1D682}"=>"S", "\u{1D683}"=>"T", "\u{1D684}"=>"U", "\u{1D685}"=>"V", "\u{1D686}"=>"W", "\u{1D687}"=>"X", "\u{1D688}"=>"Y", "\u{1D689}"=>"Z", + "\u{1D68A}"=>"a", "\u{1D68B}"=>"b", "\u{1D68C}"=>"c", "\u{1D68D}"=>"d", "\u{1D68E}"=>"e", "\u{1D68F}"=>"f", "\u{1D690}"=>"g", "\u{1D691}"=>"h", + "\u{1D692}"=>"i", "\u{1D693}"=>"j", "\u{1D694}"=>"k", "\u{1D695}"=>"l", "\u{1D696}"=>"m", "\u{1D697}"=>"n", "\u{1D698}"=>"o", "\u{1D699}"=>"p", + "\u{1D69A}"=>"q", "\u{1D69B}"=>"r", "\u{1D69C}"=>"s", "\u{1D69D}"=>"t", "\u{1D69E}"=>"u", "\u{1D69F}"=>"v", "\u{1D6A0}"=>"w", "\u{1D6A1}"=>"x", + "\u{1D6A2}"=>"y", "\u{1D6A3}"=>"z", "\u{1D6A4}"=>"\u0131", "\u{1D6A5}"=>"\u0237", "\u{1D6A8}"=>"\u0391", "\u{1D6A9}"=>"\u0392", "\u{1D6AA}"=>"\u0393", "\u{1D6AB}"=>"\u0394", + "\u{1D6AC}"=>"\u0395", "\u{1D6AD}"=>"\u0396", "\u{1D6AE}"=>"\u0397", "\u{1D6AF}"=>"\u0398", "\u{1D6B0}"=>"\u0399", "\u{1D6B1}"=>"\u039A", "\u{1D6B2}"=>"\u039B", "\u{1D6B3}"=>"\u039C", + "\u{1D6B4}"=>"\u039D", "\u{1D6B5}"=>"\u039E", "\u{1D6B6}"=>"\u039F", "\u{1D6B7}"=>"\u03A0", "\u{1D6B8}"=>"\u03A1", "\u{1D6B9}"=>"\u0398", "\u{1D6BA}"=>"\u03A3", "\u{1D6BB}"=>"\u03A4", + "\u{1D6BC}"=>"\u03A5", "\u{1D6BD}"=>"\u03A6", "\u{1D6BE}"=>"\u03A7", "\u{1D6BF}"=>"\u03A8", "\u{1D6C0}"=>"\u03A9", "\u{1D6C1}"=>"\u2207", "\u{1D6C2}"=>"\u03B1", "\u{1D6C3}"=>"\u03B2", + "\u{1D6C4}"=>"\u03B3", "\u{1D6C5}"=>"\u03B4", "\u{1D6C6}"=>"\u03B5", "\u{1D6C7}"=>"\u03B6", "\u{1D6C8}"=>"\u03B7", "\u{1D6C9}"=>"\u03B8", "\u{1D6CA}"=>"\u03B9", "\u{1D6CB}"=>"\u03BA", + "\u{1D6CC}"=>"\u03BB", "\u{1D6CD}"=>"\u03BC", "\u{1D6CE}"=>"\u03BD", "\u{1D6CF}"=>"\u03BE", "\u{1D6D0}"=>"\u03BF", "\u{1D6D1}"=>"\u03C0", "\u{1D6D2}"=>"\u03C1", "\u{1D6D3}"=>"\u03C2", + "\u{1D6D4}"=>"\u03C3", "\u{1D6D5}"=>"\u03C4", "\u{1D6D6}"=>"\u03C5", "\u{1D6D7}"=>"\u03C6", "\u{1D6D8}"=>"\u03C7", "\u{1D6D9}"=>"\u03C8", "\u{1D6DA}"=>"\u03C9", "\u{1D6DB}"=>"\u2202", + "\u{1D6DC}"=>"\u03B5", "\u{1D6DD}"=>"\u03B8", "\u{1D6DE}"=>"\u03BA", "\u{1D6DF}"=>"\u03C6", "\u{1D6E0}"=>"\u03C1", "\u{1D6E1}"=>"\u03C0", "\u{1D6E2}"=>"\u0391", "\u{1D6E3}"=>"\u0392", + "\u{1D6E4}"=>"\u0393", "\u{1D6E5}"=>"\u0394", "\u{1D6E6}"=>"\u0395", "\u{1D6E7}"=>"\u0396", "\u{1D6E8}"=>"\u0397", "\u{1D6E9}"=>"\u0398", "\u{1D6EA}"=>"\u0399", "\u{1D6EB}"=>"\u039A", + "\u{1D6EC}"=>"\u039B", "\u{1D6ED}"=>"\u039C", "\u{1D6EE}"=>"\u039D", "\u{1D6EF}"=>"\u039E", "\u{1D6F0}"=>"\u039F", "\u{1D6F1}"=>"\u03A0", "\u{1D6F2}"=>"\u03A1", "\u{1D6F3}"=>"\u0398", + "\u{1D6F4}"=>"\u03A3", "\u{1D6F5}"=>"\u03A4", "\u{1D6F6}"=>"\u03A5", "\u{1D6F7}"=>"\u03A6", "\u{1D6F8}"=>"\u03A7", "\u{1D6F9}"=>"\u03A8", "\u{1D6FA}"=>"\u03A9", "\u{1D6FB}"=>"\u2207", + "\u{1D6FC}"=>"\u03B1", "\u{1D6FD}"=>"\u03B2", "\u{1D6FE}"=>"\u03B3", "\u{1D6FF}"=>"\u03B4", "\u{1D700}"=>"\u03B5", "\u{1D701}"=>"\u03B6", "\u{1D702}"=>"\u03B7", "\u{1D703}"=>"\u03B8", + "\u{1D704}"=>"\u03B9", "\u{1D705}"=>"\u03BA", "\u{1D706}"=>"\u03BB", "\u{1D707}"=>"\u03BC", "\u{1D708}"=>"\u03BD", "\u{1D709}"=>"\u03BE", "\u{1D70A}"=>"\u03BF", "\u{1D70B}"=>"\u03C0", + "\u{1D70C}"=>"\u03C1", "\u{1D70D}"=>"\u03C2", "\u{1D70E}"=>"\u03C3", "\u{1D70F}"=>"\u03C4", "\u{1D710}"=>"\u03C5", "\u{1D711}"=>"\u03C6", "\u{1D712}"=>"\u03C7", "\u{1D713}"=>"\u03C8", + "\u{1D714}"=>"\u03C9", "\u{1D715}"=>"\u2202", "\u{1D716}"=>"\u03B5", "\u{1D717}"=>"\u03B8", "\u{1D718}"=>"\u03BA", "\u{1D719}"=>"\u03C6", "\u{1D71A}"=>"\u03C1", "\u{1D71B}"=>"\u03C0", + "\u{1D71C}"=>"\u0391", "\u{1D71D}"=>"\u0392", "\u{1D71E}"=>"\u0393", "\u{1D71F}"=>"\u0394", "\u{1D720}"=>"\u0395", "\u{1D721}"=>"\u0396", "\u{1D722}"=>"\u0397", "\u{1D723}"=>"\u0398", + "\u{1D724}"=>"\u0399", "\u{1D725}"=>"\u039A", "\u{1D726}"=>"\u039B", "\u{1D727}"=>"\u039C", "\u{1D728}"=>"\u039D", "\u{1D729}"=>"\u039E", "\u{1D72A}"=>"\u039F", "\u{1D72B}"=>"\u03A0", + "\u{1D72C}"=>"\u03A1", "\u{1D72D}"=>"\u0398", "\u{1D72E}"=>"\u03A3", "\u{1D72F}"=>"\u03A4", "\u{1D730}"=>"\u03A5", "\u{1D731}"=>"\u03A6", "\u{1D732}"=>"\u03A7", "\u{1D733}"=>"\u03A8", + "\u{1D734}"=>"\u03A9", "\u{1D735}"=>"\u2207", "\u{1D736}"=>"\u03B1", "\u{1D737}"=>"\u03B2", "\u{1D738}"=>"\u03B3", "\u{1D739}"=>"\u03B4", "\u{1D73A}"=>"\u03B5", "\u{1D73B}"=>"\u03B6", + "\u{1D73C}"=>"\u03B7", "\u{1D73D}"=>"\u03B8", "\u{1D73E}"=>"\u03B9", "\u{1D73F}"=>"\u03BA", "\u{1D740}"=>"\u03BB", "\u{1D741}"=>"\u03BC", "\u{1D742}"=>"\u03BD", "\u{1D743}"=>"\u03BE", + "\u{1D744}"=>"\u03BF", "\u{1D745}"=>"\u03C0", "\u{1D746}"=>"\u03C1", "\u{1D747}"=>"\u03C2", "\u{1D748}"=>"\u03C3", "\u{1D749}"=>"\u03C4", "\u{1D74A}"=>"\u03C5", "\u{1D74B}"=>"\u03C6", + "\u{1D74C}"=>"\u03C7", "\u{1D74D}"=>"\u03C8", "\u{1D74E}"=>"\u03C9", "\u{1D74F}"=>"\u2202", "\u{1D750}"=>"\u03B5", "\u{1D751}"=>"\u03B8", "\u{1D752}"=>"\u03BA", "\u{1D753}"=>"\u03C6", + "\u{1D754}"=>"\u03C1", "\u{1D755}"=>"\u03C0", "\u{1D756}"=>"\u0391", "\u{1D757}"=>"\u0392", "\u{1D758}"=>"\u0393", "\u{1D759}"=>"\u0394", "\u{1D75A}"=>"\u0395", "\u{1D75B}"=>"\u0396", + "\u{1D75C}"=>"\u0397", "\u{1D75D}"=>"\u0398", "\u{1D75E}"=>"\u0399", "\u{1D75F}"=>"\u039A", "\u{1D760}"=>"\u039B", "\u{1D761}"=>"\u039C", "\u{1D762}"=>"\u039D", "\u{1D763}"=>"\u039E", + "\u{1D764}"=>"\u039F", "\u{1D765}"=>"\u03A0", "\u{1D766}"=>"\u03A1", "\u{1D767}"=>"\u0398", "\u{1D768}"=>"\u03A3", "\u{1D769}"=>"\u03A4", "\u{1D76A}"=>"\u03A5", "\u{1D76B}"=>"\u03A6", + "\u{1D76C}"=>"\u03A7", "\u{1D76D}"=>"\u03A8", "\u{1D76E}"=>"\u03A9", "\u{1D76F}"=>"\u2207", "\u{1D770}"=>"\u03B1", "\u{1D771}"=>"\u03B2", "\u{1D772}"=>"\u03B3", "\u{1D773}"=>"\u03B4", + "\u{1D774}"=>"\u03B5", "\u{1D775}"=>"\u03B6", "\u{1D776}"=>"\u03B7", "\u{1D777}"=>"\u03B8", "\u{1D778}"=>"\u03B9", "\u{1D779}"=>"\u03BA", "\u{1D77A}"=>"\u03BB", "\u{1D77B}"=>"\u03BC", + "\u{1D77C}"=>"\u03BD", "\u{1D77D}"=>"\u03BE", "\u{1D77E}"=>"\u03BF", "\u{1D77F}"=>"\u03C0", "\u{1D780}"=>"\u03C1", "\u{1D781}"=>"\u03C2", "\u{1D782}"=>"\u03C3", "\u{1D783}"=>"\u03C4", + "\u{1D784}"=>"\u03C5", "\u{1D785}"=>"\u03C6", "\u{1D786}"=>"\u03C7", "\u{1D787}"=>"\u03C8", "\u{1D788}"=>"\u03C9", "\u{1D789}"=>"\u2202", "\u{1D78A}"=>"\u03B5", "\u{1D78B}"=>"\u03B8", + "\u{1D78C}"=>"\u03BA", "\u{1D78D}"=>"\u03C6", "\u{1D78E}"=>"\u03C1", "\u{1D78F}"=>"\u03C0", "\u{1D790}"=>"\u0391", "\u{1D791}"=>"\u0392", "\u{1D792}"=>"\u0393", "\u{1D793}"=>"\u0394", + "\u{1D794}"=>"\u0395", "\u{1D795}"=>"\u0396", "\u{1D796}"=>"\u0397", "\u{1D797}"=>"\u0398", "\u{1D798}"=>"\u0399", "\u{1D799}"=>"\u039A", "\u{1D79A}"=>"\u039B", "\u{1D79B}"=>"\u039C", + "\u{1D79C}"=>"\u039D", "\u{1D79D}"=>"\u039E", "\u{1D79E}"=>"\u039F", "\u{1D79F}"=>"\u03A0", "\u{1D7A0}"=>"\u03A1", "\u{1D7A1}"=>"\u0398", "\u{1D7A2}"=>"\u03A3", "\u{1D7A3}"=>"\u03A4", + "\u{1D7A4}"=>"\u03A5", "\u{1D7A5}"=>"\u03A6", "\u{1D7A6}"=>"\u03A7", "\u{1D7A7}"=>"\u03A8", "\u{1D7A8}"=>"\u03A9", "\u{1D7A9}"=>"\u2207", "\u{1D7AA}"=>"\u03B1", "\u{1D7AB}"=>"\u03B2", + "\u{1D7AC}"=>"\u03B3", "\u{1D7AD}"=>"\u03B4", "\u{1D7AE}"=>"\u03B5", "\u{1D7AF}"=>"\u03B6", "\u{1D7B0}"=>"\u03B7", "\u{1D7B1}"=>"\u03B8", "\u{1D7B2}"=>"\u03B9", "\u{1D7B3}"=>"\u03BA", + "\u{1D7B4}"=>"\u03BB", "\u{1D7B5}"=>"\u03BC", "\u{1D7B6}"=>"\u03BD", "\u{1D7B7}"=>"\u03BE", "\u{1D7B8}"=>"\u03BF", "\u{1D7B9}"=>"\u03C0", "\u{1D7BA}"=>"\u03C1", "\u{1D7BB}"=>"\u03C2", + "\u{1D7BC}"=>"\u03C3", "\u{1D7BD}"=>"\u03C4", "\u{1D7BE}"=>"\u03C5", "\u{1D7BF}"=>"\u03C6", "\u{1D7C0}"=>"\u03C7", "\u{1D7C1}"=>"\u03C8", "\u{1D7C2}"=>"\u03C9", "\u{1D7C3}"=>"\u2202", + "\u{1D7C4}"=>"\u03B5", "\u{1D7C5}"=>"\u03B8", "\u{1D7C6}"=>"\u03BA", "\u{1D7C7}"=>"\u03C6", "\u{1D7C8}"=>"\u03C1", "\u{1D7C9}"=>"\u03C0", "\u{1D7CA}"=>"\u03DC", "\u{1D7CB}"=>"\u03DD", + "\u{1D7CE}"=>"0", "\u{1D7CF}"=>"1", "\u{1D7D0}"=>"2", "\u{1D7D1}"=>"3", "\u{1D7D2}"=>"4", "\u{1D7D3}"=>"5", "\u{1D7D4}"=>"6", "\u{1D7D5}"=>"7", + "\u{1D7D6}"=>"8", "\u{1D7D7}"=>"9", "\u{1D7D8}"=>"0", "\u{1D7D9}"=>"1", "\u{1D7DA}"=>"2", "\u{1D7DB}"=>"3", "\u{1D7DC}"=>"4", "\u{1D7DD}"=>"5", + "\u{1D7DE}"=>"6", "\u{1D7DF}"=>"7", "\u{1D7E0}"=>"8", "\u{1D7E1}"=>"9", "\u{1D7E2}"=>"0", "\u{1D7E3}"=>"1", "\u{1D7E4}"=>"2", "\u{1D7E5}"=>"3", + "\u{1D7E6}"=>"4", "\u{1D7E7}"=>"5", "\u{1D7E8}"=>"6", "\u{1D7E9}"=>"7", "\u{1D7EA}"=>"8", "\u{1D7EB}"=>"9", "\u{1D7EC}"=>"0", "\u{1D7ED}"=>"1", + "\u{1D7EE}"=>"2", "\u{1D7EF}"=>"3", "\u{1D7F0}"=>"4", "\u{1D7F1}"=>"5", "\u{1D7F2}"=>"6", "\u{1D7F3}"=>"7", "\u{1D7F4}"=>"8", "\u{1D7F5}"=>"9", + "\u{1D7F6}"=>"0", "\u{1D7F7}"=>"1", "\u{1D7F8}"=>"2", "\u{1D7F9}"=>"3", "\u{1D7FA}"=>"4", "\u{1D7FB}"=>"5", "\u{1D7FC}"=>"6", "\u{1D7FD}"=>"7", + "\u{1D7FE}"=>"8", "\u{1D7FF}"=>"9", "\u{1EE00}"=>"\u0627", "\u{1EE01}"=>"\u0628", "\u{1EE02}"=>"\u062C", "\u{1EE03}"=>"\u062F", "\u{1EE05}"=>"\u0648", "\u{1EE06}"=>"\u0632", + "\u{1EE07}"=>"\u062D", "\u{1EE08}"=>"\u0637", "\u{1EE09}"=>"\u064A", "\u{1EE0A}"=>"\u0643", "\u{1EE0B}"=>"\u0644", "\u{1EE0C}"=>"\u0645", "\u{1EE0D}"=>"\u0646", "\u{1EE0E}"=>"\u0633", + "\u{1EE0F}"=>"\u0639", "\u{1EE10}"=>"\u0641", "\u{1EE11}"=>"\u0635", "\u{1EE12}"=>"\u0642", "\u{1EE13}"=>"\u0631", "\u{1EE14}"=>"\u0634", "\u{1EE15}"=>"\u062A", "\u{1EE16}"=>"\u062B", + "\u{1EE17}"=>"\u062E", "\u{1EE18}"=>"\u0630", "\u{1EE19}"=>"\u0636", "\u{1EE1A}"=>"\u0638", "\u{1EE1B}"=>"\u063A", "\u{1EE1C}"=>"\u066E", "\u{1EE1D}"=>"\u06BA", "\u{1EE1E}"=>"\u06A1", + "\u{1EE1F}"=>"\u066F", "\u{1EE21}"=>"\u0628", "\u{1EE22}"=>"\u062C", "\u{1EE24}"=>"\u0647", "\u{1EE27}"=>"\u062D", "\u{1EE29}"=>"\u064A", "\u{1EE2A}"=>"\u0643", "\u{1EE2B}"=>"\u0644", + "\u{1EE2C}"=>"\u0645", "\u{1EE2D}"=>"\u0646", "\u{1EE2E}"=>"\u0633", "\u{1EE2F}"=>"\u0639", "\u{1EE30}"=>"\u0641", "\u{1EE31}"=>"\u0635", "\u{1EE32}"=>"\u0642", "\u{1EE34}"=>"\u0634", + "\u{1EE35}"=>"\u062A", "\u{1EE36}"=>"\u062B", "\u{1EE37}"=>"\u062E", "\u{1EE39}"=>"\u0636", "\u{1EE3B}"=>"\u063A", "\u{1EE42}"=>"\u062C", "\u{1EE47}"=>"\u062D", "\u{1EE49}"=>"\u064A", + "\u{1EE4B}"=>"\u0644", "\u{1EE4D}"=>"\u0646", "\u{1EE4E}"=>"\u0633", "\u{1EE4F}"=>"\u0639", "\u{1EE51}"=>"\u0635", "\u{1EE52}"=>"\u0642", "\u{1EE54}"=>"\u0634", "\u{1EE57}"=>"\u062E", + "\u{1EE59}"=>"\u0636", "\u{1EE5B}"=>"\u063A", "\u{1EE5D}"=>"\u06BA", "\u{1EE5F}"=>"\u066F", "\u{1EE61}"=>"\u0628", "\u{1EE62}"=>"\u062C", "\u{1EE64}"=>"\u0647", "\u{1EE67}"=>"\u062D", + "\u{1EE68}"=>"\u0637", "\u{1EE69}"=>"\u064A", "\u{1EE6A}"=>"\u0643", "\u{1EE6C}"=>"\u0645", "\u{1EE6D}"=>"\u0646", "\u{1EE6E}"=>"\u0633", "\u{1EE6F}"=>"\u0639", "\u{1EE70}"=>"\u0641", + "\u{1EE71}"=>"\u0635", "\u{1EE72}"=>"\u0642", "\u{1EE74}"=>"\u0634", "\u{1EE75}"=>"\u062A", "\u{1EE76}"=>"\u062B", "\u{1EE77}"=>"\u062E", "\u{1EE79}"=>"\u0636", "\u{1EE7A}"=>"\u0638", + "\u{1EE7B}"=>"\u063A", "\u{1EE7C}"=>"\u066E", "\u{1EE7E}"=>"\u06A1", "\u{1EE80}"=>"\u0627", "\u{1EE81}"=>"\u0628", "\u{1EE82}"=>"\u062C", "\u{1EE83}"=>"\u062F", "\u{1EE84}"=>"\u0647", + "\u{1EE85}"=>"\u0648", "\u{1EE86}"=>"\u0632", "\u{1EE87}"=>"\u062D", "\u{1EE88}"=>"\u0637", "\u{1EE89}"=>"\u064A", "\u{1EE8B}"=>"\u0644", "\u{1EE8C}"=>"\u0645", "\u{1EE8D}"=>"\u0646", + "\u{1EE8E}"=>"\u0633", "\u{1EE8F}"=>"\u0639", "\u{1EE90}"=>"\u0641", "\u{1EE91}"=>"\u0635", "\u{1EE92}"=>"\u0642", "\u{1EE93}"=>"\u0631", "\u{1EE94}"=>"\u0634", "\u{1EE95}"=>"\u062A", + "\u{1EE96}"=>"\u062B", "\u{1EE97}"=>"\u062E", "\u{1EE98}"=>"\u0630", "\u{1EE99}"=>"\u0636", "\u{1EE9A}"=>"\u0638", "\u{1EE9B}"=>"\u063A", "\u{1EEA1}"=>"\u0628", "\u{1EEA2}"=>"\u062C", + "\u{1EEA3}"=>"\u062F", "\u{1EEA5}"=>"\u0648", "\u{1EEA6}"=>"\u0632", "\u{1EEA7}"=>"\u062D", "\u{1EEA8}"=>"\u0637", "\u{1EEA9}"=>"\u064A", "\u{1EEAB}"=>"\u0644", "\u{1EEAC}"=>"\u0645", + "\u{1EEAD}"=>"\u0646", "\u{1EEAE}"=>"\u0633", "\u{1EEAF}"=>"\u0639", "\u{1EEB0}"=>"\u0641", "\u{1EEB1}"=>"\u0635", "\u{1EEB2}"=>"\u0642", "\u{1EEB3}"=>"\u0631", "\u{1EEB4}"=>"\u0634", + "\u{1EEB5}"=>"\u062A", "\u{1EEB6}"=>"\u062B", "\u{1EEB7}"=>"\u062E", "\u{1EEB8}"=>"\u0630", "\u{1EEB9}"=>"\u0636", "\u{1EEBA}"=>"\u0638", "\u{1EEBB}"=>"\u063A", "\u{1F100}"=>"0.", + "\u{1F101}"=>"0,", "\u{1F102}"=>"1,", "\u{1F103}"=>"2,", "\u{1F104}"=>"3,", "\u{1F105}"=>"4,", "\u{1F106}"=>"5,", "\u{1F107}"=>"6,", "\u{1F108}"=>"7,", + "\u{1F109}"=>"8,", "\u{1F10A}"=>"9,", "\u{1F110}"=>"(A)", "\u{1F111}"=>"(B)", "\u{1F112}"=>"(C)", "\u{1F113}"=>"(D)", "\u{1F114}"=>"(E)", "\u{1F115}"=>"(F)", + "\u{1F116}"=>"(G)", "\u{1F117}"=>"(H)", "\u{1F118}"=>"(I)", "\u{1F119}"=>"(J)", "\u{1F11A}"=>"(K)", "\u{1F11B}"=>"(L)", "\u{1F11C}"=>"(M)", "\u{1F11D}"=>"(N)", + "\u{1F11E}"=>"(O)", "\u{1F11F}"=>"(P)", "\u{1F120}"=>"(Q)", "\u{1F121}"=>"(R)", "\u{1F122}"=>"(S)", "\u{1F123}"=>"(T)", "\u{1F124}"=>"(U)", "\u{1F125}"=>"(V)", + "\u{1F126}"=>"(W)", "\u{1F127}"=>"(X)", "\u{1F128}"=>"(Y)", "\u{1F129}"=>"(Z)", "\u{1F12A}"=>"\u3014S\u3015", "\u{1F12B}"=>"C", "\u{1F12C}"=>"R", "\u{1F12D}"=>"CD", + "\u{1F12E}"=>"WZ", "\u{1F130}"=>"A", "\u{1F131}"=>"B", "\u{1F132}"=>"C", "\u{1F133}"=>"D", "\u{1F134}"=>"E", "\u{1F135}"=>"F", "\u{1F136}"=>"G", + "\u{1F137}"=>"H", "\u{1F138}"=>"I", "\u{1F139}"=>"J", "\u{1F13A}"=>"K", "\u{1F13B}"=>"L", "\u{1F13C}"=>"M", "\u{1F13D}"=>"N", "\u{1F13E}"=>"O", + "\u{1F13F}"=>"P", "\u{1F140}"=>"Q", "\u{1F141}"=>"R", "\u{1F142}"=>"S", "\u{1F143}"=>"T", "\u{1F144}"=>"U", "\u{1F145}"=>"V", "\u{1F146}"=>"W", + "\u{1F147}"=>"X", "\u{1F148}"=>"Y", "\u{1F149}"=>"Z", "\u{1F14A}"=>"HV", "\u{1F14B}"=>"MV", "\u{1F14C}"=>"SD", "\u{1F14D}"=>"SS", "\u{1F14E}"=>"PPV", + "\u{1F14F}"=>"WC", "\u{1F16A}"=>"MC", "\u{1F16B}"=>"MD", "\u{1F190}"=>"DJ", "\u{1F200}"=>"\u307B\u304B", "\u{1F201}"=>"\u30B3\u30B3", "\u{1F202}"=>"\u30B5", "\u{1F210}"=>"\u624B", + "\u{1F211}"=>"\u5B57", "\u{1F212}"=>"\u53CC", "\u{1F213}"=>"\u30C7", "\u{1F214}"=>"\u4E8C", "\u{1F215}"=>"\u591A", "\u{1F216}"=>"\u89E3", "\u{1F217}"=>"\u5929", "\u{1F218}"=>"\u4EA4", + "\u{1F219}"=>"\u6620", "\u{1F21A}"=>"\u7121", "\u{1F21B}"=>"\u6599", "\u{1F21C}"=>"\u524D", "\u{1F21D}"=>"\u5F8C", "\u{1F21E}"=>"\u518D", "\u{1F21F}"=>"\u65B0", "\u{1F220}"=>"\u521D", + "\u{1F221}"=>"\u7D42", "\u{1F222}"=>"\u751F", "\u{1F223}"=>"\u8CA9", "\u{1F224}"=>"\u58F0", "\u{1F225}"=>"\u5439", "\u{1F226}"=>"\u6F14", "\u{1F227}"=>"\u6295", "\u{1F228}"=>"\u6355", + "\u{1F229}"=>"\u4E00", "\u{1F22A}"=>"\u4E09", "\u{1F22B}"=>"\u904A", "\u{1F22C}"=>"\u5DE6", "\u{1F22D}"=>"\u4E2D", "\u{1F22E}"=>"\u53F3", "\u{1F22F}"=>"\u6307", "\u{1F230}"=>"\u8D70", + "\u{1F231}"=>"\u6253", "\u{1F232}"=>"\u7981", "\u{1F233}"=>"\u7A7A", "\u{1F234}"=>"\u5408", "\u{1F235}"=>"\u6E80", "\u{1F236}"=>"\u6709", "\u{1F237}"=>"\u6708", "\u{1F238}"=>"\u7533", + "\u{1F239}"=>"\u5272", "\u{1F23A}"=>"\u55B6", "\u{1F23B}"=>"\u914D", "\u{1F240}"=>"\u3014\u672C\u3015", "\u{1F241}"=>"\u3014\u4E09\u3015", "\u{1F242}"=>"\u3014\u4E8C\u3015", "\u{1F243}"=>"\u3014\u5B89\u3015", "\u{1F244}"=>"\u3014\u70B9\u3015", + "\u{1F245}"=>"\u3014\u6253\u3015", "\u{1F246}"=>"\u3014\u76D7\u3015", "\u{1F247}"=>"\u3014\u52DD\u3015", "\u{1F248}"=>"\u3014\u6557\u3015", "\u{1F250}"=>"\u5F97", "\u{1F251}"=>"\u53EF", "\u0385"=>" \u0308\u0301", "\u03D3"=>"\u03A5\u0301", + "\u03D4"=>"\u03A5\u0308", "\u1E9B"=>"s\u0307", "\u1FC1"=>" \u0308\u0342", "\u1FCD"=>" \u0313\u0300", "\u1FCE"=>" \u0313\u0301", "\u1FCF"=>" \u0313\u0342", "\u1FDD"=>" \u0314\u0300", "\u1FDE"=>" \u0314\u0301", + "\u1FDF"=>" \u0314\u0342", "\u1FED"=>" \u0308\u0300", "\u1FEE"=>" \u0308\u0301", "\u1FFD"=>" \u0301", "\u2000"=>" ", "\u2001"=>" ", + }.freeze + + COMPOSITION_TABLE = { + "A\u0300"=>"\u00C0", "A\u0301"=>"\u00C1", "A\u0302"=>"\u00C2", "A\u0303"=>"\u00C3", "A\u0308"=>"\u00C4", "A\u030A"=>"\u00C5", "C\u0327"=>"\u00C7", "E\u0300"=>"\u00C8", + "E\u0301"=>"\u00C9", "E\u0302"=>"\u00CA", "E\u0308"=>"\u00CB", "I\u0300"=>"\u00CC", "I\u0301"=>"\u00CD", "I\u0302"=>"\u00CE", "I\u0308"=>"\u00CF", "N\u0303"=>"\u00D1", + "O\u0300"=>"\u00D2", "O\u0301"=>"\u00D3", "O\u0302"=>"\u00D4", "O\u0303"=>"\u00D5", "O\u0308"=>"\u00D6", "U\u0300"=>"\u00D9", "U\u0301"=>"\u00DA", "U\u0302"=>"\u00DB", + "U\u0308"=>"\u00DC", "Y\u0301"=>"\u00DD", "a\u0300"=>"\u00E0", "a\u0301"=>"\u00E1", "a\u0302"=>"\u00E2", "a\u0303"=>"\u00E3", "a\u0308"=>"\u00E4", "a\u030A"=>"\u00E5", + "c\u0327"=>"\u00E7", "e\u0300"=>"\u00E8", "e\u0301"=>"\u00E9", "e\u0302"=>"\u00EA", "e\u0308"=>"\u00EB", "i\u0300"=>"\u00EC", "i\u0301"=>"\u00ED", "i\u0302"=>"\u00EE", + "i\u0308"=>"\u00EF", "n\u0303"=>"\u00F1", "o\u0300"=>"\u00F2", "o\u0301"=>"\u00F3", "o\u0302"=>"\u00F4", "o\u0303"=>"\u00F5", "o\u0308"=>"\u00F6", "u\u0300"=>"\u00F9", + "u\u0301"=>"\u00FA", "u\u0302"=>"\u00FB", "u\u0308"=>"\u00FC", "y\u0301"=>"\u00FD", "y\u0308"=>"\u00FF", "A\u0304"=>"\u0100", "a\u0304"=>"\u0101", "A\u0306"=>"\u0102", + "a\u0306"=>"\u0103", "A\u0328"=>"\u0104", "a\u0328"=>"\u0105", "C\u0301"=>"\u0106", "c\u0301"=>"\u0107", "C\u0302"=>"\u0108", "c\u0302"=>"\u0109", "C\u0307"=>"\u010A", + "c\u0307"=>"\u010B", "C\u030C"=>"\u010C", "c\u030C"=>"\u010D", "D\u030C"=>"\u010E", "d\u030C"=>"\u010F", "E\u0304"=>"\u0112", "e\u0304"=>"\u0113", "E\u0306"=>"\u0114", + "e\u0306"=>"\u0115", "E\u0307"=>"\u0116", "e\u0307"=>"\u0117", "E\u0328"=>"\u0118", "e\u0328"=>"\u0119", "E\u030C"=>"\u011A", "e\u030C"=>"\u011B", "G\u0302"=>"\u011C", + "g\u0302"=>"\u011D", "G\u0306"=>"\u011E", "g\u0306"=>"\u011F", "G\u0307"=>"\u0120", "g\u0307"=>"\u0121", "G\u0327"=>"\u0122", "g\u0327"=>"\u0123", "H\u0302"=>"\u0124", + "h\u0302"=>"\u0125", "I\u0303"=>"\u0128", "i\u0303"=>"\u0129", "I\u0304"=>"\u012A", "i\u0304"=>"\u012B", "I\u0306"=>"\u012C", "i\u0306"=>"\u012D", "I\u0328"=>"\u012E", + "i\u0328"=>"\u012F", "I\u0307"=>"\u0130", "J\u0302"=>"\u0134", "j\u0302"=>"\u0135", "K\u0327"=>"\u0136", "k\u0327"=>"\u0137", "L\u0301"=>"\u0139", "l\u0301"=>"\u013A", + "L\u0327"=>"\u013B", "l\u0327"=>"\u013C", "L\u030C"=>"\u013D", "l\u030C"=>"\u013E", "N\u0301"=>"\u0143", "n\u0301"=>"\u0144", "N\u0327"=>"\u0145", "n\u0327"=>"\u0146", + "N\u030C"=>"\u0147", "n\u030C"=>"\u0148", "O\u0304"=>"\u014C", "o\u0304"=>"\u014D", "O\u0306"=>"\u014E", "o\u0306"=>"\u014F", "O\u030B"=>"\u0150", "o\u030B"=>"\u0151", + "R\u0301"=>"\u0154", "r\u0301"=>"\u0155", "R\u0327"=>"\u0156", "r\u0327"=>"\u0157", "R\u030C"=>"\u0158", "r\u030C"=>"\u0159", "S\u0301"=>"\u015A", "s\u0301"=>"\u015B", + "S\u0302"=>"\u015C", "s\u0302"=>"\u015D", "S\u0327"=>"\u015E", "s\u0327"=>"\u015F", "S\u030C"=>"\u0160", "s\u030C"=>"\u0161", "T\u0327"=>"\u0162", "t\u0327"=>"\u0163", + "T\u030C"=>"\u0164", "t\u030C"=>"\u0165", "U\u0303"=>"\u0168", "u\u0303"=>"\u0169", "U\u0304"=>"\u016A", "u\u0304"=>"\u016B", "U\u0306"=>"\u016C", "u\u0306"=>"\u016D", + "U\u030A"=>"\u016E", "u\u030A"=>"\u016F", "U\u030B"=>"\u0170", "u\u030B"=>"\u0171", "U\u0328"=>"\u0172", "u\u0328"=>"\u0173", "W\u0302"=>"\u0174", "w\u0302"=>"\u0175", + "Y\u0302"=>"\u0176", "y\u0302"=>"\u0177", "Y\u0308"=>"\u0178", "Z\u0301"=>"\u0179", "z\u0301"=>"\u017A", "Z\u0307"=>"\u017B", "z\u0307"=>"\u017C", "Z\u030C"=>"\u017D", + "z\u030C"=>"\u017E", "O\u031B"=>"\u01A0", "o\u031B"=>"\u01A1", "U\u031B"=>"\u01AF", "u\u031B"=>"\u01B0", "A\u030C"=>"\u01CD", "a\u030C"=>"\u01CE", "I\u030C"=>"\u01CF", + "i\u030C"=>"\u01D0", "O\u030C"=>"\u01D1", "o\u030C"=>"\u01D2", "U\u030C"=>"\u01D3", "u\u030C"=>"\u01D4", "\u00DC\u0304"=>"\u01D5", "\u00FC\u0304"=>"\u01D6", "\u00DC\u0301"=>"\u01D7", + "\u00FC\u0301"=>"\u01D8", "\u00DC\u030C"=>"\u01D9", "\u00FC\u030C"=>"\u01DA", "\u00DC\u0300"=>"\u01DB", "\u00FC\u0300"=>"\u01DC", "\u00C4\u0304"=>"\u01DE", "\u00E4\u0304"=>"\u01DF", "\u0226\u0304"=>"\u01E0", + "\u0227\u0304"=>"\u01E1", "\u00C6\u0304"=>"\u01E2", "\u00E6\u0304"=>"\u01E3", "G\u030C"=>"\u01E6", "g\u030C"=>"\u01E7", "K\u030C"=>"\u01E8", "k\u030C"=>"\u01E9", "O\u0328"=>"\u01EA", + "o\u0328"=>"\u01EB", "\u01EA\u0304"=>"\u01EC", "\u01EB\u0304"=>"\u01ED", "\u01B7\u030C"=>"\u01EE", "\u0292\u030C"=>"\u01EF", "j\u030C"=>"\u01F0", "G\u0301"=>"\u01F4", "g\u0301"=>"\u01F5", + "N\u0300"=>"\u01F8", "n\u0300"=>"\u01F9", "\u00C5\u0301"=>"\u01FA", "\u00E5\u0301"=>"\u01FB", "\u00C6\u0301"=>"\u01FC", "\u00E6\u0301"=>"\u01FD", "\u00D8\u0301"=>"\u01FE", "\u00F8\u0301"=>"\u01FF", + "A\u030F"=>"\u0200", "a\u030F"=>"\u0201", "A\u0311"=>"\u0202", "a\u0311"=>"\u0203", "E\u030F"=>"\u0204", "e\u030F"=>"\u0205", "E\u0311"=>"\u0206", "e\u0311"=>"\u0207", + "I\u030F"=>"\u0208", "i\u030F"=>"\u0209", "I\u0311"=>"\u020A", "i\u0311"=>"\u020B", "O\u030F"=>"\u020C", "o\u030F"=>"\u020D", "O\u0311"=>"\u020E", "o\u0311"=>"\u020F", + "R\u030F"=>"\u0210", "r\u030F"=>"\u0211", "R\u0311"=>"\u0212", "r\u0311"=>"\u0213", "U\u030F"=>"\u0214", "u\u030F"=>"\u0215", "U\u0311"=>"\u0216", "u\u0311"=>"\u0217", + "S\u0326"=>"\u0218", "s\u0326"=>"\u0219", "T\u0326"=>"\u021A", "t\u0326"=>"\u021B", "H\u030C"=>"\u021E", "h\u030C"=>"\u021F", "A\u0307"=>"\u0226", "a\u0307"=>"\u0227", + "E\u0327"=>"\u0228", "e\u0327"=>"\u0229", "\u00D6\u0304"=>"\u022A", "\u00F6\u0304"=>"\u022B", "\u00D5\u0304"=>"\u022C", "\u00F5\u0304"=>"\u022D", "O\u0307"=>"\u022E", "o\u0307"=>"\u022F", + "\u022E\u0304"=>"\u0230", "\u022F\u0304"=>"\u0231", "Y\u0304"=>"\u0232", "y\u0304"=>"\u0233", "\u00A8\u0301"=>"\u0385", "\u0391\u0301"=>"\u0386", "\u0395\u0301"=>"\u0388", "\u0397\u0301"=>"\u0389", + "\u0399\u0301"=>"\u038A", "\u039F\u0301"=>"\u038C", "\u03A5\u0301"=>"\u038E", "\u03A9\u0301"=>"\u038F", "\u03CA\u0301"=>"\u0390", "\u0399\u0308"=>"\u03AA", "\u03A5\u0308"=>"\u03AB", "\u03B1\u0301"=>"\u03AC", + "\u03B5\u0301"=>"\u03AD", "\u03B7\u0301"=>"\u03AE", "\u03B9\u0301"=>"\u03AF", "\u03CB\u0301"=>"\u03B0", "\u03B9\u0308"=>"\u03CA", "\u03C5\u0308"=>"\u03CB", "\u03BF\u0301"=>"\u03CC", "\u03C5\u0301"=>"\u03CD", + "\u03C9\u0301"=>"\u03CE", "\u03D2\u0301"=>"\u03D3", "\u03D2\u0308"=>"\u03D4", "\u0415\u0300"=>"\u0400", "\u0415\u0308"=>"\u0401", "\u0413\u0301"=>"\u0403", "\u0406\u0308"=>"\u0407", "\u041A\u0301"=>"\u040C", + "\u0418\u0300"=>"\u040D", "\u0423\u0306"=>"\u040E", "\u0418\u0306"=>"\u0419", "\u0438\u0306"=>"\u0439", "\u0435\u0300"=>"\u0450", "\u0435\u0308"=>"\u0451", "\u0433\u0301"=>"\u0453", "\u0456\u0308"=>"\u0457", + "\u043A\u0301"=>"\u045C", "\u0438\u0300"=>"\u045D", "\u0443\u0306"=>"\u045E", "\u0474\u030F"=>"\u0476", "\u0475\u030F"=>"\u0477", "\u0416\u0306"=>"\u04C1", "\u0436\u0306"=>"\u04C2", "\u0410\u0306"=>"\u04D0", + "\u0430\u0306"=>"\u04D1", "\u0410\u0308"=>"\u04D2", "\u0430\u0308"=>"\u04D3", "\u0415\u0306"=>"\u04D6", "\u0435\u0306"=>"\u04D7", "\u04D8\u0308"=>"\u04DA", "\u04D9\u0308"=>"\u04DB", "\u0416\u0308"=>"\u04DC", + "\u0436\u0308"=>"\u04DD", "\u0417\u0308"=>"\u04DE", "\u0437\u0308"=>"\u04DF", "\u0418\u0304"=>"\u04E2", "\u0438\u0304"=>"\u04E3", "\u0418\u0308"=>"\u04E4", "\u0438\u0308"=>"\u04E5", "\u041E\u0308"=>"\u04E6", + "\u043E\u0308"=>"\u04E7", "\u04E8\u0308"=>"\u04EA", "\u04E9\u0308"=>"\u04EB", "\u042D\u0308"=>"\u04EC", "\u044D\u0308"=>"\u04ED", "\u0423\u0304"=>"\u04EE", "\u0443\u0304"=>"\u04EF", "\u0423\u0308"=>"\u04F0", + "\u0443\u0308"=>"\u04F1", "\u0423\u030B"=>"\u04F2", "\u0443\u030B"=>"\u04F3", "\u0427\u0308"=>"\u04F4", "\u0447\u0308"=>"\u04F5", "\u042B\u0308"=>"\u04F8", "\u044B\u0308"=>"\u04F9", "\u0627\u0653"=>"\u0622", + "\u0627\u0654"=>"\u0623", "\u0648\u0654"=>"\u0624", "\u0627\u0655"=>"\u0625", "\u064A\u0654"=>"\u0626", "\u06D5\u0654"=>"\u06C0", "\u06C1\u0654"=>"\u06C2", "\u06D2\u0654"=>"\u06D3", "\u0928\u093C"=>"\u0929", + "\u0930\u093C"=>"\u0931", "\u0933\u093C"=>"\u0934", "\u09C7\u09BE"=>"\u09CB", "\u09C7\u09D7"=>"\u09CC", "\u0B47\u0B56"=>"\u0B48", "\u0B47\u0B3E"=>"\u0B4B", "\u0B47\u0B57"=>"\u0B4C", "\u0B92\u0BD7"=>"\u0B94", + "\u0BC6\u0BBE"=>"\u0BCA", "\u0BC7\u0BBE"=>"\u0BCB", "\u0BC6\u0BD7"=>"\u0BCC", "\u0C46\u0C56"=>"\u0C48", "\u0CBF\u0CD5"=>"\u0CC0", "\u0CC6\u0CD5"=>"\u0CC7", "\u0CC6\u0CD6"=>"\u0CC8", "\u0CC6\u0CC2"=>"\u0CCA", + "\u0CCA\u0CD5"=>"\u0CCB", "\u0D46\u0D3E"=>"\u0D4A", "\u0D47\u0D3E"=>"\u0D4B", "\u0D46\u0D57"=>"\u0D4C", "\u0DD9\u0DCA"=>"\u0DDA", "\u0DD9\u0DCF"=>"\u0DDC", "\u0DDC\u0DCA"=>"\u0DDD", "\u0DD9\u0DDF"=>"\u0DDE", + "\u1025\u102E"=>"\u1026", "\u1B05\u1B35"=>"\u1B06", "\u1B07\u1B35"=>"\u1B08", "\u1B09\u1B35"=>"\u1B0A", "\u1B0B\u1B35"=>"\u1B0C", "\u1B0D\u1B35"=>"\u1B0E", "\u1B11\u1B35"=>"\u1B12", "\u1B3A\u1B35"=>"\u1B3B", + "\u1B3C\u1B35"=>"\u1B3D", "\u1B3E\u1B35"=>"\u1B40", "\u1B3F\u1B35"=>"\u1B41", "\u1B42\u1B35"=>"\u1B43", "A\u0325"=>"\u1E00", "a\u0325"=>"\u1E01", "B\u0307"=>"\u1E02", "b\u0307"=>"\u1E03", + "B\u0323"=>"\u1E04", "b\u0323"=>"\u1E05", "B\u0331"=>"\u1E06", "b\u0331"=>"\u1E07", "\u00C7\u0301"=>"\u1E08", "\u00E7\u0301"=>"\u1E09", "D\u0307"=>"\u1E0A", "d\u0307"=>"\u1E0B", + "D\u0323"=>"\u1E0C", "d\u0323"=>"\u1E0D", "D\u0331"=>"\u1E0E", "d\u0331"=>"\u1E0F", "D\u0327"=>"\u1E10", "d\u0327"=>"\u1E11", "D\u032D"=>"\u1E12", "d\u032D"=>"\u1E13", + "\u0112\u0300"=>"\u1E14", "\u0113\u0300"=>"\u1E15", "\u0112\u0301"=>"\u1E16", "\u0113\u0301"=>"\u1E17", "E\u032D"=>"\u1E18", "e\u032D"=>"\u1E19", "E\u0330"=>"\u1E1A", "e\u0330"=>"\u1E1B", + "\u0228\u0306"=>"\u1E1C", "\u0229\u0306"=>"\u1E1D", "F\u0307"=>"\u1E1E", "f\u0307"=>"\u1E1F", "G\u0304"=>"\u1E20", "g\u0304"=>"\u1E21", "H\u0307"=>"\u1E22", "h\u0307"=>"\u1E23", + "H\u0323"=>"\u1E24", "h\u0323"=>"\u1E25", "H\u0308"=>"\u1E26", "h\u0308"=>"\u1E27", "H\u0327"=>"\u1E28", "h\u0327"=>"\u1E29", "H\u032E"=>"\u1E2A", "h\u032E"=>"\u1E2B", + "I\u0330"=>"\u1E2C", "i\u0330"=>"\u1E2D", "\u00CF\u0301"=>"\u1E2E", "\u00EF\u0301"=>"\u1E2F", "K\u0301"=>"\u1E30", "k\u0301"=>"\u1E31", "K\u0323"=>"\u1E32", "k\u0323"=>"\u1E33", + "K\u0331"=>"\u1E34", "k\u0331"=>"\u1E35", "L\u0323"=>"\u1E36", "l\u0323"=>"\u1E37", "\u1E36\u0304"=>"\u1E38", "\u1E37\u0304"=>"\u1E39", "L\u0331"=>"\u1E3A", "l\u0331"=>"\u1E3B", + "L\u032D"=>"\u1E3C", "l\u032D"=>"\u1E3D", "M\u0301"=>"\u1E3E", "m\u0301"=>"\u1E3F", "M\u0307"=>"\u1E40", "m\u0307"=>"\u1E41", "M\u0323"=>"\u1E42", "m\u0323"=>"\u1E43", + "N\u0307"=>"\u1E44", "n\u0307"=>"\u1E45", "N\u0323"=>"\u1E46", "n\u0323"=>"\u1E47", "N\u0331"=>"\u1E48", "n\u0331"=>"\u1E49", "N\u032D"=>"\u1E4A", "n\u032D"=>"\u1E4B", + "\u00D5\u0301"=>"\u1E4C", "\u00F5\u0301"=>"\u1E4D", "\u00D5\u0308"=>"\u1E4E", "\u00F5\u0308"=>"\u1E4F", "\u014C\u0300"=>"\u1E50", "\u014D\u0300"=>"\u1E51", "\u014C\u0301"=>"\u1E52", "\u014D\u0301"=>"\u1E53", + "P\u0301"=>"\u1E54", "p\u0301"=>"\u1E55", "P\u0307"=>"\u1E56", "p\u0307"=>"\u1E57", "R\u0307"=>"\u1E58", "r\u0307"=>"\u1E59", "R\u0323"=>"\u1E5A", "r\u0323"=>"\u1E5B", + "\u1E5A\u0304"=>"\u1E5C", "\u1E5B\u0304"=>"\u1E5D", "R\u0331"=>"\u1E5E", "r\u0331"=>"\u1E5F", "S\u0307"=>"\u1E60", "s\u0307"=>"\u1E61", "S\u0323"=>"\u1E62", "s\u0323"=>"\u1E63", + "\u015A\u0307"=>"\u1E64", "\u015B\u0307"=>"\u1E65", "\u0160\u0307"=>"\u1E66", "\u0161\u0307"=>"\u1E67", "\u1E62\u0307"=>"\u1E68", "\u1E63\u0307"=>"\u1E69", "T\u0307"=>"\u1E6A", "t\u0307"=>"\u1E6B", + "T\u0323"=>"\u1E6C", "t\u0323"=>"\u1E6D", "T\u0331"=>"\u1E6E", "t\u0331"=>"\u1E6F", "T\u032D"=>"\u1E70", "t\u032D"=>"\u1E71", "U\u0324"=>"\u1E72", "u\u0324"=>"\u1E73", + "U\u0330"=>"\u1E74", "u\u0330"=>"\u1E75", "U\u032D"=>"\u1E76", "u\u032D"=>"\u1E77", "\u0168\u0301"=>"\u1E78", "\u0169\u0301"=>"\u1E79", "\u016A\u0308"=>"\u1E7A", "\u016B\u0308"=>"\u1E7B", + "V\u0303"=>"\u1E7C", "v\u0303"=>"\u1E7D", "V\u0323"=>"\u1E7E", "v\u0323"=>"\u1E7F", "W\u0300"=>"\u1E80", "w\u0300"=>"\u1E81", "W\u0301"=>"\u1E82", "w\u0301"=>"\u1E83", + "W\u0308"=>"\u1E84", "w\u0308"=>"\u1E85", "W\u0307"=>"\u1E86", "w\u0307"=>"\u1E87", "W\u0323"=>"\u1E88", "w\u0323"=>"\u1E89", "X\u0307"=>"\u1E8A", "x\u0307"=>"\u1E8B", + "X\u0308"=>"\u1E8C", "x\u0308"=>"\u1E8D", "Y\u0307"=>"\u1E8E", "y\u0307"=>"\u1E8F", "Z\u0302"=>"\u1E90", "z\u0302"=>"\u1E91", "Z\u0323"=>"\u1E92", "z\u0323"=>"\u1E93", + "Z\u0331"=>"\u1E94", "z\u0331"=>"\u1E95", "h\u0331"=>"\u1E96", "t\u0308"=>"\u1E97", "w\u030A"=>"\u1E98", "y\u030A"=>"\u1E99", "\u017F\u0307"=>"\u1E9B", "A\u0323"=>"\u1EA0", + "a\u0323"=>"\u1EA1", "A\u0309"=>"\u1EA2", "a\u0309"=>"\u1EA3", "\u00C2\u0301"=>"\u1EA4", "\u00E2\u0301"=>"\u1EA5", "\u00C2\u0300"=>"\u1EA6", "\u00E2\u0300"=>"\u1EA7", "\u00C2\u0309"=>"\u1EA8", + "\u00E2\u0309"=>"\u1EA9", "\u00C2\u0303"=>"\u1EAA", "\u00E2\u0303"=>"\u1EAB", "\u1EA0\u0302"=>"\u1EAC", "\u1EA1\u0302"=>"\u1EAD", "\u0102\u0301"=>"\u1EAE", "\u0103\u0301"=>"\u1EAF", "\u0102\u0300"=>"\u1EB0", + "\u0103\u0300"=>"\u1EB1", "\u0102\u0309"=>"\u1EB2", "\u0103\u0309"=>"\u1EB3", "\u0102\u0303"=>"\u1EB4", "\u0103\u0303"=>"\u1EB5", "\u1EA0\u0306"=>"\u1EB6", "\u1EA1\u0306"=>"\u1EB7", "E\u0323"=>"\u1EB8", + "e\u0323"=>"\u1EB9", "E\u0309"=>"\u1EBA", "e\u0309"=>"\u1EBB", "E\u0303"=>"\u1EBC", "e\u0303"=>"\u1EBD", "\u00CA\u0301"=>"\u1EBE", "\u00EA\u0301"=>"\u1EBF", "\u00CA\u0300"=>"\u1EC0", + "\u00EA\u0300"=>"\u1EC1", "\u00CA\u0309"=>"\u1EC2", "\u00EA\u0309"=>"\u1EC3", "\u00CA\u0303"=>"\u1EC4", "\u00EA\u0303"=>"\u1EC5", "\u1EB8\u0302"=>"\u1EC6", "\u1EB9\u0302"=>"\u1EC7", "I\u0309"=>"\u1EC8", + "i\u0309"=>"\u1EC9", "I\u0323"=>"\u1ECA", "i\u0323"=>"\u1ECB", "O\u0323"=>"\u1ECC", "o\u0323"=>"\u1ECD", "O\u0309"=>"\u1ECE", "o\u0309"=>"\u1ECF", "\u00D4\u0301"=>"\u1ED0", + "\u00F4\u0301"=>"\u1ED1", "\u00D4\u0300"=>"\u1ED2", "\u00F4\u0300"=>"\u1ED3", "\u00D4\u0309"=>"\u1ED4", "\u00F4\u0309"=>"\u1ED5", "\u00D4\u0303"=>"\u1ED6", "\u00F4\u0303"=>"\u1ED7", "\u1ECC\u0302"=>"\u1ED8", + "\u1ECD\u0302"=>"\u1ED9", "\u01A0\u0301"=>"\u1EDA", "\u01A1\u0301"=>"\u1EDB", "\u01A0\u0300"=>"\u1EDC", "\u01A1\u0300"=>"\u1EDD", "\u01A0\u0309"=>"\u1EDE", "\u01A1\u0309"=>"\u1EDF", "\u01A0\u0303"=>"\u1EE0", + "\u01A1\u0303"=>"\u1EE1", "\u01A0\u0323"=>"\u1EE2", "\u01A1\u0323"=>"\u1EE3", "U\u0323"=>"\u1EE4", "u\u0323"=>"\u1EE5", "U\u0309"=>"\u1EE6", "u\u0309"=>"\u1EE7", "\u01AF\u0301"=>"\u1EE8", + "\u01B0\u0301"=>"\u1EE9", "\u01AF\u0300"=>"\u1EEA", "\u01B0\u0300"=>"\u1EEB", "\u01AF\u0309"=>"\u1EEC", "\u01B0\u0309"=>"\u1EED", "\u01AF\u0303"=>"\u1EEE", "\u01B0\u0303"=>"\u1EEF", "\u01AF\u0323"=>"\u1EF0", + "\u01B0\u0323"=>"\u1EF1", "Y\u0300"=>"\u1EF2", "y\u0300"=>"\u1EF3", "Y\u0323"=>"\u1EF4", "y\u0323"=>"\u1EF5", "Y\u0309"=>"\u1EF6", "y\u0309"=>"\u1EF7", "Y\u0303"=>"\u1EF8", + "y\u0303"=>"\u1EF9", "\u03B1\u0313"=>"\u1F00", "\u03B1\u0314"=>"\u1F01", "\u1F00\u0300"=>"\u1F02", "\u1F01\u0300"=>"\u1F03", "\u1F00\u0301"=>"\u1F04", "\u1F01\u0301"=>"\u1F05", "\u1F00\u0342"=>"\u1F06", + "\u1F01\u0342"=>"\u1F07", "\u0391\u0313"=>"\u1F08", "\u0391\u0314"=>"\u1F09", "\u1F08\u0300"=>"\u1F0A", "\u1F09\u0300"=>"\u1F0B", "\u1F08\u0301"=>"\u1F0C", "\u1F09\u0301"=>"\u1F0D", "\u1F08\u0342"=>"\u1F0E", + "\u1F09\u0342"=>"\u1F0F", "\u03B5\u0313"=>"\u1F10", "\u03B5\u0314"=>"\u1F11", "\u1F10\u0300"=>"\u1F12", "\u1F11\u0300"=>"\u1F13", "\u1F10\u0301"=>"\u1F14", "\u1F11\u0301"=>"\u1F15", "\u0395\u0313"=>"\u1F18", + "\u0395\u0314"=>"\u1F19", "\u1F18\u0300"=>"\u1F1A", "\u1F19\u0300"=>"\u1F1B", "\u1F18\u0301"=>"\u1F1C", "\u1F19\u0301"=>"\u1F1D", "\u03B7\u0313"=>"\u1F20", "\u03B7\u0314"=>"\u1F21", "\u1F20\u0300"=>"\u1F22", + "\u1F21\u0300"=>"\u1F23", "\u1F20\u0301"=>"\u1F24", "\u1F21\u0301"=>"\u1F25", "\u1F20\u0342"=>"\u1F26", "\u1F21\u0342"=>"\u1F27", "\u0397\u0313"=>"\u1F28", "\u0397\u0314"=>"\u1F29", "\u1F28\u0300"=>"\u1F2A", + "\u1F29\u0300"=>"\u1F2B", "\u1F28\u0301"=>"\u1F2C", "\u1F29\u0301"=>"\u1F2D", "\u1F28\u0342"=>"\u1F2E", "\u1F29\u0342"=>"\u1F2F", "\u03B9\u0313"=>"\u1F30", "\u03B9\u0314"=>"\u1F31", "\u1F30\u0300"=>"\u1F32", + "\u1F31\u0300"=>"\u1F33", "\u1F30\u0301"=>"\u1F34", "\u1F31\u0301"=>"\u1F35", "\u1F30\u0342"=>"\u1F36", "\u1F31\u0342"=>"\u1F37", "\u0399\u0313"=>"\u1F38", "\u0399\u0314"=>"\u1F39", "\u1F38\u0300"=>"\u1F3A", + "\u1F39\u0300"=>"\u1F3B", "\u1F38\u0301"=>"\u1F3C", "\u1F39\u0301"=>"\u1F3D", "\u1F38\u0342"=>"\u1F3E", "\u1F39\u0342"=>"\u1F3F", "\u03BF\u0313"=>"\u1F40", "\u03BF\u0314"=>"\u1F41", "\u1F40\u0300"=>"\u1F42", + "\u1F41\u0300"=>"\u1F43", "\u1F40\u0301"=>"\u1F44", "\u1F41\u0301"=>"\u1F45", "\u039F\u0313"=>"\u1F48", "\u039F\u0314"=>"\u1F49", "\u1F48\u0300"=>"\u1F4A", "\u1F49\u0300"=>"\u1F4B", "\u1F48\u0301"=>"\u1F4C", + "\u1F49\u0301"=>"\u1F4D", "\u03C5\u0313"=>"\u1F50", "\u03C5\u0314"=>"\u1F51", "\u1F50\u0300"=>"\u1F52", "\u1F51\u0300"=>"\u1F53", "\u1F50\u0301"=>"\u1F54", "\u1F51\u0301"=>"\u1F55", "\u1F50\u0342"=>"\u1F56", + "\u1F51\u0342"=>"\u1F57", "\u03A5\u0314"=>"\u1F59", "\u1F59\u0300"=>"\u1F5B", "\u1F59\u0301"=>"\u1F5D", "\u1F59\u0342"=>"\u1F5F", "\u03C9\u0313"=>"\u1F60", "\u03C9\u0314"=>"\u1F61", "\u1F60\u0300"=>"\u1F62", + "\u1F61\u0300"=>"\u1F63", "\u1F60\u0301"=>"\u1F64", "\u1F61\u0301"=>"\u1F65", "\u1F60\u0342"=>"\u1F66", "\u1F61\u0342"=>"\u1F67", "\u03A9\u0313"=>"\u1F68", "\u03A9\u0314"=>"\u1F69", "\u1F68\u0300"=>"\u1F6A", + "\u1F69\u0300"=>"\u1F6B", "\u1F68\u0301"=>"\u1F6C", "\u1F69\u0301"=>"\u1F6D", "\u1F68\u0342"=>"\u1F6E", "\u1F69\u0342"=>"\u1F6F", "\u03B1\u0300"=>"\u1F70", "\u03B5\u0300"=>"\u1F72", "\u03B7\u0300"=>"\u1F74", + "\u03B9\u0300"=>"\u1F76", "\u03BF\u0300"=>"\u1F78", "\u03C5\u0300"=>"\u1F7A", "\u03C9\u0300"=>"\u1F7C", "\u1F00\u0345"=>"\u1F80", "\u1F01\u0345"=>"\u1F81", "\u1F02\u0345"=>"\u1F82", "\u1F03\u0345"=>"\u1F83", + "\u1F04\u0345"=>"\u1F84", "\u1F05\u0345"=>"\u1F85", "\u1F06\u0345"=>"\u1F86", "\u1F07\u0345"=>"\u1F87", "\u1F08\u0345"=>"\u1F88", "\u1F09\u0345"=>"\u1F89", "\u1F0A\u0345"=>"\u1F8A", "\u1F0B\u0345"=>"\u1F8B", + "\u1F0C\u0345"=>"\u1F8C", "\u1F0D\u0345"=>"\u1F8D", "\u1F0E\u0345"=>"\u1F8E", "\u1F0F\u0345"=>"\u1F8F", "\u1F20\u0345"=>"\u1F90", "\u1F21\u0345"=>"\u1F91", "\u1F22\u0345"=>"\u1F92", "\u1F23\u0345"=>"\u1F93", + "\u1F24\u0345"=>"\u1F94", "\u1F25\u0345"=>"\u1F95", "\u1F26\u0345"=>"\u1F96", "\u1F27\u0345"=>"\u1F97", "\u1F28\u0345"=>"\u1F98", "\u1F29\u0345"=>"\u1F99", "\u1F2A\u0345"=>"\u1F9A", "\u1F2B\u0345"=>"\u1F9B", + "\u1F2C\u0345"=>"\u1F9C", "\u1F2D\u0345"=>"\u1F9D", "\u1F2E\u0345"=>"\u1F9E", "\u1F2F\u0345"=>"\u1F9F", "\u1F60\u0345"=>"\u1FA0", "\u1F61\u0345"=>"\u1FA1", "\u1F62\u0345"=>"\u1FA2", "\u1F63\u0345"=>"\u1FA3", + "\u1F64\u0345"=>"\u1FA4", "\u1F65\u0345"=>"\u1FA5", "\u1F66\u0345"=>"\u1FA6", "\u1F67\u0345"=>"\u1FA7", "\u1F68\u0345"=>"\u1FA8", "\u1F69\u0345"=>"\u1FA9", "\u1F6A\u0345"=>"\u1FAA", "\u1F6B\u0345"=>"\u1FAB", + "\u1F6C\u0345"=>"\u1FAC", "\u1F6D\u0345"=>"\u1FAD", "\u1F6E\u0345"=>"\u1FAE", "\u1F6F\u0345"=>"\u1FAF", "\u03B1\u0306"=>"\u1FB0", "\u03B1\u0304"=>"\u1FB1", "\u1F70\u0345"=>"\u1FB2", "\u03B1\u0345"=>"\u1FB3", + "\u03AC\u0345"=>"\u1FB4", "\u03B1\u0342"=>"\u1FB6", "\u1FB6\u0345"=>"\u1FB7", "\u0391\u0306"=>"\u1FB8", "\u0391\u0304"=>"\u1FB9", "\u0391\u0300"=>"\u1FBA", "\u0391\u0345"=>"\u1FBC", "\u00A8\u0342"=>"\u1FC1", + "\u1F74\u0345"=>"\u1FC2", "\u03B7\u0345"=>"\u1FC3", "\u03AE\u0345"=>"\u1FC4", "\u03B7\u0342"=>"\u1FC6", "\u1FC6\u0345"=>"\u1FC7", "\u0395\u0300"=>"\u1FC8", "\u0397\u0300"=>"\u1FCA", "\u0397\u0345"=>"\u1FCC", + "\u1FBF\u0300"=>"\u1FCD", "\u1FBF\u0301"=>"\u1FCE", "\u1FBF\u0342"=>"\u1FCF", "\u03B9\u0306"=>"\u1FD0", "\u03B9\u0304"=>"\u1FD1", "\u03CA\u0300"=>"\u1FD2", "\u03B9\u0342"=>"\u1FD6", "\u03CA\u0342"=>"\u1FD7", + "\u0399\u0306"=>"\u1FD8", "\u0399\u0304"=>"\u1FD9", "\u0399\u0300"=>"\u1FDA", "\u1FFE\u0300"=>"\u1FDD", "\u1FFE\u0301"=>"\u1FDE", "\u1FFE\u0342"=>"\u1FDF", "\u03C5\u0306"=>"\u1FE0", "\u03C5\u0304"=>"\u1FE1", + "\u03CB\u0300"=>"\u1FE2", "\u03C1\u0313"=>"\u1FE4", "\u03C1\u0314"=>"\u1FE5", "\u03C5\u0342"=>"\u1FE6", "\u03CB\u0342"=>"\u1FE7", "\u03A5\u0306"=>"\u1FE8", "\u03A5\u0304"=>"\u1FE9", "\u03A5\u0300"=>"\u1FEA", + "\u03A1\u0314"=>"\u1FEC", "\u00A8\u0300"=>"\u1FED", "\u1F7C\u0345"=>"\u1FF2", "\u03C9\u0345"=>"\u1FF3", "\u03CE\u0345"=>"\u1FF4", "\u03C9\u0342"=>"\u1FF6", "\u1FF6\u0345"=>"\u1FF7", "\u039F\u0300"=>"\u1FF8", + "\u03A9\u0300"=>"\u1FFA", "\u03A9\u0345"=>"\u1FFC", "\u2190\u0338"=>"\u219A", "\u2192\u0338"=>"\u219B", "\u2194\u0338"=>"\u21AE", "\u21D0\u0338"=>"\u21CD", "\u21D4\u0338"=>"\u21CE", "\u21D2\u0338"=>"\u21CF", + "\u2203\u0338"=>"\u2204", "\u2208\u0338"=>"\u2209", "\u220B\u0338"=>"\u220C", "\u2223\u0338"=>"\u2224", "\u2225\u0338"=>"\u2226", "\u223C\u0338"=>"\u2241", "\u2243\u0338"=>"\u2244", "\u2245\u0338"=>"\u2247", + "\u2248\u0338"=>"\u2249", "=\u0338"=>"\u2260", "\u2261\u0338"=>"\u2262", "\u224D\u0338"=>"\u226D", "<\u0338"=>"\u226E", ">\u0338"=>"\u226F", "\u2264\u0338"=>"\u2270", "\u2265\u0338"=>"\u2271", + "\u2272\u0338"=>"\u2274", "\u2273\u0338"=>"\u2275", "\u2276\u0338"=>"\u2278", "\u2277\u0338"=>"\u2279", "\u227A\u0338"=>"\u2280", "\u227B\u0338"=>"\u2281", "\u2282\u0338"=>"\u2284", "\u2283\u0338"=>"\u2285", + "\u2286\u0338"=>"\u2288", "\u2287\u0338"=>"\u2289", "\u22A2\u0338"=>"\u22AC", "\u22A8\u0338"=>"\u22AD", "\u22A9\u0338"=>"\u22AE", "\u22AB\u0338"=>"\u22AF", "\u227C\u0338"=>"\u22E0", "\u227D\u0338"=>"\u22E1", + "\u2291\u0338"=>"\u22E2", "\u2292\u0338"=>"\u22E3", "\u22B2\u0338"=>"\u22EA", "\u22B3\u0338"=>"\u22EB", "\u22B4\u0338"=>"\u22EC", "\u22B5\u0338"=>"\u22ED", "\u304B\u3099"=>"\u304C", "\u304D\u3099"=>"\u304E", + "\u304F\u3099"=>"\u3050", "\u3051\u3099"=>"\u3052", "\u3053\u3099"=>"\u3054", "\u3055\u3099"=>"\u3056", "\u3057\u3099"=>"\u3058", "\u3059\u3099"=>"\u305A", "\u305B\u3099"=>"\u305C", "\u305D\u3099"=>"\u305E", + "\u305F\u3099"=>"\u3060", "\u3061\u3099"=>"\u3062", "\u3064\u3099"=>"\u3065", "\u3066\u3099"=>"\u3067", "\u3068\u3099"=>"\u3069", "\u306F\u3099"=>"\u3070", "\u306F\u309A"=>"\u3071", "\u3072\u3099"=>"\u3073", + "\u3072\u309A"=>"\u3074", "\u3075\u3099"=>"\u3076", "\u3075\u309A"=>"\u3077", "\u3078\u3099"=>"\u3079", "\u3078\u309A"=>"\u307A", "\u307B\u3099"=>"\u307C", "\u307B\u309A"=>"\u307D", "\u3046\u3099"=>"\u3094", + "\u309D\u3099"=>"\u309E", "\u30AB\u3099"=>"\u30AC", "\u30AD\u3099"=>"\u30AE", "\u30AF\u3099"=>"\u30B0", "\u30B1\u3099"=>"\u30B2", "\u30B3\u3099"=>"\u30B4", "\u30B5\u3099"=>"\u30B6", "\u30B7\u3099"=>"\u30B8", + "\u30B9\u3099"=>"\u30BA", "\u30BB\u3099"=>"\u30BC", "\u30BD\u3099"=>"\u30BE", "\u30BF\u3099"=>"\u30C0", "\u30C1\u3099"=>"\u30C2", "\u30C4\u3099"=>"\u30C5", "\u30C6\u3099"=>"\u30C7", "\u30C8\u3099"=>"\u30C9", + "\u30CF\u3099"=>"\u30D0", "\u30CF\u309A"=>"\u30D1", "\u30D2\u3099"=>"\u30D3", "\u30D2\u309A"=>"\u30D4", "\u30D5\u3099"=>"\u30D6", "\u30D5\u309A"=>"\u30D7", "\u30D8\u3099"=>"\u30D9", "\u30D8\u309A"=>"\u30DA", + "\u30DB\u3099"=>"\u30DC", "\u30DB\u309A"=>"\u30DD", "\u30A6\u3099"=>"\u30F4", "\u30EF\u3099"=>"\u30F7", "\u30F0\u3099"=>"\u30F8", "\u30F1\u3099"=>"\u30F9", "\u30F2\u3099"=>"\u30FA", "\u30FD\u3099"=>"\u30FE", + "\u{11099}\u{110BA}"=>"\u{1109A}", "\u{1109B}\u{110BA}"=>"\u{1109C}", "\u{110A5}\u{110BA}"=>"\u{110AB}", "\u{11131}\u{11127}"=>"\u{1112E}", "\u{11132}\u{11127}"=>"\u{1112F}", "\u{11347}\u{1133E}"=>"\u{1134B}", "\u{11347}\u{11357}"=>"\u{1134C}", "\u{114B9}\u{114BA}"=>"\u{114BB}", + "\u{114B9}\u{114B0}"=>"\u{114BC}", "\u{114B9}\u{114BD}"=>"\u{114BE}", "\u{115B8}\u{115AF}"=>"\u{115BA}", "\u{115B9}\u{115AF}"=>"\u{115BB}", + }.freeze +end diff --git a/gxg/standard/uri/common.rb b/gxg/standard/uri/common.rb new file mode 100644 index 0000000..1d7ee59 --- /dev/null +++ b/gxg/standard/uri/common.rb @@ -0,0 +1,750 @@ +# frozen_string_literal: false +#-- +# = uri/common.rb +# +# Author:: Akira Yamada +# Revision:: $Id: common.rb 61155 2017-12-12 11:56:25Z shyouhei $ +# License:: +# You can redistribute it and/or modify it under the same term as Ruby. +# +# See URI for general documentation +# + +require "uri/rfc2396_parser" +require "uri/rfc3986_parser" + +module URI + REGEXP = RFC2396_REGEXP + Parser = RFC2396_Parser + RFC3986_PARSER = RFC3986_Parser.new + + # URI::Parser.new + DEFAULT_PARSER = Parser.new + DEFAULT_PARSER.pattern.each_pair do |sym, str| + unless REGEXP::PATTERN.const_defined?(sym) + REGEXP::PATTERN.const_set(sym, str) + end + end + DEFAULT_PARSER.regexp.each_pair do |sym, str| + const_set(sym, str) + end + + module Util # :nodoc: + def make_components_hash(klass, array_hash) + tmp = {} + if array_hash.kind_of?(Array) && + array_hash.size == klass.component.size - 1 + klass.component[1..-1].each_index do |i| + begin + tmp[klass.component[i + 1]] = array_hash[i].clone + rescue TypeError + tmp[klass.component[i + 1]] = array_hash[i] + end + end + + elsif array_hash.kind_of?(Hash) + array_hash.each do |key, value| + begin + tmp[key] = value.clone + rescue TypeError + tmp[key] = value + end + end + else + raise ArgumentError, + "expected Array of or Hash of components of #{klass} (#{klass.component[1..-1].join(', ')})" + end + tmp[:scheme] = klass.to_s.sub(/\A.*::/, '').downcase + + return tmp + end + module_function :make_components_hash + end + + # module for escaping unsafe characters with codes. + module Escape + # + # == Synopsis + # + # URI.escape(str [, unsafe]) + # + # == Args + # + # +str+:: + # String to replaces in. + # +unsafe+:: + # Regexp that matches all symbols that must be replaced with codes. + # By default uses UNSAFE. + # When this argument is a String, it represents a character set. + # + # == Description + # + # Escapes the string, replacing all unsafe characters with codes. + # + # This method is obsolete and should not be used. Instead, use + # CGI.escape, URI.encode_www_form or URI.encode_www_form_component + # depending on your specific use case. + # + # == Usage + # + # require 'uri' + # + # enc_uri = URI.escape("http://example.com/?a=\11\15") + # p enc_uri + # # => "http://example.com/?a=%09%0D" + # + # p URI.unescape(enc_uri) + # # => "http://example.com/?a=\t\r" + # + # p URI.escape("@?@!", "!?") + # # => "@%3F@%21" + # + def escape(*arg) + warn "URI.escape is obsolete", uplevel: 1 if $VERBOSE + DEFAULT_PARSER.escape(*arg) + end + alias encode escape + # + # == Synopsis + # + # URI.unescape(str) + # + # == Args + # + # +str+:: + # Unescapes the string. + # + # == Description + # + # This method is obsolete and should not be used. Instead, use + # CGI.unescape, URI.decode_www_form or URI.decode_www_form_component + # depending on your specific use case. + # + # == Usage + # + # require 'uri' + # + # enc_uri = URI.escape("http://example.com/?a=\11\15") + # p enc_uri + # # => "http://example.com/?a=%09%0D" + # + # p URI.unescape(enc_uri) + # # => "http://example.com/?a=\t\r" + # + def unescape(*arg) + warn "URI.unescape is obsolete", uplevel: 1 if $VERBOSE + DEFAULT_PARSER.unescape(*arg) + end + alias decode unescape + end # module Escape + + extend Escape + include REGEXP + + @@schemes = {} + # Returns a Hash of the defined schemes + def self.scheme_list + @@schemes + end + + # + # Base class for all URI exceptions. + # + class Error < StandardError; end + # + # Not a URI. + # + class InvalidURIError < Error; end + # + # Not a URI component. + # + class InvalidComponentError < Error; end + # + # URI is valid, bad usage is not. + # + class BadURIError < Error; end + + # + # == Synopsis + # + # URI::split(uri) + # + # == Args + # + # +uri+:: + # String with URI. + # + # == Description + # + # Splits the string on following parts and returns array with result: + # + # * Scheme + # * Userinfo + # * Host + # * Port + # * Registry + # * Path + # * Opaque + # * Query + # * Fragment + # + # == Usage + # + # require 'uri' + # + # p URI.split("http://www.ruby-lang.org/") + # # => ["http", nil, "www.ruby-lang.org", nil, nil, "/", nil, nil, nil] + # + def self.split(uri) + RFC3986_PARSER.split(uri) + end + + # + # == Synopsis + # + # URI::parse(uri_str) + # + # == Args + # + # +uri_str+:: + # String with URI. + # + # == Description + # + # Creates one of the URI's subclasses instance from the string. + # + # == Raises + # + # URI::InvalidURIError + # Raised if URI given is not a correct one. + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://www.ruby-lang.org/") + # p uri + # # => # + # p uri.scheme + # # => "http" + # p uri.host + # # => "www.ruby-lang.org" + # + # It's recommended to first ::escape the provided +uri_str+ if there are any + # invalid URI characters. + # + def self.parse(uri) + RFC3986_PARSER.parse(uri) + end + + # + # == Synopsis + # + # URI::join(str[, str, ...]) + # + # == Args + # + # +str+:: + # String(s) to work with, will be converted to RFC3986 URIs before merging. + # + # == Description + # + # Joins URIs. + # + # == Usage + # + # require 'uri' + # + # p URI.join("http://example.com/","main.rbx") + # # => # + # + # p URI.join('http://example.com', 'foo') + # # => # + # + # p URI.join('http://example.com', '/foo', '/bar') + # # => # + # + # p URI.join('http://example.com', '/foo', 'bar') + # # => # + # + # p URI.join('http://example.com', '/foo/', 'bar') + # # => # + # + # + def self.join(*str) + RFC3986_PARSER.join(*str) + end + + # + # == Synopsis + # + # URI::extract(str[, schemes][,&blk]) + # + # == Args + # + # +str+:: + # String to extract URIs from. + # +schemes+:: + # Limit URI matching to a specific schemes. + # + # == Description + # + # Extracts URIs from a string. If block given, iterates through all matched URIs. + # Returns nil if block given or array with matches. + # + # == Usage + # + # require "uri" + # + # URI.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.") + # # => ["http://foo.example.com/bla", "mailto:test@example.com"] + # + def self.extract(str, schemes = nil, &block) + warn "URI.extract is obsolete", uplevel: 1 if $VERBOSE + DEFAULT_PARSER.extract(str, schemes, &block) + end + + # + # == Synopsis + # + # URI::regexp([match_schemes]) + # + # == Args + # + # +match_schemes+:: + # Array of schemes. If given, resulting regexp matches to URIs + # whose scheme is one of the match_schemes. + # + # == Description + # Returns a Regexp object which matches to URI-like strings. + # The Regexp object returned by this method includes arbitrary + # number of capture group (parentheses). Never rely on it's number. + # + # == Usage + # + # require 'uri' + # + # # extract first URI from html_string + # html_string.slice(URI.regexp) + # + # # remove ftp URIs + # html_string.sub(URI.regexp(['ftp']) + # + # # You should not rely on the number of parentheses + # html_string.scan(URI.regexp) do |*matches| + # p $& + # end + # + def self.regexp(schemes = nil) + warn "URI.regexp is obsolete", uplevel: 1 if $VERBOSE + DEFAULT_PARSER.make_regexp(schemes) + end + + TBLENCWWWCOMP_ = {} # :nodoc: + 256.times do |i| + TBLENCWWWCOMP_[i.chr] = '%%%02X' % i + end + TBLENCWWWCOMP_[' '] = '+' + TBLENCWWWCOMP_.freeze + TBLDECWWWCOMP_ = {} # :nodoc: + 256.times do |i| + h, l = i>>4, i&15 + TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr + end + TBLDECWWWCOMP_['+'] = ' ' + TBLDECWWWCOMP_.freeze + + HTML5ASCIIINCOMPAT = defined? Encoding::UTF_7 ? [Encoding::UTF_7, Encoding::UTF_16BE, Encoding::UTF_16LE, + Encoding::UTF_32BE, Encoding::UTF_32LE] : [] # :nodoc: + + # Encode given +str+ to URL-encoded form data. + # + # This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP + # (ASCII space) to + and converts others to %XX. + # + # If +enc+ is given, convert +str+ to the encoding before percent encoding. + # + # This is an implementation of + # http://www.w3.org/TR/2013/CR-html5-20130806/forms.html#url-encoded-form-data + # + # See URI.decode_www_form_component, URI.encode_www_form + def self.encode_www_form_component(str, enc=nil) + str = str.to_s.dup + if str.encoding != Encoding::ASCII_8BIT + if enc && enc != Encoding::ASCII_8BIT + str.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace) + str.encode!(enc, fallback: ->(x){"&#{x.ord};"}) + end + str.force_encoding(Encoding::ASCII_8BIT) + end + str.gsub!(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP_) + str.force_encoding(Encoding::US_ASCII) + end + + # Decode given +str+ of URL-encoded form data. + # + # This decodes + to SP. + # + # See URI.encode_www_form_component, URI.decode_www_form + def self.decode_www_form_component(str, enc=Encoding::UTF_8) + raise ArgumentError, "invalid %-encoding (#{str})" if /%(?!\h\h)/ =~ str + str.b.gsub(/\+|%\h\h/, TBLDECWWWCOMP_).force_encoding(enc) + end + + # Generate URL-encoded form data from given +enum+. + # + # This generates application/x-www-form-urlencoded data defined in HTML5 + # from given an Enumerable object. + # + # This internally uses URI.encode_www_form_component(str). + # + # This method doesn't convert the encoding of given items, so convert them + # before call this method if you want to send data as other than original + # encoding or mixed encoding data. (Strings which are encoded in an HTML5 + # ASCII incompatible encoding are converted to UTF-8.) + # + # This method doesn't handle files. When you send a file, use + # multipart/form-data. + # + # This refers http://url.spec.whatwg.org/#concept-urlencoded-serializer + # + # URI.encode_www_form([["q", "ruby"], ["lang", "en"]]) + # #=> "q=ruby&lang=en" + # URI.encode_www_form("q" => "ruby", "lang" => "en") + # #=> "q=ruby&lang=en" + # URI.encode_www_form("q" => ["ruby", "perl"], "lang" => "en") + # #=> "q=ruby&q=perl&lang=en" + # URI.encode_www_form([["q", "ruby"], ["q", "perl"], ["lang", "en"]]) + # #=> "q=ruby&q=perl&lang=en" + # + # See URI.encode_www_form_component, URI.decode_www_form + def self.encode_www_form(enum, enc=nil) + enum.map do |k,v| + if v.nil? + encode_www_form_component(k, enc) + elsif v.respond_to?(:to_ary) + v.to_ary.map do |w| + str = encode_www_form_component(k, enc) + unless w.nil? + str << '=' + str << encode_www_form_component(w, enc) + end + end.join('&') + else + str = encode_www_form_component(k, enc) + str << '=' + str << encode_www_form_component(v, enc) + end + end.join('&') + end + + # Decode URL-encoded form data from given +str+. + # + # This decodes application/x-www-form-urlencoded data + # and returns array of key-value array. + # + # This refers http://url.spec.whatwg.org/#concept-urlencoded-parser , + # so this supports only &-separator, don't support ;-separator. + # + # ary = URI.decode_www_form("a=1&a=2&b=3") + # p ary #=> [['a', '1'], ['a', '2'], ['b', '3']] + # p ary.assoc('a').last #=> '1' + # p ary.assoc('b').last #=> '3' + # p ary.rassoc('a').last #=> '2' + # p Hash[ary] # => {"a"=>"2", "b"=>"3"} + # + # See URI.decode_www_form_component, URI.encode_www_form + def self.decode_www_form(str, enc=Encoding::UTF_8, separator: '&', use__charset_: false, isindex: false) + raise ArgumentError, "the input of #{self.name}.#{__method__} must be ASCII only string" unless str.ascii_only? + ary = [] + return ary if str.empty? + enc = Encoding.find(enc) + str.b.each_line(separator) do |string| + string.chomp!(separator) + key, sep, val = string.partition('=') + if isindex + if sep.empty? + val = key + key = '' + end + isindex = false + end + + if use__charset_ and key == '_charset_' and e = get_encoding(val) + enc = e + use__charset_ = false + end + + key.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_) + if val + val.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_) + else + val = '' + end + + ary << [key, val] + end + ary.each do |k, v| + k.force_encoding(enc) + k.scrub! + v.force_encoding(enc) + v.scrub! + end + ary + end + + private +=begin command for WEB_ENCODINGS_ + curl https://encoding.spec.whatwg.org/encodings.json| + ruby -rjson -e 'H={} + h={ + "shift_jis"=>"Windows-31J", + "euc-jp"=>"cp51932", + "iso-2022-jp"=>"cp50221", + "x-mac-cyrillic"=>"macCyrillic", + } + JSON($<.read).map{|x|x["encodings"]}.flatten.each{|x| + Encoding.find(n=h.fetch(n=x["name"].downcase,n))rescue next + x["labels"].each{|y|H[y]=n} + } + puts "{" + H.each{|k,v|puts %[ #{k.dump}=>#{v.dump},]} + puts "}" +' +=end + WEB_ENCODINGS_ = { + "unicode-1-1-utf-8"=>"utf-8", + "utf-8"=>"utf-8", + "utf8"=>"utf-8", + "866"=>"ibm866", + "cp866"=>"ibm866", + "csibm866"=>"ibm866", + "ibm866"=>"ibm866", + "csisolatin2"=>"iso-8859-2", + "iso-8859-2"=>"iso-8859-2", + "iso-ir-101"=>"iso-8859-2", + "iso8859-2"=>"iso-8859-2", + "iso88592"=>"iso-8859-2", + "iso_8859-2"=>"iso-8859-2", + "iso_8859-2:1987"=>"iso-8859-2", + "l2"=>"iso-8859-2", + "latin2"=>"iso-8859-2", + "csisolatin3"=>"iso-8859-3", + "iso-8859-3"=>"iso-8859-3", + "iso-ir-109"=>"iso-8859-3", + "iso8859-3"=>"iso-8859-3", + "iso88593"=>"iso-8859-3", + "iso_8859-3"=>"iso-8859-3", + "iso_8859-3:1988"=>"iso-8859-3", + "l3"=>"iso-8859-3", + "latin3"=>"iso-8859-3", + "csisolatin4"=>"iso-8859-4", + "iso-8859-4"=>"iso-8859-4", + "iso-ir-110"=>"iso-8859-4", + "iso8859-4"=>"iso-8859-4", + "iso88594"=>"iso-8859-4", + "iso_8859-4"=>"iso-8859-4", + "iso_8859-4:1988"=>"iso-8859-4", + "l4"=>"iso-8859-4", + "latin4"=>"iso-8859-4", + "csisolatincyrillic"=>"iso-8859-5", + "cyrillic"=>"iso-8859-5", + "iso-8859-5"=>"iso-8859-5", + "iso-ir-144"=>"iso-8859-5", + "iso8859-5"=>"iso-8859-5", + "iso88595"=>"iso-8859-5", + "iso_8859-5"=>"iso-8859-5", + "iso_8859-5:1988"=>"iso-8859-5", + "arabic"=>"iso-8859-6", + "asmo-708"=>"iso-8859-6", + "csiso88596e"=>"iso-8859-6", + "csiso88596i"=>"iso-8859-6", + "csisolatinarabic"=>"iso-8859-6", + "ecma-114"=>"iso-8859-6", + "iso-8859-6"=>"iso-8859-6", + "iso-8859-6-e"=>"iso-8859-6", + "iso-8859-6-i"=>"iso-8859-6", + "iso-ir-127"=>"iso-8859-6", + "iso8859-6"=>"iso-8859-6", + "iso88596"=>"iso-8859-6", + "iso_8859-6"=>"iso-8859-6", + "iso_8859-6:1987"=>"iso-8859-6", + "csisolatingreek"=>"iso-8859-7", + "ecma-118"=>"iso-8859-7", + "elot_928"=>"iso-8859-7", + "greek"=>"iso-8859-7", + "greek8"=>"iso-8859-7", + "iso-8859-7"=>"iso-8859-7", + "iso-ir-126"=>"iso-8859-7", + "iso8859-7"=>"iso-8859-7", + "iso88597"=>"iso-8859-7", + "iso_8859-7"=>"iso-8859-7", + "iso_8859-7:1987"=>"iso-8859-7", + "sun_eu_greek"=>"iso-8859-7", + "csiso88598e"=>"iso-8859-8", + "csisolatinhebrew"=>"iso-8859-8", + "hebrew"=>"iso-8859-8", + "iso-8859-8"=>"iso-8859-8", + "iso-8859-8-e"=>"iso-8859-8", + "iso-ir-138"=>"iso-8859-8", + "iso8859-8"=>"iso-8859-8", + "iso88598"=>"iso-8859-8", + "iso_8859-8"=>"iso-8859-8", + "iso_8859-8:1988"=>"iso-8859-8", + "visual"=>"iso-8859-8", + "csisolatin6"=>"iso-8859-10", + "iso-8859-10"=>"iso-8859-10", + "iso-ir-157"=>"iso-8859-10", + "iso8859-10"=>"iso-8859-10", + "iso885910"=>"iso-8859-10", + "l6"=>"iso-8859-10", + "latin6"=>"iso-8859-10", + "iso-8859-13"=>"iso-8859-13", + "iso8859-13"=>"iso-8859-13", + "iso885913"=>"iso-8859-13", + "iso-8859-14"=>"iso-8859-14", + "iso8859-14"=>"iso-8859-14", + "iso885914"=>"iso-8859-14", + "csisolatin9"=>"iso-8859-15", + "iso-8859-15"=>"iso-8859-15", + "iso8859-15"=>"iso-8859-15", + "iso885915"=>"iso-8859-15", + "iso_8859-15"=>"iso-8859-15", + "l9"=>"iso-8859-15", + "iso-8859-16"=>"iso-8859-16", + "cskoi8r"=>"koi8-r", + "koi"=>"koi8-r", + "koi8"=>"koi8-r", + "koi8-r"=>"koi8-r", + "koi8_r"=>"koi8-r", + "koi8-ru"=>"koi8-u", + "koi8-u"=>"koi8-u", + "dos-874"=>"windows-874", + "iso-8859-11"=>"windows-874", + "iso8859-11"=>"windows-874", + "iso885911"=>"windows-874", + "tis-620"=>"windows-874", + "windows-874"=>"windows-874", + "cp1250"=>"windows-1250", + "windows-1250"=>"windows-1250", + "x-cp1250"=>"windows-1250", + "cp1251"=>"windows-1251", + "windows-1251"=>"windows-1251", + "x-cp1251"=>"windows-1251", + "ansi_x3.4-1968"=>"windows-1252", + "ascii"=>"windows-1252", + "cp1252"=>"windows-1252", + "cp819"=>"windows-1252", + "csisolatin1"=>"windows-1252", + "ibm819"=>"windows-1252", + "iso-8859-1"=>"windows-1252", + "iso-ir-100"=>"windows-1252", + "iso8859-1"=>"windows-1252", + "iso88591"=>"windows-1252", + "iso_8859-1"=>"windows-1252", + "iso_8859-1:1987"=>"windows-1252", + "l1"=>"windows-1252", + "latin1"=>"windows-1252", + "us-ascii"=>"windows-1252", + "windows-1252"=>"windows-1252", + "x-cp1252"=>"windows-1252", + "cp1253"=>"windows-1253", + "windows-1253"=>"windows-1253", + "x-cp1253"=>"windows-1253", + "cp1254"=>"windows-1254", + "csisolatin5"=>"windows-1254", + "iso-8859-9"=>"windows-1254", + "iso-ir-148"=>"windows-1254", + "iso8859-9"=>"windows-1254", + "iso88599"=>"windows-1254", + "iso_8859-9"=>"windows-1254", + "iso_8859-9:1989"=>"windows-1254", + "l5"=>"windows-1254", + "latin5"=>"windows-1254", + "windows-1254"=>"windows-1254", + "x-cp1254"=>"windows-1254", + "cp1255"=>"windows-1255", + "windows-1255"=>"windows-1255", + "x-cp1255"=>"windows-1255", + "cp1256"=>"windows-1256", + "windows-1256"=>"windows-1256", + "x-cp1256"=>"windows-1256", + "cp1257"=>"windows-1257", + "windows-1257"=>"windows-1257", + "x-cp1257"=>"windows-1257", + "cp1258"=>"windows-1258", + "windows-1258"=>"windows-1258", + "x-cp1258"=>"windows-1258", + "x-mac-cyrillic"=>"macCyrillic", + "x-mac-ukrainian"=>"macCyrillic", + "chinese"=>"gbk", + "csgb2312"=>"gbk", + "csiso58gb231280"=>"gbk", + "gb2312"=>"gbk", + "gb_2312"=>"gbk", + "gb_2312-80"=>"gbk", + "gbk"=>"gbk", + "iso-ir-58"=>"gbk", + "x-gbk"=>"gbk", + "gb18030"=>"gb18030", + "big5"=>"big5", + "big5-hkscs"=>"big5", + "cn-big5"=>"big5", + "csbig5"=>"big5", + "x-x-big5"=>"big5", + "cseucpkdfmtjapanese"=>"cp51932", + "euc-jp"=>"cp51932", + "x-euc-jp"=>"cp51932", + "csiso2022jp"=>"cp50221", + "iso-2022-jp"=>"cp50221", + "csshiftjis"=>"Windows-31J", + "ms932"=>"Windows-31J", + "ms_kanji"=>"Windows-31J", + "shift-jis"=>"Windows-31J", + "shift_jis"=>"Windows-31J", + "sjis"=>"Windows-31J", + "windows-31j"=>"Windows-31J", + "x-sjis"=>"Windows-31J", + "cseuckr"=>"euc-kr", + "csksc56011987"=>"euc-kr", + "euc-kr"=>"euc-kr", + "iso-ir-149"=>"euc-kr", + "korean"=>"euc-kr", + "ks_c_5601-1987"=>"euc-kr", + "ks_c_5601-1989"=>"euc-kr", + "ksc5601"=>"euc-kr", + "ksc_5601"=>"euc-kr", + "windows-949"=>"euc-kr", + "utf-16be"=>"utf-16be", + "utf-16"=>"utf-16le", + "utf-16le"=>"utf-16le", + } # :nodoc: + + # :nodoc: + # return encoding or nil + # http://encoding.spec.whatwg.org/#concept-encoding-get + def self.get_encoding(label) + Encoding.find(WEB_ENCODINGS_[label.to_str.strip.downcase]) rescue nil + end +end # module URI + +module Kernel + + # + # Returns +uri+ converted to a URI object. + # + def URI(uri) + if uri.is_a?(URI::Generic) + uri + elsif uri = String.try_convert(uri) + URI.parse(uri) + else + raise ArgumentError, + "bad argument (expected URI object or URI string)" + end + end + module_function :URI +end diff --git a/gxg/standard/uri/ftp.rb b/gxg/standard/uri/ftp.rb new file mode 100644 index 0000000..9c7ddae --- /dev/null +++ b/gxg/standard/uri/ftp.rb @@ -0,0 +1,267 @@ +# frozen_string_literal: false +# = uri/ftp.rb +# +# Author:: Akira Yamada +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# Revision:: $Id: ftp.rb 59598 2017-08-15 01:05:50Z nobu $ +# +# See URI for general documentation +# + +require 'uri/generic' + +module URI + + # + # FTP URI syntax is defined by RFC1738 section 3.2. + # + # This class will be redesigned because of difference of implementations; + # the structure of its path. draft-hoffman-ftp-uri-04 is a draft but it + # is a good summary about the de facto spec. + # http://tools.ietf.org/html/draft-hoffman-ftp-uri-04 + # + class FTP < Generic + # A Default port of 21 for URI::FTP + DEFAULT_PORT = 21 + + # + # An Array of the available components for URI::FTP + # + COMPONENT = [ + :scheme, + :userinfo, :host, :port, + :path, :typecode + ].freeze + + # + # Typecode is "a", "i" or "d". + # + # * "a" indicates a text file (the FTP command was ASCII) + # * "i" indicates a binary file (FTP command IMAGE) + # * "d" indicates the contents of a directory should be displayed + # + TYPECODE = ['a', 'i', 'd'].freeze + + # Typecode prefix + # ';type=' + TYPECODE_PREFIX = ';type='.freeze + + def self.new2(user, password, host, port, path, + typecode = nil, arg_check = true) # :nodoc: + # Do not use this method! Not tested. [Bug #7301] + # This methods remains just for compatibility, + # Keep it undocumented until the active maintainer is assigned. + typecode = nil if typecode.size == 0 + if typecode && !TYPECODE.include?(typecode) + raise ArgumentError, + "bad typecode is specified: #{typecode}" + end + + # do escape + + self.new('ftp', + [user, password], + host, port, nil, + typecode ? path + TYPECODE_PREFIX + typecode : path, + nil, nil, nil, arg_check) + end + + # + # == Description + # + # Creates a new URI::FTP object from components, with syntax checking. + # + # The components accepted are +userinfo+, +host+, +port+, +path+ and + # +typecode+. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the order + # [userinfo, host, port, path, typecode] + # + # If the path supplied is absolute, it will be escaped in order to + # make it absolute in the URI. Examples: + # + # require 'uri' + # + # uri = URI::FTP.build(['user:password', 'ftp.example.com', nil, + # '/path/file.zip', 'i']) + # puts uri.to_s -> ftp://user:password@ftp.example.com/%2Fpath/file.zip;type=i + # + # uri2 = URI::FTP.build({:host => 'ftp.example.com', + # :path => 'ruby/src'}) + # puts uri2.to_s -> ftp://ftp.example.com/ruby/src + # + def self.build(args) + + # Fix the incoming path to be generic URL syntax + # FTP path -> URL path + # foo/bar /foo/bar + # /foo/bar /%2Ffoo/bar + # + if args.kind_of?(Array) + args[3] = '/' + args[3].sub(/^\//, '%2F') + else + args[:path] = '/' + args[:path].sub(/^\//, '%2F') + end + + tmp = Util::make_components_hash(self, args) + + if tmp[:typecode] + if tmp[:typecode].size == 1 + tmp[:typecode] = TYPECODE_PREFIX + tmp[:typecode] + end + tmp[:path] << tmp[:typecode] + end + + return super(tmp) + end + + # + # == Description + # + # Creates a new URI::FTP object from generic URL components with no + # syntax checking. + # + # Unlike build(), this method does not escape the path component as + # required by RFC1738; instead it is treated as per RFC2396. + # + # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+, + # +opaque+, +query+ and +fragment+, in that order. + # + def initialize(scheme, + userinfo, host, port, registry, + path, opaque, + query, + fragment, + parser = nil, + arg_check = false) + raise InvalidURIError unless path + path = path.sub(/^\//,'') + path.sub!(/^%2F/,'/') + super(scheme, userinfo, host, port, registry, path, opaque, + query, fragment, parser, arg_check) + @typecode = nil + if tmp = @path.index(TYPECODE_PREFIX) + typecode = @path[tmp + TYPECODE_PREFIX.size..-1] + @path = @path[0..tmp - 1] + + if arg_check + self.typecode = typecode + else + self.set_typecode(typecode) + end + end + end + + # typecode accessor + # + # see URI::FTP::COMPONENT + attr_reader :typecode + + # validates typecode +v+, + # returns a +true+ or +false+ boolean + # + def check_typecode(v) + if TYPECODE.include?(v) + return true + else + raise InvalidComponentError, + "bad typecode(expected #{TYPECODE.join(', ')}): #{v}" + end + end + private :check_typecode + + # Private setter for the typecode +v+ + # + # see also URI::FTP.typecode= + # + def set_typecode(v) + @typecode = v + end + protected :set_typecode + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the typecode +v+. + # (with validation) + # + # see also URI::FTP.check_typecode + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("ftp://john@ftp.example.com/my_file.img") + # #=> # + # uri.typecode = "i" + # # => "i" + # uri + # #=> # + # + def typecode=(typecode) + check_typecode(typecode) + set_typecode(typecode) + typecode + end + + def merge(oth) # :nodoc: + tmp = super(oth) + if self != tmp + tmp.set_typecode(oth.typecode) + end + + return tmp + end + + # Returns the path from an FTP URI. + # + # RFC 1738 specifically states that the path for an FTP URI does not + # include the / which separates the URI path from the URI host. Example: + # + # +ftp://ftp.example.com/pub/ruby+ + # + # The above URI indicates that the client should connect to + # ftp.example.com then cd pub/ruby from the initial login directory. + # + # If you want to cd to an absolute directory, you must include an + # escaped / (%2F) in the path. Example: + # + # +ftp://ftp.example.com/%2Fpub/ruby+ + # + # This method will then return "/pub/ruby" + # + def path + return @path.sub(/^\//,'').sub(/^%2F/,'/') + end + + # Private setter for the path of the URI::FTP + def set_path(v) + super("/" + v.sub(/^\//, "%2F")) + end + protected :set_path + + # Returns a String representation of the URI::FTP + def to_s + save_path = nil + if @typecode + save_path = @path + @path = @path + TYPECODE_PREFIX + @typecode + end + str = super + if @typecode + @path = save_path + end + + return str + end + end + @@schemes['FTP'] = FTP +end diff --git a/gxg/standard/uri/generic.rb b/gxg/standard/uri/generic.rb new file mode 100644 index 0000000..a104ab4 --- /dev/null +++ b/gxg/standard/uri/generic.rb @@ -0,0 +1,1563 @@ +# frozen_string_literal: true + +# = uri/generic.rb +# +# Author:: Akira Yamada +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# Revision:: $Id: generic.rb 61225 2017-12-14 06:30:22Z naruse $ +# +# See URI for general documentation +# + +require 'uri/common' +autoload :IPSocket, 'socket' +autoload :IPAddr, 'ipaddr' + +module URI + + # + # Base class for all URI classes. + # Implements generic URI syntax as per RFC 2396. + # + class Generic + include URI + + # + # A Default port of nil for URI::Generic + # + DEFAULT_PORT = nil + + # + # Returns default port + # + def self.default_port + self::DEFAULT_PORT + end + + # + # Returns default port + # + def default_port + self.class.default_port + end + + # + # An Array of the available components for URI::Generic + # + COMPONENT = [ + :scheme, + :userinfo, :host, :port, :registry, + :path, :opaque, + :query, + :fragment + ].freeze + + # + # Components of the URI in the order. + # + def self.component + self::COMPONENT + end + + USE_REGISTRY = false # :nodoc: + + def self.use_registry # :nodoc: + self::USE_REGISTRY + end + + # + # == Synopsis + # + # See #new + # + # == Description + # + # At first, tries to create a new URI::Generic instance using + # URI::Generic::build. But, if exception URI::InvalidComponentError is raised, + # then it URI::Escape.escape all URI components and tries again. + # + # + def self.build2(args) + begin + return self.build(args) + rescue InvalidComponentError + if args.kind_of?(Array) + return self.build(args.collect{|x| + if x.is_a?(String) + DEFAULT_PARSER.escape(x) + else + x + end + }) + elsif args.kind_of?(Hash) + tmp = {} + args.each do |key, value| + tmp[key] = if value + DEFAULT_PARSER.escape(value) + else + value + end + end + return self.build(tmp) + end + end + end + + # + # == Synopsis + # + # See #new + # + # == Description + # + # Creates a new URI::Generic instance from components of URI::Generic + # with check. Components are: scheme, userinfo, host, port, registry, path, + # opaque, query and fragment. You can provide arguments either by an Array or a Hash. + # See #new for hash keys to use or for order of array items. + # + def self.build(args) + if args.kind_of?(Array) && + args.size == ::URI::Generic::COMPONENT.size + tmp = args.dup + elsif args.kind_of?(Hash) + tmp = ::URI::Generic::COMPONENT.collect do |c| + if args.include?(c) + args[c] + else + nil + end + end + else + component = self.class.component rescue ::URI::Generic::COMPONENT + raise ArgumentError, + "expected Array of or Hash of components of #{self.class} (#{component.join(', ')})" + end + + tmp << nil + tmp << true + return self.new(*tmp) + end + # + # == Args + # + # +scheme+:: + # Protocol scheme, i.e. 'http','ftp','mailto' and so on. + # +userinfo+:: + # User name and password, i.e. 'sdmitry:bla' + # +host+:: + # Server host name + # +port+:: + # Server port + # +registry+:: + # Registry of naming authorities. + # +path+:: + # Path on server + # +opaque+:: + # Opaque part + # +query+:: + # Query data + # +fragment+:: + # A part of URI after '#' sign + # +parser+:: + # Parser for internal use [URI::DEFAULT_PARSER by default] + # +arg_check+:: + # Check arguments [false by default] + # + # == Description + # + # Creates a new URI::Generic instance from ``generic'' components without check. + # + def initialize(scheme, + userinfo, host, port, registry, + path, opaque, + query, + fragment, + parser = DEFAULT_PARSER, + arg_check = false) + @scheme = nil + @user = nil + @password = nil + @host = nil + @port = nil + @path = nil + @query = nil + @opaque = nil + @fragment = nil + @parser = parser == DEFAULT_PARSER ? nil : parser + + if arg_check + self.scheme = scheme + self.userinfo = userinfo + self.hostname = host + self.port = port + self.path = path + self.query = query + self.opaque = opaque + self.fragment = fragment + else + self.set_scheme(scheme) + self.set_userinfo(userinfo) + self.set_host(host) + self.set_port(port) + self.set_path(path) + self.query = query + self.set_opaque(opaque) + self.fragment=(fragment) + end + if registry + raise InvalidURIError, + "the scheme #{@scheme} does not accept registry part: #{registry} (or bad hostname?)" + end + + @scheme&.freeze + self.set_path('') if !@path && !@opaque # (see RFC2396 Section 5.2) + self.set_port(self.default_port) if self.default_port && !@port + end + + # + # returns the scheme component of the URI. + # + # URI("http://foo/bar/baz").scheme #=> "http" + # + attr_reader :scheme + + # returns the host component of the URI. + # + # URI("http://foo/bar/baz").host #=> "foo" + # + # It returns nil if no host component. + # + # URI("mailto:foo@example.org").host #=> nil + # + # The component doesn't contains the port number. + # + # URI("http://foo:8080/bar/baz").host #=> "foo" + # + # Since IPv6 addresses are wrapped by brackets in URIs, + # this method returns IPv6 addresses wrapped by brackets. + # This form is not appropriate to pass socket methods such as TCPSocket.open. + # If unwrapped host names are required, use "hostname" method. + # + # URI("http://[::1]/bar/baz").host #=> "[::1]" + # URI("http://[::1]/bar/baz").hostname #=> "::1" + # + attr_reader :host + + # returns the port component of the URI. + # + # URI("http://foo/bar/baz").port #=> "80" + # + # URI("http://foo:8080/bar/baz").port #=> "8080" + # + attr_reader :port + + def registry # :nodoc: + nil + end + + # returns the path component of the URI. + # + # URI("http://foo/bar/baz").path #=> "/bar/baz" + # + attr_reader :path + + # returns the query component of the URI. + # + # URI("http://foo/bar/baz?search=FooBar").query #=> "search=FooBar" + # + attr_reader :query + + # returns the opaque part of the URI. + # + # URI("mailto:foo@example.org").opaque #=> "foo@example.org" + # + # Portion of the path that does make use of the slash '/'. + # The path typically refers to the absolute path and the opaque part. + # (See RFC2396 Section 3 and 5.2.) + # + attr_reader :opaque + + # returns the fragment component of the URI. + # + # URI("http://foo/bar/baz?search=FooBar#ponies").fragment #=> "ponies" + # + attr_reader :fragment + + # returns the parser to be used. + # + # Unless a URI::Parser is defined, then DEFAULT_PARSER is used. + # + def parser + if !defined?(@parser) || !@parser + DEFAULT_PARSER + else + @parser || DEFAULT_PARSER + end + end + + # replace self by other URI object + def replace!(oth) + if self.class != oth.class + raise ArgumentError, "expected #{self.class} object" + end + + component.each do |c| + self.__send__("#{c}=", oth.__send__(c)) + end + end + private :replace! + + # + # Components of the URI in the order. + # + def component + self.class.component + end + + # + # check the scheme +v+ component against the URI::Parser Regexp for :SCHEME + # + def check_scheme(v) + if v && parser.regexp[:SCHEME] !~ v + raise InvalidComponentError, + "bad component(expected scheme component): #{v}" + end + + return true + end + private :check_scheme + + # protected setter for the scheme component +v+ + # + # see also URI::Generic.scheme= + # + def set_scheme(v) + @scheme = v&.downcase + end + protected :set_scheme + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the scheme component +v+. + # (with validation) + # + # see also URI::Generic.check_scheme + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com") + # uri.scheme = "https" + # # => "https" + # uri + # #=> # + # + def scheme=(v) + check_scheme(v) + set_scheme(v) + v + end + + # + # check the +user+ and +password+. + # + # If +password+ is not provided, then +user+ is + # split, using URI::Generic.split_userinfo, to + # pull +user+ and +password. + # + # see also URI::Generic.check_user, URI::Generic.check_password + # + def check_userinfo(user, password = nil) + if !password + user, password = split_userinfo(user) + end + check_user(user) + check_password(password, user) + + return true + end + private :check_userinfo + + # + # check the user +v+ component for RFC2396 compliance + # and against the URI::Parser Regexp for :USERINFO + # + # Can not have a registry or opaque component defined, + # with a user component defined. + # + def check_user(v) + if @opaque + raise InvalidURIError, + "can not set user with opaque" + end + + return v unless v + + if parser.regexp[:USERINFO] !~ v + raise InvalidComponentError, + "bad component(expected userinfo component or user component): #{v}" + end + + return true + end + private :check_user + + # + # check the password +v+ component for RFC2396 compliance + # and against the URI::Parser Regexp for :USERINFO + # + # Can not have a registry or opaque component defined, + # with a user component defined. + # + def check_password(v, user = @user) + if @opaque + raise InvalidURIError, + "can not set password with opaque" + end + return v unless v + + if !user + raise InvalidURIError, + "password component depends user component" + end + + if parser.regexp[:USERINFO] !~ v + raise InvalidComponentError, + "bad password component" + end + + return true + end + private :check_password + + # + # Sets userinfo, argument is string like 'name:pass' + # + def userinfo=(userinfo) + if userinfo.nil? + return nil + end + check_userinfo(*userinfo) + set_userinfo(*userinfo) + # returns userinfo + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the +user+ component. + # (with validation) + # + # see also URI::Generic.check_user + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://john:S3nsit1ve@my.example.com") + # uri.user = "sam" + # # => "sam" + # uri + # #=> # + # + def user=(user) + check_user(user) + set_user(user) + # returns user + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the +password+ component. + # (with validation) + # + # see also URI::Generic.check_password + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://john:S3nsit1ve@my.example.com") + # uri.password = "V3ry_S3nsit1ve" + # # => "V3ry_S3nsit1ve" + # uri + # #=> # + # + def password=(password) + check_password(password) + set_password(password) + # returns password + end + + # protect setter for the +user+ component, and +password+ if available. + # (with validation) + # + # see also URI::Generic.userinfo= + # + def set_userinfo(user, password = nil) + unless password + user, password = split_userinfo(user) + end + @user = user + @password = password if password + + [@user, @password] + end + protected :set_userinfo + + # protected setter for the user component +v+ + # + # see also URI::Generic.user= + # + def set_user(v) + set_userinfo(v, @password) + v + end + protected :set_user + + # protected setter for the password component +v+ + # + # see also URI::Generic.password= + # + def set_password(v) + @password = v + # returns v + end + protected :set_password + + # returns the userinfo +ui+ as user, password + # if properly formatted as 'user:password' + def split_userinfo(ui) + return nil, nil unless ui + user, password = ui.split(':', 2) + + return user, password + end + private :split_userinfo + + # escapes 'user:password' +v+ based on RFC 1738 section 3.1 + def escape_userpass(v) + parser.escape(v, /[@:\/]/o) # RFC 1738 section 3.1 #/ + end + private :escape_userpass + + # returns the userinfo, either as 'user' or 'user:password' + def userinfo + if @user.nil? + nil + elsif @password.nil? + @user + else + @user + ':' + @password + end + end + + # returns the user component + def user + @user + end + + # returns the password component + def password + @password + end + + # + # check the host +v+ component for RFC2396 compliance + # and against the URI::Parser Regexp for :HOST + # + # Can not have a registry or opaque component defined, + # with a host component defined. + # + def check_host(v) + return v unless v + + if @opaque + raise InvalidURIError, + "can not set host with registry or opaque" + elsif parser.regexp[:HOST] !~ v + raise InvalidComponentError, + "bad component(expected host component): #{v}" + end + + return true + end + private :check_host + + # protected setter for the host component +v+ + # + # see also URI::Generic.host= + # + def set_host(v) + @host = v + end + protected :set_host + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the host component +v+. + # (with validation) + # + # see also URI::Generic.check_host + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com") + # uri.host = "foo.com" + # # => "foo.com" + # uri + # #=> # + # + def host=(v) + check_host(v) + set_host(v) + v + end + + # extract the host part of the URI and unwrap brackets for IPv6 addresses. + # + # This method is same as URI::Generic#host except + # brackets for IPv6 (and future IP) addresses are removed. + # + # u = URI("http://[::1]/bar") + # p u.hostname #=> "::1" + # p u.host #=> "[::1]" + # + def hostname + v = self.host + /\A\[(.*)\]\z/ =~ v ? $1 : v + end + + # set the host part of the URI as the argument with brackets for IPv6 addresses. + # + # This method is same as URI::Generic#host= except + # the argument can be bare IPv6 address. + # + # u = URI("http://foo/bar") + # p u.to_s #=> "http://foo/bar" + # u.hostname = "::1" + # p u.to_s #=> "http://[::1]/bar" + # + # If the argument seems IPv6 address, + # it is wrapped by brackets. + # + def hostname=(v) + v = "[#{v}]" if /\A\[.*\]\z/ !~ v && /:/ =~ v + self.host = v + end + + # + # check the port +v+ component for RFC2396 compliance + # and against the URI::Parser Regexp for :PORT + # + # Can not have a registry or opaque component defined, + # with a port component defined. + # + def check_port(v) + return v unless v + + if @opaque + raise InvalidURIError, + "can not set port with registry or opaque" + elsif !v.kind_of?(Integer) && parser.regexp[:PORT] !~ v + raise InvalidComponentError, + "bad component(expected port component): #{v.inspect}" + end + + return true + end + private :check_port + + # protected setter for the port component +v+ + # + # see also URI::Generic.port= + # + def set_port(v) + v = v.empty? ? nil : v.to_i unless !v || v.kind_of?(Integer) + @port = v + end + protected :set_port + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the port component +v+. + # (with validation) + # + # see also URI::Generic.check_port + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com") + # uri.port = 8080 + # # => 8080 + # uri + # #=> # + # + def port=(v) + check_port(v) + set_port(v) + port + end + + def check_registry(v) # :nodoc: + raise InvalidURIError, "can not set registry" + end + private :check_registry + + def set_registry(v) #:nodoc: + raise InvalidURIError, "can not set registry" + end + protected :set_registry + + def registry=(v) + raise InvalidURIError, "can not set registry" + end + + # + # check the path +v+ component for RFC2396 compliance + # and against the URI::Parser Regexp + # for :ABS_PATH and :REL_PATH + # + # Can not have a opaque component defined, + # with a path component defined. + # + def check_path(v) + # raise if both hier and opaque are not nil, because: + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + # hier_part = ( net_path | abs_path ) [ "?" query ] + if v && @opaque + raise InvalidURIError, + "path conflicts with opaque" + end + + # If scheme is ftp, path may be relative. + # See RFC 1738 section 3.2.2, and RFC 2396. + if @scheme && @scheme != "ftp" + if v && v != '' && parser.regexp[:ABS_PATH] !~ v + raise InvalidComponentError, + "bad component(expected absolute path component): #{v}" + end + else + if v && v != '' && parser.regexp[:ABS_PATH] !~ v && + parser.regexp[:REL_PATH] !~ v + raise InvalidComponentError, + "bad component(expected relative path component): #{v}" + end + end + + return true + end + private :check_path + + # protected setter for the path component +v+ + # + # see also URI::Generic.path= + # + def set_path(v) + @path = v + end + protected :set_path + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the path component +v+. + # (with validation) + # + # see also URI::Generic.check_path + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com/pub/files") + # uri.path = "/faq/" + # # => "/faq/" + # uri + # #=> # + # + def path=(v) + check_path(v) + set_path(v) + v + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the query component +v+. + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com/?id=25") + # uri.query = "id=1" + # # => "id=1" + # uri + # #=> # + # + def query=(v) + return @query = nil unless v + raise InvalidURIError, "query conflicts with opaque" if @opaque + + x = v.to_str + v = x.dup if x.equal? v + v.encode!(Encoding::UTF_8) rescue nil + v.delete!("\t\r\n") + v.force_encoding(Encoding::ASCII_8BIT) + v.gsub!(/(?!%\h\h|[!$-&(-;=?-_a-~])./n.freeze){'%%%02X' % $&.ord} + v.force_encoding(Encoding::US_ASCII) + @query = v + end + + # + # check the opaque +v+ component for RFC2396 compliance and + # against the URI::Parser Regexp for :OPAQUE + # + # Can not have a host, port, user or path component defined, + # with an opaque component defined. + # + def check_opaque(v) + return v unless v + + # raise if both hier and opaque are not nil, because: + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + # hier_part = ( net_path | abs_path ) [ "?" query ] + if @host || @port || @user || @path # userinfo = @user + ':' + @password + raise InvalidURIError, + "can not set opaque with host, port, userinfo or path" + elsif v && parser.regexp[:OPAQUE] !~ v + raise InvalidComponentError, + "bad component(expected opaque component): #{v}" + end + + return true + end + private :check_opaque + + # protected setter for the opaque component +v+ + # + # see also URI::Generic.opaque= + # + def set_opaque(v) + @opaque = v + end + protected :set_opaque + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the opaque component +v+. + # (with validation) + # + # see also URI::Generic.check_opaque + # + def opaque=(v) + check_opaque(v) + set_opaque(v) + v + end + + # + # check the fragment +v+ component against the URI::Parser Regexp for :FRAGMENT + # + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the fragment component +v+. + # (with validation) + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com/?id=25#time=1305212049") + # uri.fragment = "time=1305212086" + # # => "time=1305212086" + # uri + # #=> # + # + def fragment=(v) + return @fragment = nil unless v + + x = v.to_str + v = x.dup if x.equal? v + v.encode!(Encoding::UTF_8) rescue nil + v.delete!("\t\r\n") + v.force_encoding(Encoding::ASCII_8BIT) + v.gsub!(/(?!%\h\h|[!-~])./n){'%%%02X' % $&.ord} + v.force_encoding(Encoding::US_ASCII) + @fragment = v + end + + # + # Checks if URI has a path + # + def hierarchical? + if @path + true + else + false + end + end + + # + # Checks if URI is an absolute one + # + def absolute? + if @scheme + true + else + false + end + end + alias absolute absolute? + + # + # Checks if URI is relative + # + def relative? + !absolute? + end + + # + # returns an Array of the path split on '/' + # + def split_path(path) + path.split("/", -1) + end + private :split_path + + # + # Merges a base path +base+, with relative path +rel+, + # returns a modified base path. + # + def merge_path(base, rel) + + # RFC2396, Section 5.2, 5) + # RFC2396, Section 5.2, 6) + base_path = split_path(base) + rel_path = split_path(rel) + + # RFC2396, Section 5.2, 6), a) + base_path << '' if base_path.last == '..' + while i = base_path.index('..') + base_path.slice!(i - 1, 2) + end + + if (first = rel_path.first) and first.empty? + base_path.clear + rel_path.shift + end + + # RFC2396, Section 5.2, 6), c) + # RFC2396, Section 5.2, 6), d) + rel_path.push('') if rel_path.last == '.' || rel_path.last == '..' + rel_path.delete('.') + + # RFC2396, Section 5.2, 6), e) + tmp = [] + rel_path.each do |x| + if x == '..' && + !(tmp.empty? || tmp.last == '..') + tmp.pop + else + tmp << x + end + end + + add_trailer_slash = !tmp.empty? + if base_path.empty? + base_path = [''] # keep '/' for root directory + elsif add_trailer_slash + base_path.pop + end + while x = tmp.shift + if x == '..' + # RFC2396, Section 4 + # a .. or . in an absolute path has no special meaning + base_path.pop if base_path.size > 1 + else + # if x == '..' + # valid absolute (but abnormal) path "/../..." + # else + # valid absolute path + # end + base_path << x + tmp.each {|t| base_path << t} + add_trailer_slash = false + break + end + end + base_path.push('') if add_trailer_slash + + return base_path.join('/') + end + private :merge_path + + # + # == Args + # + # +oth+:: + # URI or String + # + # == Description + # + # Destructive form of #merge + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com") + # uri.merge!("/main.rbx?page=1") + # p uri + # # => # + # + def merge!(oth) + t = merge(oth) + if self == t + nil + else + replace!(t) + self + end + end + + # + # == Args + # + # +oth+:: + # URI or String + # + # == Description + # + # Merges two URI's. + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com") + # p uri.merge("/main.rbx?page=1") + # # => # + # + def merge(oth) + rel = parser.send(:convert_to_uri, oth) + + if rel.absolute? + #raise BadURIError, "both URI are absolute" if absolute? + # hmm... should return oth for usability? + return rel + end + + unless self.absolute? + raise BadURIError, "both URI are relative" + end + + base = self.dup + + authority = rel.userinfo || rel.host || rel.port + + # RFC2396, Section 5.2, 2) + if (rel.path.nil? || rel.path.empty?) && !authority && !rel.query + base.fragment=(rel.fragment) if rel.fragment + return base + end + + base.query = nil + base.fragment=(nil) + + # RFC2396, Section 5.2, 4) + if !authority + base.set_path(merge_path(base.path, rel.path)) if base.path && rel.path + else + # RFC2396, Section 5.2, 4) + base.set_path(rel.path) if rel.path + end + + # RFC2396, Section 5.2, 7) + base.set_userinfo(rel.userinfo) if rel.userinfo + base.set_host(rel.host) if rel.host + base.set_port(rel.port) if rel.port + base.query = rel.query if rel.query + base.fragment=(rel.fragment) if rel.fragment + + return base + end # merge + alias + merge + + # :stopdoc: + def route_from_path(src, dst) + case dst + when src + # RFC2396, Section 4.2 + return '' + when %r{(?:\A|/)\.\.?(?:/|\z)} + # dst has abnormal absolute path, + # like "/./", "/../", "/x/../", ... + return dst.dup + end + + src_path = src.scan(%r{[^/]*/}) + dst_path = dst.scan(%r{[^/]*/?}) + + # discard same parts + while !dst_path.empty? && dst_path.first == src_path.first + src_path.shift + dst_path.shift + end + + tmp = dst_path.join + + # calculate + if src_path.empty? + if tmp.empty? + return './' + elsif dst_path.first.include?(':') # (see RFC2396 Section 5) + return './' + tmp + else + return tmp + end + end + + return '../' * src_path.size + tmp + end + private :route_from_path + # :startdoc: + + # :stopdoc: + def route_from0(oth) + oth = parser.send(:convert_to_uri, oth) + if self.relative? + raise BadURIError, + "relative URI: #{self}" + end + if oth.relative? + raise BadURIError, + "relative URI: #{oth}" + end + + if self.scheme != oth.scheme + return self, self.dup + end + rel = URI::Generic.new(nil, # it is relative URI + self.userinfo, self.host, self.port, + nil, self.path, self.opaque, + self.query, self.fragment, parser) + + if rel.userinfo != oth.userinfo || + rel.host.to_s.downcase != oth.host.to_s.downcase || + rel.port != oth.port + + if self.userinfo.nil? && self.host.nil? + return self, self.dup + end + + rel.set_port(nil) if rel.port == oth.default_port + return rel, rel + end + rel.set_userinfo(nil) + rel.set_host(nil) + rel.set_port(nil) + + if rel.path && rel.path == oth.path + rel.set_path('') + rel.query = nil if rel.query == oth.query + return rel, rel + elsif rel.opaque && rel.opaque == oth.opaque + rel.set_opaque('') + rel.query = nil if rel.query == oth.query + return rel, rel + end + + # you can modify `rel', but can not `oth'. + return oth, rel + end + private :route_from0 + # :startdoc: + + # + # == Args + # + # +oth+:: + # URI or String + # + # == Description + # + # Calculates relative path from oth to self + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse('http://my.example.com/main.rbx?page=1') + # p uri.route_from('http://my.example.com') + # #=> # + # + def route_from(oth) + # you can modify `rel', but can not `oth'. + begin + oth, rel = route_from0(oth) + rescue + raise $!.class, $!.message + end + if oth == rel + return rel + end + + rel.set_path(route_from_path(oth.path, self.path)) + if rel.path == './' && self.query + # "./?foo" -> "?foo" + rel.set_path('') + end + + return rel + end + + alias - route_from + + # + # == Args + # + # +oth+:: + # URI or String + # + # == Description + # + # Calculates relative path to oth from self + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse('http://my.example.com') + # p uri.route_to('http://my.example.com/main.rbx?page=1') + # #=> # + # + def route_to(oth) + parser.send(:convert_to_uri, oth).route_from(self) + end + + # + # Returns normalized URI. + # + # require 'uri' + # + # URI("HTTP://my.EXAMPLE.com").normalize + # #=> # + # + # Normalization here means: + # + # * scheme and host are converted to lowercase, + # * an empty path component is set to "/". + # + def normalize + uri = dup + uri.normalize! + uri + end + + # + # Destructive version of #normalize + # + def normalize! + if path&.empty? + set_path('/') + end + if scheme && scheme != scheme.downcase + set_scheme(self.scheme.downcase) + end + if host && host != host.downcase + set_host(self.host.downcase) + end + end + + # + # Constructs String from URI + # + def to_s + str = ''.dup + if @scheme + str << @scheme + str << ':' + end + + if @opaque + str << @opaque + else + if @host || %w[file postgres].include?(@scheme) + str << '//' + end + if self.userinfo + str << self.userinfo + str << '@' + end + if @host + str << @host + end + if @port && @port != self.default_port + str << ':' + str << @port.to_s + end + str << @path + if @query + str << '?' + str << @query + end + end + if @fragment + str << '#' + str << @fragment + end + str + end + + # + # Compares two URIs + # + def ==(oth) + if self.class == oth.class + self.normalize.component_ary == oth.normalize.component_ary + else + false + end + end + + def hash + self.component_ary.hash + end + + def eql?(oth) + self.class == oth.class && + parser == oth.parser && + self.component_ary.eql?(oth.component_ary) + end + +=begin + +--- URI::Generic#===(oth) + +=end +# def ===(oth) +# raise NotImplementedError +# end + +=begin +=end + + + # returns an Array of the components defined from the COMPONENT Array + def component_ary + component.collect do |x| + self.send(x) + end + end + protected :component_ary + + # == Args + # + # +components+:: + # Multiple Symbol arguments defined in URI::HTTP + # + # == Description + # + # Selects specified components from URI + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse('http://myuser:mypass@my.example.com/test.rbx') + # p uri.select(:userinfo, :host, :path) + # # => ["myuser:mypass", "my.example.com", "/test.rbx"] + # + def select(*components) + components.collect do |c| + if component.include?(c) + self.send(c) + else + raise ArgumentError, + "expected of components of #{self.class} (#{self.class.component.join(', ')})" + end + end + end + + def inspect + "#<#{self.class} #{self}>" + end + + # + # == Args + # + # +v+:: + # URI or String + # + # == Description + # + # attempts to parse other URI +oth+, + # returns [parsed_oth, self] + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com") + # uri.coerce("http://foo.com") + # #=> [#, #] + # + def coerce(oth) + case oth + when String + oth = parser.parse(oth) + else + super + end + + return oth, self + end + + # returns a proxy URI. + # The proxy URI is obtained from environment variables such as http_proxy, + # ftp_proxy, no_proxy, etc. + # If there is no proper proxy, nil is returned. + # + # If the optional parameter, +env+, is specified, it is used instead of ENV. + # + # Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.) + # are examined too. + # + # But http_proxy and HTTP_PROXY is treated specially under CGI environment. + # It's because HTTP_PROXY may be set by Proxy: header. + # So HTTP_PROXY is not used. + # http_proxy is not used too if the variable is case insensitive. + # CGI_HTTP_PROXY can be used instead. + def find_proxy(env=ENV) + raise BadURIError, "relative URI: #{self}" if self.relative? + name = self.scheme.downcase + '_proxy' + proxy_uri = nil + if name == 'http_proxy' && env.include?('REQUEST_METHOD') # CGI? + # HTTP_PROXY conflicts with *_proxy for proxy settings and + # HTTP_* for header information in CGI. + # So it should be careful to use it. + pairs = env.reject {|k, v| /\Ahttp_proxy\z/i !~ k } + case pairs.length + when 0 # no proxy setting anyway. + proxy_uri = nil + when 1 + k, _ = pairs.shift + if k == 'http_proxy' && env[k.upcase] == nil + # http_proxy is safe to use because ENV is case sensitive. + proxy_uri = env[name] + else + proxy_uri = nil + end + else # http_proxy is safe to use because ENV is case sensitive. + proxy_uri = env.to_hash[name] + end + if !proxy_uri + # Use CGI_HTTP_PROXY. cf. libwww-perl. + proxy_uri = env["CGI_#{name.upcase}"] + end + elsif name == 'http_proxy' + unless proxy_uri = env[name] + if proxy_uri = env[name.upcase] + warn 'The environment variable HTTP_PROXY is discouraged. Use http_proxy.', uplevel: 1 + end + end + else + proxy_uri = env[name] || env[name.upcase] + end + + if proxy_uri.nil? || proxy_uri.empty? + return nil + end + + if self.hostname + begin + addr = IPSocket.getaddress(self.hostname) + return nil if /\A127\.|\A::1\z/ =~ addr + rescue SocketError + end + end + + name = 'no_proxy' + if no_proxy = env[name] || env[name.upcase] + return nil unless URI::Generic.use_proxy?(self.hostname, addr, self.port, no_proxy) + end + URI.parse(proxy_uri) + end + + def self.use_proxy?(hostname, addr, port, no_proxy) # :nodoc: + no_proxy.scan(/(?!\.)([^:,\s]+)(?::(\d+))?/) {|p_host, p_port| + if !p_port || port == p_port.to_i + if /(\A|\.)#{Regexp.quote p_host}\z/i =~ hostname + return false + elsif addr + begin + return false if IPAddr.new(p_host).include?(addr) + rescue IPAddr::InvalidAddressError + next + end + end + end + } + true + end + end +end diff --git a/gxg/standard/uri/http.rb b/gxg/standard/uri/http.rb new file mode 100644 index 0000000..9ce9a7f --- /dev/null +++ b/gxg/standard/uri/http.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: false +# = uri/http.rb +# +# Author:: Akira Yamada +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# Revision:: $Id: http.rb 59836 2017-09-11 06:12:40Z hsbt $ +# +# See URI for general documentation +# + +require 'uri/generic' + +module URI + + # + # The syntax of HTTP URIs is defined in RFC1738 section 3.3. + # + # Note that the Ruby URI library allows HTTP URLs containing usernames and + # passwords. This is not legal as per the RFC, but used to be + # supported in Internet Explorer 5 and 6, before the MS04-004 security + # update. See . + # + class HTTP < Generic + # A Default port of 80 for URI::HTTP + DEFAULT_PORT = 80 + + # An Array of the available components for URI::HTTP + COMPONENT = %i[ + scheme + userinfo host port + path + query + fragment + ].freeze + + # + # == Description + # + # Create a new URI::HTTP object from components, with syntax checking. + # + # The components accepted are userinfo, host, port, path, query and + # fragment. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the order + # [userinfo, host, port, path, query, fragment]. + # + # Example: + # + # newuri = URI::HTTP.build(host: 'www.example.com', path: '/foo/bar') + # + # newuri = URI::HTTP.build([nil, "www.example.com", nil, "/path", + # "query", 'fragment']) + # + # Currently, if passed userinfo components this method generates + # invalid HTTP URIs as per RFC 1738. + # + def self.build(args) + tmp = Util.make_components_hash(self, args) + super(tmp) + end + + # + # == Description + # + # Returns the full path for an HTTP request, as required by Net::HTTP::Get. + # + # If the URI contains a query, the full path is URI#path + '?' + URI#query. + # Otherwise, the path is simply URI#path. + # + # Example: + # + # newuri = URI::HTTP.build(path: '/foo/bar', query: 'test=true') + # newuri.request_uri # => "/foo/bar?test=true" + # + def request_uri + return unless @path + + url = @query ? "#@path?#@query" : @path.dup + url.start_with?(?/.freeze) ? url : ?/ + url + end + end + + @@schemes['HTTP'] = HTTP + +end diff --git a/gxg/standard/uri/https.rb b/gxg/standard/uri/https.rb new file mode 100644 index 0000000..0ea4448 --- /dev/null +++ b/gxg/standard/uri/https.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: false +# = uri/https.rb +# +# Author:: Akira Yamada +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# Revision:: $Id: https.rb 53141 2015-12-16 05:07:31Z naruse $ +# +# See URI for general documentation +# + +require 'uri/http' + +module URI + + # The default port for HTTPS URIs is 443, and the scheme is 'https:' rather + # than 'http:'. Other than that, HTTPS URIs are identical to HTTP URIs; + # see URI::HTTP. + class HTTPS < HTTP + # A Default port of 443 for URI::HTTPS + DEFAULT_PORT = 443 + end + @@schemes['HTTPS'] = HTTPS +end diff --git a/gxg/standard/uri/ldap.rb b/gxg/standard/uri/ldap.rb new file mode 100644 index 0000000..8cb3494 --- /dev/null +++ b/gxg/standard/uri/ldap.rb @@ -0,0 +1,261 @@ +# frozen_string_literal: false +# = uri/ldap.rb +# +# Author:: +# Takaaki Tateishi +# Akira Yamada +# License:: +# URI::LDAP is copyrighted free software by Takaaki Tateishi and Akira Yamada. +# You can redistribute it and/or modify it under the same term as Ruby. +# Revision:: $Id: ldap.rb 53141 2015-12-16 05:07:31Z naruse $ +# +# See URI for general documentation +# + +require 'uri/generic' + +module URI + + # + # LDAP URI SCHEMA (described in RFC2255) + # ldap:///[?[?[?[?]]]] + # + class LDAP < Generic + + # A Default port of 389 for URI::LDAP + DEFAULT_PORT = 389 + + # An Array of the available components for URI::LDAP + COMPONENT = [ + :scheme, + :host, :port, + :dn, + :attributes, + :scope, + :filter, + :extensions, + ].freeze + + # Scopes available for the starting point. + # + # * SCOPE_BASE - the Base DN + # * SCOPE_ONE - one level under the Base DN, not including the base DN and + # not including any entries under this. + # * SCOPE_SUB - subtress, all entries at all levels + # + SCOPE = [ + SCOPE_ONE = 'one', + SCOPE_SUB = 'sub', + SCOPE_BASE = 'base', + ].freeze + + # + # == Description + # + # Create a new URI::LDAP object from components, with syntax checking. + # + # The components accepted are host, port, dn, attributes, + # scope, filter, and extensions. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the order + # [host, port, dn, attributes, scope, filter, extensions]. + # + # Example: + # + # newuri = URI::LDAP.build({:host => 'ldap.example.com', + # :dn> => '/dc=example'}) + # + # newuri = URI::LDAP.build(["ldap.example.com", nil, + # "/dc=example;dc=com", "query", nil, nil, nil]) + # + def self.build(args) + tmp = Util::make_components_hash(self, args) + + if tmp[:dn] + tmp[:path] = tmp[:dn] + end + + query = [] + [:extensions, :filter, :scope, :attributes].collect do |x| + next if !tmp[x] && query.size == 0 + query.unshift(tmp[x]) + end + + tmp[:query] = query.join('?') + + return super(tmp) + end + + # + # == Description + # + # Create a new URI::LDAP object from generic URI components as per + # RFC 2396. No LDAP-specific syntax checking is performed. + # + # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+, + # +opaque+, +query+ and +fragment+, in that order. + # + # Example: + # + # uri = URI::LDAP.new("ldap", nil, "ldap.example.com", nil, + # "/dc=example;dc=com", "query", nil, nil, nil, nil) + # + # + # See also URI::Generic.new + # + def initialize(*arg) + super(*arg) + + if @fragment + raise InvalidURIError, 'bad LDAP URL' + end + + parse_dn + parse_query + end + + # private method to cleanup +dn+ from using the +path+ component attribute + def parse_dn + @dn = @path[1..-1] + end + private :parse_dn + + # private method to cleanup +attributes+, +scope+, +filter+ and +extensions+, + # from using the +query+ component attribute + def parse_query + @attributes = nil + @scope = nil + @filter = nil + @extensions = nil + + if @query + attrs, scope, filter, extensions = @query.split('?') + + @attributes = attrs if attrs && attrs.size > 0 + @scope = scope if scope && scope.size > 0 + @filter = filter if filter && filter.size > 0 + @extensions = extensions if extensions && extensions.size > 0 + end + end + private :parse_query + + # private method to assemble +query+ from +attributes+, +scope+, +filter+ and +extensions+. + def build_path_query + @path = '/' + @dn + + query = [] + [@extensions, @filter, @scope, @attributes].each do |x| + next if !x && query.size == 0 + query.unshift(x) + end + @query = query.join('?') + end + private :build_path_query + + # returns dn. + def dn + @dn + end + + # private setter for dn +val+ + def set_dn(val) + @dn = val + build_path_query + @dn + end + protected :set_dn + + # setter for dn +val+ + def dn=(val) + set_dn(val) + val + end + + # returns attributes. + def attributes + @attributes + end + + # private setter for attributes +val+ + def set_attributes(val) + @attributes = val + build_path_query + @attributes + end + protected :set_attributes + + # setter for attributes +val+ + def attributes=(val) + set_attributes(val) + val + end + + # returns scope. + def scope + @scope + end + + # private setter for scope +val+ + def set_scope(val) + @scope = val + build_path_query + @scope + end + protected :set_scope + + # setter for scope +val+ + def scope=(val) + set_scope(val) + val + end + + # returns filter. + def filter + @filter + end + + # private setter for filter +val+ + def set_filter(val) + @filter = val + build_path_query + @filter + end + protected :set_filter + + # setter for filter +val+ + def filter=(val) + set_filter(val) + val + end + + # returns extensions. + def extensions + @extensions + end + + # private setter for extensions +val+ + def set_extensions(val) + @extensions = val + build_path_query + @extensions + end + protected :set_extensions + + # setter for extensions +val+ + def extensions=(val) + set_extensions(val) + val + end + + # Checks if URI has a path + # For URI::LDAP this will return +false+ + def hierarchical? + false + end + end + + @@schemes['LDAP'] = LDAP +end diff --git a/gxg/standard/uri/ldaps.rb b/gxg/standard/uri/ldaps.rb new file mode 100644 index 0000000..d03f8ef --- /dev/null +++ b/gxg/standard/uri/ldaps.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: false +# = uri/ldap.rb +# +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See URI for general documentation +# + +require 'uri/ldap' + +module URI + + # The default port for LDAPS URIs is 636, and the scheme is 'ldaps:' rather + # than 'ldap:'. Other than that, LDAPS URIs are identical to LDAP URIs; + # see URI::LDAP. + class LDAPS < LDAP + # A Default port of 636 for URI::LDAPS + DEFAULT_PORT = 636 + end + @@schemes['LDAPS'] = LDAPS +end diff --git a/gxg/standard/uri/mailto.rb b/gxg/standard/uri/mailto.rb new file mode 100644 index 0000000..bb41153 --- /dev/null +++ b/gxg/standard/uri/mailto.rb @@ -0,0 +1,294 @@ +# frozen_string_literal: false +# = uri/mailto.rb +# +# Author:: Akira Yamada +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# Revision:: $Id: mailto.rb 57066 2016-12-12 15:24:28Z naruse $ +# +# See URI for general documentation +# + +require 'uri/generic' + +module URI + + # + # RFC6068, The mailto URL scheme + # + class MailTo < Generic + include REGEXP + + # A Default port of nil for URI::MailTo + DEFAULT_PORT = nil + + # An Array of the available components for URI::MailTo + COMPONENT = [ :scheme, :to, :headers ].freeze + + # :stopdoc: + # "hname" and "hvalue" are encodings of an RFC 822 header name and + # value, respectively. As with "to", all URL reserved characters must + # be encoded. + # + # "#mailbox" is as specified in RFC 822 [RFC822]. This means that it + # consists of zero or more comma-separated mail addresses, possibly + # including "phrase" and "comment" components. Note that all URL + # reserved characters in "to" must be encoded: in particular, + # parentheses, commas, and the percent sign ("%"), which commonly occur + # in the "mailbox" syntax. + # + # Within mailto URLs, the characters "?", "=", "&" are reserved. + + # ; RFC 6068 + # hfields = "?" hfield *( "&" hfield ) + # hfield = hfname "=" hfvalue + # hfname = *qchar + # hfvalue = *qchar + # qchar = unreserved / pct-encoded / some-delims + # some-delims = "!" / "$" / "'" / "(" / ")" / "*" + # / "+" / "," / ";" / ":" / "@" + # + # ; RFC3986 + # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + # pct-encoded = "%" HEXDIG HEXDIG + HEADER_REGEXP = /\A(?(?:%\h\h|[!$'-.0-;@-Z_a-z~])*=(?:%\h\h|[!$'-.0-;@-Z_a-z~])*)(?:&\g)*\z/ + # practical regexp for email address + # http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address + EMAIL_REGEXP = /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z/ + # :startdoc: + + # + # == Description + # + # Creates a new URI::MailTo object from components, with syntax checking. + # + # Components can be provided as an Array or Hash. If an Array is used, + # the components must be supplied as [to, headers]. + # + # If a Hash is used, the keys are the component names preceded by colons. + # + # The headers can be supplied as a pre-encoded string, such as + # "subject=subscribe&cc=address", or as an Array of Arrays like + # [['subject', 'subscribe'], ['cc', 'address']] + # + # Examples: + # + # require 'uri' + # + # m1 = URI::MailTo.build(['joe@example.com', 'subject=Ruby']) + # puts m1.to_s -> mailto:joe@example.com?subject=Ruby + # + # m2 = URI::MailTo.build(['john@example.com', [['Subject', 'Ruby'], ['Cc', 'jack@example.com']]]) + # puts m2.to_s -> mailto:john@example.com?Subject=Ruby&Cc=jack@example.com + # + # m3 = URI::MailTo.build({:to => 'listman@example.com', :headers => [['subject', 'subscribe']]}) + # puts m3.to_s -> mailto:listman@example.com?subject=subscribe + # + def self.build(args) + tmp = Util.make_components_hash(self, args) + + case tmp[:to] + when Array + tmp[:opaque] = tmp[:to].join(',') + when String + tmp[:opaque] = tmp[:to].dup + else + tmp[:opaque] = '' + end + + if tmp[:headers] + query = + case tmp[:headers] + when Array + tmp[:headers].collect { |x| + if x.kind_of?(Array) + x[0] + '=' + x[1..-1].join + else + x.to_s + end + }.join('&') + when Hash + tmp[:headers].collect { |h,v| + h + '=' + v + }.join('&') + else + tmp[:headers].to_s + end + unless query.empty? + tmp[:opaque] << '?' << query + end + end + + super(tmp) + end + + # + # == Description + # + # Creates a new URI::MailTo object from generic URL components with + # no syntax checking. + # + # This method is usually called from URI::parse, which checks + # the validity of each component. + # + def initialize(*arg) + super(*arg) + + @to = nil + @headers = [] + + # The RFC3986 parser does not normally populate opaque + @opaque = "?#{@query}" if @query && !@opaque + + unless @opaque + raise InvalidComponentError, + "missing opaque part for mailto URL" + end + to, header = @opaque.split('?', 2) + # allow semicolon as a addr-spec separator + # http://support.microsoft.com/kb/820868 + unless /\A(?:[^@,;]+@[^@,;]+(?:\z|[,;]))*\z/ =~ to + raise InvalidComponentError, + "unrecognised opaque part for mailtoURL: #{@opaque}" + end + + if arg[10] # arg_check + self.to = to + self.headers = header + else + set_to(to) + set_headers(header) + end + end + + # The primary e-mail address of the URL, as a String + attr_reader :to + + # E-mail headers set by the URL, as an Array of Arrays + attr_reader :headers + + # check the to +v+ component + def check_to(v) + return true unless v + return true if v.size == 0 + + v.split(/[,;]/).each do |addr| + # check url safety as path-rootless + if /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*\z/ !~ addr + raise InvalidComponentError, + "an address in 'to' is invalid as URI #{addr.dump}" + end + + # check addr-spec + # don't s/\+/ /g + addr.gsub!(/%\h\h/, URI::TBLDECWWWCOMP_) + if EMAIL_REGEXP !~ addr + raise InvalidComponentError, + "an address in 'to' is invalid as uri-escaped addr-spec #{addr.dump}" + end + end + + true + end + private :check_to + + # private setter for to +v+ + def set_to(v) + @to = v + end + protected :set_to + + # setter for to +v+ + def to=(v) + check_to(v) + set_to(v) + v + end + + # check the headers +v+ component against either + # * HEADER_REGEXP + def check_headers(v) + return true unless v + return true if v.size == 0 + if HEADER_REGEXP !~ v + raise InvalidComponentError, + "bad component(expected opaque component): #{v}" + end + + true + end + private :check_headers + + # private setter for headers +v+ + def set_headers(v) + @headers = [] + if v + v.split('&').each do |x| + @headers << x.split(/=/, 2) + end + end + end + protected :set_headers + + # setter for headers +v+ + def headers=(v) + check_headers(v) + set_headers(v) + v + end + + # Constructs String from URI + def to_s + @scheme + ':' + + if @to + @to + else + '' + end + + if @headers.size > 0 + '?' + @headers.collect{|x| x.join('=')}.join('&') + else + '' + end + + if @fragment + '#' + @fragment + else + '' + end + end + + # Returns the RFC822 e-mail text equivalent of the URL, as a String. + # + # Example: + # + # require 'uri' + # + # uri = URI.parse("mailto:ruby-list@ruby-lang.org?Subject=subscribe&cc=myaddr") + # uri.to_mailtext + # # => "To: ruby-list@ruby-lang.org\nSubject: subscribe\nCc: myaddr\n\n\n" + # + def to_mailtext + to = URI.decode_www_form_component(@to) + head = '' + body = '' + @headers.each do |x| + case x[0] + when 'body' + body = URI.decode_www_form_component(x[1]) + when 'to' + to << ', ' + URI.decode_www_form_component(x[1]) + else + head << URI.decode_www_form_component(x[0]).capitalize + ': ' + + URI.decode_www_form_component(x[1]) + "\n" + end + end + + "To: #{to} +#{head} +#{body} +" + end + alias to_rfc822text to_mailtext + end + + @@schemes['MAILTO'] = MailTo +end diff --git a/gxg/standard/uri/rfc2396_parser.rb b/gxg/standard/uri/rfc2396_parser.rb new file mode 100644 index 0000000..b9e7b2b --- /dev/null +++ b/gxg/standard/uri/rfc2396_parser.rb @@ -0,0 +1,544 @@ +# frozen_string_literal: false +#-- +# = uri/common.rb +# +# Author:: Akira Yamada +# Revision:: $Id$ +# License:: +# You can redistribute it and/or modify it under the same term as Ruby. +# +# See URI for general documentation +# + +module URI + # + # Includes URI::REGEXP::PATTERN + # + module RFC2396_REGEXP + # + # Patterns used to parse URI's + # + module PATTERN + # :stopdoc: + + # RFC 2396 (URI Generic Syntax) + # RFC 2732 (IPv6 Literal Addresses in URL's) + # RFC 2373 (IPv6 Addressing Architecture) + + # alpha = lowalpha | upalpha + ALPHA = "a-zA-Z" + # alphanum = alpha | digit + ALNUM = "#{ALPHA}\\d" + + # hex = digit | "A" | "B" | "C" | "D" | "E" | "F" | + # "a" | "b" | "c" | "d" | "e" | "f" + HEX = "a-fA-F\\d" + # escaped = "%" hex hex + ESCAPED = "%[#{HEX}]{2}" + # mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | + # "(" | ")" + # unreserved = alphanum | mark + UNRESERVED = "\\-_.!~*'()#{ALNUM}" + # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + # "$" | "," + # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + # "$" | "," | "[" | "]" (RFC 2732) + RESERVED = ";/?:@&=+$,\\[\\]" + + # domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum + DOMLABEL = "(?:[#{ALNUM}](?:[-#{ALNUM}]*[#{ALNUM}])?)" + # toplabel = alpha | alpha *( alphanum | "-" ) alphanum + TOPLABEL = "(?:[#{ALPHA}](?:[-#{ALNUM}]*[#{ALNUM}])?)" + # hostname = *( domainlabel "." ) toplabel [ "." ] + HOSTNAME = "(?:#{DOMLABEL}\\.)*#{TOPLABEL}\\.?" + + # :startdoc: + end # PATTERN + + # :startdoc: + end # REGEXP + + # class that Parses String's into URI's + # + # It contains a Hash set of patterns and Regexp's that match and validate. + # + class RFC2396_Parser + include RFC2396_REGEXP + + # + # == Synopsis + # + # URI::Parser.new([opts]) + # + # == Args + # + # The constructor accepts a hash as options for parser. + # Keys of options are pattern names of URI components + # and values of options are pattern strings. + # The constructor generates set of regexps for parsing URIs. + # + # You can use the following keys: + # + # * :ESCAPED (URI::PATTERN::ESCAPED in default) + # * :UNRESERVED (URI::PATTERN::UNRESERVED in default) + # * :DOMLABEL (URI::PATTERN::DOMLABEL in default) + # * :TOPLABEL (URI::PATTERN::TOPLABEL in default) + # * :HOSTNAME (URI::PATTERN::HOSTNAME in default) + # + # == Examples + # + # p = URI::Parser.new(:ESCAPED => "(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})") + # u = p.parse("http://example.jp/%uABCD") #=> # + # URI.parse(u.to_s) #=> raises URI::InvalidURIError + # + # s = "http://example.com/ABCD" + # u1 = p.parse(s) #=> # + # u2 = URI.parse(s) #=> # + # u1 == u2 #=> true + # u1.eql?(u2) #=> false + # + def initialize(opts = {}) + @pattern = initialize_pattern(opts) + @pattern.each_value(&:freeze) + @pattern.freeze + + @regexp = initialize_regexp(@pattern) + @regexp.each_value(&:freeze) + @regexp.freeze + end + + # The Hash of patterns. + # + # see also URI::Parser.initialize_pattern + attr_reader :pattern + + # The Hash of Regexp + # + # see also URI::Parser.initialize_regexp + attr_reader :regexp + + # Returns a split URI against regexp[:ABS_URI] + def split(uri) + case uri + when '' + # null uri + + when @regexp[:ABS_URI] + scheme, opaque, userinfo, host, port, + registry, path, query, fragment = $~[1..-1] + + # URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + # hier_part = ( net_path | abs_path ) [ "?" query ] + # opaque_part = uric_no_slash *uric + + # abs_path = "/" path_segments + # net_path = "//" authority [ abs_path ] + + # authority = server | reg_name + # server = [ [ userinfo "@" ] hostport ] + + if !scheme + raise InvalidURIError, + "bad URI(absolute but no scheme): #{uri}" + end + if !opaque && (!path && (!host && !registry)) + raise InvalidURIError, + "bad URI(absolute but no path): #{uri}" + end + + when @regexp[:REL_URI] + scheme = nil + opaque = nil + + userinfo, host, port, registry, + rel_segment, abs_path, query, fragment = $~[1..-1] + if rel_segment && abs_path + path = rel_segment + abs_path + elsif rel_segment + path = rel_segment + elsif abs_path + path = abs_path + end + + # URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + + # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + + # net_path = "//" authority [ abs_path ] + # abs_path = "/" path_segments + # rel_path = rel_segment [ abs_path ] + + # authority = server | reg_name + # server = [ [ userinfo "@" ] hostport ] + + else + raise InvalidURIError, "bad URI(is not URI?): #{uri}" + end + + path = '' if !path && !opaque # (see RFC2396 Section 5.2) + ret = [ + scheme, + userinfo, host, port, # X + registry, # X + path, # Y + opaque, # Y + query, + fragment + ] + return ret + end + + # + # == Args + # + # +uri+:: + # String + # + # == Description + # + # parses +uri+ and constructs either matching URI scheme object + # (FTP, HTTP, HTTPS, LDAP, LDAPS, or MailTo) or URI::Generic + # + # == Usage + # + # p = URI::Parser.new + # p.parse("ldap://ldap.example.com/dc=example?user=john") + # #=> # + # + def parse(uri) + scheme, userinfo, host, port, + registry, path, opaque, query, fragment = self.split(uri) + + if scheme && URI.scheme_list.include?(scheme.upcase) + URI.scheme_list[scheme.upcase].new(scheme, userinfo, host, port, + registry, path, opaque, query, + fragment, self) + else + Generic.new(scheme, userinfo, host, port, + registry, path, opaque, query, + fragment, self) + end + end + + + # + # == Args + # + # +uris+:: + # an Array of Strings + # + # == Description + # + # Attempts to parse and merge a set of URIs + # + def join(*uris) + uris[0] = convert_to_uri(uris[0]) + uris.inject :merge + end + + # + # :call-seq: + # extract( str ) + # extract( str, schemes ) + # extract( str, schemes ) {|item| block } + # + # == Args + # + # +str+:: + # String to search + # +schemes+:: + # Patterns to apply to +str+ + # + # == Description + # + # Attempts to parse and merge a set of URIs + # If no +block+ given , then returns the result, + # else it calls +block+ for each element in result. + # + # see also URI::Parser.make_regexp + # + def extract(str, schemes = nil) + if block_given? + str.scan(make_regexp(schemes)) { yield $& } + nil + else + result = [] + str.scan(make_regexp(schemes)) { result.push $& } + result + end + end + + # returns Regexp that is default self.regexp[:ABS_URI_REF], + # unless +schemes+ is provided. Then it is a Regexp.union with self.pattern[:X_ABS_URI] + def make_regexp(schemes = nil) + unless schemes + @regexp[:ABS_URI_REF] + else + /(?=#{Regexp.union(*schemes)}:)#{@pattern[:X_ABS_URI]}/x + end + end + + # + # :call-seq: + # escape( str ) + # escape( str, unsafe ) + # + # == Args + # + # +str+:: + # String to make safe + # +unsafe+:: + # Regexp to apply. Defaults to self.regexp[:UNSAFE] + # + # == Description + # + # constructs a safe String from +str+, removing unsafe characters, + # replacing them with codes. + # + def escape(str, unsafe = @regexp[:UNSAFE]) + unless unsafe.kind_of?(Regexp) + # perhaps unsafe is String object + unsafe = Regexp.new("[#{Regexp.quote(unsafe)}]", false) + end + str.gsub(unsafe) do + us = $& + tmp = '' + us.each_byte do |uc| + tmp << sprintf('%%%02X', uc) + end + tmp + end.force_encoding(Encoding::US_ASCII) + end + + # + # :call-seq: + # unescape( str ) + # unescape( str, unsafe ) + # + # == Args + # + # +str+:: + # String to remove escapes from + # +unsafe+:: + # Regexp to apply. Defaults to self.regexp[:ESCAPED] + # + # == Description + # + # Removes escapes from +str+ + # + def unescape(str, escaped = @regexp[:ESCAPED]) + str.gsub(escaped) { [$&[1, 2].hex].pack('C') }.force_encoding(str.encoding) + end + + @@to_s = Kernel.instance_method(:to_s) + def inspect + @@to_s.bind(self).call + end + + private + + # Constructs the default Hash of patterns + def initialize_pattern(opts = {}) + ret = {} + ret[:ESCAPED] = escaped = (opts.delete(:ESCAPED) || PATTERN::ESCAPED) + ret[:UNRESERVED] = unreserved = opts.delete(:UNRESERVED) || PATTERN::UNRESERVED + ret[:RESERVED] = reserved = opts.delete(:RESERVED) || PATTERN::RESERVED + ret[:DOMLABEL] = opts.delete(:DOMLABEL) || PATTERN::DOMLABEL + ret[:TOPLABEL] = opts.delete(:TOPLABEL) || PATTERN::TOPLABEL + ret[:HOSTNAME] = hostname = opts.delete(:HOSTNAME) + + # RFC 2396 (URI Generic Syntax) + # RFC 2732 (IPv6 Literal Addresses in URL's) + # RFC 2373 (IPv6 Addressing Architecture) + + # uric = reserved | unreserved | escaped + ret[:URIC] = uric = "(?:[#{unreserved}#{reserved}]|#{escaped})" + # uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" | + # "&" | "=" | "+" | "$" | "," + ret[:URIC_NO_SLASH] = uric_no_slash = "(?:[#{unreserved};?:@&=+$,]|#{escaped})" + # query = *uric + ret[:QUERY] = query = "#{uric}*" + # fragment = *uric + ret[:FRAGMENT] = fragment = "#{uric}*" + + # hostname = *( domainlabel "." ) toplabel [ "." ] + # reg-name = *( unreserved / pct-encoded / sub-delims ) # RFC3986 + unless hostname + ret[:HOSTNAME] = hostname = "(?:[a-zA-Z0-9\\-.]|%\\h\\h)+" + end + + # RFC 2373, APPENDIX B: + # IPv6address = hexpart [ ":" IPv4address ] + # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT + # hexpart = hexseq | hexseq "::" [ hexseq ] | "::" [ hexseq ] + # hexseq = hex4 *( ":" hex4) + # hex4 = 1*4HEXDIG + # + # XXX: This definition has a flaw. "::" + IPv4address must be + # allowed too. Here is a replacement. + # + # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT + ret[:IPV4ADDR] = ipv4addr = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}" + # hex4 = 1*4HEXDIG + hex4 = "[#{PATTERN::HEX}]{1,4}" + # lastpart = hex4 | IPv4address + lastpart = "(?:#{hex4}|#{ipv4addr})" + # hexseq1 = *( hex4 ":" ) hex4 + hexseq1 = "(?:#{hex4}:)*#{hex4}" + # hexseq2 = *( hex4 ":" ) lastpart + hexseq2 = "(?:#{hex4}:)*#{lastpart}" + # IPv6address = hexseq2 | [ hexseq1 ] "::" [ hexseq2 ] + ret[:IPV6ADDR] = ipv6addr = "(?:#{hexseq2}|(?:#{hexseq1})?::(?:#{hexseq2})?)" + + # IPv6prefix = ( hexseq1 | [ hexseq1 ] "::" [ hexseq1 ] ) "/" 1*2DIGIT + # unused + + # ipv6reference = "[" IPv6address "]" (RFC 2732) + ret[:IPV6REF] = ipv6ref = "\\[#{ipv6addr}\\]" + + # host = hostname | IPv4address + # host = hostname | IPv4address | IPv6reference (RFC 2732) + ret[:HOST] = host = "(?:#{hostname}|#{ipv4addr}|#{ipv6ref})" + # port = *digit + ret[:PORT] = port = '\d*' + # hostport = host [ ":" port ] + ret[:HOSTPORT] = hostport = "#{host}(?::#{port})?" + + # userinfo = *( unreserved | escaped | + # ";" | ":" | "&" | "=" | "+" | "$" | "," ) + ret[:USERINFO] = userinfo = "(?:[#{unreserved};:&=+$,]|#{escaped})*" + + # pchar = unreserved | escaped | + # ":" | "@" | "&" | "=" | "+" | "$" | "," + pchar = "(?:[#{unreserved}:@&=+$,]|#{escaped})" + # param = *pchar + param = "#{pchar}*" + # segment = *pchar *( ";" param ) + segment = "#{pchar}*(?:;#{param})*" + # path_segments = segment *( "/" segment ) + ret[:PATH_SEGMENTS] = path_segments = "#{segment}(?:/#{segment})*" + + # server = [ [ userinfo "@" ] hostport ] + server = "(?:#{userinfo}@)?#{hostport}" + # reg_name = 1*( unreserved | escaped | "$" | "," | + # ";" | ":" | "@" | "&" | "=" | "+" ) + ret[:REG_NAME] = reg_name = "(?:[#{unreserved}$,;:@&=+]|#{escaped})+" + # authority = server | reg_name + authority = "(?:#{server}|#{reg_name})" + + # rel_segment = 1*( unreserved | escaped | + # ";" | "@" | "&" | "=" | "+" | "$" | "," ) + ret[:REL_SEGMENT] = rel_segment = "(?:[#{unreserved};@&=+$,]|#{escaped})+" + + # scheme = alpha *( alpha | digit | "+" | "-" | "." ) + ret[:SCHEME] = scheme = "[#{PATTERN::ALPHA}][\\-+.#{PATTERN::ALPHA}\\d]*" + + # abs_path = "/" path_segments + ret[:ABS_PATH] = abs_path = "/#{path_segments}" + # rel_path = rel_segment [ abs_path ] + ret[:REL_PATH] = rel_path = "#{rel_segment}(?:#{abs_path})?" + # net_path = "//" authority [ abs_path ] + ret[:NET_PATH] = net_path = "//#{authority}(?:#{abs_path})?" + + # hier_part = ( net_path | abs_path ) [ "?" query ] + ret[:HIER_PART] = hier_part = "(?:#{net_path}|#{abs_path})(?:\\?(?:#{query}))?" + # opaque_part = uric_no_slash *uric + ret[:OPAQUE_PART] = opaque_part = "#{uric_no_slash}#{uric}*" + + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + ret[:ABS_URI] = abs_uri = "#{scheme}:(?:#{hier_part}|#{opaque_part})" + # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + ret[:REL_URI] = rel_uri = "(?:#{net_path}|#{abs_path}|#{rel_path})(?:\\?#{query})?" + + # URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + ret[:URI_REF] = "(?:#{abs_uri}|#{rel_uri})?(?:##{fragment})?" + + ret[:X_ABS_URI] = " + (#{scheme}): (?# 1: scheme) + (?: + (#{opaque_part}) (?# 2: opaque) + | + (?:(?: + //(?: + (?:(?:(#{userinfo})@)? (?# 3: userinfo) + (?:(#{host})(?::(\\d*))?))? (?# 4: host, 5: port) + | + (#{reg_name}) (?# 6: registry) + ) + | + (?!//)) (?# XXX: '//' is the mark for hostport) + (#{abs_path})? (?# 7: path) + )(?:\\?(#{query}))? (?# 8: query) + ) + (?:\\#(#{fragment}))? (?# 9: fragment) + " + + ret[:X_REL_URI] = " + (?: + (?: + // + (?: + (?:(#{userinfo})@)? (?# 1: userinfo) + (#{host})?(?::(\\d*))? (?# 2: host, 3: port) + | + (#{reg_name}) (?# 4: registry) + ) + ) + | + (#{rel_segment}) (?# 5: rel_segment) + )? + (#{abs_path})? (?# 6: abs_path) + (?:\\?(#{query}))? (?# 7: query) + (?:\\#(#{fragment}))? (?# 8: fragment) + " + + ret + end + + # Constructs the default Hash of Regexp's + def initialize_regexp(pattern) + ret = {} + + # for URI::split + ret[:ABS_URI] = Regexp.new('\A\s*' + pattern[:X_ABS_URI] + '\s*\z', Regexp::EXTENDED) + ret[:REL_URI] = Regexp.new('\A\s*' + pattern[:X_REL_URI] + '\s*\z', Regexp::EXTENDED) + + # for URI::extract + ret[:URI_REF] = Regexp.new(pattern[:URI_REF]) + ret[:ABS_URI_REF] = Regexp.new(pattern[:X_ABS_URI], Regexp::EXTENDED) + ret[:REL_URI_REF] = Regexp.new(pattern[:X_REL_URI], Regexp::EXTENDED) + + # for URI::escape/unescape + ret[:ESCAPED] = Regexp.new(pattern[:ESCAPED]) + ret[:UNSAFE] = Regexp.new("[^#{pattern[:UNRESERVED]}#{pattern[:RESERVED]}]") + + # for Generic#initialize + ret[:SCHEME] = Regexp.new("\\A#{pattern[:SCHEME]}\\z") + ret[:USERINFO] = Regexp.new("\\A#{pattern[:USERINFO]}\\z") + ret[:HOST] = Regexp.new("\\A#{pattern[:HOST]}\\z") + ret[:PORT] = Regexp.new("\\A#{pattern[:PORT]}\\z") + ret[:OPAQUE] = Regexp.new("\\A#{pattern[:OPAQUE_PART]}\\z") + ret[:REGISTRY] = Regexp.new("\\A#{pattern[:REG_NAME]}\\z") + ret[:ABS_PATH] = Regexp.new("\\A#{pattern[:ABS_PATH]}\\z") + ret[:REL_PATH] = Regexp.new("\\A#{pattern[:REL_PATH]}\\z") + ret[:QUERY] = Regexp.new("\\A#{pattern[:QUERY]}\\z") + ret[:FRAGMENT] = Regexp.new("\\A#{pattern[:FRAGMENT]}\\z") + + ret + end + + def convert_to_uri(uri) + if uri.is_a?(URI::Generic) + uri + elsif uri = String.try_convert(uri) + parse(uri) + else + raise ArgumentError, + "bad argument (expected URI object or URI string)" + end + end + + end # class Parser +end # module URI diff --git a/gxg/standard/uri/rfc3986_parser.rb b/gxg/standard/uri/rfc3986_parser.rb new file mode 100644 index 0000000..8712800 --- /dev/null +++ b/gxg/standard/uri/rfc3986_parser.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: false +module URI + class RFC3986_Parser # :nodoc: + # URI defined in RFC3986 + # this regexp is modified not to host is not empty string + RFC3986_URI = /\A(?(?[A-Za-z][+\-.0-9A-Za-z]*):(?\/\/(?(?:(?(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?(?\[(?:(?(?:\h{1,4}:){6}(?\h{1,4}:\h{1,4}|(?(?[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g\.\g\.\g))|::(?:\h{1,4}:){5}\g|\h{1,4}?::(?:\h{1,4}:){4}\g|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g|(?(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])+))?(?::(?\d*))?)(?(?:\/(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?\/(?:(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g)*)?)|(?\g(?:\/\g)*)|(?))(?:\?(?[^#]*))?(?:\#(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/ + RFC3986_relative_ref = /\A(?(?\/\/(?(?:(?(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?(?\[(?(?:\h{1,4}:){6}(?\h{1,4}:\h{1,4}|(?(?[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g\.\g\.\g))|::(?:\h{1,4}:){5}\g|\h{1,4}?::(?:\h{1,4}:){4}\g|(?:(?:\h{1,4}:){,1}\h{1,4})?::(?:\h{1,4}:){3}\g|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?v\h+\.[!$&-.0-;=A-Z_a-z~]+)\])|\g|(?(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])+))?(?::(?\d*))?)(?(?:\/(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?\/(?:(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g)*)?)|(?(?(?:%\h\h|[!$&-.0-9;=@-Z_a-z~])+)(?:\/\g)*)|(?))(?:\?(?[^#]*))?(?:\#(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/ + attr_reader :regexp + + def initialize + @regexp = default_regexp.each_value(&:freeze).freeze + end + + def split(uri) #:nodoc: + begin + uri = uri.to_str + rescue NoMethodError + raise InvalidURIError, "bad URI(is not URI?): #{uri}" + end + uri.ascii_only? or + raise InvalidURIError, "URI must be ascii only #{uri.dump}" + if m = RFC3986_URI.match(uri) + query = m["query".freeze] + scheme = m["scheme".freeze] + opaque = m["path-rootless".freeze] + if opaque + opaque << "?#{query}" if query + [ scheme, + nil, # userinfo + nil, # host + nil, # port + nil, # registry + nil, # path + opaque, + nil, # query + m["fragment".freeze] + ] + else # normal + [ scheme, + m["userinfo".freeze], + m["host".freeze], + m["port".freeze], + nil, # registry + (m["path-abempty".freeze] || + m["path-absolute".freeze] || + m["path-empty".freeze]), + nil, # opaque + query, + m["fragment".freeze] + ] + end + elsif m = RFC3986_relative_ref.match(uri) + [ nil, # scheme + m["userinfo".freeze], + m["host".freeze], + m["port".freeze], + nil, # registry, + (m["path-abempty".freeze] || + m["path-absolute".freeze] || + m["path-noscheme".freeze] || + m["path-empty".freeze]), + nil, # opaque + m["query".freeze], + m["fragment".freeze] + ] + else + raise InvalidURIError, "bad URI(is not URI?): #{uri}" + end + end + + def parse(uri) # :nodoc: + scheme, userinfo, host, port, + registry, path, opaque, query, fragment = self.split(uri) + scheme_list = URI.scheme_list + if scheme && scheme_list.include?(uc = scheme.upcase) + scheme_list[uc].new(scheme, userinfo, host, port, + registry, path, opaque, query, + fragment, self) + else + Generic.new(scheme, userinfo, host, port, + registry, path, opaque, query, + fragment, self) + end + end + + + def join(*uris) # :nodoc: + uris[0] = convert_to_uri(uris[0]) + uris.inject :merge + end + + @@to_s = Kernel.instance_method(:to_s) + def inspect + @@to_s.bind(self).call + end + + private + + def default_regexp # :nodoc: + { + SCHEME: /\A[A-Za-z][A-Za-z0-9+\-.]*\z/, + USERINFO: /\A(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*\z/, + HOST: /\A(?:(?\[(?:(?(?:\h{1,4}:){6}(?\h{1,4}:\h{1,4}|(?(?[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g\.\g\.\g))|::(?:\h{1,4}:){5}\g|\h{,4}::(?:\h{1,4}:){4}\g|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g|(?(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])*))\z/, + ABS_PATH: /\A\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*(?:\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*)*\z/, + REL_PATH: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+(?:\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*)*\z/, + QUERY: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*\z/, + FRAGMENT: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*\z/, + OPAQUE: /\A(?:[^\/].*)?\z/, + PORT: /\A[\x09\x0a\x0c\x0d ]*\d*[\x09\x0a\x0c\x0d ]*\z/, + } + end + + def convert_to_uri(uri) + if uri.is_a?(URI::Generic) + uri + elsif uri = String.try_convert(uri) + parse(uri) + else + raise ArgumentError, + "bad argument (expected URI object or URI string)" + end + end + + end # class Parser +end # module URI diff --git a/gxg/todo/uuidtools/uuidtools.rb b/gxg/todo/uuidtools/uuidtools.rb new file mode 100644 index 0000000..1b8ae81 --- /dev/null +++ b/gxg/todo/uuidtools/uuidtools.rb @@ -0,0 +1,753 @@ +# encoding:utf-8 +#-- +# Copyright (C) 2005-2014 Bob Aman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#++ + +require 'uri' +require 'time' +require 'thread' +require 'digest/sha1' +require 'digest/md5' + +#require 'uuidtools/version' +unless defined? UUIDTools::VERSION + module UUIDTools + module VERSION #:nodoc: + MAJOR = 2 + MINOR = 1 + TINY = 5 + + STRING = [MAJOR, MINOR, TINY].join('.') + end + end +end + +#begin +# require 'securerandom' +#rescue LoadError +# require File.join(File.dirname(__FILE__), 'compat', 'securerandom') +#end + +module UUIDTools + ## + # UUIDTools was designed to be a simple library for generating any + # of the various types of UUIDs. It conforms to RFC 4122 whenever + # possible. + # + # @example + # UUID.md5_create(UUID_DNS_NAMESPACE, "www.widgets.com") + # # => # + # UUID.sha1_create(UUID_DNS_NAMESPACE, "www.widgets.com") + # # => # + # UUID.timestamp_create + # # => # + # UUID.random_create + # # => # + class UUID + include Comparable + + ## + # @api private + @@last_timestamp = nil + + ## + # @api private + @@last_node_id = nil + + ## + # @api private + @@last_clock_sequence = nil + + ## + # @api private + @@state_file = nil + + ## + # @api private + @@mutex = Mutex.new + + ## + # Creates a new UUID structure from its component values. + # @see UUID.md5_create + # @see UUID.sha1_create + # @see UUID.timestamp_create + # @see UUID.random_create + # @api private + def initialize(time_low, time_mid, time_hi_and_version, + clock_seq_hi_and_reserved, clock_seq_low, nodes) + unless time_low >= 0 && time_low < 4294967296 + raise ArgumentError, + "Expected unsigned 32-bit number for time_low, got #{time_low}." + end + unless time_mid >= 0 && time_mid < 65536 + raise ArgumentError, + "Expected unsigned 16-bit number for time_mid, got #{time_mid}." + end + unless time_hi_and_version >= 0 && time_hi_and_version < 65536 + raise ArgumentError, + "Expected unsigned 16-bit number for time_hi_and_version, " + + "got #{time_hi_and_version}." + end + unless clock_seq_hi_and_reserved >= 0 && clock_seq_hi_and_reserved < 256 + raise ArgumentError, + "Expected unsigned 8-bit number for clock_seq_hi_and_reserved, " + + "got #{clock_seq_hi_and_reserved}." + end + unless clock_seq_low >= 0 && clock_seq_low < 256 + raise ArgumentError, + "Expected unsigned 8-bit number for clock_seq_low, " + + "got #{clock_seq_low}." + end + unless nodes.kind_of?(Enumerable) + raise TypeError, + "Expected Enumerable, got #{nodes.class.name}." + end + unless nodes.size == 6 + raise ArgumentError, + "Expected nodes to have size of 6." + end + for node in nodes + unless node >= 0 && node < 256 + raise ArgumentError, + "Expected unsigned 8-bit number for each node, " + + "got #{node}." + end + end + @time_low = time_low + @time_mid = time_mid + @time_hi_and_version = time_hi_and_version + @clock_seq_hi_and_reserved = clock_seq_hi_and_reserved + @clock_seq_low = clock_seq_low + @nodes = nodes + end + + ## + # Returns the value of attribute `time_low` + attr_accessor :time_low + + ## + # Returns the value of attribute `time_mid` + attr_accessor :time_mid + + ## + # Returns the value of attribute `time_hi_and_version` + attr_accessor :time_hi_and_version + + ## + # Returns the value of attribute `clock_seq_hi_and_reserved` + attr_accessor :clock_seq_hi_and_reserved + + ## + # Returns the value of attribute `clock_seq_low` + attr_accessor :clock_seq_low + + ## + # Returns the value of attribute `nodes` + attr_accessor :nodes + + ## + # Parses a UUID from a string. + def self.parse(uuid_string) + unless uuid_string.kind_of? String + raise TypeError, + "Expected String, got #{uuid_string.class.name} instead." + end + uuid_components = uuid_string.downcase.scan(UUIDTools::UUID_REGEXP).first + raise ArgumentError, "Invalid UUID format." if uuid_components.nil? + time_low = uuid_components[0].to_i(16) + time_mid = uuid_components[1].to_i(16) + time_hi_and_version = uuid_components[2].to_i(16) + clock_seq_hi_and_reserved = uuid_components[3].to_i(16) + clock_seq_low = uuid_components[4].to_i(16) + nodes = [] + for i in 0..5 + nodes << uuid_components[5][(i * 2)..(i * 2) + 1].to_i(16) + end + return self.new(time_low, time_mid, time_hi_and_version, + clock_seq_hi_and_reserved, clock_seq_low, nodes) + end + + ## + # Parses a UUID from a raw byte string. + def self.parse_raw(raw_string) + unless raw_string.kind_of? String + raise TypeError, + "Expected String, got #{raw_string.class.name} instead." + end + integer = self.convert_byte_string_to_int(raw_string) + + time_low = (integer >> 96) & 0xFFFFFFFF + time_mid = (integer >> 80) & 0xFFFF + time_hi_and_version = (integer >> 64) & 0xFFFF + clock_seq_hi_and_reserved = (integer >> 56) & 0xFF + clock_seq_low = (integer >> 48) & 0xFF + nodes = [] + for i in 0..5 + nodes << ((integer >> (40 - (i * 8))) & 0xFF) + end + return self.new(time_low, time_mid, time_hi_and_version, + clock_seq_hi_and_reserved, clock_seq_low, nodes) + end + + ## + # Parses a UUID from an Integer. + def self.parse_int(uuid_int) + unless uuid_int.kind_of?(Integer) + raise ArgumentError, + "Expected Integer, got #{uuid_int.class.name} instead." + end + return self.parse_raw(self.convert_int_to_byte_string(uuid_int, 16)) + end + + ## + # Parse a UUID from a hexdigest String. + def self.parse_hexdigest(uuid_hexdigest) + unless uuid_hexdigest.kind_of?(String) + raise ArgumentError, + "Expected String, got #{uuid_hexdigest.class.name} instead." + end + return self.parse_int(uuid_hexdigest.to_i(16)) + end + + ## + # Creates a UUID from a random value. + def self.random_create() + new_uuid = self.parse_raw(SecureRandom.random_bytes(16)) + new_uuid.time_hi_and_version &= 0x0FFF + new_uuid.time_hi_and_version |= (4 << 12) + new_uuid.clock_seq_hi_and_reserved &= 0x3F + new_uuid.clock_seq_hi_and_reserved |= 0x80 + return new_uuid + end + + ## + # Creates a UUID from a timestamp. + def self.timestamp_create(timestamp=nil) + # We need a lock here to prevent two threads from ever + # getting the same timestamp. + @@mutex.synchronize do + # Always use GMT to generate UUIDs. + if timestamp.nil? + gmt_timestamp = Time.now.gmtime + else + gmt_timestamp = timestamp.gmtime + end + # Convert to 100 nanosecond blocks + gmt_timestamp_100_nanoseconds = (gmt_timestamp.tv_sec * 10000000) + + (gmt_timestamp.tv_usec * 10) + 0x01B21DD213814000 + mac_address = self.mac_address + node_id = 0 + if mac_address != nil + nodes = mac_address.split(":").collect do |octet| + octet.to_i(16) + end + else + nodes = SecureRandom.random_bytes(6).unpack("C*") + nodes[0] |= 0b00000001 + end + for i in 0..5 + node_id += (nodes[i] << (40 - (i * 8))) + end + clock_sequence = @@last_clock_sequence + if clock_sequence.nil? + clock_sequence = self.convert_byte_string_to_int( + SecureRandom.random_bytes(16) + ) + end + if @@last_node_id != nil && @@last_node_id != node_id + # The node id has changed. Change the clock id. + clock_sequence = self.convert_byte_string_to_int( + SecureRandom.random_bytes(16) + ) + elsif @@last_timestamp != nil && + gmt_timestamp_100_nanoseconds <= @@last_timestamp + clock_sequence = clock_sequence + 1 + end + @@last_timestamp = gmt_timestamp_100_nanoseconds + @@last_node_id = node_id + @@last_clock_sequence = clock_sequence + + time_low = gmt_timestamp_100_nanoseconds & 0xFFFFFFFF + time_mid = ((gmt_timestamp_100_nanoseconds >> 32) & 0xFFFF) + time_hi_and_version = ((gmt_timestamp_100_nanoseconds >> 48) & 0x0FFF) + time_hi_and_version |= (1 << 12) + clock_seq_low = clock_sequence & 0xFF; + clock_seq_hi_and_reserved = (clock_sequence & 0x3F00) >> 8 + clock_seq_hi_and_reserved |= 0x80 + + return self.new(time_low, time_mid, time_hi_and_version, + clock_seq_hi_and_reserved, clock_seq_low, nodes) + end + end + + ## + # Creates a UUID using the MD5 hash. (Version 3) + def self.md5_create(namespace, name) + return self.create_from_hash(Digest::MD5, namespace, name) + end + + ## + # Creates a UUID using the SHA1 hash. (Version 5) + def self.sha1_create(namespace, name) + return self.create_from_hash(Digest::SHA1, namespace, name) + end + + ## + # This method applies only to version 1 UUIDs. + # Checks if the node ID was generated from a random number + # or from an IEEE 802 address (MAC address). + # Always returns false for UUIDs that aren't version 1. + # This should not be confused with version 4 UUIDs where + # more than just the node id is random. + def random_node_id? + return false if self.version != 1 + return ((self.nodes.first & 0x01) == 1) + end + + ## + # Returns true if this UUID is the + # nil UUID (00000000-0000-0000-0000-000000000000). + def nil_uuid? + return false if self.time_low != 0 + return false if self.time_mid != 0 + return false if self.time_hi_and_version != 0 + return false if self.clock_seq_hi_and_reserved != 0 + return false if self.clock_seq_low != 0 + self.nodes.each do |node| + return false if node != 0 + end + return true + end + + ## + # Returns the UUID version type. + # Possible values: + # 1 - Time-based with unique or random host identifier + # 2 - DCE Security version (with POSIX UIDs) + # 3 - Name-based (MD5 hash) + # 4 - Random + # 5 - Name-based (SHA-1 hash) + def version + return (time_hi_and_version >> 12) + end + + ## + # Returns the UUID variant. + # Possible values: + # 0b000 - Reserved, NCS backward compatibility. + # 0b100 - The variant specified in this document. + # 0b110 - Reserved, Microsoft Corporation backward compatibility. + # 0b111 - Reserved for future definition. + def variant + variant_raw = (clock_seq_hi_and_reserved >> 5) + result = nil + if (variant_raw >> 2) == 0 + result = 0x000 + elsif (variant_raw >> 1) == 2 + result = 0x100 + else + result = variant_raw + end + return (result >> 6) + end + + ## + # Returns true if this UUID is valid. + def valid? + if [0b000, 0b100, 0b110, 0b111].include?(self.variant) && + (1..5).include?(self.version) + return true + else + return false + end + end + + ## + # Returns the IEEE 802 address used to generate this UUID or + # nil if a MAC address was not used. + def mac_address + return nil if self.version != 1 + return nil if self.random_node_id? + return (self.nodes.collect do |node| + sprintf("%2.2x", node) + end).join(":") + end + + ## + # Returns the timestamp used to generate this UUID + def timestamp + return nil if self.version != 1 + gmt_timestamp_100_nanoseconds = 0 + gmt_timestamp_100_nanoseconds += + ((self.time_hi_and_version & 0x0FFF) << 48) + gmt_timestamp_100_nanoseconds += (self.time_mid << 32) + gmt_timestamp_100_nanoseconds += self.time_low + return Time.at( + (gmt_timestamp_100_nanoseconds - 0x01B21DD213814000) / 10000000.0) + end + + ## + # Compares two UUIDs lexically + def <=>(other_uuid) + return nil unless other_uuid.is_a?(UUIDTools::UUID) + check = self.time_low <=> other_uuid.time_low + return check if check != 0 + check = self.time_mid <=> other_uuid.time_mid + return check if check != 0 + check = self.time_hi_and_version <=> other_uuid.time_hi_and_version + return check if check != 0 + check = self.clock_seq_hi_and_reserved <=> + other_uuid.clock_seq_hi_and_reserved + return check if check != 0 + check = self.clock_seq_low <=> other_uuid.clock_seq_low + return check if check != 0 + for i in 0..5 + if (self.nodes[i] < other_uuid.nodes[i]) + return -1 + end + if (self.nodes[i] > other_uuid.nodes[i]) + return 1 + end + end + return 0 + end + + ## + # Returns a representation of the object's state + def inspect + return "#" + end + + ## + # Returns the hex digest of the UUID object. + def hexdigest + (self.frozen? ? + generate_hexdigest : (@hexdigest ||= generate_hexdigest) + ).dup + end + + ## + # Returns the raw bytes that represent this UUID. + def raw + (self.frozen? ? generate_raw : (@raw ||= generate_raw)).dup + end + + ## + # Returns a string representation for this UUID. + def to_s + (self.frozen? ? generate_s : (@string ||= generate_s)).dup + end + alias_method :to_str, :to_s + + ## + # Returns an integer representation for this UUID. + def to_i + self.frozen? ? generate_i : (@integer ||= generate_i) + end + + ## + # Returns a URI string for this UUID. + def to_uri + return "urn:uuid:#{self.to_s}" + end + + ## + # Returns an integer hash value. + def hash + self.frozen? ? generate_hash : (@hash ||= generate_hash) + end + + protected + ## + # Generates the hex digest of the UUID object. + # + # @api private + def generate_hexdigest + return self.to_i.to_s(16).rjust(32, "0") + end + + # Generates an integer hash value. + # + # @api private + def generate_hash + return self.to_i % 0x3fffffff + end + + ## + # Generates an integer representation for this UUID. + # + # @api private + def generate_i + return (begin + bytes = (time_low << 96) + (time_mid << 80) + + (time_hi_and_version << 64) + (clock_seq_hi_and_reserved << 56) + + (clock_seq_low << 48) + for i in 0..5 + bytes += (nodes[i] << (40 - (i * 8))) + end + bytes + end) + end + + ## + # Generates a string representation for this UUID. + # + # @api private + def generate_s + result = sprintf("%8.8x-%4.4x-%4.4x-%2.2x%2.2x-", @time_low, @time_mid, + @time_hi_and_version, @clock_seq_hi_and_reserved, @clock_seq_low); + for i in 0..5 + result << sprintf("%2.2x", @nodes[i]) + end + return result.downcase + end + + ## + # Generates the raw bytes that represent this UUID. + # + # @api private + def generate_raw + return self.class.convert_int_to_byte_string(self.to_i, 16) + end + + public + ## + # Returns true if this UUID is exactly equal to the other UUID. + def eql?(other) + return self == other + end + + # + # Determine what OS we're running on. Helps decide how to find the MAC + # + def self.os_class + require 'rbconfig' + os_platform = RbConfig::CONFIG['target_os'] + os_class = nil + if (os_platform =~ /win/i && !(os_platform =~ /darwin/i)) || + os_platform =~ /w32/i + os_class = :windows + elsif os_platform =~ /solaris/i + os_class = :solaris + elsif os_platform =~ /netbsd/i + os_class = :netbsd + elsif os_platform =~ /openbsd/i + os_class = :openbsd + end + end + + # making these class variables helps with testing + @ifconfig_command = "ifconfig" + @ifconfig_path_default = "/sbin/ifconfig" + @ip_command = "ip" + @ip_path_default = "/sbin/ip" + + class << self + attr_accessor :ifconfig_command, :ifconfig_path_default + attr_accessor :ip_command, :ip_path_default + end + + # + # Find the path of the ifconfig(8) command if it is present + # + def self.ifconfig_path + path = `which #{UUID.ifconfig_command} 2>/dev/null`.strip + path = UUID.ifconfig_path_default if (path == "" && File.exist?(UUID.ifconfig_path_default)) + return (path === "" ? nil : path) + end + + # + # Find the path of the ip(8) command if it is present + # + def self.ip_path + path = `which #{UUID.ip_command} 2>/dev/null`.strip + path = UUID.ip_path_default if (path == "" && File.exist?(UUID.ip_path_default)) + return (path === "" ? nil : path) + end + + # + # Call the ifconfig or ip command that is found + # + def self.ifconfig(all=nil) + # find the path of the ifconfig command + ifconfig_path = UUID.ifconfig_path + + # if it does not exist, try the ip command + if ifconfig_path == nil + ifconfig_path = "#{UUID.ip_path} addr list" + # all makes no sense when using ip(1) + all = nil + end + + all_switch = all == nil ? "" : "-a" + return `#{ifconfig_path} #{all_switch}` if not ifconfig_path == nil + end + + # Match and return the first Mac address found + def self.first_mac(instring) + mac_regexps = [ + Regexp.new("address:? (#{(["[0-9a-fA-F]{2}"] * 6).join(":")})"), + Regexp.new("addr:? (#{(["[0-9a-fA-F]{2}"] * 6).join(":")})"), + Regexp.new("ether:? (#{(["[0-9a-fA-F]{1,2}"] * 6).join(":")})"), + Regexp.new("HWaddr:? (#{(["[0-9a-fA-F]{2}"] * 6).join(":")})"), + Regexp.new("link/ether? (#{(["[0-9a-fA-F]{2}"] * 6).join(":")})"), + Regexp.new("(#{(["[0-9a-fA-F]{2}"] * 6).join(":")})"), + Regexp.new("(#{(["[0-9a-fA-F]{2}"] * 6).join("-")})") + ] + parse_mac = lambda do |output| + (mac_regexps.map do |regexp| + result = output[regexp, 1] + result.downcase.gsub(/-/, ":") if result != nil + end).compact.first + end + + mac = parse_mac.call(instring) + if mac + # expand octets that were compressed (solaris) + return (mac.split(':').map do |octet| + (octet.length == 1 ? "0#{octet}" : octet) + end).join(':') + else + return nil + end + end + + ## + # Returns the MAC address of the current computer's network card. + # Returns nil if a MAC address could not be found. + def self.mac_address + if !defined?(@@mac_address) + require 'rbconfig' + + os_class = UUID.os_class + + if os_class == :windows + begin + @@mac_address = UUID.first_mac `ipconfig /all` + rescue + end + else # linux, bsd, macos, solaris + @@mac_address = UUID.first_mac(UUID.ifconfig(:all)) + end + + if @@mac_address != nil + if @@mac_address.respond_to?(:to_str) + @@mac_address = @@mac_address.to_str + else + @@mac_address = @@mac_address.to_s + end + @@mac_address.downcase! + @@mac_address.strip! + end + + # Verify that the MAC address is in the right format. + # Nil it out if it isn't. + unless @@mac_address.respond_to?(:scan) && + @@mac_address.scan(/#{(["[0-9a-f]{2}"] * 6).join(":")}/) + @@mac_address = nil + end + end + return @@mac_address + end + + ## + # Allows users to set the MAC address manually in cases where the MAC + # address cannot be obtained programatically. + def self.mac_address=(new_mac_address) + @@mac_address = new_mac_address + end + + # The following methods are not part of the public API, + # and generally should not be called directly. + + + ## + # Creates a new UUID from a SHA1 or MD5 hash + # + # @api private + def self.create_from_hash(hash_class, namespace, name) + if hash_class == Digest::MD5 + version = 3 + elsif hash_class == Digest::SHA1 + version = 5 + else + raise ArgumentError, + "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}." + end + hash = hash_class.new + hash.update(namespace.raw) + hash.update(name) + hash_string = hash.to_s[0..31] + new_uuid = self.parse("#{hash_string[0..7]}-#{hash_string[8..11]}-" + + "#{hash_string[12..15]}-#{hash_string[16..19]}-#{hash_string[20..31]}") + + new_uuid.time_hi_and_version &= 0x0FFF + new_uuid.time_hi_and_version |= (version << 12) + new_uuid.clock_seq_hi_and_reserved &= 0x3F + new_uuid.clock_seq_hi_and_reserved |= 0x80 + return new_uuid + end + + ## + # @api private + def self.convert_int_to_byte_string(integer, size) + byte_string = "" + if byte_string.respond_to?(:force_encoding) + byte_string.force_encoding(Encoding::ASCII_8BIT) + end + for i in 0..(size - 1) + byte_string << ((integer >> (((size - 1) - i) * 8)) & 0xFF) + end + return byte_string + end + + ## + # @api private + def self.convert_byte_string_to_int(byte_string) + if byte_string.respond_to?(:force_encoding) + byte_string.force_encoding(Encoding::ASCII_8BIT) + end + integer = 0 + size = byte_string.size + for i in 0..(size - 1) + ordinal = (byte_string[i].respond_to?(:ord) ? + byte_string[i].ord : byte_string[i]) + integer += (ordinal << (((size - 1) - i) * 8)) + end + return integer + end + end + + ## + # Constant Regexp that matches a UUID and captures its components. + UUID_REGEXP = Regexp.new("^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-" + + "([0-9a-f]{2})([0-9a-f]{2})-([0-9a-f]{12})$") + + ## + # Constant that represents the DNS namespace. + UUID_DNS_NAMESPACE = UUID.parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + + ## + # Constant that represents the URL namespace. + UUID_URL_NAMESPACE = UUID.parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8") + + ## + # Constant that represents the OID namespace. + UUID_OID_NAMESPACE = UUID.parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8") + + ## + # Constant that represents the X500 namespace. + UUID_X500_NAMESPACE = UUID.parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8") +end diff --git a/gxg/todo/xmlsimple/xmlsimple.rb b/gxg/todo/xmlsimple/xmlsimple.rb new file mode 100644 index 0000000..963049e --- /dev/null +++ b/gxg/todo/xmlsimple/xmlsimple.rb @@ -0,0 +1,1040 @@ +# = XmlSimple +# +# Author:: Maik Schmidt +# Copyright:: Copyright (c) 2003-2014 Maik Schmidt +# License:: Distributes under the same terms as Ruby. +# +require 'rexml/document' +require 'stringio' + +# Easy API to maintain XML (especially configuration files). +class XmlSimple + include REXML + + @@VERSION = '1.1.5' + + # A simple cache for XML documents that were already transformed + # by xml_in. + class Cache + # Creates and initializes a new Cache object. + def initialize + @mem_share_cache = {} + @mem_copy_cache = {} + end + + # Saves a data structure into a file. + # + # data:: + # Data structure to be saved. + # filename:: + # Name of the file belonging to the data structure. + def save_storable(data, filename) + cache_file = get_cache_filename(filename) + File.open(cache_file, "w+") { |f| Marshal.dump(data, f) } + end + + # Restores a data structure from a file. If restoring the data + # structure failed for any reason, nil will be returned. + # + # filename:: + # Name of the file belonging to the data structure. + def restore_storable(filename) + cache_file = get_cache_filename(filename) + return nil unless File::exist?(cache_file) + return nil unless File::mtime(cache_file).to_i > File::mtime(filename).to_i + data = nil + File.open(cache_file) { |f| data = Marshal.load(f) } + data + end + + # Saves a data structure in a shared memory cache. + # + # data:: + # Data structure to be saved. + # filename:: + # Name of the file belonging to the data structure. + def save_mem_share(data, filename) + @mem_share_cache[filename] = [Time::now.to_i, data] + end + + # Restores a data structure from a shared memory cache. You + # should consider these elements as "read only". If restoring + # the data structure failed for any reason, nil will be + # returned. + # + # filename:: + # Name of the file belonging to the data structure. + def restore_mem_share(filename) + get_from_memory_cache(filename, @mem_share_cache) + end + + # Copies a data structure to a memory cache. + # + # data:: + # Data structure to be copied. + # filename:: + # Name of the file belonging to the data structure. + def save_mem_copy(data, filename) + @mem_share_cache[filename] = [Time::now.to_i, Marshal.dump(data)] + end + + # Restores a data structure from a memory cache. If restoring + # the data structure failed for any reason, nil will be + # returned. + # + # filename:: + # Name of the file belonging to the data structure. + def restore_mem_copy(filename) + data = get_from_memory_cache(filename, @mem_share_cache) + data = Marshal.load(data) unless data.nil? + data + end + + private + + # Returns the "cache filename" belonging to a filename, i.e. + # the extension '.xml' in the original filename will be replaced + # by '.stor'. If filename does not have this extension, '.stor' + # will be appended. + # + # filename:: + # Filename to get "cache filename" for. + def get_cache_filename(filename) + filename.sub(/(\.xml)?$/, '.stor') + end + + # Returns a cache entry from a memory cache belonging to a + # certain filename. If no entry could be found for any reason, + # nil will be returned. + # + # filename:: + # Name of the file the cache entry belongs to. + # cache:: + # Memory cache to get entry from. + def get_from_memory_cache(filename, cache) + return nil unless cache[filename] + return nil unless cache[filename][0] > File::mtime(filename).to_i + return cache[filename][1] + end + end + + # Create a "global" cache. + @@cache = Cache.new + + # Creates and initializes a new XmlSimple object. + # + # defaults:: + # Default values for options. + def initialize(defaults = nil) + unless defaults.nil? || defaults.is_a?(Hash) + raise ArgumentError, "Options have to be a Hash." + end + @default_options = normalize_option_names(defaults, (KNOWN_OPTIONS['in'] + KNOWN_OPTIONS['out']).uniq) + @options = Hash.new + @_var_values = nil + end + + # Converts an XML document in the same way as the Perl module XML::Simple. + # + # string:: + # XML source. Could be one of the following: + # + # - nil: Tries to load and parse '.xml'. + # - filename: Tries to load and parse filename. + # - IO object: Reads from object until EOF is detected and parses result. + # - XML string: Parses string. + # + # options:: + # Options to be used. + def xml_in(string = nil, options = nil) + handle_options('in', options) + + # If no XML string or filename was supplied look for scriptname.xml. + if string.nil? + string = File::basename($0).dup + string = string.sub(/\.[^.]+$/, '') + string += '.xml' + + directory = File::dirname($0) + @options['searchpath'].unshift(directory) unless directory.nil? + end + + if string.is_a?(String) + if string =~ /<.*?>/m + @doc = parse(string) + elsif string == '-' + @doc = parse($stdin.read) + else + filename = find_xml_file(string, @options['searchpath']) + + if @options.has_key?('cache') + @options['cache'].each { |scheme| + case(scheme) + when 'storable' + content = @@cache.restore_storable(filename) + when 'mem_share' + content = @@cache.restore_mem_share(filename) + when 'mem_copy' + content = @@cache.restore_mem_copy(filename) + else + raise ArgumentError, "Unsupported caching scheme: <#{scheme}>." + end + return content if content + } + end + + @doc = load_xml_file(filename) + end + elsif string.respond_to?(:read) + @doc = parse(string.read) + else + raise ArgumentError, "Could not parse object of type: <#{string.class}>." + end + + result = collapse(@doc.root) + result = @options['keeproot'] ? merge({}, @doc.root.name, result) : result + put_into_cache(result, filename) + result + end + + # This is the functional version of the instance method xml_in. + def XmlSimple.xml_in(string = nil, options = nil) + xml_simple = XmlSimple.new + xml_simple.xml_in(string, options) + end + + # Converts a data structure into an XML document. + # + # ref:: + # Reference to data structure to be converted into XML. + # options:: + # Options to be used. + def xml_out(ref, options = nil) + handle_options('out', options) + if ref.is_a?(Array) + ref = { @options['anonymoustag'] => ref } + end + + if @options['keeproot'] + keys = ref.keys + if keys.size == 1 + ref = ref[keys[0]] + @options['rootname'] = keys[0] + end + elsif @options['rootname'] == '' + if ref.is_a?(Hash) + refsave = ref + ref = {} + refsave.each { |key, value| + if !scalar(value) + ref[key] = value + else + ref[key] = [ value.to_s ] + end + } + end + end + + @ancestors = [] + xml = value_to_xml(ref, @options['rootname'], '') + @ancestors = nil + + if @options['xmldeclaration'] + xml = @options['xmldeclaration'] + "\n" + xml + end + + if @options.has_key?('outputfile') + if @options['outputfile'].kind_of?(IO) + return @options['outputfile'].write(xml) + else + File.open(@options['outputfile'], "w") { |file| file.write(xml) } + end + end + xml + end + + # This is the functional version of the instance method xml_out. + def XmlSimple.xml_out(hash, options = nil) + xml_simple = XmlSimple.new + xml_simple.xml_out(hash, options) + end + + private + + # Declare options that are valid for xml_in and xml_out. + KNOWN_OPTIONS = { + 'in' => %w( + keyattr keeproot forcecontent contentkey noattr searchpath + forcearray suppressempty anonymoustag cache grouptags + normalisespace normalizespace variables varattr keytosymbol + attrtosymbol attrprefix conversions + ), + 'out' => %w( + keyattr keeproot contentkey noattr rootname + xmldeclaration outputfile noescape suppressempty + anonymoustag indent grouptags noindent attrprefix + ) + } + + # Define some reasonable defaults. + DEF_KEY_ATTRIBUTES = [] + DEF_ROOT_NAME = 'opt' + DEF_CONTENT_KEY = 'content' + DEF_XML_DECLARATION = "" + DEF_ANONYMOUS_TAG = 'anon' + DEF_FORCE_ARRAY = true + DEF_INDENTATION = ' ' + DEF_KEY_TO_SYMBOL = false + DEF_ATTR_TO_SYMBOL = false + + # Normalizes option names in a hash, i.e., turns all + # characters to lower case and removes all underscores. + # Additionally, this method checks if an unknown option + # was used, and raises an according exception. + # + # options:: + # Hash to be normalized. + # known_options:: + # List of known options. + def normalize_option_names(options, known_options) + return nil if options.nil? + result = Hash.new + options.each { |key, value| + lkey = key.to_s.downcase.gsub(/_/, '') + if !known_options.member?(lkey) + raise ArgumentError, "Unrecognized option: #{lkey}." + end + result[lkey] = value + } + result + end + + # Merges a set of options with the default options. + # + # direction:: + # 'in': If options should be handled for xml_in. + # 'out': If options should be handled for xml_out. + # options:: + # Options to be merged with the default options. + def handle_options(direction, options) + @options = options || Hash.new + + raise ArgumentError, "Options must be a Hash!" unless @options.is_a?(Hash) + + unless KNOWN_OPTIONS.has_key?(direction) + raise ArgumentError, "Unknown direction: <#{direction}>." + end + + known_options = KNOWN_OPTIONS[direction] + @options = normalize_option_names(@options, known_options) + + unless @default_options.nil? + known_options.each { |option| + unless @options.has_key?(option) + if @default_options.has_key?(option) + @options[option] = @default_options[option] + end + end + } + end + + unless @options.has_key?('noattr') + @options['noattr'] = false + end + + if @options.has_key?('rootname') + @options['rootname'] = '' if @options['rootname'].nil? + else + @options['rootname'] = DEF_ROOT_NAME + end + + if @options.has_key?('xmldeclaration') && @options['xmldeclaration'] == true + @options['xmldeclaration'] = DEF_XML_DECLARATION + end + + @options['keytosymbol'] = DEF_KEY_TO_SYMBOL unless @options.has_key?('keytosymbol') + + @options['attrtosymbol'] = DEF_ATTR_TO_SYMBOL unless @options.has_key?('attrtosymbol') + + if @options.has_key?('contentkey') + if @options['contentkey'] =~ /^-(.*)$/ + @options['contentkey'] = $1 + @options['collapseagain'] = true + end + else + @options['contentkey'] = DEF_CONTENT_KEY + end + + unless @options.has_key?('normalisespace') + @options['normalisespace'] = @options['normalizespace'] + end + @options['normalisespace'] = 0 if @options['normalisespace'].nil? + + if @options.has_key?('searchpath') + unless @options['searchpath'].is_a?(Array) + @options['searchpath'] = [ @options['searchpath'] ] + end + else + @options['searchpath'] = [] + end + + if @options.has_key?('cache') && scalar(@options['cache']) + @options['cache'] = [ @options['cache'] ] + end + + @options['anonymoustag'] = DEF_ANONYMOUS_TAG unless @options.has_key?('anonymoustag') + + if !@options.has_key?('indent') || @options['indent'].nil? + @options['indent'] = DEF_INDENTATION + end + + @options['indent'] = '' if @options.has_key?('noindent') + + # Special cleanup for 'keyattr' which could be an array or + # a hash or left to default to array. + if @options.has_key?('keyattr') + if !scalar(@options['keyattr']) + # Convert keyattr => { elem => '+attr' } + # to keyattr => { elem => ['attr', '+'] } + if @options['keyattr'].is_a?(Hash) + @options['keyattr'].each { |key, value| + if value =~ /^([-+])?(.*)$/ + @options['keyattr'][key] = [$2, $1 ? $1 : ''] + end + } + elsif !@options['keyattr'].is_a?(Array) + raise ArgumentError, "'keyattr' must be String, Hash, or Array!" + end + else + @options['keyattr'] = [ @options['keyattr'] ] + end + else + @options['keyattr'] = DEF_KEY_ATTRIBUTES + end + + if @options.has_key?('forcearray') + if @options['forcearray'].is_a?(Regexp) + @options['forcearray'] = [ @options['forcearray'] ] + end + + if @options['forcearray'].is_a?(Array) + force_list = @options['forcearray'] + unless force_list.empty? + @options['forcearray'] = {} + force_list.each { |tag| + if tag.is_a?(Regexp) + unless @options['forcearray']['_regex'].is_a?(Array) + @options['forcearray']['_regex'] = [] + end + @options['forcearray']['_regex'] << tag + else + @options['forcearray'][tag] = true + end + } + else + @options['forcearray'] = false + end + else + @options['forcearray'] = @options['forcearray'] ? true : false + end + else + @options['forcearray'] = DEF_FORCE_ARRAY + end + + if @options.has_key?('grouptags') && !@options['grouptags'].is_a?(Hash) + raise ArgumentError, "Illegal value for 'GroupTags' option - expected a Hash." + end + + if @options.has_key?('variables') && !@options['variables'].is_a?(Hash) + raise ArgumentError, "Illegal value for 'Variables' option - expected a Hash." + end + + if @options.has_key?('variables') + @_var_values = @options['variables'] + elsif @options.has_key?('varattr') + @_var_values = {} + end + end + + # Actually converts an XML document element into a data structure. + # + # element:: + # The document element to be collapsed. + def collapse(element) + result = @options['noattr'] ? {} : get_attributes(element) + + if @options['normalisespace'] == 2 + result.each { |k, v| result[k] = normalise_space(v) } + end + + if element.has_elements? + element.each_element { |child| + value = collapse(child) + if empty(value) && (element.attributes.empty? || @options['noattr']) + next if @options.has_key?('suppressempty') && @options['suppressempty'] == true + end + result = merge(result, child.name, value) + } + if has_mixed_content?(element) + # normalisespace? + content = element.texts.map { |x| x.to_s } + content = content[0] if content.size == 1 + result[@options['contentkey']] = content + end + elsif element.has_text? # i.e. it has only text. + return collapse_text_node(result, element) + end + + # Turn Arrays into Hashes if key fields present. + count = fold_arrays(result) + + # Disintermediate grouped tags. + if @options.has_key?('grouptags') + result.each { |key, value| + next unless (value.is_a?(Hash) && (value.size == 1)) + child_key, child_value = value.to_a[0] + if @options['grouptags'][key] == child_key + result[key] = child_value + end + } + end + + # Fold Hashes containing a single anonymous Array up into just the Array. + if count == 1 + anonymoustag = @options['anonymoustag'] + if result.has_key?(anonymoustag) && result[anonymoustag].is_a?(Array) + return result[anonymoustag] + end + end + + if result.empty? && @options.has_key?('suppressempty') + return @options['suppressempty'] == '' ? '' : nil + end + + result + end + + # Collapses a text node and merges it with an existing Hash, if + # possible. + # Thanks to Curtis Schofield for reporting a subtle bug. + # + # hash:: + # Hash to merge text node value with, if possible. + # element:: + # Text node to be collapsed. + def collapse_text_node(hash, element) + value = node_to_text(element) + if empty(value) && !element.has_attributes? + return {} + end + + if element.has_attributes? && !@options['noattr'] + return merge(hash, @options['contentkey'], value) + else + if @options['forcecontent'] + return merge(hash, @options['contentkey'], value) + else + return value + end + end + end + + # Folds all arrays in a Hash. + # + # hash:: + # Hash to be folded. + def fold_arrays(hash) + fold_amount = 0 + keyattr = @options['keyattr'] + if (keyattr.is_a?(Array) || keyattr.is_a?(Hash)) + hash.each { |key, value| + if value.is_a?(Array) + if keyattr.is_a?(Array) + hash[key] = fold_array(value) + else + hash[key] = fold_array_by_name(key, value) + end + fold_amount += 1 + end + } + end + fold_amount + end + + # Folds an Array to a Hash, if possible. Folding happens + # according to the content of keyattr, which has to be + # an array. + # + # array:: + # Array to be folded. + def fold_array(array) + hash = Hash.new + array.each { |x| + return array unless x.is_a?(Hash) + key_matched = false + @options['keyattr'].each { |key| + if x.has_key?(key) + key_matched = true + value = x[key] + return array if value.is_a?(Hash) || value.is_a?(Array) + value = normalise_space(value) if @options['normalisespace'] == 1 + x.delete(key) + hash[value] = x + break + end + } + return array unless key_matched + } + hash = collapse_content(hash) if @options['collapseagain'] + hash + end + + # Folds an Array to a Hash, if possible. Folding happens + # according to the content of keyattr, which has to be + # a Hash. + # + # name:: + # Name of the attribute to be folded upon. + # array:: + # Array to be folded. + def fold_array_by_name(name, array) + return array unless @options['keyattr'].has_key?(name) + key, flag = @options['keyattr'][name] + + hash = Hash.new + array.each { |x| + if x.is_a?(Hash) && x.has_key?(key) + value = x[key] + return array if value.is_a?(Hash) || value.is_a?(Array) + value = normalise_space(value) if @options['normalisespace'] == 1 + hash[value] = x + hash[value]["-#{key}"] = hash[value][key] if flag == '-' + hash[value].delete(key) unless flag == '+' + else + $stderr.puts("Warning: <#{name}> element has no '#{key}' attribute.") + return array + end + } + hash = collapse_content(hash) if @options['collapseagain'] + hash + end + + # Tries to collapse a Hash even more ;-) + # + # hash:: + # Hash to be collapsed again. + def collapse_content(hash) + content_key = @options['contentkey'] + hash.each_value { |value| + return hash unless value.is_a?(Hash) && value.size == 1 && value.has_key?(content_key) + hash.each_key { |key| hash[key] = hash[key][content_key] } + } + hash + end + + # Adds a new key/value pair to an existing Hash. If the key to be added + # does already exist and the existing value associated with key is not + # an Array, it will be converted into an Array. Then the new value is + # appended to that Array. + # + # hash:: + # Hash to add key/value pair to. + # key:: + # Key to be added. + # value:: + # Value to be associated with key. + def merge(hash, key, value) + if value.is_a?(String) + value = normalise_space(value) if @options['normalisespace'] == 2 + + if conv = @options['conversions'] and conv = conv.find {|c,_| c.match(key)} and conv = conv.at(1) + value = conv.call(value) + end + + # do variable substitutions + unless @_var_values.nil? || @_var_values.empty? + value = value.gsub(/\$\{(\w+)\}/) { |x| get_var($1) } + end + + # look for variable definitions + if @options.has_key?('varattr') + varattr = @options['varattr'] + if hash.has_key?(varattr) + set_var(hash[varattr], value) + end + end + end + + #patch for converting keys to symbols + if @options.has_key?('keytosymbol') + if @options['keytosymbol'] == true + key = key.to_s.downcase.to_sym + end + end + + if hash.has_key?(key) + if hash[key].is_a?(Array) + hash[key] << value + else + hash[key] = [ hash[key], value ] + end + elsif value.is_a?(Array) # Handle anonymous arrays. + hash[key] = [ value ] + else + if force_array?(key) + hash[key] = [ value ] + else + hash[key] = value + end + end + hash + end + + # Checks, if the 'forcearray' option has to be used for + # a certain key. + def force_array?(key) + return false if key == @options['contentkey'] + return true if @options['forcearray'] == true + forcearray = @options['forcearray'] + if forcearray.is_a?(Hash) + return true if forcearray.has_key?(key) + return false unless forcearray.has_key?('_regex') + forcearray['_regex'].each { |x| return true if key =~ x } + end + return false + end + + # Converts the attributes array of a document node into a Hash. + # Returns an empty Hash, if node has no attributes. + # + # node:: + # Document node to extract attributes from. + def get_attributes(node) + attributes = {} + if @options['attrprefix'] + node.attributes.each { |n,v| attributes["@" + n] = v } + elsif @options.has_key?('attrtosymbol') and @options['attrtosymbol'] == true + #patch for converting attribute names to symbols + node.attributes.each { |n,v| attributes[n.to_sym] = v } + else + node.attributes.each { |n,v| attributes[n] = v } + end + + attributes + end + + # Determines, if a document element has mixed content. + # + # element:: + # Document element to be checked. + def has_mixed_content?(element) + element.has_text? && + element.has_elements? && + !element.texts.join('').strip.empty? + end + + # Called when a variable definition is encountered in the XML. + # A variable definition looks like + # value + # where attrname matches the varattr setting. + def set_var(name, value) + @_var_values[name] = value + end + + # Called during variable substitution to get the value for the + # named variable. + def get_var(name) + if @_var_values.has_key?(name) + return @_var_values[name] + else + return "${#{name}}" + end + end + + # Recurses through a data structure building up and returning an + # XML representation of that structure as a string. + # + # ref:: + # Reference to the data structure to be encoded. + # name:: + # The XML tag name to be used for this item. + # indent:: + # A string of spaces for use as the current indent level. + def value_to_xml(ref, name, indent) + named = !name.nil? && name != '' + nl = @options.has_key?('noindent') ? '' : "\n" + + if !scalar(ref) + if @ancestors.member?(ref) + raise ArgumentError, "Circular data structures not supported!" + end + @ancestors << ref + else + if named + return [indent, '<', name, '>', @options['noescape'] ? ref.to_s : escape_value(ref.to_s), '', nl].join('') + else + return ref.to_s + nl + end + end + + # Unfold hash to array if possible. + if ref.is_a?(Hash) && !ref.empty? && !@options['keyattr'].empty? && indent != '' + ref = hash_to_array(name, ref) + end + + result = [] + if ref.is_a?(Hash) + # Reintermediate grouped values if applicable. + if @options.has_key?('grouptags') + ref.each { |key, value| + if @options['grouptags'].has_key?(key) + ref[key] = { @options['grouptags'][key] => value } + end + } + end + + nested = [] + text_content = nil + if named + result << (indent + '<' + name) + end + + if !ref.empty? + ref.each { |key, value| + next if !key.nil? && key.to_s[0, 1] == '-' + if value.nil? + unless @options.has_key?('suppressempty') && @options['suppressempty'].nil? + raise ArgumentError, "Use of uninitialized value!" + end + value = {} + end + + # Check for the '@' attribute prefix to allow separation of attributes and elements + + if (@options['noattr'] || + (@options['attrprefix'] && !(key =~ /^@(.*)/)) || + !scalar(value) + ) && + key != @options['contentkey'] + nested << value_to_xml(value, key, indent + @options['indent']) + else + value = value.to_s + value = escape_value(value) unless @options['noescape'] + if key == @options['contentkey'] + text_content = value + else + result << (' ' + ($1||key) + '="' + value + '"') + end + end + } + else + text_content = '' + end + + if !nested.empty? || !text_content.nil? + if named + result << '>' + if !text_content.nil? + result << text_content + nested[0] = nested[0].sub(/^\s+/, '') if !nested.empty? + else + result << nl + end + if !nested.empty? + result << nested << indent + end + result << ('' + nl) + else + result << nested + end + else + result << (' />' + nl) + end + elsif ref.is_a?(Array) + ref.each { |value| + if scalar(value) + result << (indent + '<' + name + '>') + result << (@options['noescape'] ? value.to_s : escape_value(value.to_s)) + result << ('' + nl) + elsif value.is_a?(Hash) + result << value_to_xml(value, name, indent) + else + result << (indent + '<' + name + '>' + nl) + result << value_to_xml(value, @options['anonymoustag'], indent + @options['indent']) + result << (indent + '' + nl) + end + } + else + # Probably, this is obsolete. + raise ArgumentError, "Can't encode a value of type: #{ref.type}." + end + @ancestors.pop if !scalar(ref) + result.join('') + end + + # Checks, if a certain value is a "scalar" value. Whatever + # that will be in Ruby ... ;-) + # + # value:: + # Value to be checked. + def scalar(value) + return false if value.is_a?(Hash) || value.is_a?(Array) + return true + end + + # Attempts to unfold a hash of hashes into an array of hashes. Returns + # a reference to th array on success or the original hash, if unfolding + # is not possible. + # + # parent:: + # + # hashref:: + # Reference to the hash to be unfolded. + def hash_to_array(parent, hashref) + arrayref = [] + hashref.each { |key, value| + return hashref unless value.is_a?(Hash) + + if @options['keyattr'].is_a?(Hash) + return hashref unless @options['keyattr'].has_key?(parent) + arrayref << { @options['keyattr'][parent][0] => key }.update(value) + else + arrayref << { @options['keyattr'][0] => key }.update(value) + end + } + arrayref + end + + # Replaces XML markup characters by their external entities. + # + # data:: + # The string to be escaped. + def escape_value(data) + REXML::Text::normalize(data) + end + + # Removes leading and trailing whitespace and sequences of + # whitespaces from a string. + # + # text:: + # String to be normalised. + def normalise_space(text) + text.strip.gsub(/\s\s+/, ' ') + end + + # Checks, if an object is nil, an empty String or an empty Hash. + # Thanks to Norbert Gawor for a bugfix. + # + # value:: + # Value to be checked for emptyness. + def empty(value) + case value + when Hash + return value.empty? + when String + return value !~ /\S/m + else + return value.nil? + end + end + + # Converts a document node into a String. + # If the node could not be converted into a String + # for any reason, default will be returned. + # + # node:: + # Document node to be converted. + # default:: + # Value to be returned, if node could not be converted. + def node_to_text(node, default = nil) + if node.is_a?(REXML::Element) + node.texts.map { |t| t.value }.join('') + elsif node.is_a?(REXML::Attribute) + node.value.nil? ? default : node.value.strip + elsif node.is_a?(REXML::Text) + node.value.strip + else + default + end + end + + # Parses an XML string and returns the according document. + # + # xml_string:: + # XML string to be parsed. + # + # The following exception may be raised: + # + # REXML::ParseException:: + # If the specified file is not wellformed. + def parse(xml_string) + Document.new(xml_string) + end + + # Searches in a list of paths for a certain file. Returns + # the full path to the file, if it could be found. Otherwise, + # an exception will be raised. + # + # filename:: + # Name of the file to search for. + # searchpath:: + # List of paths to search in. + def find_xml_file(file, searchpath) + filename = File::basename(file) + + if filename != file + return file if File::file?(file) + else + searchpath.each { |path| + full_path = File::join(path, filename) + return full_path if File::file?(full_path) + } + end + + if searchpath.empty? + return file if File::file?(file) + raise ArgumentError, "File does not exist: #{file}." + end + raise ArgumentError, "Could not find <#{filename}> in <#{searchpath.join(':')}>" + end + + # Loads and parses an XML configuration file. + # + # filename:: + # Name of the configuration file to be loaded. + # + # The following exceptions may be raised: + # + # Errno::ENOENT:: + # If the specified file does not exist. + # REXML::ParseException:: + # If the specified file is not wellformed. + def load_xml_file(filename) + parse(IO::read(filename)) + end + + # Caches the data belonging to a certain file. + # + # data:: + # Data to be cached. + # filename:: + # Name of file the data was read from. + def put_into_cache(data, filename) + if @options.has_key?('cache') + @options['cache'].each { |scheme| + case(scheme) + when 'storable' + @@cache.save_storable(data, filename) + when 'mem_share' + @@cache.save_mem_share(data, filename) + when 'mem_copy' + @@cache.save_mem_copy(data, filename) + else + raise ArgumentError, "Unsupported caching scheme: <#{scheme}>." + end + } + end + end +end + +# vim:sw=2 diff --git a/gxg/web/gxg.rb b/gxg/web/gxg.rb new file mode 100644 index 0000000..93b4916 --- /dev/null +++ b/gxg/web/gxg.rb @@ -0,0 +1,301 @@ +# GxG for Opal +# Place Holders: +module GxG + LOG = nil + DISPATCHER = nil + OBJECT_SPACE = {:registry => {}} +end +$object_space = {:registry => {}} +# Alterations to basic Object +class Object + private + # + def bytes(*args) + GxG::ByteArray::try_convert(args) + end + # + def new_message(*args) + # if args[0].is_a?(Hash) + # ::GxG::Events::Message.new({:sender => self, :subject => args[1], :body => nil}.merge(args[0])) + # else + ::GxG::Events::Message.new({:sender => self, :subject => args[1], :body => args[0]}) + # end + end + # logging hooks + def log_unknown(message = nil, progname = nil, &block) + # a.k.a 'unknown' + if ::GxG::LOG + ::GxG::LOG.unknown(message, progname, self, &block) + end + end + alias :log_trace :log_unknown + def log_fatal(message = nil, progname = nil, &block) + if ::GxG::LOG + ::GxG::LOG.fatal(message, progname, self, &block) + end + end + def log_error(message = nil, progname = nil, &block) + if ::GxG::LOG + ::GxG::LOG.error(message, progname, self, &block) + end + end + def log_warn(message = nil, progname = nil, &block) + if ::GxG::LOG + ::GxG::LOG.warn(message, progname, self, &block) + end + end + alias :log_warning :log_warn + def log_info(message = nil, progname = nil, &block) + if ::GxG::LOG + ::GxG::LOG.info(message, progname, self, &block) + end + end + def log_debug(message = nil, progname = nil, &block) + if ::GxG::LOG + ::GxG::LOG.debug(message, progname, self, &block) + end + end + # + def post_event(queue_name=:root,dispatcher=nil,&block) + result = false + if dispatcher.is_a?(::GxG::Events::EventDispatcher) + result = dispatcher.post_event(queue_name,&block) + else + if ::GxG::DISPATCHER + result = ::GxG::DISPATCHER.post_event(queue_name,&block) + else + log_error("No Event Dispatcher Defined") + end + end + result + end + # + def post_event_at(at_time=Time.now,dispatcher=nil,&block) + result = nil + if dispatcher.is_a?(::GxG::Events::EventDispatcher) + result = dispatcher.add_timer({:when => at_time},&block) + else + if ::GxG::DISPATCHER + result = ::GxG::DISPATCHER.add_timer({:when => at_time},&block) + else + log_error("No Event Dispatcher Defined") + end + end + result + end + # + def post_event_periodic(interval=0.333,dispatcher=nil,&block) + result = nil + if dispatcher.is_a?(::GxG::Events::EventDispatcher) + result = dispatcher.add_periodic_timer({:interval => interval},&block) + else + if ::GxG::DISPATCHER + result = ::GxG::DISPATCHER.add_periodic_timer({:interval => interval},&block) + else + log_error("No Event Dispatcher Defined") + end + end + result + end + # + def cancel_periodic(reference=nil,dispatcher=nil) + result = false + if reference.is_a?(Hash) + if dispatcher.is_a?(::GxG::Events::EventDispatcher) + result = dispatcher.cancel_timer(reference) + else + if ::GxG::DISPATCHER + result = ::GxG::DISPATCHER.cancel_timer(reference) + else + log_error("No Event Dispatcher Defined") + end + end + end + result + end + # + public + def is_any?(*args) + result = false + args.flatten.to_enum.each do |thing| + if thing.class == Class + if self.is_a?(thing) + result = true + break + end + end + end + result + end +end +# +module GxG + # Generic toolbox of methods + def self.uuid_generate() + SecureRandom.uuid() + end + # + def self.passes_needed(size_used=0, container_limit=0) + if size_used > 0 and container_limit > 0 + needed_raw = size_used.to_f / container_limit.to_f + overhang = needed_raw - needed_raw.to_i.to_f + needed_raw = needed_raw.to_i.to_f + if overhang > 0.0 + needed_raw += 1.0 + end + needed_raw.to_i + else + 0 + end + end + # + def self.apportioned_ranges(how_much_data=0, container_limit=0, original_offset=0) + result = [] + the_count = ::GxG::passes_needed(how_much_data, container_limit) + if the_count > 0 + offset = original_offset + the_count.times do + if (offset + (container_limit - 1)) <= (how_much_data - 1) + end_point = (offset + (container_limit - 1)) + else + end_point = (how_much_data - 1) + end + result << ((offset)..(end_point)) + offset = (end_point + 1) + end + end + result + end + # + def self.valid_uuid?(uuid=nil,strict=true) + if uuid.is_any?(::String, ::Symbol) + if strict == true + pattern = /[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[4][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]/ + else + pattern = /[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]/ + end + if uuid.to_s.match(pattern) + if uuid.to_s.size == 36 + true + else + false + end + else + false + end + else + false + end + end + # + def self.replace_registry(new_registry={}) + $object_space[:registry] = new_registry + true + end + # + def self.register(something=nil, parent=nil) + unless $object_space[:registry][(something.uuid.to_s.to_sym)] + if parent + ordinal = GxG::children_of(parent).size + else + ordinal = 0 + end + $object_space[:registry][(something.uuid.to_s.to_sym)] = {:object => something, :parent => parent, :ordinal => ordinal} + end + true + end + # + def self.unregister(the_uuid=nil) + if $object_space[:registry][(the_uuid.to_s.to_sym)] && GxG::DISPLAY_DETAILS[:object].uuid.to_s.to_sym != the_uuid.to_s.to_sym + $object_space[:registry].delete(the_uuid.to_s.to_sym) + end + true + end + # + def self.register_uuid_list() + $object_space[:registry].keys + end + # + def self.record_by_uuid(the_uuid=nil) + result = nil + if $object_space[:registry][(the_uuid.to_s.to_sym)] + result = $object_space[:registry][(the_uuid.to_s.to_sym)] + end + result + end + # + def self.object_by_uuid(the_uuid=nil) + (GxG::record_by_uuid(the_uuid) || {})[:object] + end + # + def self.children_of(something=nil) + result = [] + buffer = [] + $object_space[:registry].values.each do |entry| + if entry[:parent].object_id == something.object_id + buffer << entry + end + end + buffer = buffer.sort { |record_a,record_b| record_a[:ordinal] <=> record_b[:ordinal] } + buffer.each do |the_record| + result << the_record[:object] + end + result + end + # + def self.set_ordinal(the_selector=nil,the_ordinal=0) + result = false + if the_selector.is_any?(String, Symbol) + the_object = GxG::object_by_uuid(the_selector.to_sym) + else + the_object = the_selector + end + parent = GxG::parent_of(the_object) + if parent + list = GxG::children_of(parent) + current = list.find_index(the_object) + if current + if the_ordinal >= (list.size - 1) + list << list.delete_at(current) + else + if the_ordinal < 0 + list = list.unshift(list.delete_at(current)) + else + list = list.insert(the_ordinal,list.delete_at(current)) + end + end + # + list.each_with_index do |an_object, indexer| + record = GxG::record_by_uuid(an_object.uuid.to_sym) + record[:ordinal] = indexer + end + result = true + end + end + result + end + # + def self.parent_of(something=nil) + result = nil + $object_space[:registry].values.each do |entry| + if entry[:object].object_id == something.object_id + result = entry[:parent] + break + end + end + result + end + # + def self.lineage_of(something=nil) + result = [] + the_parent = GxG::parent_of(something) + until the_parent == nil do + result.unshift(the_parent) + the_parent = GxG::parent_of(the_parent) + end + # first = base progenitor. + result + end + # +end +# \ No newline at end of file diff --git a/gxg/web/gxg_applications.rb b/gxg/web/gxg_applications.rb new file mode 100644 index 0000000..fd878b9 --- /dev/null +++ b/gxg/web/gxg_applications.rb @@ -0,0 +1,519 @@ +module GxG + # ---------------------------------------------------------------------------------------------------- + class Application + def initialize(settings={}) + # @process = server-side process uuid + @process = settings[:application] + # receives a :source PersistedHash defining the application internals. + @definition = settings[:source] + @title = "Untitled" + # @state is persisted as state-data on the server when altered. + @state = (settings[:state] || {}) + # Gui component if any: + @viewports = {} + if settings[:viewport].is_a?(::GxG::Gui::ApplicationViewport) + @viewports[(settings[:viewport].uuid.to_s.to_sym)] = settings[:viewport] + end + if settings[:viewports].is_a?(::Array) + settings[:viewports].each do |the_viewport| + if the_viewport.is_a?(::GxG::Gui::ApplicationViewport) + @viewports[(the_viewport.uuid.to_s.to_sym)] = the_viewport + end + end + end + @windows = {} + if settings[:window].is_a?(::GxG::Gui::Window) + @windows[(settings[:window].uuid.to_s.to_sym)] = settings[:window] + end + if settings[:windows].is_a?(::Array) + settings[:windows].each do |the_window| + if the_window.is_a?(::GxG::Gui::Window) + @windows[(the_window.uuid.to_s.to_sym)] = the_window + end + end + end + @libraries = {} + # Event Supports: + @event_handlers = {} + @event_listeners = {} + # Runtime flags: + @credentialed = (settings[:credentialed] || false) + @unique = (settings[:unique] || true) + # Application location: + @location = settings[:location] + # Internal Heap: + @heap = {} + # ### Compile Source + if @definition.is_a?(::String) + if @definition.base64? + @definition = @definition.decode64 + end + if @definition.json? + begin + @definition = ::JSON::parse(@definition,{:symbolize_names => true}) + @definition = ::GxG::Database::process_import(@definition) + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:definition => @definition}}) + end + else + # error - malformed source. + end + end + if @definition.is_a?(::GxG::Database::DetachedHash) + @title = @definition.title + # Question: should I overwrite the potentially passed settings options? + the_opts = @definition[:options].unpersist + if the_opts[:credentialed] == true + @credentialed = true + else + @credentialed = false + end + if the_opts[:unique] == true + @unique = true + else + @unique = false + end + end + # Load Libraries + # You can use ruby or javascript for the :native code portion which will be added to the page body. + # you can then also set a wrapper library script on the library for smooth integration. + # use get_library(uuid/title) to access the library in order to call methods. + if @definition.is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedHash) + # libarary requirement format: {:library => "", :type => "", :minimum => 0.0, :maximum => nil} + load_list = [] + if @definition[:requirements].is_any?(::GxG::Database::DetachedArray, ::GxG::Database::PersistedArray) + @definition[:requirements].each do |the_requirement| + unless GxG::DISPLAY_DETAILS[:object].library_loaded?(the_requirement) + load_list << the_requirement + end + end + end + GxG::DISPLAY_DETAILS[:object].load_libraries(load_list) do |all_loaded| + if all_loaded == true + # Link to libraries required: + if @definition[:requirements].is_a?(::GxG::Database::PersistedArray) + @definition[:requirements].each do |the_requirement| + the_library = GxG::DISPLAY_DETAILS[:object].get_library(the_requirement) + if the_library + @libraries[(the_library.uuid)] = the_library + end + end + end + # Set Script: + if @definition[:script].size > 0 + if @definition[:script].base64? + instance_eval(@definition[:script].to_s.decode64) + else + instance_eval(@definition[:script].to_s) + end + end + else + log_warn("Library load error - not processing main application script for #{@definition.title}.") + end + end + end + # + self + end + # + def get_resource(the_reference=nil) + result = nil + if @definition + @definition[:content].each do |the_object| + if (the_object.title == the_reference || the_object.uuid == the_refrence.to_s.to_sym) + result = the_object + break + end + end + end + result + end + # + def require_resource(the_reference=nil) + # call self.require_resource("script-name") to load content.script-name + result = false + if @definition + # content source record format: {:component => "script", :type => "text/ruby", :script => ""} + the_script = self.get_resource(the_reference) + if the_script + if the_script[:component].to_s == "script" && the_script[:type].to_s == "text/ruby" + if the_script[:script].size > 0 + if the_script[:script].base64? + instance_eval(the_script[:script].to_s.decode64) + else + instance_eval(the_script[:script].to_s) + end + result = true + end + end + end + end + result + end + # + def title() + @title + end + # + def credentialed() + @credentialed + end + # + def unique() + @unique + end + # + def location() + @location + end + # Libraries: + def libraries() + @libraries + end + # + def state_data() + @state + end + # + def state_pull() + GxG::CONNECTION.app_state_pull({:application => @process.to_s}) do |response| + if response.is_a?(::Hash) + if response[:result].is_a?(::Hash) + @state = response[:result] + true + else + false + end + else + false + end + end + end + # + def state_push() + GxG::CONNECTION.app_state_push({:application => @process.to_s, :data => @state}) do |response| + if response.is_a?(::Hash) + if response[:result] == true + true + else + false + end + else + false + end + end + end + # + def get_library(the_reference=nil) + result = nil + if the_reference.is_any?(::String, ::Symbol) + result = @libraries[(the_reference.to_s.to_sym)] + unless result + @libraries.values.each do |the_library| + if the_library.title == the_reference + result = the_library + break + end + end + end + end + result + end + # + # Viewport Supports: + def link_viewport(the_viewport=nil) + if the_viewport.is_a?(::GxG::Gui::ApplicationViewport) + @viewports[(the_viewport.uuid.to_s.to_sym)] = the_viewport + true + else + false + end + end + # + def unlink_viewport(the_reference=nil) + if the_reference.is_any?(::String, ::Symbol) + if @viewports[(the_reference.to_s.to_sym)] + @viewports.delete((the_reference.to_s.to_sym)) + else + @viewports.values.each do |the_viewport| + if the_viewport.title == the_reference + @viewports.delete(the_viewport.uuid) + break + end + end + end + true + else + false + end + end + # + def viewports() + @viewports + end + # + def get_viewport(the_reference=nil) + result = nil + if the_reference.is_any?(::String, ::Symbol) + result = @viewports[(the_reference.to_s.to_sym)] + unless result + @viewports.values.each do |the_viewport| + if the_viewport.title == the_reference + result = the_viewport + break + end + end + end + end + result + end + # + # Window Supports: + def link_window(the_window=nil) + if the_window.is_a?(::GxG::Gui::Window) + @windows[(the_window.uuid.to_s.to_sym)] = the_window + # Review: add link to window object to application ?? + true + else + false + end + end + # + def unlink_window(the_reference=nil) + if the_reference.is_any?(::String, ::Symbol) + if @windows[(the_reference.to_s.to_sym)] + @windows.delete((the_reference.to_s.to_sym)) + else + @windows.values.each do |the_window| + if the_window.title == the_reference + @windows.delete(the_window.uuid) + break + end + end + end + true + else + false + end + end + # + def windows() + @windows + end + # + def get_window(the_reference=nil) + result = nil + if the_reference.is_any?(::String, ::Symbol) + result = @windows[(the_reference.to_s.to_sym)] + unless result + @windows.values.each do |the_window| + if the_window.title == the_reference + result = the_window + break + end + end + end + end + result + end + # + def process_uuid() + @process + end + # + def source_uuid() + if @definition.is_a?(::GxG::Database::DetachedHash) + @definition.uuid() + else + nil + end + end + # + def search_resources(the_reference=nil) + result = nil + if the_reference.is_any?(::String, ::Symbol) + if @definition + @definition[:content].search do |item,selector,container| + if item.is_a?(::GxG::Database::DetachedHash) + if the_reference.to_s.to_sym == item.uuid().to_s.to_sym || the_reference.to_s == item.title().to_s + result = item + break + end + end + end + end + end + result + end + # Event Supports: + ## Internal method on + def on(the_event_type=nil, captured=false, &block) + if the_event_type.is_any?(::String, ::Symbol) + if block.respond_to?(:call) + @event_handlers[(the_event_type.to_s.downcase.to_sym)] = block + @event_listeners[(the_event_type.to_s.downcase.to_sym)] = `#{self.method(:event_handler).to_proc}` + `#{self.element}.addEventListener(#{the_event_type.to_s.downcase}, #{@event_listeners[(the_event_type.to_s.downcase.to_sym)]}, #{captured})` + end + end + end + # + def remove_listener(the_event_type=nil) + if @event_handlers[(the_event_type.to_s.downcase.to_sym)] && @event_listeners[(the_event_type.to_s.downcase.to_sym)] + `#{self.element}.removeEventListener(#{the_event_type.to_s.downcase},#{@event_listeners[(the_event_type.to_s.downcase.to_sym)]});` + @event_handlers.delete(the_event_type.to_s.downcase.to_sym) + @event_listeners.delete(the_event_type.to_s.downcase.to_sym) + end + true + end + # + def event_handler(the_event) + # format the event record for travel in the system. + # See: https://www.w3schools.com/jsref/dom_obj_event.asp + # Setup event type groupings to glean appropriate details from the event. + the_type = `#{the_event}.type`.to_s.downcase.to_sym + event_record = {} + if @event_handlers[(the_type)] + genre = (GxG::Gui::EventHandlers::EVENT_GENRES[(the_type)] || :event) + if genre + fields = GxG::Gui::EventHandlers::EVENT_FIELDS[(genre)] + if fields + # + %x{ + serializer = function thisOne (e) { + if (e) { + var o = { + eventName: e.toString(), + altKey: e.altKey, + bubbles: e.bubbles, + button: e.button, + buttons: e.buttons, + cancelBubble: e.cancelBubble, + cancelable: e.cancelable, + clientX: e.clientX, + clientY: e.clientY, + composed: e.composed, + ctrlKey: e.ctrlKey, + currentTarget: e.currentTarget ? e.currentTarget.outerHTML : null, + defaultPrevented: e.defaultPrevented, + detail: e.detail, + eventPhase: e.eventPhase, + fromElement: e.fromElement ? e.fromElement.outerHTML : null, + isTrusted: e.isTrusted, + layerX: e.layerX, + layerY: e.layerY, + metaKey: e.metaKey, + movementX: e.movementX, + movementY: e.movementY, + offsetX: e.offsetX, + offsetY: e.offsetY, + pageX: e.pageX, + pageY: e.pageY, + relatedTarget: e.relatedTarget ? e.relatedTarget.outerHTML : null, + returnValue: e.returnValue, + screenX: e.screenX, + screenY: e.screenY, + shiftKey: e.shiftKey, + sourceCapabilities: e.sourceCapabilities ? e.sourceCapabilities.toString() : null, + target: e.target ? e.target.outerHTML : null, + timeStamp: e.timeStamp, + toElement: e.toElement ? e.toElement.outerHTML : null, + type: e.type, + view: e.view ? e.view.toString() : null, + which: e.which, + x: e.x, + y: e.y + }; + return o; + }; + }; + event_record = JSON.stringify(serializer(#{the_event})); + } + event_record = ::JSON::parse(event_record.to_s, {:symbolize_names => true}) + # puts "Got: #{event_record.inspect}" + end + end + # + @event_handlers[(the_type)].call(event_record) + # + end + end + # + def run(data={}) + # override this in your object script. + if data.is_a?(::Hash) + if data[:restore] == true + self.state_pull + end + end + end + # + def exitready?(data={}) + # override this in your object script. + :ready + end + # + def before_exit(data={}) + # override this in your object script. + true + end + def exit(data={}) + # override this in your object script. + # GxG::APPLICATIONS[:processes] + GxG::CONNECTION.application_close({:application => @process}) do |response| + if response.is_a?(::Hash) + if response[:result] == true + GxG::APPLICATIONS[:processes].delete(@process) + else + log_warn("Could not close application #{@process.inspect} : #{response.inspect}") + end + else + log_warn("Malformed response: #{response.inspect}") + end + end + true + end + # + def window_close(details={}) + # override this in your object script. + if details.is_a?(::Hash) + if GxG::valid_uuid?(details[:window]) + the_window = GxG::DISPLAY_DETAILS[:object].get_window(details[:window]) + if the_window + # TODO: re-think documents and such. + # Document data needs saving first ?? + @viewports.keys.each do |the_reference| + the_viewport = the_window.find_child(the_reference) + if the_viewport + the_viewport.set_application(nil) + self.unlink_viewport(the_reference) + end + end + self.unlink_window(the_window.uuid) + GxG::DISPLAY_DETAILS[:object].window_close(the_window.uuid) + unless @windows.size > 0 + self.exit() + end + end + else + log_warn("Invalid Argument passed: #{details.inspect}") + end + end + end + # + # Viewport Management: + def viewport_clear(the_reference=nil) + viewport = self.get_viewport(the_reference) + if viewport + viewport.gxg_each_child do |the_item| + unless the_item == viewport + the_item.destroy + end + end + true + else + false + end + end + # + end +end \ No newline at end of file diff --git a/gxg/web/gxg_augments.rb b/gxg/web/gxg_augments.rb new file mode 100644 index 0000000..47eb10b --- /dev/null +++ b/gxg/web/gxg_augments.rb @@ -0,0 +1,1270 @@ +# Alteration to Opal's Buffer::Array +class Buffer + class Array + def native_object() + `#@native` + end + end +end +# Place holders for Hash & Array modifications: +module GxG + module Database + class Field + # + end + class PersistedArray + # + end + class PersistedHash + # + end + end +end +# Logger (minimal) support: +class Logger + DEBUG = 0 + INFO = 1 + WARN = 2 + ERROR = 3 + FATAL = 4 + UNKNOWN = 5 + TRACE = 5 + # + def initialize(loglevel=3) + @level = loglevel + end + # + def level() + @level + end + # + def level=(loglevel=3) + @level = loglevel + end + # + def add(severity = nil, message = nil, progname = nil, origin = nil, &block) + # + unless [::Logger::DEBUG, ::Logger::INFO, ::Logger::WARN, ::Logger::ERROR, ::Logger::FATAL, ::Logger::UNKNOWN].include?(severity) + severity = ::Logger::UNKNOWN + end + if progname + unless message + message = progname + progname = nil + end + end + begin + unless message + if block.respond_to?(:call) + message = block.call() + end + end + levels = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL", "ANY"] + puts "#{levels[(severity)].to_s}, [#{Time.now.strftime('%Y-%m-%dT%H:%M:%S%z')}##{$$.inspect}] #{Time.now.to_i.to_s} -- #{progname.inspect}: #{message.to_s}\n" + # + true + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:severity => severity, :message => message, :progname => progname, :block => block}}) + false + end + end + # + def debug(progname = nil, &block) + add(DEBUG, nil, progname, &block) + end + # + def info(progname = nil, &block) + add(INFO, nil, progname, &block) + end + # + def warn(progname = nil, &block) + add(WARN, nil, progname, &block) + end + alias :warning :warn + # + def error(progname = nil, &block) + add(ERROR, nil, progname, &block) + end + # + def fatal(progname = nil, &block) + add(FATAL, nil, progname, &block) + end + # + def unknown(progname = nil, &block) + add(UNKNOWN, nil, progname, &block) + end + alias :trace :unknown + # +end +# +class Time + def to_d() + BigDecimal("%#{::Float::DIG}f" % self.to_f) + end +end +# +class DateTime < Date + # Review : totally revamp and rethink this. + # Notes, see: https://ruby-doc.org/stdlib-2.5.0/libdoc/date/rdoc/DateTime.html#method-c-parse + def intialize(*args) + @data = Time.parse(*args) + super(*args) + end + # + def to_time() + ::Time.parse(self.to_s) + end + # + def to_d() + BigDecimal(self.to_time.to_d) + end + # + def self.now() + DateTime.parse(Time.now.strftime('%Y-%m-%dT%H:%M:%S%z')) + end + + def to_s() + self.strftime('%Y-%m-%dT%H:%M:%S%z') + end + + def to_json() + self.strftime('%Y-%m-%dT%H:%M:%S%z') + end + + def strftime(fmt='%FT%T%:z') + super(fmt) + end + + def self._strptime(str, fmt='%FT%T%z') + super(str, fmt) + end + + def iso8601_timediv(n) # :nodoc: + n = n.to_i + strftime('T%T' + + if n < 1 + '' + else + '.%0*d' % [n, (sec_fraction * 10**n).round] + end + + '%:z') + end + + private :iso8601_timediv + + def iso8601(n=0) + super() + iso8601_timediv(n) + end + + def rfc3339(n=0) iso8601(n) end + + def xmlschema(n=0) iso8601(n) end # :nodoc: + + def jisx0301(n=0) + super() + iso8601_timediv(n) + end +end +# ######## Hassle-free Cloning support +#class NilClass +# public +# def initialize_clone +# nil +# end +# alias :initialize_dup :initialize_clone +# alias :dup :initialize_clone +# def clone() +# initialize_clone +# end +#end +## +#class FalseClass +# public +# def initialize_clone +# false +# end +# alias :initialize_dup :initialize_clone +# alias :dup :initialize_clone +# def clone() +# initialize_clone +# end +#end +## +#class TrueClass +# public +# def initialize_clone +# true +# end +# alias :initialize_dup :initialize_clone +# alias :dup :initialize_clone +# def clone() +# initialize_clone +# end +#end +## +class Integer +# public +# def initialize_clone +# (self | self).to_i +# end +# alias :initialize_dup :initialize_clone +# alias :dup :initialize_clone +# def clone() +# initialize_clone +# end + def to_d() + BigDecimal("%#{::Float::DIG}f" % self.to_f) + end +end +## +class Float + public + def to_d() + BigDecimal("%#{::Float::DIG}f" % self) + end +# def initialize_clone +# (self + 0.0) +# end +# alias :initialize_dup :initialize_clone +# alias :dup :initialize_clone +# def clone() +# initialize_clone +# end +end +# +#class Symbol +# def initialize_clone +# (self.to_s.dup.to_sym) +# end +# alias :initialize_dup :initialize_clone +# alias :dup :initialize_clone +# def clone() +# initialize_clone +# end +#end +# +class String + # + def cast_to_ascii() + `new window.TextDecoder("ascii").decode(new window.TextEncoder().encode(#{self.clone}.toString()))`.to_s + end + # + def valid_date?() + # Match for Date pattern + if (/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/.match(self)) + true + else + false + end + end + # + def valid_datetime?() + # Match for ISO 8601 pattern + if (/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9][+-][0-9][0-9]:[0-9][0-9]/.match(self)) + true + else + false + end + end + # + def valid_datetime_nolocale?() + # Match for not-so-much ISO 8601-ish pattern + if (/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]/.match(self)) + true + else + false + end + end + # + def valid_path?() + if (/.*(?:\\|\/)(.+)$/.match(self) || (self.split(" ").size == 1 || self.split("/").size > 1 || self == "/")) + true + else + false + end + end + # + def valid_jid?() + if /^(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?$/.match(self) + true + else + false + end + end + # + def camel_case?() + # self.match(/([A-Z][a-z]+[A-Z][a-zA-Z]+)/) + if self.match(/[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*/) + true + else + false + end + end + # + def split_camelcase() + if self.camel_case?() + self.split(/(?=[A-Z])/) + else + self + end + end + # + # def write(*args) + # # no-op + # end + # # + # def mime_type() + # result = nil + # raw = ::MimeMagic.by_magic(::StringIO.new(self.cast_to_ascii(self.clone))) + # if raw + # result = {:type => (raw.type), :mediatype => (raw.mediatype), :subtype => (raw.subtype)} + # end + # result + # end + # + def json_1?() + self.slice(0,1) == '{' + end + # + def json_2?() + (self.json_1?() && (self.match('"apiVersion"\s?:\s?"2.0"') || false)) + end + # + def json?() + self.json_1?() || self.json_2?() + end + # + def from_json(symbolize_names = true) + if self.json? + the_object = ::JSON::parse(self,{:symbolize_names => symbolize_names}) + if the_object.is_any?(::Hash, ::Array) + the_object.process! do |value, selector, container| + if value.is_a?(::String) + if (value.valid_datetime? || value.valid_datetime_nolocale?) + container[(selector)] = ::DateTime::parse(value) + else + item = value.numeric_values() + if item.is_a?(::Hash) + if item[:integer] + item = item[:integer] + container[(selector)] = item + else + if item[:float] + item = item[:float] + container[(selector)] = item + end + end + end + end + # + if value[0..6] == "binary:" && value[7..-1].base64? + container[(selector)] = GxG::ByteArray.new(value[7..-1].decode64) + end + end + nil + end + # + end + else + self + end + end + # Base64 Stuff: + def base64?() + # RFC 4648 + # SOMEDAY: use regex based detection, current is open to some bugs. + # See: http://www.perlmonks.org/?node_id=775820 + # See: http://mattfaus.com/blog/2007/02/14/base64-regular-expression/ + # See: http://stackoverflow.com/questions/475074/regex-to-parse-or-validate-base64-data + # Regex Testing: http://www.myregexp.com/signedJar.html + # For now: + begin + # Weird: the word 'page' and 'form' are considered a base64 strings. (time to monkey patch a fix) + if ! ["page","form", "left"].include?(self) + # ^(?:[A-Za-z0-9+/]{4})+(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$ + # failing: ^(?:[A-Za-z0-9+\/]{4}\n?)*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$ + # ^@(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$ + # /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}[AEIMQUYcgkosw048]=|[A-Za-z0-9+\/][AQgw]==)?\z/ + # (?:[A-Za-z0-9+\/]{4}){2,}(?:[A-Za-z0-9+\/]{2}[AEIMQUYcgkosw048]=|[A-Za-z0-9+\/][AQgw]==) + # if (self.size > 0 && /^(?:[A-Za-z0-9+\/]{4})+(?:[A-Za-z0-9+\/]{2}[AEIMQUYcgkosw048]=|[A-Za-z0-9+\/][AQgw]==)?\z/.match(self)) + if (self.size > 0) + begin + Base64::strict_decode64(self) + true + rescue Exception => the_error + false + end + else + false + end + else + false + end + rescue Exception => the_error + false + end + end + # + def encode64() + # RFC 4648 + if self.base64?() + self + else + ::Base64::strict_encode64(self) + end + end + # + def decode64() + # RFC 4648 + if self.base64?() + ::Base64::strict_decode64(self) + else + self + end + end + # + def encrypt(withkey="") + # + if withkey.to_s.size > 0 + keybytes = ::GxG::ByteArray.new(withkey.to_s) + container = ::GxG::ByteArray.new(self.to_s) + # + container.each_index do |index| + the_value = container[(index)] + # + keybytes.each do |the_byte| + [127,63,31,15,7,3,1,0].each do |the_bit| + if the_byte > the_bit + the_value += 1 + if the_value > 255 + the_value = 0 + end + else + the_value -= 1 + if the_value < 0 + the_value = 255 + end + end + end + end + # + container[(index)] = the_value + end + # + result = container.to_s + result + else + self.dup + end + end + # + def decrypt(withkey="") + # + if withkey.to_s.size > 0 + keybytes = ::GxG::ByteArray.new(withkey.to_s) + container = ::GxG::ByteArray.new(self.to_s) + # + container.each_index do |index| + the_value = container[(index)] + # + keybytes.each do |the_byte| + [127,63,31,15,7,3,1,0].each do |the_bit| + if the_byte > the_bit + the_value -= 1 + if the_value < 0 + the_value = 255 + end + else + the_value += 1 + if the_value > 255 + the_value = 0 + end + end + end + end + # + container[(index)] = the_value + end + # + result = container.to_s + result + else + self.dup + end + end + # + def to_d() + BigDecimal(self) + end +end +# +class Hash + # + def self.process(the_hash={},&block) + new_hash = {} + if block.respond_to?(:call) + if the_hash.is_any?(::Array, ::Hash, ::Struct, ::GxG::Database::PersistedHash, ::GxG::Database::PersistedArray) + new_hash = the_hash.process!(&block) + else + raise ArgumentError, "you must pass a Hash or an Array, or a ByteArray" + end + end + new_hash + end + # + def self.search(the_hash={},&block) + new_hash = {} + if block.respond_to?(:call) + if the_hash.is_any?(::Array, ::Hash, ::Struct, ::GxG::Database::PersistedHash, ::GxG::Database::PersistedArray) + new_hash = the_hash.search(&block) + else + raise ArgumentError, "you must pass a Hash or an Array, or a ByteArray" + end + end + new_hash + end + # + def iterative(&block) + result = [] + visit = Proc.new do |the_node=nil, accumulator=[]| + node_stack = [] + if the_node + node_stack << ({:parent => nil, :parent_selector => nil, :object => (the_node)}) + while (node_stack.size > 0) do + a_node = node_stack.shift + # + if a_node[:object].is_any?(::Hash, ::Struct, ::GxG::Database::PersistedHash) + a_node[:object].each_pair do |the_key, the_value| + node_stack << ({:parent => a_node[:object], :parent_selector => the_key, :object => the_value}) + end + end + if a_node[:object].is_any?(::Array, ::GxG::Database::PersistedArray) + a_node[:object].each_with_index do |the_value, the_index| + node_stack << ({:parent => a_node[:object], :parent_selector => the_index, :object => the_value}) + end + end + # + accumulator << a_node + end + end + accumulator + end + # + children_of = Proc.new do |the_db=[], the_parent=nil| + list = [] + the_db.each do |node| + if node[:parent].object_id == the_parent.object_id + list << node + end + end + list + end + # + begin + database = visit.call(self,[]) + link_db = children_of.call(database, self) + if block.respond_to?(:call) + while (link_db.size > 0) do + entry = link_db.shift + unless entry[:object].object_id == self.object_id + # calls with parameters: the_value, the_key/the_index (the_selector), the_container + raw_result = block.call(entry[:object], entry[:parent_selector], entry[:parent]) + if raw_result + result << raw_result + end + end + if entry[:object].object_id != nil.object_id + children = children_of.call(database, entry[:object]) + children.each do |child| + link_db << child + end + end + end + end + # + rescue Exception => the_error + log_error({:error => the_error, :parameters => {}}) + end + # + result + end + # + def process(&block) + result = self.clone + result.iterative(&block) + result + end + # + def process!(&block) + self.iterative(&block) + self + end + # + def search(&block) + results = [] + if block.respond_to?(:call) + results = self.iterative(&block) + end + results + end + # + def paths_to(the_object=nil,base_path="") + # new idea here: + search_results = [] + unless base_path[0] == "/" + base_path = ("/" + base_path) + end + if base_path.size > 1 + path_stack = base_path.split("/")[1..-1].reverse + else + path_stack = [] + end + origin = self.get_at_path(base_path) + container_stack = [{:selector => nil, :container => origin, :prefix => "/"}] + find_container = Proc.new do |the_container| + result = nil + container_stack.each_with_index do |entry, index| + if entry[:container] == the_container + result = entry + break + end + end + result + end + last_container = origin + found = false + # tester = {:a=>1, :b=>2, :c=>[0, 5], :testing=>{:d=>4.0, :e=>0.9, :f => nil}} + if origin.is_any?(::Hash, ::Array, ::Struct, ::GxG::Database::PersistedHash, ::GxG::Database::PersistedArray) + origin.process! do |the_value, selector, container| + if last_container.object_id != container.object_id + container_record = find_container.call(container) + if container_record + path_stack = container_record[:prefix].split("/").reverse + if path_stack.size == 0 + path_stack << "" + end + end + last_container = container + end + if selector.is_a?(Symbol) + safe_key = (":" + selector.to_s) + else + safe_key = selector.to_s + end + safe_key = safe_key.gsub("/","%2f") + path_stack.unshift(safe_key) + # compare the_value + found = false + if the_value.is_a?(::GxG::Database::Field) + if (the_value.content == the_object) + found = true + end + else + if (the_value == the_object) + found = true + end + end + if found + search_results << ("/" + path_stack.reverse.join("/")) + end + # + if the_value.is_any?(::Array, ::Hash, ::Struct, ::GxG::Database::PersistedHash, ::GxG::Database::PersistedArray) + container_stack.unshift({:selector => selector, :container => the_value, :prefix => (path_stack.reverse.join("/"))}) + end + path_stack.shift + # + nil + end + else + search_results << ("/" + path_stack.join("/")) + end + search_results + end + # + def get_at_path(the_path="/") + result = nil + if the_path == "/" + result = self + else + object_stack = [(self)] + path_stack = the_path.split("/") + path_stack.to_enum.each do |path_element| + element = nil + if path_element.size > 0 + if (path_element =~ /^(?:[0-9])*[0-9](?:[0-9])*$/) == 0 + element = path_element.to_i + else + element = path_element + element = element.gsub("%2f","/") + if element[0] == ":" + element = element[(1..-1)].to_sym + end + end + end + if element + result = object_stack.first[(element)] + if result.is_a?(NilClass) + break + else + object_stack.unshift(result) + end + else + # ignore double slashes? '//' + # break + end + end + end + result + end + # + def set_at_path(the_path="/",the_value=nil) + result = nil + if the_path != "/" + container = self.get_at_path(::File::dirname(the_path)) + if container + raw_selector = ::File::basename(the_path) + selector = nil + if raw_selector.size > 0 + if (raw_selector =~ /^(?:[0-9])*[0-9](?:[0-9])*$/) == 0 + selector = raw_selector.to_i + else + selector = raw_selector + selector = selector.gsub("%2f","/") + if selector[0] == ":" + selector = selector[(1..-1)].to_sym + end + end + end + if selector + container[(selector)] = the_value + result = container[(selector)] + else + # ignore double slashes? '//' + # break + end + # + end + end + result + end + # + # + def symbolize_keys() + self.process! do |value, selector, container| + if container.is_a?(::Hash) + unless selector.is_a?(::Symbol) + container[(selector.to_sym)] = container.delete(selector) + end + end + nil + end + self + end + # + def gxg_export() + result = {:type => "Hash", :content => {}} + export_db = [{:parent => nil, :parent_selector => nil, :object => self, :record => result}] + children_of = Proc.new do |the_parent=nil| + list = [] + export_db.each do |node| + if node[:parent].object_id == the_parent.object_id + list << node + end + end + list + end + export_record = Proc.new do |the_value| + if the_value.is_any?(Integer, Float, String) + if the_value.is_a?(Numeric) + if the_value.to_s.include?(".") + {:type => "Float", :content => the_value} + else + {:type => "Integer", :content => the_value} + end + else + {:type => (the_value.class.to_s), :content => the_value} + end + else + {:type => (the_value.class.to_s), :content => the_value.to_s} + end + end + # Build up export_db: + self.search do |the_value, the_selector, the_container| + if the_value.is_a?(::Hash) + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => {:type => "Hash", :content => {}}} + else + if the_value.is_a?(::Array) + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => {:type => "Array", :content => []}} + else + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => export_record.call(the_value)} + end + end + end + # Collect children export content: + link_db =[(export_db[0])] + while link_db.size > 0 do + entry = link_db.shift + children_of.call(entry[:object]).each do |the_child| + entry[:record][:content][(the_child[:parent_selector])] = the_child[:record] + if the_child[:object].is_any?(Hash, Array) + link_db << the_child + end + end + end + # + result + end + # + def self.gxg_import(the_exported_record=nil) + result = nil + if the_exported_record.is_a?(Hash) + if the_exported_record[:type] == "Array" + result = ::Array::gxg_import(the_exported_record) + else + import_value = Proc.new do |type,value| + if value.is_a?(String) + begin + the_class = eval(type) + if the_class.respond_to?(:parse) + value = the_class.parse(value) + else + if the_class.respond_to?(:new) + value = the_class.new(value) + else + if the_class.respond_to?(:try_convert) + value = the_class.try_convert(value) + end + end + end + rescue Exception => the_error + end + end + value + end + if the_exported_record[:type] == "Hash" + result = {} + import_db = [{:parent => nil, :parent_selector => nil, :object => result, :record => the_exported_record}] + while import_db.size > 0 do + entry = import_db.shift + if entry[:record][:content].is_a?(Hash) + entry[:record][:content].each_pair do |selector, value| + if value[:type] == "Hash" + entry[:object][(selector)] = {} + import_db << {:parent => entry[:object], :parent_selector => selector, :object => entry[:object][(selector)], :record => value} + else + if value[:type] == "Array" + entry[:object][(selector)] = [] + import_db << {:parent => entry[:object], :parent_selector => selector, :object => entry[:object][(selector)], :record => value} + else + entry[:object][(selector)] = import_value.call(value[:type], value[:content]) + end + end + end + else + if entry[:record][:content].is_a?(Array) + entry[:record][:content].each_with_index do |value, selector| + if value[:type] == "Hash" + entry[:object][(selector)] = {} + import_db << {:parent => entry[:object], :parent_selector => selector, :object => entry[:object][(selector)], :record => value} + else + if value[:type] == "Array" + entry[:object][(selector)] = [] + import_db << {:parent => entry[:object], :parent_selector => selector, :object => entry[:object][(selector)], :record => value} + else + entry[:object][(selector)] = import_value.call(value[:type], value[:content]) + end + end + end + end + end + end + else + result = import_value.call(the_exported_record[:type], the_exported_record[:content]) + end + end + end + result + end + # +end +# +class Array + # + def self.process(the_array=[],&block) + new_array = [] + if block.respond_to?(:call) + if the_array.is_any?(::Array, ::Hash, ::GxG::ByteArray, ::Struct, ::GxG::Database::PersistedHash, ::GxG::Database::PersistedArray) + new_array = the_array.process(&block) + else + raise ArgumentError, "you must pass a Array, a Hash, or a ByteArray" + end + end + new_array + end + # + def self.search(the_array=[],&block) + new_array = [] + if block.respond_to?(:call) + if the_array.is_any?(::Array, ::Hash, ::GxG::ByteArray, ::Struct, ::GxG::Database::PersistedHash, ::GxG::Database::PersistedArray) + new_array = the_array.search(&block) + else + raise ArgumentError, "you must pass a Array, a Hash, or a ByteArray" + end + end + new_array + end + # + def iterative(&block) + result = [] + visit = Proc.new do |the_node=nil, accumulator=[]| + node_stack = [] + if the_node + node_stack << ({:parent => nil, :parent_selector => nil, :object => (the_node)}) + while (node_stack.size > 0) do + a_node = node_stack.shift + # + if a_node[:object].is_any?(::Hash, ::Struct, ::GxG::Database::PersistedHash) + a_node[:object].each_pair do |the_key, the_value| + node_stack << ({:parent => a_node[:object], :parent_selector => the_key, :object => the_value}) + end + end + if a_node[:object].is_any?(::Array, ::GxG::Database::PersistedArray) + a_node[:object].each_with_index do |the_value, the_index| + node_stack << ({:parent => a_node[:object], :parent_selector => the_index, :object => the_value}) + end + end + # + accumulator << a_node + end + end + accumulator + end + # + children_of = Proc.new do |the_db=[], the_parent=nil| + list = [] + the_db.each do |node| + if node[:parent].object_id == the_parent.object_id + list << node + end + end + list + end + # + begin + database = visit.call(self,[]) + link_db = children_of.call(database, self) + if block.respond_to?(:call) + while (link_db.size > 0) do + entry = link_db.shift + unless entry[:object].object_id == self.object_id + # calls with parameters: the_value, the_key/the_index (the_selector), the_container + raw_result = block.call(entry[:object], entry[:parent_selector], entry[:parent]) + if raw_result + result << raw_result + end + end + if entry[:object].object_id != nil.object_id + children = children_of.call(database, entry[:object]) + children.each do |child| + link_db << child + end + end + end + end + # + rescue Exception => the_error + log_error({:error => the_error, :parameters => {}}) + end + # + result + end + # + def process(&block) + result = self.clone + result.iterative(&block) + result + end + # + def process!(&block) + self.iterative(&block) + self + end + # + def search(&block) + results = [] + if block.respond_to?(:call) + results = self.iterative(&block) + end + results + end + # + # + def paths_to(the_object=nil,base_path="") + # new idea here: + search_results = [] + unless base_path[0] == "/" + base_path = ("/" + base_path) + end + if base_path.size > 1 + path_stack = base_path.split("/")[1..-1].reverse + else + path_stack = [] + end + origin = self.get_at_path(base_path) + container_stack = [{:selector => nil, :container => origin}] + find_container = Proc.new do |the_container| + result = nil + container_stack.each_with_index do |entry, index| + if entry[:container] == the_container + result = entry + break + end + end + result + end + last_container = origin + found = false + # tester = {:a=>1, :b=>2, :c=>[0, 5], :testing=>{:d=>4.0, :e=>0.9, :f => nil}} + if origin.is_any?(::Hash, ::Array, ::Struct, ::GxG::Database::PersistedHash, ::GxG::Database::PersistedArray) + origin.process! do |the_value, selector, container| + if last_container.object_id != container.object_id + container_record = find_container.call(container) + if container_record + path_stack = container_record[:prefix].split("/").reverse + if path_stack.size == 0 + path_stack << "" + end + end + last_container = container + end + if selector.is_a?(Symbol) + safe_key = (":" + selector.to_s) + else + safe_key = selector.to_s + end + safe_key = safe_key.gsub("/","%2f") + path_stack.unshift(safe_key) + # compare the_value + found = false + if the_value.is_a?(::GxG::Database::Field) + if (the_value.content == the_object) + found = true + end + else + if (the_value == the_object) + found = true + end + end + if found + search_results << ("/" + path_stack.reverse.join("/")) + end + # + if the_value.is_any?(::Array, ::Hash, ::Struct, ::GxG::Database::PersistedHash, ::GxG::Database::PersistedArray) + container_stack.unshift({:selector => selector, :container => the_value, :prefix => (path_stack.reverse.join("/"))}) + end + path_stack.shift + # + nil + end + else + search_results << ("/" + path_stack.join("/")) + end + search_results + end + # + def get_at_path(the_path="/") + # /^(?:[0-9])*[0-9](?:[0-9])*$/ = nil if an alpha present there, else 0 only numeric + # Attribution : http://stackoverflow.com/questions/1240674/regex-match-a-string-containing-numbers-and-letters-but-not-a-string-of-just-nu + # + # if ":" detected do: (str.gsub("%2f","/").to_sym) as key else (str.gsub("%2f","/")) + result = nil + if the_path == "/" + result = self + else + object_stack = [(self)] + path_stack = the_path.split("/") + path_stack.to_enum.each do |path_element| + element = nil + if path_element.size > 0 + if (path_element =~ /^(?:[0-9])*[0-9](?:[0-9])*$/) == 0 + element = path_element.to_i + else + element = path_element + element = element.gsub("%2f","/") + if element[0] == ":" + element = element[(1..-1)].to_sym + end + end + end + if element + result = object_stack.first[(element)] + if result.is_a?(NilClass) + break + else + object_stack.unshift(result) + end + else + # ignore double slashes? '//' + # break + end + end + end + result + end + # + def set_at_path(the_path="/",the_value=nil) + result = nil + if the_path != "/" + container = self.get_at_path(::File::dirname(the_path)) + if container + raw_selector = ::File::basename(the_path) + selector = nil + if raw_selector.size > 0 + if (raw_selector =~ /^(?:[0-9])*[0-9](?:[0-9])*$/) == 0 + selector = raw_selector.to_i + else + selector = raw_selector + selector = selector.gsub("%2f","/") + if selector[0] == ":" + selector = selector[(1..-1)].to_sym + end + end + end + if selector + container[(selector)] = the_value + result = container[(selector)] + else + # ignore double slashes? '//' + # break + end + # + end + end + result + end + # + def gxg_export() + result = {:type => "Array", :content => []} + export_db = [{:parent => nil, :parent_selector => nil, :object => self, :record => result}] + children_of = Proc.new do |the_parent=nil| + list = [] + export_db.each do |node| + if node[:parent].object_id == the_parent.object_id + list << node + end + end + list + end + export_record = Proc.new do |the_value| + if the_value.is_any?(Integer, Float, String) + if the_value.is_a?(Numeric) + if the_value.to_s.include?(".") + {:type => "Float", :content => the_value} + else + {:type => "Integer", :content => the_value} + end + else + {:type => (the_value.class.to_s), :content => the_value} + end + else + {:type => (the_value.class.to_s), :content => the_value.to_s} + end + end + # Build up export_db: + self.search do |the_value, the_selector, the_container| + if the_value.is_a?(::Hash) + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => {:type => "Hash", :content => {}}} + else + if the_value.is_a?(::Array) + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => {:type => "Array", :content => []}} + else + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => export_record.call(the_value)} + end + end + end + # Collect children export content: + link_db =[(export_db[0])] + while link_db.size > 0 do + entry = link_db.shift + children_of.call(entry[:object]).each do |the_child| + entry[:record][:content][(the_child[:parent_selector])] = the_child[:record] + if the_child[:object].is_any?(Hash, Array) + link_db << the_child + end + end + end + # + result + end + # + def self.gxg_import(the_exported_record=nil) + result = nil + if the_exported_record.is_a?(Hash) + if the_exported_record[:type] == "Hash" + result = ::Hash::gxg_import(the_exported_record) + else + import_value = Proc.new do |type,value| + if value.is_a?(String) + begin + the_class = eval(type) + if the_class.respond_to?(:parse) + value = the_class.parse(value) + else + if the_class.respond_to?(:new) + value = the_class.new(value) + else + if the_class.respond_to?(:try_convert) + value = the_class.try_convert(value) + end + end + end + rescue Exception => the_error + end + end + value + end + if the_exported_record[:type] == "Array" + result = [] + import_db = [{:parent => nil, :parent_selector => nil, :object => result, :record => the_exported_record}] + while import_db.size > 0 do + entry = import_db.shift + if entry[:record][:content].is_a?(Hash) + entry[:record][:content].each_pair do |selector, value| + if value[:type] == "Hash" + entry[:object][(selector)] = {} + import_db << {:parent => entry[:object], :parent_selector => selector, :object => entry[:object][(selector)], :record => value} + else + if value[:type] == "Array" + entry[:object][(selector)] = [] + import_db << {:parent => entry[:object], :parent_selector => selector, :object => entry[:object][(selector)], :record => value} + else + entry[:object][(selector)] = import_value.call(value[:type], value[:content]) + end + end + end + else + if entry[:record][:content].is_a?(Array) + entry[:record][:content].each_with_index do |value, selector| + if value[:type] == "Hash" + entry[:object][(selector)] = {} + import_db << {:parent => entry[:object], :parent_selector => selector, :object => entry[:object][(selector)], :record => value} + else + if value[:type] == "Array" + entry[:object][(selector)] = [] + import_db << {:parent => entry[:object], :parent_selector => selector, :object => entry[:object][(selector)], :record => value} + else + entry[:object][(selector)] = import_value.call(value[:type], value[:content]) + end + end + end + end + end + end + else + result = import_value.call(the_exported_record[:type], the_exported_record[:content]) + end + end + end + result + end +end +# \ No newline at end of file diff --git a/gxg/web/gxg_comm.rb b/gxg/web/gxg_comm.rb new file mode 100644 index 0000000..b18d4dd --- /dev/null +++ b/gxg/web/gxg_comm.rb @@ -0,0 +1,1081 @@ +# +#require 'gxg_uri' +# +module GxG + module Networking + # WebSocket Connector: + class SocketMonitor + # + def initialize() + @event_handlers = {} + self + end + # + def on(the_operation=nil, &block) + if the_operation.is_any?(::String, ::Symbol) + if block.respond_to?(:call) + @event_handlers[(the_operation.to_s.downcase.to_sym)] = block + end + end + end + # + def follow_up(options={}) + operation_frame = GxG::DISPLAY_DETAILS[:socket].next_message() + if operation_frame.is_a?(::Hash) + the_operation = (operation_frame.keys[0]) + if @event_handlers[(the_operation)].respond_to?(:call) + @event_handlers[(the_operation)].call(operation_frame[(the_operation)]) + end + end + end + # + end + # + class Socket + # + def inbox() + @inbox + end + # + def add_to_inbox(payload="") + begin + @inbox << ::JSON.parse(payload.decode64,{:symbolize_names => true}) + rescue Exception => the_error + log_error({:error => the_error, :parameters => payload.decode64}) + end + end + # + def error_message(the_message) + log_error({:error => Exception.new(the_message)}) + end + # + def next_message() + @inbox.shift + end + # + def write(the_data="") + # Expects: String or Hash + if the_data.is_any?(::Hash, ::GxG::Database::DetachedHash) + if the_data.is_a?(::Hash) + the_data = JSON.generate(the_data).encode64 + else + # Discouraged: no context of an operation this way. Not good. (deprecate?) + the_data = JSON.generate(the_data.export).encode64 + end + end + thesocket = @socket + if thesocket + `thesocket.send(the_data)` + true + else + false + end + end + # + def close() + thesocket = @socket + if thesocket + `thesocket.close` + @socket = nil + true + else + false + end + end + # + def open?() + if @socket + true + else + false + end + end + # + def open(the_location) + @socket = nil + if (`window.location['href']`).include?("https://") + the_url = "wss://#{the_location}" + else + the_url = "ws://#{the_location}" + end + begin + greeting = {:attach_display => true}.to_json.encode64 + the_socket = `new WebSocket(the_url)` + %x{ + the_socket.onopen = function(e) { + the_socket.send(greeting); + }; + the_socket.onmessage = function(event) { + Opal.eval("GxG::DISPLAY_DETAILS[:socket].add_to_inbox('" + event.data + "')"); + }; + the_socket.onclose = function(event) { + Opal.eval("GxG::DISPLAY_DETAILS[:socket].close"); + }; + the_socket.onerror = function(error) { + Opal.eval("GxG::DISPLAY_DETAILS[:socket].error_message('" + error.message + "')"); + }; + } + @socket = the_socket + rescue Exception => the_error + log_error(:error => the_error, :parameters => {:url => the_url.to_s}) + end + if @socket + ::GxG::DISPLAY_DETAILS[:socket] = self + end + end + # + def set_remote_uuid(the_uuid=nil) + if GxG::valid_uuid?(the_uuid) + @remote_uuid = the_uuid.to_sym + true + else + false + end + end + # + def initialize() + @uuid = GxG::uuid_generate().to_sym + @remote_uuid = nil + @inbox = [] + self + end + end + end + # XHR Connector: + module Networking + class Channel + # + end + # + class Connector + # + def remote_uuid() + @remote_uuid + end + # + def open?() + @active + end + # + def close() + the_message = new_message({:close => true}) + the_message[:sender] = @uuid + the_message[:to] = @remote_uuid + self.pull(the_message,{:synchronous => true}) + @active = false + @csrf = nil + if @heart_beat_timer.is_a?(::Hash) + GxG::DISPATCHER.cancel_timer(@heart_beat_timer) + end + true + end + # + def push(the_message=nil, options={}) + if @active + # Uses PUT method, with data payload + if the_message.is_a?(::GxG::Events::Message) + if @relative_url.to_s.size > 0 + the_url = "#{@host_prefix}#{@relative_url.to_s}?display=#{@display_path.to_s}" + else + the_url = "#{@host_prefix}?display=#{@display_path.to_s}" + end + payload = the_message.body.to_json.to_s + the_xhr = `new XMLHttpRequest` + `#{the_xhr}.open('PUT',#{the_url},true)` + `#{the_xhr}.timeout = #{(options[:timeout] || 5000)}` + `#{the_xhr}.setRequestHeader('X-Requested-With','XMLHttpRequest')` + `#{the_xhr}.setRequestHeader('Accept','application/json')` + `#{the_xhr}.setRequestHeader('X-CSRF-Token', #{@csrf})` + `#{the_xhr}.setRequestHeader('Content-Type', 'application/json')` + # unless (`navigator.userAgent`).include?("Firefox") || (`navigator.userAgent`).include?("Chrome") + # `#{the_xhr}.setRequestHeader('Content-Length', #{payload.size})` + # end + failed = Proc.new do |reason=nil| + the_message.fail(reason) + end + succeeded = Proc.new do + response_code = (`#{the_xhr}.status`).to_i + if response_code < 300 + begin + the_message.succeed(::JSON.parse(`#{the_xhr}.response`,{:symbolize_names => true})) + rescue Exception => the_error + log_error({:error => the_error, :parameters => `#{the_xhr}.response`}) + end + else + failed.call(response_code) + end + end + `#{the_xhr}.addEventListener('load', #{succeeded})` + `#{the_xhr}.addEventListener('error', #{failed})` + `#{the_xhr}.addEventListener('timeout', #{failed})` + `#{the_xhr}.addEventListener('abort', #{failed})` + `#{the_xhr}.send(#{payload})` + # + true + else + # error? + false + end + end + # + end + # + def pull(the_message=nil, options={:synchronous => false}) + if @active + # Uses GET method, with in-URL query specifiers (no data payload) + # @display_path + if the_message.is_a?(::GxG::Events::Message) + the_query = "display=#{@display_path.to_s}&details=#{the_message.body.to_json.encode64}" + if @relative_url.to_s.size > 0 + the_url = (@host_prefix.to_s + @relative_url.to_s + "?" + the_query) + else + the_url = (@host_prefix.to_s + "?" + the_query) + end + the_xhr = `new XMLHttpRequest` + if options[:synchronous] == true + `#{the_xhr}.open('GET',#{the_url.to_s},false)` + else + `#{the_xhr}.open('GET',#{the_url.to_s},true)` + `#{the_xhr}.timeout = #{(options[:timeout] || 600000)}` + end + `#{the_xhr}.setRequestHeader('X-Requested-With','XMLHttpRequest')` + `#{the_xhr}.setRequestHeader('Accept','application/json')` + failed = Proc.new do |reason=nil| + the_message.fail(reason) + end + succeeded = Proc.new do + response_code = (`#{the_xhr}.status`).to_i + if response_code < 300 + begin + the_message.succeed(::JSON.parse(`#{the_xhr}.response`,{:symbolize_names => true})) + rescue Exception => the_error + log_error({:error => the_error, :parameters => `#{the_xhr}.response`}) + end + else + failed.call(response_code) + end + end + `#{the_xhr}.addEventListener('load', #{succeeded})` + `#{the_xhr}.addEventListener('error', #{failed})` + `#{the_xhr}.addEventListener('timeout', #{failed})` + `#{the_xhr}.addEventListener('abort', #{failed})` + `#{the_xhr}.send(null)` + # + true + else + # error? + false + end + end + # + end + # + def push_message(the_message=nil) + # TODO: establish a sensible routing scheme and make better use of message :sender and :to in that idea. + end + # + def pull_message(options={}) + # Use to pull a message portion or look for application messages on the server. + end + # + def process_pending(options={}) + # + end + # Support-level methods for pulling/pushing objects or object-deltas. + def push_object_portions(details={}, error_handler=nil, &the_handler) + # supply thread and index to send, if successful, will post event to keep chain going. + # Details: {:thread => "", :index => 0, :portions => 100} + end + # Search The Database(s) + def search_database(details={}, error_handler=nil, &the_handler) + # FOR NOW: limit 100 records returned at a time. + # Use: {:limit => 100, :offset => 0} || {:page => 1} + if details.is_a?(::Hash) && the_handler + self.pull_data({:search_database => {:criteria => details}},error_handler) do |data| + if data.is_a?(::Hash) + the_handler.call(data) + end + end + true + else + false + end + end + # Search The Database(s) & PULL a batch of records and formats + def search_pull(details={}, error_handler=nil, &the_handler) + # FOR NOW: limit 100 records returned at a time. + # Use: {:limit => 100, :offset => 0} || {:page => 1} + if details.is_a?(::Hash) && the_handler + self.pull_data({:search_pull => {:criteria => details}},error_handler) do |data| + if data.is_a?(::Hash) + if data[:result] + if data[:result].is_a?(::String) + if data[:result].to_s.base64? + data = data[:result].decode64 + end + if data.to_s.json? + begin + data = ::JSON::parse(data.to_s,{:symbolize_names => true}) + the_handler.call(::GxG::Database::process_detached_import(data)) + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:data => data}}) + end + end + # + else + # error + log_error({:error => Exception.new("Malformed Response Data"), :parameters => {:data => data[:result]}}) + end + else + if data[:error] + if error_handler.respond_to?(:call) + error_handler.call(data) + end + end + end + # + end + end + true + else + false + end + end + # High-level methods for pulling/pushing objects or object-deltas. + def destroy_object(the_specifier=nil,error_handler=nil,&the_handler) + if the_specifier && the_handler + self.pull_data({:destroy_object => {:path => the_specifier}},error_handler) do |data| + if data.is_a?(::Hash) + the_handler.call(data) + end + end + true + else + false + end + end + # + def push_data(details={}, error_handler=nil,&the_handler) + if details.is_a?(::Hash) && the_handler + # + the_message = new_message(details) + the_message[:sender] = @uuid + the_message[:to] = @remote_uuid + if error_handler.respond_to?(:call) + the_message.on(:fail,&error_handler) + end + the_message.on(:success) do |response| + the_handler.call(response) + end + GxG::CONNECTION.push(the_message) + # + true + else + false + end + end + # + def push_object(details={}, error_handler=nil,&the_handler) + if details.is_a?(::Hash) && the_handler + # ???? + # Details: {:path => "/path/to/object", :object => (BinaryArray/String/PersistedHash)} + exception = nil + payload = {} + if details[:object].is_any?(::GxG::BinaryArray, ::String) + # Binary payload + if details[:path].to_s.size > 0 + payload[:path] = details[:path] + payload[:object] = details[:object].to_s.encode64 + else + exception = "You MUST specify a :path to store the data into." + end + else + if details[:object].is_a?(::GxG::Database::DetachedHash) + # DB Object / VFS payload + if details[:path].to_s.size > 0 + payload[:path] = details[:path] + end + payload[:uuid] = details[:object].uuid.to_s + # Generate Importation Records + format_list = [] + importation_record = {:formats => {}, :records => []} + # + if details[:object].format.to_s.size > 0 + format_list << details[:object].format + end + details[:object].search do |value, selector, container| + if value.is_a?(::GxG::Database::DetachedHash) + if value.format.to_s.size > 0 + format_list << value.format + end + end + if value.is_a?(::GxG::Database::PersistedArray) + if value.constraint.to_s.size > 0 + format_list << value.constraint + end + end + end + format_list.each do |the_specifier| + format_record = GxG::DB[:formats][(the_specifier.to_s.to_sym)].clone + if format_record.is_a?(::Hash) + format_record[:content] = format_record[:content].gxg_export + importation_record[:formats][(the_specifier.to_s.to_sym)] = format_record + end + end + importation_record[:records] << details[:object].export + # + payload[:object] = importation_record + else + exception = "Invalid Data Type" + end + end + if exception + if error_handler.respond_to?(:call) + error_handler.call({:result => nil, :error => exception}) + end + false + else + self.push_data({:put_object => payload},error_handler) do |data| + if data.is_a?(::Hash) + the_handler.call(data) + end + end + true + end + else + false + end + end + # + def pull_data(the_specifier={},error_handler=nil,&the_handler) + if the_specifier && the_handler + # accepts entire command frame as specifier: + the_message = new_message(the_specifier) + the_message[:sender] = @uuid + the_message[:to] = @remote_uuid + if error_handler.respond_to?(:call) + the_message.on(:fail,&error_handler) + end + the_message.on(:success) do |response| + if response.is_a?(::Hash) + data = nil + if response[:result].is_a?(::String) + if response[:result].to_s.base64? + data = response[:result].decode64 + end + if data.to_s.json? + begin + data = ::JSON::parse(data.to_s,{:symbolize_names => true}) + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:data => data}}) + end + end + the_handler.call(data) + else + # error + log_error({:error => Exception.new("Malformed Response Data"), :parameters => {:data => response[:result]}}) + end + end + end + GxG::CONNECTION.pull(the_message) + end + end + # + def pull_object(the_specifier=nil,error_handler=nil,&the_handler) + if the_specifier && the_handler + # GxG::DISPLAY_DETAILS[:object].set_busy(true) + if GxG::valid_uuid?(the_specifier.to_s) + op_frame = {:get_object => {:uuid => the_specifier}} + else + op_frame = {:get_object => {:path => the_specifier}} + end + self.pull_data(op_frame,error_handler) do |data| + if data.is_a?(::Hash) + # puts "Got: #{data.inspect}" + the_handler.call(::GxG::Database::process_detached_import(data)) + # xxx + # if data[:result] + # if data[:result].is_a?(::String) + # if data[:result].to_s.base64? + # data = data[:result].decode64 + # end + # if data.to_s.json? + # begin + # data = ::JSON::parse(data.to_s,{:symbolize_names => true}) + # the_handler.call(::GxG::Database::process_detached_import(data)) + # rescue Exception => the_error + # log_error({:error => the_error, :parameters => {:data => data}}) + # end + # end + # # + # else + # # error + # log_error({:error => Exception.new("Malformed Response Data"), :parameters => {:data => data[:result]}}) + # end + # else + # if data[:error] + # if error_handler.respond_to?(:call) + # error_handler.call(data) + # end + # end + # end + # GxG::DISPLAY_DETAILS[:object].set_busy(false) + end + end + true + else + false + end + end + # + def load_format(details={}, error_handler=nil, &the_handler) + if details && the_handler + self.pull_data({:get_format => details},error_handler) do |data| + if data.is_a?(::Hash) + if data[:result] + if data[:result].is_a?(::String) + if data[:result].to_s.base64? + data = data[:result].decode64 + end + if data.to_s.json? + begin + data = ::JSON::parse(data.to_s,{:symbolize_names => true}) + ::GxG::Database::process_detached_import(data) + the_handler.call({:result => true}) + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:data => data}}) + end + end + # + else + # error + log_error({:error => Exception.new("Malformed Response Data"), :parameters => {:data => data[:result]}}) + end + else + if data[:error] + if error_handler.respond_to?(:call) + error_handler.call(data) + end + end + end + # + end + end + true + else + false + end + end + # + def store_format(details={}, error_handler=nil, &the_handler) + if details && the_handler + if details.keys.size == 0 + format_list = ::GxG::DB[:formats].keys + else + format_list = [] + ::GxG::DB[:formats].keys.each do |the_uuid| + if details[:uuid].to_s.size > 0 + if the_uuid == details[:uuid] + unless format_list.include?(the_uuid) + format_list << the_uuid + end + end + end + if details[:type].to_s.size > 0 + if ::GxG::DB[:formats][(the_uuid)][:type] == details[:type] + unless format_list.include?(the_uuid) + format_list << the_uuid + end + end + end + if details[:ufs].to_s.size > 0 + if ::GxG::DB[:formats][(the_uuid)][:ufs] == details[:ufs] + unless format_list.include?(the_uuid) + format_list << the_uuid + end + end + end + if details[:title].to_s.size > 0 + if ::GxG::DB[:formats][(the_uuid)][:title] == details[:title] + unless format_list.include?(the_uuid) + format_list << the_uuid + end + end + end + if details[:version].to_s.size > 0 + if ::GxG::DB[:formats][(the_uuid)][:version] == details[:version] + unless format_list.include?(the_uuid) + format_list << the_uuid + end + end + end + end + end + importation_record = {:formats => {}, :records => []} + format_list.each do |the_uuid| + format_record = ::GxG::DB[:formats][(the_uuid.to_s.to_sym)].clone + format_record[:content] = format_record[:content].gxg_export + importation_record[:formats][(the_uuid.to_s.to_sym)] = format_record + end + self.push_data({:put_format => importation_record},error_handler) do |data| + if data.is_a?(::Hash) + the_handler.call(data) + end + end + true + else + false + end + end + # + def get_permissions(the_specifier=nil,error_handler=nil,&the_handler) + if the_specifier && the_handler + # GxG::DISPLAY_DETAILS[:object].set_busy(true) + self.pull_data({:get_permissions => {:path => the_specifier}},error_handler) do |data| + if data.is_a?(::Hash) + the_handler.call(data) + # GxG::DISPLAY_DETAILS[:object].set_busy(false) + end + end + true + else + false + end + end + # + # Libarary Loading Support: + def library_pull(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + self.pull_data({:get_library => details},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # Format Support: + def format_pull(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_any?(::Array, ::Hash) && the_handler + self.pull_data({:format_pull => details},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # TODO: format_push(data={}) + def format_push(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + self.push_data({:format_push => details},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # + def format_list(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + self.pull_data({:format_list => details},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # Admin: + def admin(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + self.pull_data({:admin => details},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # + # VFS: + def vfs(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + if [:vfs_mkfile, :vfs_mkdir, :vfs_rename, :vfs_destroy, :vfs_copy, :vfs_move, :set_permissions].include?(details.keys[0]) + self.pull_data(details, error_handler) do |data| + # Review : further decoding needed? base64?? + the_handler.call(data) + end + true + else + false + end + else + false + end + else + false + end + end + # + # Application Support: + def app_state_push(details={}, error_handler=nil, &the_handler) + if details.is_a?(::Hash) && the_handler + self.push_data({:app_state_push => {:application => details[:application], :data => details[:data]}},error_handler) do |data| + if data.is_a?(::Hash) + the_handler.call(data) + end + end + true + else + false + end + end + # + def app_state_pull(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + self.pull_data({:app_state_pull => {:application => details[:application]}},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # + def application_open(details={}, error_handler=nil, &the_handler) + # Specify either a :path or :name + if self.open? + if details.is_a?(::Hash) && the_handler + if details[:name] + self.pull_data({:application_open => {:restore => (details[:restore] || false), :name => details[:name]}},error_handler) do |data| + the_handler.call(data) + end + else + # Review : remember to call using 'path' - NOT 'location' anymore: + self.pull_data({:application_open => {:restore => (details[:restore] || false), :path => details[:path]}},error_handler) do |data| + the_handler.call(data) + end + end + true + else + false + end + end + end + # + def application_close(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + self.pull_data({:application_close => {:application => details[:application]}},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # + def application_list(error_handler=nil, &the_handler) + if self.open? + if the_handler + self.pull_data({:application_list => true},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # + def application_menu(error_handler=nil, &the_handler) + if self.open? + if the_handler + self.pull_data({:application_menu => true},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # + def entries(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + GxG::DISPLAY_DETAILS[:object].set_busy(true) + # Review : remember to use 'path' - NOT 'location' anymore: + self.pull_data({:entries => {:path => details[:path]}},error_handler) do |data| + the_handler.call(data) + GxG::DISPLAY_DETAILS[:object].set_busy(false) + end + true + else + false + end + end + end + # + def heartbeat() + if @active + heartbeat_message = new_message({:heartbeat => true}) + heartbeat_message[:sender] = @uuid.to_s + heartbeat_message[:to] = @remote_uuid.to_s + heartbeat_message.on(:success) do |response| + if response.is_a?(::Hash) + # FIXME: possible conflict with proper credential setting. + if response[:result][:status] == "credentialed" + # GxG::DISPLAY_DETAILS[:logged_in] = true + else + # GxG::DISPLAY_DETAILS[:logged_in] = false + end + GxG::DISPLAY_DETAILS[:server_status] = response[:result][:server].to_s.to_sym + end + end + GxG::CONNECTION.pull(heartbeat_message) + end + end + # + # Set Credential: + def update_credential(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + content = details.to_json.to_s.encrypt(@csrf.to_s).encode64 + the_message = new_message({:update_credential => content}) + the_message[:sender] = @uuid + the_message[:to] = @remote_uuid + the_message[:body][:thread] = the_message.id().to_s + the_message.on(:success) do |the_response| + if the_response.is_a?(::Hash) + if the_response[:status] == "credentialed" + GxG::DISPLAY_DETAILS[:logged_in] = true + end + end + the_handler.call(the_response) + end + if error_handler + the_message.on(:fail) do |the_response| + error_handler.call(the_response) + end + end + GxG::CONNECTION.push(the_message) + end + end + end + # + def downgrade_credential(&the_handler) + # ???? + if @active + downgrade_message = new_message({:downgrade_credential => true}) + downgrade_message[:sender] = @uuid.to_s + downgrade_message[:to] = @remote_uuid.to_s + downgrade_message.on(:success) do |response| + # var evt = new CustomEvent('credentialchanged', { detail: "state" }); ?? + # window.dispatchEvent(evt); + if response.is_a?(::Hash) + # FIXME: possible conflict with proper credential setting. + if response[:status] == "uncredentialed" + GxG::DISPLAY_DETAILS[:logged_in] = false + if the_handler.respond_to?(:call) + the_handler.call(response) + end + new_event = `new CustomEvent('credentialchanged', { detail: #{response[:status].to_s} })` + `window.dispatchEvent(#{new_event})` + else + # GxG::DISPLAY_DETAILS[:logged_in] = true + end + # GxG::DISPLAY_DETAILS[:server_status] = response[:server].to_s.to_sym + end + end + GxG::CONNECTION.pull(downgrade_message) + end + end + # + def action(details={}, error_handler=nil, &the_handler) + # ???? + end + # + def relative_url() + @relative_url + end + # + def query() + @query + end + # + def article() + @article + end + # + def display_path() + @display_path + end + # + def initialize() + if (`window.location['href']`).include?("https://") + @host_prefix = ("https://" + `window.location['host']`) + else + @host_prefix = ("http://" + `window.location['host']`) + end + @relative_url = "" + # Find Relative URL (Note: now provided by host in introduction) + page_name = File::basename(`window.location['pathname']`) + subpath_array = (`window.location['pathname']`.split("/")) + found_at = subpath_array.index(page_name) + if found_at.is_a?(::Numeric) + if found_at > 1 + @relative_url = subpath_array[(0..(found_at - 1))].join("/") + end + end + # Process Query if found + @article = nil + @query = {} + query_string = `window.location.href`.split("?")[1] + if query_string + @article = query_string.split("#")[1] + query_string = query_string.split("#")[0] + if query_string.include?("&") + query_array = query_string.split("&") + else + query_array = query_string.split("&") + end + query_array.each do |entry| + buffer = entry.split("=") + @query[(buffer[0].to_s.to_sym)] = buffer[1] + end + else + @article = `window.location.href`.split("#")[1] + end + # + @uuid = GxG.uuid_generate().to_sym + @remote_uuid = nil + @active = true + @display_path = nil + @in_busy = false + @out_busy = false + @pending = {} + @channels = {} + @buffers = {} + @csrf = nil + @the_connector = self + # Make introduction to host and aquire csrf token + the_introduction = new_message({:introduction => @uuid.to_s}) + the_introduction[:sender] = @uuid.to_s + the_introduction[:to] = @host_prefix.to_s + the_introduction.on(:success) do |response| + if response[:result] == "OK" + if response[:csrf] + @csrf = response[:csrf] + end + if response[:status] == "credentialed" + GxG::DISPLAY_DETAILS[:logged_in] = true + else + GxG::DISPLAY_DETAILS[:logged_in] = false + end + if response[:relative_url] + @relative_url = response[:relative_url] + end + # + if response[:display] == "unavailable" + `alert('Display Resource Unavailable')` + else + @display_path = response[:display] + @active = true + # Are we logged in at the server and recovering from a crash ?? + GxG::DISPATCHER.post_event(:display) do + until (`document.readyState`.to_s == "complete") do + sleep 0.5 + end + ::GxG::DISPLAY_DETAILS[:query] = @query + ::GxG::DISPLAY_DETAILS[:article] = @article + %x{ + var do_unload = function (the_event) { + Opal.GxG.$eval("GxG::DISPLAY_DETAILS[:socket].close"); + if (the_event) { + the_event.returnValue = "closing..."; + } + for (var i = 0; i < 500000000; i++) { } + return "closing..."; + }; + window.addEventListener('unload', do_unload,false); + window.addEventListener('onunload', do_unload,false); + window.addEventListener('beforeunload', do_unload,false); + window.addEventListener('onbeforeunload', do_unload,false); + window.onbeforeunload = do_unload; + window.beforeunload = do_unload; + } + # `window.addEventListener('unload', do_unload,false);function do_unload() { Opal.GxG.$eval("GxG::CONNECTION.close"); };` + # `window.addEventListener('beforeunload', do_unload,false);function do_unload() { Opal.GxG.$eval("GxG::CONNECTION.close"); };` + # `window.addEventListener('onbeforeunload', do_unload,false);function do_unload() { Opal.GxG.$eval("GxG::CONNECTION.close"); };` + # Load Page + GxG::DISPATCHER.post_event(:display) do + # sleep 0.5 + ::GxG::Gui::Vdom::navigate_to(`window.location['pathname']`) + end + # Establish WebSocket Channel: + ::GxG::DISPLAY_DETAILS[:socket] = ::GxG::Networking::Socket.new + ::GxG::DISPLAY_DETAILS[:socket].open((`window.location['host']` + "/" + @relative_url + "/ws").gsub("//","/")) + ::GxG::SOCKET_MONITOR.on(:attach_socket) do |the_uuid| + if ::GxG::valid_uuid?(the_uuid) + the_message = new_message({:attach_socket => the_uuid.to_s}) + the_message[:sender] = @uuid + the_message[:to] = @remote_uuid + # Review : add fail/succeed code blocks?? + GxG::CONNECTION.push(the_message) + end + end + ::GxG::DISPATCHER.add_periodic_timer({:interval => 0.333}) do + ::GxG::SOCKET_MONITOR.follow_up + end + # + end + # + end + # + end + end + the_introduction.on(:fail) do |response| + @active = false + log_error({:error => Exception.new("Communication Error"), :parameters => {:response => response}}) + end + GxG::DISPATCHER.post_event(:communications) do + GxG::CONNECTION.pull(the_introduction) + @heart_beat_timer = GxG::DISPATCHER.add_periodic_timer({:interval => 60.0}) do + GxG::CONNECTION.heartbeat() + end + # ???? + end + # # + # `window.addEventListener('unload', do_unload,false);function do_unload() { Opal.GxG.$eval("GxG::CONNECTION.close"); };` + # `window.addEventListener('beforeunload', do_unload,false);function do_unload() { Opal.GxG.$eval("GxG::CONNECTION.close"); };` + # `window.addEventListener('onbeforeunload', do_unload,false);function do_unload() { Opal.GxG.$eval("GxG::CONNECTION.close"); };` + # `window.onbeforeunload = function do_reload (the_event) { + # Opal.eval("GxG::CONNECTION.close"); + # the_event.returnValue = "closing..."; + # return "closing..."; + # }; + # ` + # + self + end + # + end + end +end diff --git a/gxg/web/gxg_database.rb b/gxg/web/gxg_database.rb new file mode 100644 index 0000000..726a3b2 --- /dev/null +++ b/gxg/web/gxg_database.rb @@ -0,0 +1,2319 @@ +# +module GxG + module Database + def self.element_tables() + { + :unspecified => nil, + :element_boolean => [::TrueClass, ::FalseClass, ::NilClass], + :element_integer => [::Integer], + :element_float => [::Float], + :element_bigdecimal => [::BigDecimal], + :element_datetime => [::Time], + :element_text => [::String], + :element_binary => [::GxG::ByteArray], + :element_array => [::Array], + :element_hash => [::Hash] + } + end + # + def self.valid_field_classes() + [::TrueClass, ::FalseClass, ::NilClass, ::Integer, ::Float, ::BigDecimal, ::Time, ::String, ::GxG::ByteArray] + end + # + def self.element_table_index(the_key=:unspecified) + (::GxG::Database::element_tables().keys.index(the_key.to_sym) || 0) + end + # + def self.element_table_by_index(the_index=0) + (::GxG::Database::element_tables().keys[(the_index)] || :unspecified) + end + # + def self.element_table_for_instance(the_instance=nil) + result = :unspecified + # + table = ::GxG::Database::element_tables() + table.keys.each do |the_key| + unless the_key == :unspecified + if the_instance.is_any?(table[(the_key)]) + result = the_key + break + end + end + end + # + result + end + # + def self.persistable?(the_object) + result = true + unless the_object.is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + if the_object.is_any?(::Hash, ::Array) + the_object.search do |item, selector, container| + if result + if ::GxG::Database::element_table_for_instance(item) == :unspecified + result = false + break + end + end + nil + end + else + if ::GxG::Database::element_table_for_instance(the_object) == :unspecified + result = false + end + end + end + result + end + # + def self.iterative_detached_persist(old_root=nil) + # New DetachedArray, or DetachedHash interface: + # + result = nil + begin + # db open? + # write permissions on db? + unless old_root.is_any?(::Array, ::Hash) + raise ArgumentError, "You MUST provide either an Array or a Hash." + end + unless GxG::Database::persistable?(old_root) + raise ArgumentError, "The object you are attempting to persist contains a non-persistable item." + end + # + if old_root.is_a?(::Array) + original_partner = ::GxG::Database::DetachedArray.new() + # + end + if old_root.is_a?(::Hash) + original_partner = ::GxG::Database::DetachedHash.new() + end + # + paring_data = [{:parent => nil, :parent_selector => nil, :object => old_root, :partner => original_partner}] + children_of = Proc.new do |the_parent=nil| + list = [] + paring_data.each do |node| + if node[:parent].object_id == the_parent.object_id + list << node + end + end + list + end + # + parent_of = Proc.new do |the_parent=nil| + output = nil + paring_data.each do |entry| + if entry[:object].object_id == the_parent.object_id + output = entry + end + end + output + end + find_partner = Proc.new do |the_object| + found_partner = nil + paring_data.each do |the_record| + if the_record.is_a?(::Hash) + if the_record[:object].object_id == the_object.object_id + found_partner = the_record[:partner] + break + end + end + end + found_partner + end + # build paring data: + delegate_permission = nil + old_root.search do |the_value, the_selector, the_container| + if the_value.is_a?(::Array) + paring_data << {:parent => the_container, :parent_selector => the_selector, :object => the_value, :partner => ::GxG::Database::DetachedArray.new()} + else + if the_value.is_a?(::Hash) + paring_data << {:parent => the_container, :parent_selector => the_selector, :object => the_value, :partner => ::GxG::Database::DetachedHash.new()} + else + paring_data << {:parent => the_container, :parent_selector => the_selector, :object => the_value, :partner => the_value} + end + end + end + # + # Assign objects to structure in order by parent / parent_selector + link_db = [(paring_data[0])] + while link_db.size > 0 + entry = link_db.shift + if entry.is_a?(::Hash) + if entry[:object].is_any?(::Array, ::Hash) + # get children and assign + children = children_of.call(entry[:object]) + children.each do |child| + entry[:partner][(child[:parent_selector])] = child[:partner] + if child[:partner].is_any?(::GxG::Database::DetachedArray, ::GxG::Database::DetachedHash) + link_db << child + end + end + end + end + end + result = original_partner + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:object => old_root}}) + end + result + end + # + # + # Format support: + def self.detached_format_find(the_criteria={}) + result = nil + tests = {} + [:uuid, :ufs, :title, :version].each do |the_key| + if the_criteria[(the_key)] + tests[(the_key)] = the_criteria[(the_key)] + end + end + GxG::DB[:formats].each_pair do |uuid,record| + score = 0 + tests.keys.each do |the_test| + if record[(the_test)] == tests[(the_test)] + score += 1 + end + end + if tests.keys.size == score + result = record + break + end + end + result + end + # + def self.detached_format_load(the_criteria={}, &block) + # If already loaded and block provided: return an Array of format records. + # Load any missing formats from server, and call block if provided. + # Note: results are in NO particular order (DO NOT INDEX into the result set from source criteria) + success = false + needed = [] + results = [] + if the_criteria.is_a?(::Hash) + loaded = ::GxG::Database::detached_format_find(the_criteria) + if loaded + results << loaded + if block.respond_to?(:call) + block.call(results) + end + success = true + else + needed << the_criteria + end + else + if the_criteria.is_a?(::Array) + the_criteria.each do |the_specifier| + loaded = ::GxG::Database::detached_format_find(the_specifier) + if loaded + results << loaded + else + needed << the_specifier + end + end + end + end + if needed.size > 0 + GxG::CONNECTION.format_pull(needed) do |response| + if response.is_a?(::Hash) + if response[:result].is_a?(::Array) + response[:result].each do |format_record| + GxG::DB[:formats][(format_record[:uuid].to_sym)] = format_record + results << format_record + end + if block.respond_to?(:call) + block.call(results) + end + success = true + end + end + end + else + if results.size > 0 + if block.respond_to?(:call) + block.call(results) + end + success = true + end + end + # + success + end + # + def self.process_detached_import(import_manifest=nil) + result = [] + if import_manifest.is_a?(::Hash) + if import_manifest[:formats].size > 0 + # import formats: GxG::DB[:formats] + import_manifest[:formats].keys.each do |the_key| + unless GxG::DB[:formats][(the_key)] + # Review do ::Hash.gxg_import on format content + format_record = import_manifest[:formats][(the_key)] + if format_record[:version].is_a?(::String) + format_record[:version] = BigDecimal(format_record[:version].to_s) + end + format_record[:content] = ::Hash.gxg_import(format_record[:content]) + GxG::DB[:formats][(the_key)] = format_record + end + end + end + # + if import_manifest[:records].size > 1 + import_manifest[:records].each do |the_record| + result << GxG::Database::DetachedHash::import(the_record) + end + else + result << GxG::Database::DetachedHash::import(import_manifest[:records][0]) + end + end + result + end + # + def self.process_import(import_manifest=nil) + result = [] + if import_manifest.is_a?(::Hash) + if import_manifest[:formats].size > 0 + # import formats: GxG::DB[:formats] + import_manifest[:formats].keys.each do |the_key| + unless GxG::DB[:formats][(the_key)] + # Review do ::Hash.gxg_import on format content + format_record = import_manifest[:formats][(the_key)] + if format_record[:version].is_a?(::String) + format_record[:version] = BigDecimal(format_record[:version].to_s) + end + format_record[:content] = ::Hash.gxg_import(format_record[:content]) + GxG::DB[:formats][(the_key)] = format_record + end + end + end + # + if import_manifest[:records].size > 1 + import_manifest[:records].each do |the_record| + result << GxG::Database::PersistedHash::import(the_record) + end + else + result << GxG::Database::PersistedHash::import(import_manifest[:records][0]) + end + end + result + end + # + class DetachedHash + # + public + # + def uuid() + @uuid.clone + end + # + def uuid=(the_uuid=nil) + if GxG::valid_uuid?(the_uuid) + @uuid = the_uuid.to_s.to_sym + end + end + # + def title() + @title.clone + end + # + def title=(the_title=nil) + if the_title + @title = the_title.to_s[0..255] + self.increment_version + end + end + # + def version() + @version.clone + end + # + def version=(the_version=nil) + if the_version.is_a?(::Numeric) + @version = the_version.to_s("F").to_d + end + end + # + def element_version(key=nil) + result = 0.0 + if key.is_a?(::Symbol) + if key.to_s.size > 256 + log_warn({:warning => "Attempted oversized key usage (limited to 256 characters), truncated #{key.inspect} to #{key.to_s[(0..255)].to_sym.inspect}"}) + key = key.to_s[(0..255)].to_sym + end + # + if @property_links[(key.to_s.to_sym)] + result = @property_links[(key.to_s.to_sym)][:record][:version] + else + result = 0.0 + end + # + end + result + end + # + def set_element_version(element_key, the_version=nil) + result = false + if @property_links[(element_key)] + if the_version.is_a?(::Numeric) + @property_links[(element_key)][:record][:version] = (((the_version.to_f) * 10000.0).to_i.to_f / 10000.0) + result = true + else + log_warning("Attempted to set version to an invalid version value #{the_version.inspect} for #{element_key.inspect} on Object #{@uuid.inspect}") + end + else + log_warning("Attempted to set version with an invalid key #{element_key.inspect} on Object #{@uuid.inspect}") + end + result + end + # + def format() + @format.clone + end + # + def format=(the_format=nil) + if GxG::valid_uuid?(the_format) + @format = the_format.to_s.to_sym + end + end + # + def parent() + @parent + end + # + def parent=(object=nil) + if object.is_any?([::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray]) + # Review : parent can only be set once -- is this best?? + unless @parent + @parent = object + end + end + end + # + def write_reserved?() + true + end + # + def release_reservation() + true + end + # + def get_reservation() + true + end + # + def wait_for_reservation(timeout=nil) + true + end + # ### Review: move to Database class as generic toolbox method at some point: + # + def self.import(import_record=nil) + result = nil + unless import_record.is_any?(::Hash, ::GxG::Database::DetachedHash) + raise Exception, "Malformed import record passed." + end + # {:type => :element_hash, :uuid => @uuid.clone, :title => @title.clone, :version => @version.clone, :format => @format.clone, :content => {}} + import_db = [{:target => ::GxG::Database::DetachedHash.new, :record => import_record}] + result = import_db[0][:target] + while import_db.size > 0 do + entry = import_db.shift + case entry[:record][:type].to_s.to_sym + when :element_hash + entry[:target].uuid = entry[:record][:uuid].to_s.to_sym + entry[:target].title = entry[:record][:title].to_s + entry[:record][:content].keys.each do |the_key| + case entry[:record][:content][(the_key)][:type].to_s.to_sym + when :element_hash + new_target = GxG::Database::DetachedHash.new + entry[:target][(the_key)] = new_target + import_db << {:target => new_target, :record => (entry[:record][:content][(the_key)])} + entry[:target][(the_key)].parent = (entry[:target]) + when :element_array + new_target = GxG::Database::DetachedArray.new + entry[:target][(the_key)] = new_target + import_db << {:target => new_target, :record => (entry[:record][:content][(the_key)])} + entry[:target][(the_key)].parent = (entry[:target]) + when :element_boolean, :element_integer, :element_float, :element_text + entry[:target][(the_key)] = entry[:record][:content][(the_key)][:content] + when :element_bigdecimal + entry[:target][(the_key)] = BigDecimal(entry[:record][:content][(the_key)][:content].to_s) + when :element_datetime + entry[:target][(the_key)] = ::Chronic::parse(entry[:record][:content][(the_key)][:content].to_s) + when :element_binary + entry[:target][(the_key)] = ::GxG::ByteArray.new(entry[:record][:content][(the_key)][:content].to_s.decode64) + end + end + entry[:target].version = BigDecimal(entry[:record][:version].to_s) + entry[:target].format = entry[:record][:format].to_s.to_sym + when :element_array + entry[:target].uuid = entry[:record][:uuid].to_s.to_sym + entry[:target].title = entry[:record][:title].to_s + entry[:record][:content].each_index do |indexer| + case entry[:record][:content][(indexer)][:type].to_s.to_sym + when :element_hash + new_target = GxG::Database::DetachedHash.new + entry[:target][(indexer)] = new_target + import_db << {:target => new_target, :record => (entry[:record][:content][(indexer)])} + entry[:target][(indexer)].parent =(entry[:target]) + when :element_array + new_target = GxG::Database::DetachedArray.new + entry[:target][(indexer)] = new_target + import_db << {:target => new_target, :record => (entry[:record][:content][(indexer)])} + entry[:target][(indexer)].parent = (entry[:target]) + when :element_boolean, :element_integer, :element_float, :element_text + entry[:target][(indexer)] = entry[:record][:content][(indexer)][:content] + when :element_bigdecimal + entry[:target][(indexer)] = BigDecimal(entry[:record][:content][(indexer)][:content].to_s) + when :element_datetime + entry[:target][(indexer)] = ::Chronic::parse(entry[:record][:content][(indexer)][:content].to_s) + when :element_binary + entry[:target][(indexer)] = ::GxG::ByteArray.new(entry[:record][:content][(indexer)][:content].to_s.decode64) + end + end + entry[:target].version = BigDecimal(entry[:record][:version].to_s) + entry[:target].constraint = entry[:record][:constraint].to_s.to_sym + end + end + # + result + end + # + def self.create_from_format(the_format=nil) + # GxG::DB[:formats][(the_key)] + end + # + def ufs() + if @format + record = GxG::DB[:formats][(@format.to_s.to_sym)] + if record + record[:ufs].to_s.to_sym + else + "" + end + else + "" + end + end + # + def increment_version() + @version += 0.0001 + end + # + def initialize() + @uuid = ::GxG::uuid_generate().to_s.to_sym + @title = "Untitled #{@uuid.to_s}" + @version = BigDecimal("0.0") + @format = nil + @parent = nil + @property_links = {} + self + end + # + def inspect() + # FORNOW: make re-entrant (yes, I know!) Fortunately, circular links are impossible with DetachedHash. + # TODO: make interative instead of re-entrant. + last_key = @property_links.keys.last + result = "{" + @property_links.keys.each do |element_key| + result = result + (":#{element_key.to_s} => " + @property_links[(element_key)][:content].inspect) + # if @property_links[(element_key)][:loaded] == true + # result = result + (":#{element_key.to_s} => " + @property_links[(element_key)][:content].inspect) + # else + # result = result + (":#{element_key.to_s} => (Not Loaded)") + # end + unless last_key == element_key + result = result + ", " + end + end + result = result + "}" + result + end + # + def alive?() + true + end + # + def save() + result = false + if self.alive? + begin + # Review : expand to save to server + result = true + rescue Exception => the_error + log_error({:error => the_error}) + end + end + result + end + # + def destroy() + result = false + if self.alive? + begin + # Review : expand to destroy on server the stored object in question. + result = true + rescue Exception => the_error + log_error({:error => the_error}) + end + end + result + end + # + def size() + @property_links.size + end + # + def keys() + @property_links.keys + end + # + def []=(key=nil, value=nil) + # Only works with Symbols. + unless self.alive?() + raise Exception, "Attempted to alter a defunct structure" + end + result = nil + if key.is_a?(::Symbol) + if key.to_s.size > 256 + log_warn({:warning => "Attempted oversized key usage (limited to 256 characters), truncated #{key.inspect} to #{key.to_s[(0..255)].to_sym.inspect}"}) + key = key.to_s[(0..255)].to_sym + end + property_key = key.to_s.to_sym + # Review : rewrite ???? + if value.is_any?(::NilClass, ::TrueClass, ::FalseClass, ::Integer, ::Float, ::BigDecimal, ::String, ::Time, ::GxG::ByteArray, ::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray, ::Hash, ::Array) + unless self.write_reserved?() + self.get_reservation() + end + if self.write_reserved?() + # Further screen value provided: + # ### Check provided DetachedHashes and DetachedArrays + # ### Check provided Hashes and Arrays + # ### Property Exists? + if @property_links[(key.to_s.to_sym)] + # set property to value + operation = :set_value + else + if @format + raise Exception, "Formatted - the structure cannot be altered" + else + # add property : value pair + operation = :add_value + end + end + # ### Prepare new value + new_value = { + :linkid => nil, + :content => nil, + :loaded => true, + :state => 0, + :record => { + :parent_uuid => @uuid.to_s, + :property => key.to_s, + :ordinal => 0, + :version => BigDecimal("0.0"), + :element => "element_boolean", + :element_boolean => -1, + :element_integer => 0, + :element_float => 0.0, + :element_bigdecimal => BigDecimal("0.0"), + :element_datetime => ::Time.now, + :time_offset => 0.0, + :time_prior => 0.0, + :time_after => 0.0, + :length => 0, + :element_text => "", + :element_binary => nil, + :element_array => nil, + :element_hash => nil + } + } + # + if value.is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + # ### Assimilate DetachedHash or DetachedArray + new_value[:content] = value + new_value[:loaded] = true + new_value[:state] = new_value[:content].hash + # Note: version is sync'd here, but don't rely upon property version with linked structures, but use structure's version method. + new_value[:record][:version] = new_value[:content].version() + if value.is_a?(::GxG::Database::DetachedHash) + new_value[:record][:element] = "element_hash" + new_value[:record][:element_hash] = value + else + new_value[:record][:element] = "element_array" + new_value[:record][:element_array] = value + end + else + # ### Persist Hashes and Arrays + if value.is_any?(::Hash, ::Array) + new_value[:content] = ::GxG::Database::iterative_detached_persist(value) + new_value[:loaded] = true + new_value[:state] = new_value[:content].hash + # Note: version is sync'd here, but don't rely upon property version with linked structures, but use structure's version method. + new_value[:record][:version] = new_value[:content].version() + if value.is_a?(::Hash) + new_value[:record][:element] = "element_hash" + new_value[:record][:element_hash] = new_value[:content] + else + new_value[:record][:element] = "element_array" + new_value[:record][:element_array] = new_value[:content] + end + else + # ### Persist Base Element Values + new_value[:record][:element] = ::GxG::Database::element_table_for_instance(value).to_s + case new_value[:record][:element] + when "element_boolean" + if value.class == ::NilClass + new_value[:record][:element_boolean] = -1 + new_value[:content] = nil + end + if value.class == ::FalseClass + new_value[:record][:element_boolean] = 0 + new_value[:content] = false + end + if value.class == ::TrueClass + new_value[:record][:element_boolean] = 1 + new_value[:content] = true + end + when "element_integer" + new_value[:record][:element_integer] = value + new_value[:content] = value + when "element_float" + new_value[:record][:element_float] = value + new_value[:content] = value + when "element_bigdecimal" + new_value[:record][:element_bigdecimal] = BigDecimal(value.to_s) + new_value[:content] = new_value[:record][:element_bigdecimal] + when "element_datetime" + new_value[:record][:element_datetime] = value + new_value[:content] = value + when "element_text" + new_value[:record][:element_text] = value + new_value[:record][:length] = value.size + new_value[:content] = value + when "element_binary" + # Note: be sure to keep version & length sync'd with linked binary element record + if operation == :set_value + new_value[:record][:version] = @property_links[(property_key)][:record][:version] + new_value[:record][:element_binary] = @property_links[(property_key)][:record][:element_binary] + end + new_value[:record][:length] = value.size + new_value[:content] = value + else + raise Exception, "Unable to map an element type for value: #{value.inspect}" + end + end + end + # ### Commit Changes + case operation + when :set_value + # Note: Set In-memory value only, don't save unless you have to. + if new_value[:content].is_a?(@property_links[(property_key)][:content].class) || ([true, false, nil].include?(new_value[:content]) && [true, false, nil].include?(@property_links[(property_key)][:content])) + # Replace value directly, but don't save yet unless you have to (:state refers to the last 'loaded-in-from-db' state of the data). + new_value[:linkid] = @property_links[(property_key)][:linkid] + if new_value[:content].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + new_value[:record][:version] = new_value[:content].version() + else + new_value[:record][:version] = (@property_links[(property_key)][:record][:version] + 0.0001) + end + new_value[:record][:ordinal] = @property_links[(property_key)][:record][:ordinal] + new_value[:loaded] = true + new_value[:state] = @property_links[(property_key)][:state] + # + @property_links[(property_key)] = new_value + # + else + # Other class value being substituted. + if new_value[:content].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + new_value[:record][:version] = new_value[:content].version() + else + new_value[:record][:version] = (((@property_links[(property_key)][:record][:version].to_f + 0.0001) * 10000.0).to_i.to_f / 10000.0) + end + new_value[:record][:ordinal] = @property_links[(property_key)][:record][:ordinal] + new_value[:loaded] = true + # + @property_links[(property_key)] = new_value + end + result = @property_links[(property_key)][:content] + # + when :add_value + # Note: set in-memory value and save. + if new_value[:content].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + new_value[:record][:version] = new_value[:content].version() + else + new_value[:record][:version] = BigDecimal("0.0") + end + new_value[:record][:ordinal] = @property_links.keys.size + new_value[:loaded] = true + # + @property_links[(property_key)] = new_value + # Review : is this wise to do this here?? + # property_write(property_key) + # refresh_ordinals + end + # ### Handle coordination between persisted objects: + if @property_links[(property_key)][:content].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + @property_links[(property_key)][:content].parent = (self) + end + # + self.increment_version() + result = @property_links[(property_key)][:content] + # + else + raise Exception, "You do not have sufficient privileges to make this change. (write-reservation)" + end + else + raise Exception, "The value is not persistable." + end + else + raise Exception, "You must provide a property key in the form of a Symbol." + end + result + end + # + def [](key=nil) + result = nil + # if key exists + if self.alive?() + if key.is_a?(::Symbol) + if key.to_s.size > 256 + log_warn({:warning => "Attempted oversized key usage (limited to 256 characters), truncated #{key.inspect} to #{key.to_s[(0..255)].to_sym.inspect}"}) + key = key.to_s[(0..255)].to_sym + end + property_key = key.to_s.to_sym + if @property_links[(property_key)] + result = @property_links[(property_key)][:content] + end + else + raise ArgumentError, "You must specify with a Symbol, not a #{key.class.inspect}" + end + else + raise Exception, "Attempted to access a defunct structure" + end + result + end + # + def include?(the_key) + @property_links.keys.include?(the_key) + end + # + def delete(key=nil) + result = nil + if key.is_a?(::Symbol) + if key.to_s.size > 256 + log_warn({:warning => "Attempted oversized key usage (limited to 256 characters), truncated #{key.inspect} to #{key.to_s[(0..255)].to_sym.inspect}"}) + key = key.to_s[(0..255)].to_sym + end + if @format + raise Exception, "Formatted - the structure cannot be altered" + else + if @property_links[(key)] + result = @property_links[(key)][:content] + @property_links.delete(key) + # Review : send a 'delete_element' to server with the uuid of the DetachedHash and the key name. + # ### -OR- develop @ the server: an 'overwrite' push call that will trim missing elements from stored copy. + end + end + else + raise ArgumentError, "You must specify with a Symbol, not a #{key.class.inspect}" + end + # + result + end + # + def unpersist() + result = {} + if self.alive? + # + export_db = [{:parent => nil, :parent_selector => nil, :object => self, :record => result}] + children_of = Proc.new do |the_parent=nil| + list = [] + export_db.each do |node| + if node[:parent].object_id == the_parent.object_id + list << node + end + end + list + end + # Build up export_db: + self.search do |the_value, the_selector, the_container| + if the_value.is_a?(::GxG::Database::DetachedHash) + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => {}} + else + if the_value.is_a?(::GxG::Database::DetachedArray) + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => []} + else + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => the_value} + end + end + end + # Collect children export content: + link_db =[(export_db[0])] + while link_db.size > 0 do + entry = link_db.shift + children_of.call(entry[:object]).each do |the_child| + entry[:record][(the_child[:parent_selector])] = the_child[:record] + if the_child[:object].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + link_db << the_child + end + end + end + # + end + result + end + # + def export(options={:exclude_file_segments=>false}) + exclude_file_segments = (options[:exclude_file_segments] || false) + if options[:clone] == true + # Review : why are cloned objects unformatted? sync issues?? + result = {:type => :element_hash, :uuid => GxG::uuid_generate.to_s.to_sym, :title => @title.clone, :version => @version.to_s("F"), :content => {}} + else + result = {:type => :element_hash, :uuid => @uuid.clone, :title => @title.clone, :version => @version.to_s("F"), :format => @format.clone, :content => {}} + end + export_db = [{:parent => nil, :parent_selector => nil, :object => self, :record => result}] + children_of = Proc.new do |the_parent=nil| + list = [] + export_db.each do |node| + if node[:parent].object_id == the_parent.object_id + list << node + end + end + list + end + # Build up export_db: + self.search do |the_value, the_selector, the_container| + if the_value.is_a?(::GxG::Database::DetachedHash) + if options[:clone] == true + the_uuid = GxG::uuid_generate.to_s.to_sym + else + the_uuid = the_value.uuid().clone + end + if (exclude_file_segments == true) && (the_selector == :file_segments || the_selector == :segments || the_selector == :portions) + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => {}, :record => {:type => :element_hash, :uuid => the_uuid, :title => the_value.title().clone, :version => the_value.version().to_s("F"), :format => the_value.format().clone, :content => {}}} + else + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => {:type => :element_hash, :uuid => the_uuid, :title => the_value.title().clone, :version => the_value.version().to_s("F"), :format => the_value.format().clone, :content => {}}} + end + end + if the_value.is_a?(::GxG::Database::DetachedArray) + if options[:clone] == true + the_uuid = GxG::uuid_generate.to_s.to_sym + else + the_uuid = the_value.uuid().clone + end + if (exclude_file_segments == true) && (the_selector == :file_segments || the_selector == :segments || the_selector == :portions) + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => [], :record => {:type => :element_array, :uuid => the_uuid, :title => the_value.title().clone, :version => the_value.version().to_s("F"), :constraint => the_value.constraint().clone, :content => []}} + else + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => {:type => :element_array, :uuid => the_uuid, :title => the_value.title().clone, :version => the_value.version().to_s("F"), :constraint => the_value.constraint().clone, :content => []}} + end + end + if the_value.is_any?(::NilClass, ::TrueClass, ::FalseClass, ::Integer, ::Float, ::BigDecimal, ::String, ::Time, ::GxG::ByteArray) + data_type = GxG::Database::element_table_for_instance(the_value) + # + case data_type + when :element_bigdecimal + data = the_value.to_s + when :element_datetime + data = the_value.iso8601.to_s + when :element_binary + data = the_value.to_s.encode64 + when :element_text + data = the_value.to_s + else + data = the_value + end + # + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => {:type => data_type, :version => (the_container.version(the_selector) || BigDecimal("0.0")).to_s("F"), :content => data}} + end + end + # Collect children export content: + link_db = [(export_db[0])] + while link_db.size > 0 do + entry = link_db.shift + children_of.call(entry[:object]).each do |the_child| + entry[:record][:content][(the_child[:parent_selector])] = the_child[:record] + if the_child[:object].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + link_db << the_child + end + end + end + # + result + end + # + def each_pair(&block) + collection = {} + @property_links.keys.each do |key| + collection[(key)] = (self[(key)]) + end + if block.respond_to?(:call) + collection.to_enum(:each_pair).each do |key,value| + block.call(key,value) + end + else + collection.to_enum(:each_pair) + end + end + # + def iterative(options={:include_inactive => true}, &block) + result = [] + visit = Proc.new do |the_node=nil, accumulator=[]| + node_stack = [] + if the_node + node_stack << ({:parent => nil, :parent_selector => nil, :object => (the_node)}) + while (node_stack.size > 0) do + a_node = node_stack.shift + # + if a_node[:object].is_a?(::GxG::Database::DetachedHash) + if a_node[:object].alive? + a_node[:object].each_pair do |the_key, the_value| + node_stack << ({:parent => a_node[:object], :parent_selector => the_key, :object => the_value}) + end + else + if options[:include_inactive] + accumulator << a_node + end + end + end + if a_node[:object].is_a?(::GxG::Database::DetachedArray) + if a_node[:object].alive? + a_node[:object].each_with_index do |the_value, the_index| + node_stack << ({:parent => a_node[:object], :parent_selector => the_index, :object => the_value}) + end + else + if options[:include_inactive] + accumulator << a_node + end + end + end + end + end + accumulator + end + # + children_of = Proc.new do |the_db=[], the_parent=nil| + list = [] + the_db.each do |node| + if node[:parent].object_id == the_parent.object_id + list << node + end + end + list + end + # + begin + database = visit.call(self,[]) + link_db = children_of.call(database, self) + if block.respond_to?(:call) + while (link_db.size > 0) do + entry = link_db.shift + unless entry[:object].object_id == self.object_id + # calls with parameters: the_value, the_key/the_index (the_selector), the_container + raw_result = block.call(entry[:object], entry[:parent_selector], entry[:parent]) + if raw_result + result << raw_result + end + end + if entry[:object].object_id != nil.object_id + children = children_of.call(database, entry[:object]) + children.each do |child| + link_db << child + end + end + end + end + # + rescue Exception => the_error + log_error({:error => the_error, :parameters => {}}) + end + # + result + end + alias :process :iterative + # + def process!(options={:include_inactive => true}, &block) + self.iterative(options, &block) + self + end + # + def search(options={:include_inactive => true}, &block) + results = [] + if block.respond_to?(:call) + results = self.iterative(options, &block) + end + results + end + # + def paths_to(the_object=nil,base_path="") + # new idea here: + search_results = [] + unless base_path[0] == "/" + base_path = ("/" << base_path) + end + if base_path.size > 1 + path_stack = base_path.split("/")[1..-1].reverse + else + path_stack = [] + end + origin = self.get_at_path(base_path) + container_stack = [{:selector => nil, :container => origin}] + find_container = Proc.new do |the_container| + result = nil + container_stack.each_with_index do |entry, index| + if entry[:container] == the_container + result = entry + break + end + end + result + end + last_container = origin + found = false + # tester = {:a=>1, :b=>2, :c=>[0, 5], :testing=>{:d=>4.0, :e=>0.9, :f => nil}} + if origin.is_any?(::Hash, ::Array, ::Struct, ::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + origin.process! do |the_value, selector, container| + if last_container.object_id != container.object_id + container_record = find_container.call(container) + if container_record + path_stack = container_record[:prefix].split("/").reverse + if path_stack.size == 0 + path_stack << "" + end + end + last_container = container + end + if selector.is_a?(Symbol) + safe_key = (":" + selector.to_s) + else + safe_key = selector.to_s + end + safe_key.gsub!("/","%2f") + path_stack.unshift(safe_key) + # compare the_value + found = false + if (the_value == the_object) + found = true + end + if found + search_results << ("/" << path_stack.reverse.join("/")) + end + # + if the_value.is_any?(::Array, ::Hash, ::Struct, ::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + container_stack.unshift({:selector => selector, :container => the_value, :prefix => (path_stack.reverse.join("/"))}) + end + path_stack.shift + # + nil + end + else + search_results << ("/" << path_stack.join("/")) + end + search_results + end + # + def get_at_path(the_path="/") + result = nil + if the_path == "/" + result = self + else + object_stack = [(self)] + path_stack = the_path.split("/") + path_stack.to_enum.each do |path_element| + element = nil + if path_element.size > 0 + if (path_element =~ /^(?:[0-9])*[0-9](?:[0-9])*$/) == 0 + element = path_element.to_i + else + element = path_element + element.gsub!("%2f","/") + if element[0] == ":" + element = element[(1..-1)].to_sym + end + end + end + if element + result = object_stack.first[(element)] + if result.is_a?(NilClass) + break + else + object_stack.unshift(result) + end + else + # ignore double slashes? '//' + # break + end + end + end + result + end + # + def set_at_path(the_path="/",the_value=nil) + result = nil + if the_path != "/" + container = self.get_at_path(::File::dirname(the_path)) + if container + raw_selector = ::File::basename(the_path) + selector = nil + if raw_selector.size > 0 + if (raw_selector =~ /^(?:[0-9])*[0-9](?:[0-9])*$/) == 0 + selector = raw_selector.to_i + else + selector = raw_selector + selector.gsub!("%2f","/") + if selector[0] == ":" + selector = selector[(1..-1)].to_sym + end + end + end + if selector + container[(selector)] = the_value + result = container[(selector)] + else + # ignore double slashes? '//' + # break + end + # + end + end + result + end + # + end + # + class DetachedArray + # + private + # + def element_destroy(indexer=nil) + # Review : develop on-server element destroy command path ?? + end + # Note: property_read handled by load_property_links + def element_write(indexer=nil) + # Review : not needed on webclient version ?? + end + # + def load_element_links + # Review : not needed on webclient version ?? + end + # + def refresh_ordinals() + new_links = [] + @element_links.each_with_index do |the_key, ordinal| + @element_links[(ordinal)][:record][:ordinal] = ordinal + new_links << @element_links[(ordinal)] + end + @element_links = new_links + end + # + public + # + def uuid() + @uuid.clone + end + # + def uuid=(the_uuid=nil) + if GxG::valid_uuid?(the_uuid) + @uuid = the_uuid.to_s.to_sym + end + end + # + def title() + @title.clone + end + # + def title=(the_title=nil) + if the_title + @title = the_title.to_s[0..255] + self.increment_version + end + end + # + def version() + @version.clone + end + # + def version=(the_version=nil) + if the_version.is_a?(::Numeric) + @version = the_version.to_s("F").to_d + end + end + # + def element_version(index=nil) + result = 0.0 + if index.is_a?(::Numeric) + # + if @element_links[(index)] + result = @element_links[(index)][:record][:version] + else + result = 0.0 + end + # + end + result + end + # + def set_element_version(element_index, the_version=nil) + result = false + if @element_links[(element_index)] + if the_version.is_a?(::Numeric) + @element_links[(element_index)][:record][:version] = (((the_version.to_f) * 10000.0).to_i.to_f / 10000.0) + result = true + else + log_warning("Attempted to set version to an invalid version value #{the_version.inspect} for index #{element_index.inspect} on Object #{@uuid.inspect}") + end + else + log_warning("Attempted to set version with an invalid index #{element_index.inspect} on Object #{@uuid.inspect}") + end + result + end + # + def constraint() + @constraint.clone + end + # + def constraint=(the_format=nil) + if GxG::valid_uuid?(the_format) + @constraint = the_format.to_s.to_sym + end + end + # + def parent() + @parent + end + # + def parent=(object=nil) + if object.is_any?([::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray]) + # Review : parent can only be set once -- is this best?? + unless @parent + @parent = object + end + end + end + # Permission Management: + def get_permissions() + # Review : not needed on webclient version ?? + end + # + def set_permissions(credential=nil, the_permission=nil) + # Review : not needed on webclient version ?? + end + # + def reservation() + # Review : not needed on webclient version ?? + end + # + def write_permission?() + true + end + # + def write_reserved?() + true + end + # + def release_reservation() + true + end + # + def get_reservation() + true + end + # + def wait_for_reservation(timeout=nil) + true + end + # + def get_delegate() + # Review : not needed on webclient version ?? + end + # + def set_delegate(the_delegate=nil) + # Review : not needed on webclient version ?? + end + # + def increment_version() + if self.alive? + if self.write_reserved?() + @version = (((@version + 0.0001) * 10000.0).to_i.to_f / 10000.0) + end + end + end + # + def save_version() + # Review : not needed on webclient version ?? + end + # + def clear_constraint() + if self.alive? + if @constraint + @constraint = nil + end + end + true + end + # + def constraint() + @constraint.clone + end + # + def ufs() + if @format + record = GxG::DB[:formats][(@format.to_s.to_sym)] + if record + record[:ufs].to_s.to_sym + else + "" + end + else + "" + end + end + # + def refresh_links() + # Review : not needed on webclient ?? + end + # + def initialize() + @uuid = ::GxG::uuid_generate().to_s.to_sym + @title = "Untitled #{@uuid.to_s}" + @version = 0.0 + @constraint = nil + @parent = nil + @data = [] + @element_links = [] + self + end + # + def inspect() + # FORNOW: make re-entrant (yes, I know!) Fortunately, circular links are impossible with DetachedArray. + # TODO: make interative instead of re-entrant. + result = "[" + @element_links.each_index do |element_index| + result = result + @element_links[(element_index)][:content].inspect + # if @element_links[(element_index)].is_a?(::Hash) + # result = result + @element_links[(element_index)][:content].inspect + # else + # result = result + "(Not Loaded)" + # end + unless element_index == (@element_links.size - 1) + result = result + ", " + end + end + result = result + "]" + result + end + # + def alive?() + true + end + # + def db_address() + nil + end + # + def deactivate() + # Review : not needed on webclient ?? + end + # + def save() + # Review : create a save-to-server command path here. + end + # + def destroy() + # Review : create a destroy-at-server command path here. + end + # + def size() + @element_links.size + end + # + def structure_attached?() + # Review : not needed on webclient ?? + false + end + def structure_detach() + # Review : not needed on webclient ?? + true + end + # + def []=(indexer=nil, value=nil) + # Only works with Integer indexes. + unless self.alive?() + raise Exception, "Attempted to alter a defunct structure" + end + result = nil + if indexer.is_a?(::Integer) + # Review : rewrite ???? + if value.is_any?(::GxG::Database::valid_field_classes()) || value.is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray, ::Hash, ::Array) + unless self.write_reserved?() + self.get_reservation() + end + if self.write_reserved?() + # Further screen value provided: + # ### Check provided DetachedHashes and DetachedArrays + # ### Check provided Hashes and Arrays + if value.is_any?(::Hash, ::Array) + value.search do |the_value, the_selector, the_container| + unless the_value.is_any?(::GxG::Database::valid_field_classes()) || the_value.is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray, ::Hash, ::Array) + raise Exception, "The structure you are attaching has an unpersistable item within it (not supported)." + end + end + end + # ### Property Exists? + if @element_links[(indexer)] + # set property to value + operation = :set_value + else + if @constraint + raise Exception, "Formatted - the structure cannot be altered" + else + # add property : value pair + operation = :add_value + end + end + # ### Prepare new value + new_value = { + :linkid => nil, + :content => nil, + :loaded => false, + :state => 0, + :record => { + :parent_uuid => @uuid.to_s, + :ordinal => 0, + :version => 0.0, + :element => "element_boolean", + :element_boolean => -1, + :element_integer => 0, + :element_float => 0.0, + :element_bigdecimal => BigDecimal("0.0"), + :element_datetime => ::Time.now, + :time_offset => 0.0, + :time_prior => 0.0, + :time_after => 0.0, + :length => 0, + :element_text => "", + :element_binary => nil, + :element_array => nil, + :element_hash => nil + } + } + # + if value.is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + # ### Assimilate DetachedHash or DetachedArray + new_value[:content] = value + new_value[:loaded] = true + new_value[:state] = new_value[:content].hash + # Note: version is sync'd here, but don't rely upon property version with linked structures, but use structure's version method. + new_value[:record][:version] = new_value[:content].version() + if value.is_a?(::GxG::Database::DetachedHash) + new_value[:record][:element] = "element_hash" + new_value[:record][:element_hash] = value + else + new_value[:record][:element] = "element_array" + new_value[:record][:element_array] = value + end + else + # ### Persist Hashes and Arrays + if value.is_any?(::Hash, ::Array) + new_value[:content] = ::GxG::Database::iterative_detached_persist(value) + new_value[:loaded] = true + new_value[:state] = new_value[:content].hash + # Note: version is sync'd here, but don't rely upon property version with linked structures, but use structure's version method. + new_value[:record][:version] = new_value[:content].version() + if value.is_a?(::Hash) + new_value[:record][:element] = "element_hash" + new_value[:record][:element_hash] = new_value[:content] + else + new_value[:record][:element] = "element_array" + new_value[:record][:element_array] = new_value[:content] + end + else + # ### Persist Base Element Values + new_value[:record][:element] = ::GxG::Database::element_table_for_instance(value).to_s + case new_value[:record][:element] + when "element_boolean" + case value.class + when ::NilClass + new_value[:record][:element_boolean] = -1 + new_value[:content] = nil + when ::FalseClass + new_value[:record][:element_boolean] = 0 + new_value[:content] = false + when ::TrueClass + new_value[:record][:element_boolean] = 1 + new_value[:content] = true + end + when "element_integer" + new_value[:record][:element_integer] = value + new_value[:content] = value + when "element_float" + new_value[:record][:element_float] = value + new_value[:content] = value + when "element_bigdecimal" + new_value[:record][:element_bigdecimal] = BigDecimal(value.to_s) + new_value[:content] = new_value[:record][:element_bigdecimal] + when "element_datetime" + new_value[:record][:element_datetime] = value + new_value[:content] = value + when "element_text" + new_value[:record][:element_text] = value + new_value[:record][:length] = value.size + new_value[:content] = value + when "element_binary" + # Note: be sure to keep version & length sync'd with linked binary element record + if operation == :set_value + new_value[:record][:version] = @element_links[(indexer)][:record][:version] + new_value[:record][:element_binary_uuid] = @element_links[(indexer)][:record][:element_binary_uuid] + end + new_value[:record][:length] = value.size + new_value[:content] = value + else + raise Exception, "Unable to map an element type for value: #{value.inspect}" + end + end + end + # ### Commit Changes + case operation + when :set_value + # Note: Set In-memory value only, don't save unless you have to. + if new_value[:content].is_a?(@element_links[(indexer)][:content].class) || ([true, false, nil].include?(new_value[:content]) && [true, false, nil].include?(@element_links[(indexer)][:content])) + # Replace value directly, but don't save yet unless you have to (:state refers to the last 'loaded-in-from-db' state of the data). + new_value[:linkid] = @element_links[(indexer)][:linkid] + if new_value[:content].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + new_value[:record][:version] = new_value[:content].version() + else + new_value[:record][:version] = (((@element_links[(indexer)][:record][:version].to_f + 0.0001) * 10000.0).to_i.to_f / 10000.0) + end + new_value[:record][:ordinal] = @element_links[(indexer)][:record][:ordinal] + new_value[:loaded] = true + new_value[:state] = @element_links[(indexer)][:state] + # + if new_value[:content].is_a?(::String) + @element_links[(indexer)] = new_value + end + # + else + # Other class value being substituted. + if new_value[:content].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + new_value[:record][:version] = new_value[:content].version() + else + new_value[:record][:version] = (((@element_links[(indexer)][:record][:version].to_f + 0.0001) * 10000.0).to_i.to_f / 10000.0) + end + new_value[:record][:ordinal] = @element_links[(indexer)][:record][:ordinal] + new_value[:loaded] = true + # + @element_links[(indexer)] = new_value + end + # + when :add_value + # Note: set in-memory value and save. + if new_value[:content].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + new_value[:record][:version] = new_value[:content].version() + else + new_value[:record][:version] = 0.0 + end + new_value[:record][:ordinal] = @element_links.size + new_value[:loaded] = true + # + @element_links[(indexer)] = new_value + end + # ### Handle coordination between persisted objects: + if @element_links[(indexer)][:content].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + @element_links[(indexer)][:content].parent = (self) + end + # + self.increment_version() + result = @element_links[(indexer)][:content] + # + else + raise Exception, "You do not have sufficient privileges to make this change. (write-reservation)" + end + else + raise Exception, "The value is not persistable." + end + else + raise Exception, "You must provide a index in the form of an Integer." + end + result + end + # + # + def [](indexer=nil) + result = nil + # if exists + # if is not loaded, load element (if String or ByteArray) + if self.alive?() + if indexer.is_a?(::Integer) + if @element_links[(indexer)] + result = @element_links[(indexer)][:content] + end + else + raise ArgumentError, "You must specify with an Integer, not a #{indexer.class.inspect}" + end + else + raise Exception, "Attempted to access a defunct structure" + end + result + end + # + def delete_at(indexer=nil) + result = nil + # if exists + # if is not loaded, load element + if self.alive?() + if indexer.is_a?(::Integer) + if self.write_reserved?() + # + if @element_links[(indexer)] + # This will load the unloaded prior to separation. + result = self[(indexer)] + the_link = @element_links.delete_at(indexer) + # Review : element-unlink on server side ?? + if the_link[:content].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + the_link[:content].save + end + self.increment_version() + end + # + else + raise Exception, "You do not have sufficient privileges to make this change" + end + else + raise ArgumentError, "You must specify with a Symbol, not a #{key.class.inspect}" + end + else + raise Exception, "Attempted to access a defunct structure" + end + result + end + # + def <<(*args) + if args.size > 0 + args.each do |item| + self[(@element_links.size)] = item + end + end + self + end + # + def push(*args) + if args.size > 0 + args.each do |item| + self << item + end + end + self + end + # + def pop() + self.delete_at((@element_links.size - 1)) + end + # + def shift() + self.delete_at(0) + end + # + def insert(the_index=nil, the_object=nil) + # resovle the_index : the_index < 0 : the_index = (size - the_index) + # if the_index == 0 : @element_links.unshift(nil-record); self[0] = the_object + # if the_index > size-1 : push(the_object) + # else: @element_links.insert(the_index, nil-record); self[(the_index)] = the_object + # vette the_object --> persistable? + if ::GxG::Database::persistable?(the_object) + new_value = { + :linkid => nil, + :content => nil, + :loaded => false, + :state => 0, + :record => { + :parent_uuid => @uuid.to_s, + :ordinal => 0, + :version => BigDecimal("0.0"), + :element => "element_boolean", + :element_boolean => -1, + :element_integer => 0, + :element_float => 0.0, + :element_bigdecimal => BigDecimal("0.0"), + :element_datetime => ::DateTime.now, + :time_offset => 0.0, + :time_prior => 0.0, + :time_after => 0.0, + :length => 0, + :element_text => "", + :element_text_uuid => "", + :element_binary_uuid => "", + :element_array_uuid => "", + :element_hash_uuid => "" + } + } + the_index = the_index.to_i + if the_index < 0 + the_index = (self.size - the_index) + end + if the_index == 0 + new_value[:record][:ordinal] = the_index + @element_links.unshift(new_value) + refresh_ordinals + self[0] = the_object + else + if the_index > (self.size - 1) + self.push(the_object) + else + new_value[:record][:ordinal] = the_index + @element_links.insert(the_index, new_value) + refresh_ordinals + self[(the_index)] = the_object + end + end + end + self + end + # ???? def insert - ::GxG::Database::persistable?(the_object) + def unshift(the_object=nil) + self.insert(0,the_object) + end + # + def swap(first_index=nil, last_index=nil) + result = false + if first_index && last_index + if (0..(self.size - 1)).include?(first_index.to_i) && (0..(self.size - 1)).include?(last_index.to_i) + if first_index.to_i != last_index.to_i + record = @element_links[(first_index)] + @element_links[(first_index)] = @element_links[(last_index)] + @element_links[(last_index)] = record + result = true + end + end + end + result + end + # + def include?(the_value) + result = false + @element_links.each do |element| + if element[:content] == the_value + result = true + break + end + end + result + end + # + def find_index(the_value) + result = nil + @element_links.each_with_index do |element, indexer| + if element[:content] == the_value + result = indexer + break + end + end + result + end + # + def export(options={:exclude_file_segments=>false}) + if self.alive? + exclude_file_segments = (options[:exclude_file_segments] || false) + if options[:clone] == true + # Review : why are cloned objects unconstrained? sync issues?? + result = {:type => :element_array, :uuid => GxG::uuid_generate.to_s.to_sym, :title => @title.clone, :version => @version.clone, :content => []} + else + result = {:type => :element_array, :uuid => @uuid.clone, :title => @title.clone, :version => @version.clone, :constraint => @constraint.clone, :content => []} + end + export_db = [{:parent => nil, :parent_selector => nil, :object => self, :record => result}] + children_of = Proc.new do |the_parent=nil| + list = [] + export_db.each do |node| + if node[:parent].object_id == the_parent.object_id + list << node + end + end + list + end + # Build up export_db: + self.search do |the_value, the_selector, the_container| + if the_value.is_a?(::GxG::Database::DetachedHash) + if options[:clone] == true + the_uuid = GxG::uuid_generate.to_s.to_sym + else + the_uuid = the_value.uuid().clone + end + if (exclude_file_segments == true) && (the_selector == :file_segments || the_selector == :segments || the_selector == :portions) + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => {}, :record => {:type => :element_hash, :uuid => the_uuid, :title => the_value.title().clone, :version => the_value.version().clone, :format => the_value.format().clone, :content => {}}} + else + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => {:type => :element_hash, :uuid => the_uuid, :title => the_value.title().clone, :version => the_value.version().clone, :format => the_value.format().clone, :content => {}}} + end + end + if the_value.is_a?(::GxG::Database::DetachedArray) + if options[:clone] == true + the_uuid = GxG::uuid_generate.to_s.to_sym + else + the_uuid = the_value.uuid().clone + end + if (exclude_file_segments == true) && (the_selector == :file_segments || the_selector == :segments || the_selector == :portions) + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => [], :record => {:type => :element_array, :uuid => the_uuid, :title => the_value.title().clone, :version => the_value.version().clone, :constraint => the_value.constraint().clone, :content => []}} + else + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => {:type => :element_array, :uuid => the_uuid, :title => the_value.title().clone, :version => the_value.version().clone, :constraint => the_value.constraint().clone, :content => []}} + end + end + if the_value.is_any?(::GxG::Database::valid_field_classes()) + data_type = GxG::Database::element_table_for_instance(the_value) + case data_type + when :element_bigdecimal + data = the_value.to_s + when :element_datetime + data = the_value.iso8601.to_s + when :element_binary + data = the_value.to_s.encode64 + when :element_text + data = the_value.to_s + else + data = the_value + end + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => {:type => data_type, :version => (the_container.version(the_selector) || 0.0), :content => data}} + end + end + # Collect children export content: + link_db = [(export_db[0])] + while link_db.size > 0 do + entry = link_db.shift + children_of.call(entry[:object]).each do |the_child| + entry[:record][:content][(the_child[:parent_selector])] = the_child[:record] + if the_child[:object].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + link_db << the_child + end + end + end + else + result = nil + end + result + end + # + def sync_export(options={}) + # Review : does this work with DetachedArray ?? + self.save + end + # + def first() + if @element_links.size > 0 + self[0] + else + nil + end + end + # + def last() + if @element_links.size > 0 + self[(@element_links.size - 1)] + else + nil + end + end + # + def each(&block) + if block.respond_to?(:call) + load_element_links + if @element_links.size > 0 + @element_links.each_index do |index| + block.call(self[(index)]) + end + end + self + else + self.to_enum(:each) + end + end + # + def each_index(&block) + if block.respond_to?(:call) + load_element_links + if @element_links.size > 0 + @element_links.each_index do |index| + block.call(index) + end + end + self + else + self.to_enum(:each_index) + end + end + # + def each_with_index(offset=0,&block) + if block.respond_to?(:call) + load_element_links + if @element_links.size > 0 + @element_links.to_enum(:each).with_index(offset).each do |entry, index| + block.call(self[(index)], index) + end + end + self + else + self.to_enum(:each_with_index,offset) + end + end + # + def iterative(options={:include_inactive => true}, &block) + result = [] + visit = Proc.new do |the_node=nil, accumulator=[]| + node_stack = [] + if the_node + node_stack << ({:parent => nil, :parent_selector => nil, :object => (the_node)}) + while (node_stack.size > 0) do + a_node = node_stack.shift + # + if a_node[:object].is_a?(::GxG::Database::DetachedHash) + if a_node[:object].alive? + a_node[:object].each_pair do |the_key, the_value| + node_stack << ({:parent => a_node[:object], :parent_selector => the_key, :object => the_value}) + end + else + if options[:include_inactive] + accumulator << a_node + end + end + end + if a_node[:object].is_a?(::GxG::Database::DetachedArray) + if a_node[:object].alive? + a_node[:object].each_with_index do |the_value, the_index| + node_stack << ({:parent => a_node[:object], :parent_selector => the_index, :object => the_value}) + end + else + if options[:include_inactive] + accumulator << a_node + end + end + end + # + if a_node.alive? || options[:include_inactive] + accumulator << a_node + end + end + end + accumulator + end + # + children_of = Proc.new do |the_db=[], the_parent=nil| + list = [] + the_db.each do |node| + if node[:parent].object_id == the_parent.object_id + list << node + end + end + list + end + # + begin + database = visit.call(self,[]) + link_db = children_of.call(database, self) + if block.respond_to?(:call) + while (link_db.size > 0) do + entry = link_db.shift + unless entry[:object].object_id == self.object_id + # calls with parameters: the_value, the_key/the_index (the_selector), the_container + raw_result = block.call(entry[:object], entry[:parent_selector], entry[:parent]) + if raw_result + result << raw_result + end + end + if entry[:object].object_id != nil.object_id + children = children_of.call(database, entry[:object]) + children.each do |child| + link_db << child + end + end + end + end + # + rescue Exception => the_error + log_error({:error => the_error, :parameters => {}}) + end + # + result + end + alias :process :iterative + # + def process!(options={:include_inactive => true}, &block) + self.iterative(options, &block) + self + end + # + def search(options={:include_inactive => true}, &block) + results = [] + if block.respond_to?(:call) + results = self.iterative(options, &block) + end + results + end + # + def paths_to(the_object=nil,base_path="") + # new idea here: + search_results = [] + unless base_path[0] == "/" + base_path = ("/" << base_path) + end + if base_path.size > 1 + path_stack = base_path.split("/")[1..-1].reverse + else + path_stack = [] + end + origin = self.get_at_path(base_path) + container_stack = [{:selector => nil, :container => origin}] + find_container = Proc.new do |the_container| + result = nil + container_stack.each_with_index do |entry, index| + if entry[:container] == the_container + result = entry + break + end + end + result + end + last_container = origin + found = false + # tester = {:a=>1, :b=>2, :c=>[0, 5], :testing=>{:d=>4.0, :e=>0.9, :f => nil}} + if origin.is_any?(::Hash, ::Array, ::Struct, ::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + origin.process! do |the_value, selector, container| + if last_container.object_id != container.object_id + container_record = find_container.call(container) + if container_record + path_stack = container_record[:prefix].split("/").reverse + if path_stack.size == 0 + path_stack << "" + end + end + last_container = container + end + if selector.is_a?(Symbol) + safe_key = (":" + selector.to_s) + else + safe_key = selector.to_s + end + safe_key.gsub!("/","%2f") + path_stack.unshift(safe_key) + # compare the_value + found = false + if (the_value == the_object) + found = true + end + if found + search_results << ("/" << path_stack.reverse.join("/")) + end + # + if the_value.is_any?(::Array, ::Hash, ::Struct, ::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + container_stack.unshift({:selector => selector, :container => the_value, :prefix => (path_stack.reverse.join("/"))}) + end + path_stack.shift + # + nil + end + else + search_results << ("/" << path_stack.join("/")) + end + search_results + end + # + def get_at_path(the_path="/") + # /^(?:[0-9])*[0-9](?:[0-9])*$/ = nil if an alpha present there, else 0 only numeric + # Attribution : http://stackoverflow.com/questions/1240674/regex-match-a-string-containing-numbers-and-letters-but-not-a-string-of-just-nu + # + # if ":" detected do: (str.gsub("%2f","/").to_sym) as key else (str.gsub("%2f","/")) + result = nil + if the_path == "/" + result = self + else + object_stack = [(self)] + path_stack = the_path.split("/") + path_stack.to_enum.each do |path_element| + element = nil + if path_element.size > 0 + if (path_element =~ /^(?:[0-9])*[0-9](?:[0-9])*$/) == 0 + element = path_element.to_i + else + element = path_element + element.gsub!("%2f","/") + if element[0] == ":" + element = element[(1..-1)].to_sym + end + end + end + if element + result = object_stack.first[(element)] + if result.is_a?(NilClass) + break + else + object_stack.unshift(result) + end + else + # ignore double slashes? '//' + # break + end + end + end + result + end + # + def set_at_path(the_path="/",the_value=nil) + result = nil + if the_path != "/" + container = self.get_at_path(::File::dirname(the_path)) + if container + raw_selector = ::File::basename(the_path) + selector = nil + if raw_selector.size > 0 + if (raw_selector =~ /^(?:[0-9])*[0-9](?:[0-9])*$/) == 0 + selector = raw_selector.to_i + else + selector = raw_selector + selector.gsub!("%2f","/") + if selector[0] == ":" + selector = selector[(1..-1)].to_sym + end + end + end + if selector + container[(selector)] = the_value + result = container[(selector)] + else + # ignore double slashes? '//' + # break + end + # + end + end + result + end + # + def unpersist() + result = [] + if self.alive? + # + export_db = [{:parent => nil, :parent_selector => nil, :object => self, :record => result}] + children_of = Proc.new do |the_parent=nil| + list = [] + export_db.each do |node| + if node[:parent].object_id == the_parent.object_id + list << node + end + end + list + end + # Build up export_db: + self.search do |the_value, the_selector, the_container| + if the_value.is_a?(::GxG::Database::DetachedHash) + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => {}} + else + if the_value.is_a?(::GxG::Database::DetachedArray) + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => []} + else + export_db << {:parent => the_container, :parent_selector => the_selector.clone, :object => the_value, :record => the_value} + end + end + end + # Collect children export content: + link_db =[(export_db[0])] + while link_db.size > 0 do + entry = link_db.shift + children_of.call(entry[:object]).each do |the_child| + entry[:record][(the_child[:parent_selector])] = the_child[:record] + if the_child[:object].is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedArray) + link_db << the_child + end + end + end + # + end + result + end + # + end + # ### Compatibility with legacy code + class PeristedArray < ::GxG::Database::DetachedArray + end + # + class PeristedHash < ::GxG::Database::DetachedHash + def self.import(import_record=nil) + result = nil + unless import_record.is_any?(::Hash, ::GxG::Database::DetachedHash, ::GxG::Database::PersistedHash) + raise Exception, "Malformed import record passed." + end + # {:type => :element_hash, :uuid => @uuid.clone, :title => @title.clone, :version => @version.clone, :format => @format.clone, :content => {}} + import_db = [{:target => ::GxG::Database::PersistedHash.new, :record => import_record}] + result = import_db[0][:target] + while import_db.size > 0 do + entry = import_db.shift + case entry[:record][:type].to_s.to_sym + when :element_hash + entry[:target].uuid = entry[:record][:uuid].to_s.to_sym + entry[:target].title = entry[:record][:title].to_s + entry[:record][:content].keys.each do |the_key| + case entry[:record][:content][(the_key)][:type].to_s.to_sym + when :element_hash + new_target = GxG::Database::PersistedHash.new + entry[:target][(the_key)] = new_target + import_db << {:target => new_target, :record => (entry[:record][:content][(the_key)])} + entry[:target][(the_key)].parent = (entry[:target]) + when :element_array + new_target = GxG::Database::PersistedArray.new + entry[:target][(the_key)] = new_target + import_db << {:target => new_target, :record => (entry[:record][:content][(the_key)])} + entry[:target][(the_key)].parent = (entry[:target]) + when :element_boolean, :element_integer, :element_float, :element_text + entry[:target][(the_key)] = entry[:record][:content][(the_key)][:content] + when :element_bigdecimal + entry[:target][(the_key)] = BigDecimal(entry[:record][:content][(the_key)][:content].to_s) + when :element_datetime + entry[:target][(the_key)] = ::Chronic::parse(entry[:record][:content][(the_key)][:content].to_s) + when :element_binary + entry[:target][(the_key)] = ::GxG::ByteArray.new(entry[:record][:content][(the_key)][:content].to_s.decode64) + end + end + entry[:target].version = BigDecimal(entry[:record][:version].to_s) + entry[:target].format = entry[:record][:format].to_s.to_sym + when :element_array + entry[:target].uuid = entry[:record][:uuid].to_s.to_sym + entry[:target].title = entry[:record][:title].to_s + entry[:record][:content].each_index do |indexer| + case entry[:record][:content][(indexer)][:type].to_s.to_sym + when :element_hash + new_target = GxG::Database::PersistedHash.new + entry[:target][(indexer)] = new_target + import_db << {:target => new_target, :record => (entry[:record][:content][(indexer)])} + entry[:target][(indexer)].parent =(entry[:target]) + when :element_array + new_target = GxG::Database::PersistedArray.new + entry[:target][(indexer)] = new_target + import_db << {:target => new_target, :record => (entry[:record][:content][(indexer)])} + entry[:target][(indexer)].parent = (entry[:target]) + when :element_boolean, :element_integer, :element_float, :element_text + entry[:target][(indexer)] = entry[:record][:content][(indexer)][:content] + when :element_bigdecimal + entry[:target][(indexer)] = BigDecimal(entry[:record][:content][(indexer)][:content].to_s) + when :element_datetime + entry[:target][(indexer)] = ::Chronic::parse(entry[:record][:content][(indexer)][:content].to_s) + when :element_binary + entry[:target][(indexer)] = ::GxG::ByteArray.new(entry[:record][:content][(indexer)][:content].to_s.decode64) + end + end + entry[:target].version = BigDecimal(entry[:record][:version].to_s) + entry[:target].constraint = entry[:record][:constraint].to_s.to_sym + end + end + # + result + end + # + end + # + # + # + end +end +# perisistence hooks: +class Array + def persist() + GxG::Database::iterative_detached_persist(self) + end +end +class Hash + def persist() + GxG::Database::iterative_detached_persist(self) + end +end +# \ No newline at end of file diff --git a/gxg/web/gxg_datetime.rb b/gxg/web/gxg_datetime.rb new file mode 100644 index 0000000..69fbbd5 --- /dev/null +++ b/gxg/web/gxg_datetime.rb @@ -0,0 +1,3193 @@ +# format.rb: Written by Tadayoshi Funaba 1999-2009 +# $Id: format.rb,v 2.43 2008-01-17 20:16:31+09 tadf Exp $ + +class Date + + module Format # :nodoc: + + MONTHS = { + 'january' => 1, 'february' => 2, 'march' => 3, 'april' => 4, + 'may' => 5, 'june' => 6, 'july' => 7, 'august' => 8, + 'september'=> 9, 'october' =>10, 'november' =>11, 'december' =>12 + } + + DAYS = { + 'sunday' => 0, 'monday' => 1, 'tuesday' => 2, 'wednesday'=> 3, + 'thursday' => 4, 'friday' => 5, 'saturday' => 6 + } + + ABBR_MONTHS = { + 'jan' => 1, 'feb' => 2, 'mar' => 3, 'apr' => 4, + 'may' => 5, 'jun' => 6, 'jul' => 7, 'aug' => 8, + 'sep' => 9, 'oct' =>10, 'nov' =>11, 'dec' =>12 + } + + ABBR_DAYS = { + 'sun' => 0, 'mon' => 1, 'tue' => 2, 'wed' => 3, + 'thu' => 4, 'fri' => 5, 'sat' => 6 + } + + ZONES = { + 'ut' => 0*3600, 'gmt' => 0*3600, 'est' => -5*3600, 'edt' => -4*3600, + 'cst' => -6*3600, 'cdt' => -5*3600, 'mst' => -7*3600, 'mdt' => -6*3600, + 'pst' => -8*3600, 'pdt' => -7*3600, + 'a' => 1*3600, 'b' => 2*3600, 'c' => 3*3600, 'd' => 4*3600, + 'e' => 5*3600, 'f' => 6*3600, 'g' => 7*3600, 'h' => 8*3600, + 'i' => 9*3600, 'k' => 10*3600, 'l' => 11*3600, 'm' => 12*3600, + 'n' => -1*3600, 'o' => -2*3600, 'p' => -3*3600, 'q' => -4*3600, + 'r' => -5*3600, 's' => -6*3600, 't' => -7*3600, 'u' => -8*3600, + 'v' => -9*3600, 'w' =>-10*3600, 'x' =>-11*3600, 'y' =>-12*3600, + 'z' => 0*3600, + + 'utc' => 0*3600, 'wet' => 0*3600, + 'at' => -2*3600, 'brst'=> -2*3600, 'ndt' => -(2*3600+1800), + 'art' => -3*3600, 'adt' => -3*3600, 'brt' => -3*3600, 'clst'=> -3*3600, + 'nst' => -(3*3600+1800), + 'ast' => -4*3600, 'clt' => -4*3600, + 'akdt'=> -8*3600, 'ydt' => -8*3600, + 'akst'=> -9*3600, 'hadt'=> -9*3600, 'hdt' => -9*3600, 'yst' => -9*3600, + 'ahst'=>-10*3600, 'cat' =>-10*3600, 'hast'=>-10*3600, 'hst' =>-10*3600, + 'nt' =>-11*3600, + 'idlw'=>-12*3600, + 'bst' => 1*3600, 'cet' => 1*3600, 'fwt' => 1*3600, 'met' => 1*3600, + 'mewt'=> 1*3600, 'mez' => 1*3600, 'swt' => 1*3600, 'wat' => 1*3600, + 'west'=> 1*3600, + 'cest'=> 2*3600, 'eet' => 2*3600, 'fst' => 2*3600, 'mest'=> 2*3600, + 'mesz'=> 2*3600, 'sast'=> 2*3600, 'sst' => 2*3600, + 'bt' => 3*3600, 'eat' => 3*3600, 'eest'=> 3*3600, 'msk' => 3*3600, + 'msd' => 4*3600, 'zp4' => 4*3600, + 'zp5' => 5*3600, 'ist' => (5*3600+1800), + 'zp6' => 6*3600, + 'wast'=> 7*3600, + 'cct' => 8*3600, 'sgt' => 8*3600, 'wadt'=> 8*3600, + 'jst' => 9*3600, 'kst' => 9*3600, + 'east'=> 10*3600, 'gst' => 10*3600, + 'eadt'=> 11*3600, + 'idle'=> 12*3600, 'nzst'=> 12*3600, 'nzt' => 12*3600, + 'nzdt'=> 13*3600, + + 'afghanistan' => 16200, 'alaskan' => -32400, + 'arab' => 10800, 'arabian' => 14400, + 'arabic' => 10800, 'atlantic' => -14400, + 'aus central' => 34200, 'aus eastern' => 36000, + 'azores' => -3600, 'canada central' => -21600, + 'cape verde' => -3600, 'caucasus' => 14400, + 'cen. australia' => 34200, 'central america' => -21600, + 'central asia' => 21600, 'central europe' => 3600, + 'central european' => 3600, 'central pacific' => 39600, + 'central' => -21600, 'china' => 28800, + 'dateline' => -43200, 'e. africa' => 10800, + 'e. australia' => 36000, 'e. europe' => 7200, + 'e. south america' => -10800, 'eastern' => -18000, + 'egypt' => 7200, 'ekaterinburg' => 18000, + 'fiji' => 43200, 'fle' => 7200, + 'greenland' => -10800, 'greenwich' => 0, + 'gtb' => 7200, 'hawaiian' => -36000, + 'india' => 19800, 'iran' => 12600, + 'jerusalem' => 7200, 'korea' => 32400, + 'mexico' => -21600, 'mid-atlantic' => -7200, + 'mountain' => -25200, 'myanmar' => 23400, + 'n. central asia' => 21600, 'nepal' => 20700, + 'new zealand' => 43200, 'newfoundland' => -12600, + 'north asia east' => 28800, 'north asia' => 25200, + 'pacific sa' => -14400, 'pacific' => -28800, + 'romance' => 3600, 'russian' => 10800, + 'sa eastern' => -10800, 'sa pacific' => -18000, + 'sa western' => -14400, 'samoa' => -39600, + 'se asia' => 25200, 'malay peninsula' => 28800, + 'south africa' => 7200, 'sri lanka' => 21600, + 'taipei' => 28800, 'tasmania' => 36000, + 'tokyo' => 32400, 'tonga' => 46800, + 'us eastern' => -18000, 'us mountain' => -25200, + 'vladivostok' => 36000, 'w. australia' => 28800, + 'w. central africa' => 3600, 'w. europe' => 3600, + 'west asia' => 18000, 'west pacific' => 36000, + 'yakutsk' => 32400 + } + + [MONTHS, DAYS, ABBR_MONTHS, ABBR_DAYS, ZONES].each do |x| + x.freeze + end + + class Bag # :nodoc: + + def initialize + @elem = {} + end + + def method_missing(t, *args, &block) + t = t.to_s + set = t.chomp!('=') + t = t.intern + if set + @elem[t] = args[0] + else + @elem[t] + end + end + + def to_hash + @elem.reject{|k, v| /\A_/ =~ k.to_s || v.nil?} + end + + end + + end + + def strftime(fmt='%F') + Time.now.strftime(fmt) + end + +# alias_method :format, :strftime + + def asctime() strftime('%c') end + + alias_method :ctime, :asctime + + def iso8601() strftime('%F') end + + def rfc3339() strftime('%FT%T%:z') end + + def xmlschema() iso8601 end # :nodoc: + + def rfc2822() strftime('%a, %-d %b %Y %T %z') end + + alias_method :rfc822, :rfc2822 + + def httpdate() new_offset(0).strftime('%a, %d %b %Y %T GMT') end # :nodoc: + + def jisx0301 + if jd < 2405160 + strftime('%F') + else + case jd + when 2405160...2419614 + g = 'M%02d' % (year - 1867) + when 2419614...2424875 + g = 'T%02d' % (year - 1911) + when 2424875...2447535 + g = 'S%02d' % (year - 1925) + else + g = 'H%02d' % (year - 1988) + end + g + strftime('.%m.%d') + end + end + +=begin + def beat(n=0) + i, f = (new_offset(HOURS_IN_DAY).day_fraction * 1000).divmod(1) + ('@%03d' % i) + + if n < 1 + '' + else + '.%0*d' % [n, (f / Rational(1, 10**n)).round] + end + end +=end + + def self.num_pattern? (s) # :nodoc: + /\A%[EO]?[CDdeFGgHIjkLlMmNQRrSsTUuVvWwXxYy\d]/ =~ s || /\A\d/ =~ s + end + + private_class_method :num_pattern? + + def self._strptime_i(str, fmt, e) # :nodoc: + fmt.scan(/%([EO]?(?::{1,3}z|.))|(.)/m) do |s, c| + a = $& + if s + case s + when 'A', 'a' + return unless str.sub!(/\A(#{Format::DAYS.keys.join('|')})/io, '') || + str.sub!(/\A(#{Format::ABBR_DAYS.keys.join('|')})/io, '') + val = Format::DAYS[$1.downcase] || Format::ABBR_DAYS[$1.downcase] + return unless val + e.wday = val + when 'B', 'b', 'h' + return unless str.sub!(/\A(#{Format::MONTHS.keys.join('|')})/io, '') || + str.sub!(/\A(#{Format::ABBR_MONTHS.keys.join('|')})/io, '') + val = Format::MONTHS[$1.downcase] || Format::ABBR_MONTHS[$1.downcase] + return unless val + e.mon = val + when 'C', 'EC' + return unless str.sub!(if num_pattern?($') + then /\A([-+]?\d{1,2})/ + else /\A([-+]?\d{1,})/ + end, '') + val = $1.to_i + e._cent = val + when 'c', 'Ec' + return unless _strptime_i(str, '%a %b %e %H:%M:%S %Y', e) + when 'D' + return unless _strptime_i(str, '%m/%d/%y', e) + when 'd', 'e', 'Od', 'Oe' + return unless str.sub!(/\A( \d|\d{1,2})/, '') + val = $1.to_i + return unless (1..31) === val + e.mday = val + when 'F' + return unless _strptime_i(str, '%Y-%m-%d', e) + when 'G' + return unless str.sub!(if num_pattern?($') + then /\A([-+]?\d{1,4})/ + else /\A([-+]?\d{1,})/ + end, '') + val = $1.to_i + e.cwyear = val + when 'g' + return unless str.sub!(/\A(\d{1,2})/, '') + val = $1.to_i + return unless (0..99) === val + e.cwyear = val + e._cent ||= if val >= 69 then 19 else 20 end + when 'H', 'k', 'OH' + return unless str.sub!(/\A( \d|\d{1,2})/, '') + val = $1.to_i + return unless (0..24) === val + e.hour = val + when 'I', 'l', 'OI' + return unless str.sub!(/\A( \d|\d{1,2})/, '') + val = $1.to_i + return unless (1..12) === val + e.hour = val + when 'j' + return unless str.sub!(/\A(\d{1,3})/, '') + val = $1.to_i + return unless (1..366) === val + e.yday = val + when 'L' + return unless str.sub!(if num_pattern?($') + then /\A([-+]?\d{1,3})/ + else /\A([-+]?\d{1,})/ + end, '') +# val = Rational($1.to_i, 10**3) + val = Rational($1.to_i, 10**$1.size) + e.sec_fraction = val + when 'M', 'OM' + return unless str.sub!(/\A(\d{1,2})/, '') + val = $1.to_i + return unless (0..59) === val + e.min = val + when 'm', 'Om' + return unless str.sub!(/\A(\d{1,2})/, '') + val = $1.to_i + return unless (1..12) === val + e.mon = val + when 'N' + return unless str.sub!(if num_pattern?($') + then /\A([-+]?\d{1,9})/ + else /\A([-+]?\d{1,})/ + end, '') +# val = Rational($1.to_i, 10**9) + val = Rational($1.to_i, 10**$1.size) + e.sec_fraction = val + when 'n', 't' + return unless _strptime_i(str, "\s", e) + when 'P', 'p' + return unless str.sub!(/\A([ap])(?:m\b|\.m\.)/i, '') + e._merid = if $1.downcase == 'a' then 0 else 12 end + when 'Q' + return unless str.sub!(/\A(-?\d{1,})/, '') + val = Rational($1.to_i, 10**3) + e.seconds = val + when 'R' + return unless _strptime_i(str, '%H:%M', e) + when 'r' + return unless _strptime_i(str, '%I:%M:%S %p', e) + when 'S', 'OS' + return unless str.sub!(/\A(\d{1,2})/, '') + val = $1.to_i + return unless (0..60) === val + e.sec = val + when 's' + return unless str.sub!(/\A(-?\d{1,})/, '') + val = $1.to_i + e.seconds = val + when 'T' + return unless _strptime_i(str, '%H:%M:%S', e) + when 'U', 'W', 'OU', 'OW' + return unless str.sub!(/\A(\d{1,2})/, '') + val = $1.to_i + return unless (0..53) === val + e.__send__(if s[-1,1] == 'U' then :wnum0= else :wnum1= end, val) + when 'u', 'Ou' + return unless str.sub!(/\A(\d{1})/, '') + val = $1.to_i + return unless (1..7) === val + e.cwday = val + when 'V', 'OV' + return unless str.sub!(/\A(\d{1,2})/, '') + val = $1.to_i + return unless (1..53) === val + e.cweek = val + when 'v' + return unless _strptime_i(str, '%e-%b-%Y', e) + when 'w' + return unless str.sub!(/\A(\d{1})/, '') + val = $1.to_i + return unless (0..6) === val + e.wday = val + when 'X', 'EX' + return unless _strptime_i(str, '%H:%M:%S', e) + when 'x', 'Ex' + return unless _strptime_i(str, '%m/%d/%y', e) + when 'Y', 'EY' + return unless str.sub!(if num_pattern?($') + then /\A([-+]?\d{1,4})/ + else /\A([-+]?\d{1,})/ + end, '') + val = $1.to_i + e.year = val + when 'y', 'Ey', 'Oy' + return unless str.sub!(/\A(\d{1,2})/, '') + val = $1.to_i + return unless (0..99) === val + e.year = val + e._cent ||= if val >= 69 then 19 else 20 end + when 'Z', /\A:{0,3}z/ + return unless str.sub!(/\A((?:gmt|utc?)?[-+]\d+(?:[,.:]\d+(?::\d+)?)? + |[[:alpha:].\s]+(?:standard|daylight)\s+time\b + |[[:alpha:]]+(?:\s+dst)?\b + )/ix, '') + val = $1 + e.zone = val + offset = zone_to_diff(val) + e.offset = offset + when '%' + return unless str.sub!(/\A%/, '') + when '+' + return unless _strptime_i(str, '%a %b %e %H:%M:%S %Z %Y', e) + else + return unless str.sub!(Regexp.new('\\A' + Regexp.quote(a)), '') + end + else + case c + when /\A\s/ + str.sub!(/\A\s+/, '') + else + return unless str.sub!(Regexp.new('\\A' + Regexp.quote(a)), '') + end + end + end + end + + private_class_method :_strptime_i + + def self._strptime(str, fmt='%F') + str = str.dup + e = Format::Bag.new + return unless _strptime_i(str, fmt, e) + + if e._cent + if e.cwyear + e.cwyear += e._cent * 100 + end + if e.year + e. year += e._cent * 100 + end + end + + if e._merid + if e.hour + e.hour %= 12 + e.hour += e._merid + end + end + + unless str.empty? + e.leftover = str + end + + e.to_hash + end + + def self.s3e(e, y, m, d, bc=false) + unless String === m + m = m.to_s + end + + if y && m && !d + y, m, d = d, y, m + end + + if y == nil + if d && d.size > 2 + y = d + d = nil + end + if d && d[0,1] == "'" + y = d + d = nil + end + end + + if y + y.scan(/(\d+)(.+)?/) + if $2 + y, d = d, $1 + end + end + + if m + if m[0,1] == "'" || m.size > 2 + y, m, d = m, d, y # us -> be + end + end + + if d + if d[0,1] == "'" || d.size > 2 + y, d = d, y + end + end + + if y + y =~ /([-+])?(\d+)/ + if $1 || $2.size > 2 + c = false + end + iy = $&.to_i + if bc + iy = -iy + 1 + end + e.year = iy + end + + if m + m =~ /\d+/ + e.mon = $&.to_i + end + + if d + d =~ /\d+/ + e.mday = $&.to_i + end + + if c != nil + e._comp = c + end + + end + + private_class_method :s3e + + def self._parse_day(str, e) # :nodoc: + if str.sub!(/\b(#{Format::ABBR_DAYS.keys.join('|')})[^-\d\s]*/io, ' ') + e.wday = Format::ABBR_DAYS[$1.downcase] + true +=begin + elsif str.sub!(/\b(?!\dth)(su|mo|tu|we|th|fr|sa)\b/i, ' ') + e.wday = %w(su mo tu we th fr sa).index($1.downcase) + true +=end + end + end + + def self._parse_time(str, e) # :nodoc: + if str.sub!( + /( + (?: + \d+\s*:\s*\d+ + (?: + \s*:\s*\d+(?:[,.]\d*)? + )? + | + \d+\s*h(?:\s*\d+m?(?:\s*\d+s?)?)? + ) + (?: + \s* + [ap](?:m\b|\.m\.) + )? + | + \d+\s*[ap](?:m\b|\.m\.) + ) + (?: + \s* + ( + (?:gmt|utc?)?[-+]\d+(?:[,.:]\d+(?::\d+)?)? + | + [[:alpha:].\s]+(?:standard|daylight)\stime\b + | + [[:alpha:]]+(?:\sdst)?\b + ) + )? + /ix, + ' ') + + t = $1 + e.zone = $2 if $2 + + t =~ /\A(\d+)h? + (?:\s*:?\s*(\d+)m? + (?: + \s*:?\s*(\d+)(?:[,.](\d+))?s? + )? + )? + (?:\s*([ap])(?:m\b|\.m\.))?/ix + + e.hour = $1.to_i + e.min = $2.to_i if $2 + e.sec = $3.to_i if $3 + e.sec_fraction = Rational($4.to_i, 10**$4.size) if $4 + + if $5 + e.hour %= 12 + if $5.downcase == 'p' + e.hour += 12 + end + end + true + end + end + +=begin + def self._parse_beat(str, e) # :nodoc: + if str.sub!(/@\s*(\d+)(?:[,.](\d*))?/, ' ') + beat = Rational($1.to_i) + beat += Rational($2.to_i, 10**$2.size) if $2 + secs = Rational(beat, 1000) + h, min, s, fr = self.day_fraction_to_time(secs) + e.hour = h + e.min = min + e.sec = s + e.sec_fraction = fr * 86400 + e.zone = '+01:00' + true + end + end +=end + + def self._parse_eu(str, e) # :nodoc: + if str.sub!( + /'?(\d+)[^-\d\s]* + \s* + (#{Format::ABBR_MONTHS.keys.join('|')})[^-\d\s']* + (?: + \s* + (c(?:e|\.e\.)|b(?:ce|\.c\.e\.)|a(?:d|\.d\.)|b(?:c|\.c\.))? + \s* + ('?-?\d+(?:(?:st|nd|rd|th)\b)?) + )? + /iox, + ' ') # ' + s3e(e, $4, Format::ABBR_MONTHS[$2.downcase], $1, + $3 && $3[0,1].downcase == 'b') + true + end + end + + def self._parse_us(str, e) # :nodoc: + if str.sub!( + /\b(#{Format::ABBR_MONTHS.keys.join('|')})[^-\d\s']* + \s* + ('?\d+)[^-\d\s']* + (?: + \s* + (c(?:e|\.e\.)|b(?:ce|\.c\.e\.)|a(?:d|\.d\.)|b(?:c|\.c\.))? + \s* + ('?-?\d+) + )? + /iox, + ' ') # ' + s3e(e, $4, Format::ABBR_MONTHS[$1.downcase], $2, + $3 && $3[0,1].downcase == 'b') + true + end + end + + def self._parse_iso(str, e) # :nodoc: + if str.sub!(/('?[-+]?\d+)-(\d+)-('?-?\d+)/, ' ') + s3e(e, $1, $2, $3) + true + end + end + + def self._parse_iso2(str, e) # :nodoc: + if str.sub!(/\b(\d{2}|\d{4})?-?w(\d{2})(?:-?(\d))?\b/i, ' ') + e.cwyear = $1.to_i if $1 + e.cweek = $2.to_i + e.cwday = $3.to_i if $3 + true + elsif str.sub!(/-w-(\d)\b/i, ' ') + e.cwday = $1.to_i + true + elsif str.sub!(/--(\d{2})?-(\d{2})\b/, ' ') + e.mon = $1.to_i if $1 + e.mday = $2.to_i + true + elsif str.sub!(/--(\d{2})(\d{2})?\b/, ' ') + e.mon = $1.to_i + e.mday = $2.to_i if $2 + true + elsif /[,.](\d{2}|\d{4})-\d{3}\b/ !~ str && + str.sub!(/\b(\d{2}|\d{4})-(\d{3})\b/, ' ') + e.year = $1.to_i + e.yday = $2.to_i + true + elsif /\d-\d{3}\b/ !~ str && + str.sub!(/\b-(\d{3})\b/, ' ') + e.yday = $1.to_i + true + end + end + + def self._parse_jis(str, e) # :nodoc: + if str.sub!(/\b([mtsh])(\d+)\.(\d+)\.(\d+)/i, ' ') + era = { 'm'=>1867, + 't'=>1911, + 's'=>1925, + 'h'=>1988 + }[$1.downcase] + e.year = $2.to_i + era + e.mon = $3.to_i + e.mday = $4.to_i + true + end + end + + def self._parse_vms(str, e) # :nodoc: + if str.sub!(/('?-?\d+)-(#{Format::ABBR_MONTHS.keys.join('|')})[^-]* + -('?-?\d+)/iox, ' ') + s3e(e, $3, Format::ABBR_MONTHS[$2.downcase], $1) + true + elsif str.sub!(/\b(#{Format::ABBR_MONTHS.keys.join('|')})[^-]* + -('?-?\d+)(?:-('?-?\d+))?/iox, ' ') + s3e(e, $3, Format::ABBR_MONTHS[$1.downcase], $2) + true + end + end + + def self._parse_sla(str, e) # :nodoc: + if str.sub!(%r|('?-?\d+)/\s*('?\d+)(?:\D\s*('?-?\d+))?|, ' ') # ' + s3e(e, $1, $2, $3) + true + end + end + + def self._parse_dot(str, e) # :nodoc: + if str.sub!(%r|('?-?\d+)\.\s*('?\d+)\.\s*('?-?\d+)|, ' ') # ' + s3e(e, $1, $2, $3) + true + end + end + + def self._parse_year(str, e) # :nodoc: + if str.sub!(/'(\d+)\b/, ' ') + e.year = $1.to_i + true + end + end + + def self._parse_mon(str, e) # :nodoc: + if str.sub!(/\b(#{Format::ABBR_MONTHS.keys.join('|')})\S*/io, ' ') + e.mon = Format::ABBR_MONTHS[$1.downcase] + true + end + end + + def self._parse_mday(str, e) # :nodoc: + if str.sub!(/(\d+)(st|nd|rd|th)\b/i, ' ') + e.mday = $1.to_i + true + end + end + + def self._parse_ddd(str, e) # :nodoc: + if str.sub!( + /([-+]?)(\d{2,14}) + (?: + \s* + t? + \s* + (\d{2,6})?(?:[,.](\d*))? + )? + (?: + \s* + ( + z\b + | + [-+]\d{1,4}\b + | + \[[-+]?\d[^\]]*\] + ) + )? + /ix, + ' ') + case $2.size + when 2 + if $3.nil? && $4 + e.sec = $2[-2, 2].to_i + else + e.mday = $2[ 0, 2].to_i + end + when 4 + if $3.nil? && $4 + e.sec = $2[-2, 2].to_i + e.min = $2[-4, 2].to_i + else + e.mon = $2[ 0, 2].to_i + e.mday = $2[ 2, 2].to_i + end + when 6 + if $3.nil? && $4 + e.sec = $2[-2, 2].to_i + e.min = $2[-4, 2].to_i + e.hour = $2[-6, 2].to_i + else + e.year = ($1 + $2[ 0, 2]).to_i + e.mon = $2[ 2, 2].to_i + e.mday = $2[ 4, 2].to_i + end + when 8, 10, 12, 14 + if $3.nil? && $4 + e.sec = $2[-2, 2].to_i + e.min = $2[-4, 2].to_i + e.hour = $2[-6, 2].to_i + e.mday = $2[-8, 2].to_i + if $2.size >= 10 + e.mon = $2[-10, 2].to_i + end + if $2.size == 12 + e.year = ($1 + $2[-12, 2]).to_i + end + if $2.size == 14 + e.year = ($1 + $2[-14, 4]).to_i + e._comp = false + end + else + e.year = ($1 + $2[ 0, 4]).to_i + e.mon = $2[ 4, 2].to_i + e.mday = $2[ 6, 2].to_i + e.hour = $2[ 8, 2].to_i if $2.size >= 10 + e.min = $2[10, 2].to_i if $2.size >= 12 + e.sec = $2[12, 2].to_i if $2.size >= 14 + e._comp = false + end + when 3 + if $3.nil? && $4 + e.sec = $2[-2, 2].to_i + e.min = $2[-3, 1].to_i + else + e.yday = $2[ 0, 3].to_i + end + when 5 + if $3.nil? && $4 + e.sec = $2[-2, 2].to_i + e.min = $2[-4, 2].to_i + e.hour = $2[-5, 1].to_i + else + e.year = ($1 + $2[ 0, 2]).to_i + e.yday = $2[ 2, 3].to_i + end + when 7 + if $3.nil? && $4 + e.sec = $2[-2, 2].to_i + e.min = $2[-4, 2].to_i + e.hour = $2[-6, 2].to_i + e.mday = $2[-7, 1].to_i + else + e.year = ($1 + $2[ 0, 4]).to_i + e.yday = $2[ 4, 3].to_i + end + end + if $3 + if $4 + case $3.size + when 2, 4, 6 + e.sec = $3[-2, 2].to_i + e.min = $3[-4, 2].to_i if $3.size >= 4 + e.hour = $3[-6, 2].to_i if $3.size >= 6 + end + else + case $3.size + when 2, 4, 6 + e.hour = $3[ 0, 2].to_i + e.min = $3[ 2, 2].to_i if $3.size >= 4 + e.sec = $3[ 4, 2].to_i if $3.size >= 6 + end + end + end + if $4 + e.sec_fraction = Rational($4.to_i, 10**$4.size) + end + if $5 + e.zone = $5 + if e.zone[0,1] == '[' + o, n, = e.zone[1..-2].split(':') + e.zone = n || o + if /\A\d/ =~ o + o = format('+%s', o) + end + e.offset = zone_to_diff(o) + end + end + true + end + end + + private_class_method :_parse_day, :_parse_time, # :_parse_beat, + :_parse_eu, :_parse_us, :_parse_iso, :_parse_iso2, + :_parse_jis, :_parse_vms, :_parse_sla, :_parse_dot, + :_parse_year, :_parse_mon, :_parse_mday, :_parse_ddd + + def self._parse(str, comp=true) + # Newer MRI version (written in C converts non-strings to strings + # and also has other checks like all ascii. + str = str.to_str if !str.kind_of?(::String) && str.respond_to?(:to_str) + str = str.dup + + e = Format::Bag.new + + e._comp = comp + + str.gsub!(/[^-+',.\/:@[:alnum:]\[\]]+/, ' ') + + _parse_time(str, e) # || _parse_beat(str, e) + _parse_day(str, e) + + _parse_eu(str, e) || + _parse_us(str, e) || + _parse_iso(str, e) || + _parse_jis(str, e) || + _parse_vms(str, e) || + _parse_sla(str, e) || + _parse_dot(str, e) || + _parse_iso2(str, e) || + _parse_year(str, e) || + _parse_mon(str, e) || + _parse_mday(str, e) || + _parse_ddd(str, e) + + if str.sub!(/\b(bc\b|bce\b|b\.c\.|b\.c\.e\.)/i, ' ') + if e.year + e.year = -e.year + 1 + end + end + + if str.sub!(/\A\s*(\d{1,2})\s*\z/, ' ') + if e.hour && !e.mday + v = $1.to_i + if (1..31) === v + e.mday = v + end + end + if e.mday && !e.hour + v = $1.to_i + if (0..24) === v + e.hour = v + end + end + end + + if e._comp + if e.cwyear + if e.cwyear >= 0 && e.cwyear <= 99 + e.cwyear += if e.cwyear >= 69 + then 1900 else 2000 end + end + end + if e.year + if e.year >= 0 && e.year <= 99 + e.year += if e.year >= 69 + then 1900 else 2000 end + end + end + end + + e.offset ||= zone_to_diff(e.zone) if e.zone + + e.to_hash + end + + def self._iso8601(str) # :nodoc: + h = {} + if /\A\s* + (?: + (?[-+]?\d{2,} | -) - (?\d{2})? - (?\d{2}) + | (?[-+]?\d{2,})? - (?\d{3}) + | (?\d{4}|\d{2})? - w(?\d{2}) - (?\d) + | -w- (?\d) + ) + (?: + t + (?\d{2}) : (?\d{2}) (?: :(?\d{2})(?:[,.](?\d+))?)? + (?z | [-+]\d{2}(?::?\d{2})?)? + )? + \s*\z/ix =~ str + + if mday + h[:mday] = i mday + h[:year] = comp_year69(year) if year != "-" + + if mon + h[:mon] = i mon + else + return {} if year != "-" + end + elsif yday + h[:yday] = i yday + h[:year] = comp_year69(year) if year + elsif cwday + h[:cweek] = i cweek + h[:cwday] = i cwday + h[:cwyear] = comp_year69(cwyear) if cwyear + elsif cwday2 + h[:cwday] = i cwday2 + end + + if hour + h[:hour] = i hour + h[:min] = i min + h[:sec] = i sec if sec + end + + h[:sec_fraction] = Rational(sec_fraction.to_i, 10**sec_fraction.size) if sec_fraction # JRuby bug fix! + set_zone(h, zone) + + elsif /\A\s* + (?: + (?[-+]?(?:\d{4}|\d{2})|--) (?\d{2}|-) (?\d{2}) + | (?[-+]?(?:\d{4}|\d{2})) (?\d{3}) + | -(?\d{3}) + | (?\d{4}|\d{2}|-) w(?\d{2}|-) (?\d) + ) + (?: + t? + (?\d{2}) (?\d{2}) (?:(?\d{2})(?:[,.](?\d+))?)? + (?z | [-+]\d{2}(?:\d{2})?)? + )? + \s*\z/ix =~ str + + if mday + h[:mday] = i mday + h[:year] = comp_year69(year) if year != "--" + if mon != "-" + h[:mon] = i mon + else + return {} if year != "--" + end + elsif yday + h[:yday] = i yday + h[:year] = comp_year69(year) + elsif yday2 + h[:yday] = i yday2 + elsif cwday + h[:cweek] = i cweek if cweek != "-" + h[:cwday] = i cwday + h[:cwyear] = comp_year69(cwyear) if cwyear != "-" + end + + if hour + h[:hour] = i hour + h[:min] = i min + h[:sec] = i sec if sec + end + + h[:sec_fraction] = Rational(sec_fraction.to_i, 10**sec_fraction.size) if sec_fraction # JRuby bug fix! + set_zone(h, zone) + + elsif /\A\s* + (?\d{2}) + (?: + : (?\d{2}) + (?: + :(?\d{2})(?:[,.](?\d+))? + (?z | [-+]\d{2}(?: :?\d{2})?)? + )? + | + (?\d{2}) + (?: + (?\d{2})(?:[,.](?\d+))? + (?z | [-+]\d{2}(?:\d{2})?)? + )? + ) + \s*\z/ix =~ str + + h[:hour] = i hour + h[:min] = i min + h[:sec] = i sec if sec + h[:sec_fraction] = Rational(sec_fraction.to_i, 10**sec_fraction.size) if sec_fraction # JRuby bug fix! + set_zone(h, zone) + end + h + end + + def self._rfc3339(str) # :nodoc: + if /\A\s*-?\d{4}-\d{2}-\d{2} # allow minus, anyway + (t|\s) + \d{2}:\d{2}:\d{2}(\.\d+)? + (z|[-+]\d{2}:\d{2})\s*\z/ix =~ str + _parse(str) + else + {} + end + end + + def self._xmlschema(str) # :nodoc: + if /\A\s*(-?\d{4,})(?:-(\d{2})(?:-(\d{2}))?)? + (?:t + (\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?)? + (z|[-+]\d{2}:\d{2})?\s*\z/ix =~ str + e = Format::Bag.new + e.year = $1.to_i + e.mon = $2.to_i if $2 + e.mday = $3.to_i if $3 + e.hour = $4.to_i if $4 + e.min = $5.to_i if $5 + e.sec = $6.to_i if $6 + e.sec_fraction = Rational($7.to_i, 10**$7.size) if $7 + if $8 + e.zone = $8 + e.offset = zone_to_diff($8) + end + e.to_hash + elsif /\A\s*(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))? + (z|[-+]\d{2}:\d{2})?\s*\z/ix =~ str + e = Format::Bag.new + e.hour = $1.to_i if $1 + e.min = $2.to_i if $2 + e.sec = $3.to_i if $3 + e.sec_fraction = Rational($4.to_i, 10**$4.size) if $4 + if $5 + e.zone = $5 + e.offset = zone_to_diff($5) + end + e.to_hash + elsif /\A\s*(?:--(\d{2})(?:-(\d{2}))?|---(\d{2})) + (z|[-+]\d{2}:\d{2})?\s*\z/ix =~ str + e = Format::Bag.new + e.mon = $1.to_i if $1 + e.mday = $2.to_i if $2 + e.mday = $3.to_i if $3 + if $4 + e.zone = $4 + e.offset = zone_to_diff($4) + end + e.to_hash + else + {} + end + end + + def self._rfc2822(str) # :nodoc: + if /\A\s*(?:(?:#{Format::ABBR_DAYS.keys.join('|')})\s*,\s+)? + \d{1,2}\s+ + (?:#{Format::ABBR_MONTHS.keys.join('|')})\s+ + -?(\d{2,})\s+ # allow minus, anyway + \d{2}:\d{2}(:\d{2})?\s* + (?:[-+]\d{4}|ut|gmt|e[sd]t|c[sd]t|m[sd]t|p[sd]t|[a-ik-z])\s*\z/iox =~ str + e = _parse(str, false) + if $1.size < 4 + if e[:year] < 50 + e[:year] += 2000 + elsif e[:year] < 1000 + e[:year] += 1900 + end + end + e + else + {} + end + end + + class << self; alias_method :_rfc822, :_rfc2822 end + + def self._httpdate(str) # :nodoc: + if /\A\s*(#{Format::ABBR_DAYS.keys.join('|')})\s*,\s+ + \d{2}\s+ + (#{Format::ABBR_MONTHS.keys.join('|')})\s+ + -?\d{4}\s+ # allow minus, anyway + \d{2}:\d{2}:\d{2}\s+ + gmt\s*\z/iox =~ str + _rfc2822(str) + elsif /\A\s*(#{Format::DAYS.keys.join('|')})\s*,\s+ + \d{2}\s*-\s* + (#{Format::ABBR_MONTHS.keys.join('|')})\s*-\s* + \d{2}\s+ + \d{2}:\d{2}:\d{2}\s+ + gmt\s*\z/iox =~ str + _parse(str) + elsif /\A\s*(#{Format::ABBR_DAYS.keys.join('|')})\s+ + (#{Format::ABBR_MONTHS.keys.join('|')})\s+ + \d{1,2}\s+ + \d{2}:\d{2}:\d{2}\s+ + \d{4}\s*\z/iox =~ str + _parse(str) + else + {} + end + end + + def self._jisx0301(str) # :nodoc: + if /\A\s*[mtsh]?\d{2}\.\d{2}\.\d{2} + (t + (\d{2}:\d{2}(:\d{2}([,.]\d*)?)? + (z|[-+]\d{2}(:?\d{2})?)?)?)?\s*\z/ix =~ str + if /\A\s*\d/ =~ str + _parse(str.sub(/\A\s*(\d)/, 'h\1')) + else + _parse(str) + end + else + _iso8601(str) + end + end + + t = Module.new do + + private + + def zone_to_diff(zone) # :nodoc: + zone = zone.downcase + if zone.sub!(/\s+(standard|daylight)\s+time\z/, '') + dst = $1 == 'daylight' + else + dst = zone.sub!(/\s+dst\z/, '') + end + if Format::ZONES.include?(zone) + offset = Format::ZONES[zone] + offset += 3600 if dst + elsif zone.sub!(/\A(?:gmt|utc?)?([-+])/, '') + sign = $1 + if zone.include?(':') + hour, min, sec, = zone.split(':') + elsif zone.include?(',') || zone.include?('.') + hour, fr, = zone.split(/[,.]/) + min = Rational(fr.to_i, 10**fr.size) * 60 + else + case zone.size + when 3 + hour = zone[0,1] + min = zone[1,2] + else + hour = zone[0,2] + min = zone[2,2] + sec = zone[4,2] + end + end + offset = hour.to_i * 3600 + min.to_i * 60 + sec.to_i + offset *= -1 if sign == '-' + end + offset + end + + end + + extend t + include t + + extend Module.new { + private + def set_zone(h, zone) + if zone + h[:zone] = zone + h[:offset] = zone_to_diff(zone) + end + end + + def comp_year69(year) + y = i year + if year.length < 4 + if y >= 69 + y + 1900 + else + y + 2000 + end + else + y + end + end + + def i(str) + Integer(str, 10) + end + } +end + +class DateTime < Date + + def strftime(fmt='%FT%T%:z') + super(fmt) + end + + def self._strptime(str, fmt='%FT%T%z') + super(str, fmt) + end + + def iso8601_timediv(n) # :nodoc: + n = n.to_i + strftime('T%T' + + if n < 1 + '' + else + '.%0*d' % [n, (sec_fraction * 10**n).round] + end + + '%:z') + end + + private :iso8601_timediv + + def iso8601(n=0) + super() + iso8601_timediv(n) + end + + def rfc3339(n=0) iso8601(n) end + + def xmlschema(n=0) iso8601(n) end # :nodoc: + + def jisx0301(n=0) + super() + iso8601_timediv(n) + end + +end +# +# date.rb - date and time library +# +# Author: Tadayoshi Funaba 1998-2011 +# +# Documentation: William Webber +# +#-- +# $Id: date.rb,v 2.37 2008-01-17 20:16:31+09 tadf Exp $ +#++ +# +# == Overview +# +# This file provides two classes for working with +# dates and times. +# +# The first class, Date, represents dates. +# It works with years, months, weeks, and days. +# See the Date class documentation for more details. +# +# The second, DateTime, extends Date to include hours, +# minutes, seconds, and fractions of a second. It +# provides basic support for time zones. See the +# DateTime class documentation for more details. +# +# === Ways of calculating the date. +# +# In common usage, the date is reckoned in years since or +# before the Common Era (CE/BCE, also known as AD/BC), then +# as a month and day-of-the-month within the current year. +# This is known as the *Civil* *Date*, and abbreviated +# as +civil+ in the Date class. +# +# Instead of year, month-of-the-year, and day-of-the-month, +# the date can also be reckoned in terms of year and +# day-of-the-year. This is known as the *Ordinal* *Date*, +# and is abbreviated as +ordinal+ in the Date class. (Note +# that referring to this as the Julian date is incorrect.) +# +# The date can also be reckoned in terms of year, week-of-the-year, +# and day-of-the-week. This is known as the *Commercial* +# *Date*, and is abbreviated as +commercial+ in the +# Date class. The commercial week runs Monday (day-of-the-week +# 1) to Sunday (day-of-the-week 7), in contrast to the civil +# week which runs Sunday (day-of-the-week 0) to Saturday +# (day-of-the-week 6). The first week of the commercial year +# starts on the Monday on or before January 1, and the commercial +# year itself starts on this Monday, not January 1. +# +# For scientific purposes, it is convenient to refer to a date +# simply as a day count, counting from an arbitrary initial +# day. The date first chosen for this was January 1, 4713 BCE. +# A count of days from this date is the *Julian* *Day* *Number* +# or *Julian* *Date*, which is abbreviated as +jd+ in the +# Date class. This is in local time, and counts from midnight +# on the initial day. The stricter usage is in UTC, and counts +# from midday on the initial day. This is referred to in the +# Date class as the *Astronomical* *Julian* *Day* *Number*, and +# abbreviated as +ajd+. In the Date class, the Astronomical +# Julian Day Number includes fractional days. +# +# Another absolute day count is the *Modified* *Julian* *Day* +# *Number*, which takes November 17, 1858 as its initial day. +# This is abbreviated as +mjd+ in the Date class. There +# is also an *Astronomical* *Modified* *Julian* *Day* *Number*, +# which is in UTC and includes fractional days. This is +# abbreviated as +amjd+ in the Date class. Like the Modified +# Julian Day Number (and unlike the Astronomical Julian +# Day Number), it counts from midnight. +# +# Alternative calendars such as the Chinese Lunar Calendar, +# the Islamic Calendar, or the French Revolutionary Calendar +# are not supported by the Date class; nor are calendars that +# are based on an Era different from the Common Era, such as +# the Japanese Imperial Calendar or the Republic of China +# Calendar. +# +# === Calendar Reform +# +# The standard civil year is 365 days long. However, the +# solar year is fractionally longer than this. To account +# for this, a *leap* *year* is occasionally inserted. This +# is a year with 366 days, the extra day falling on February 29. +# In the early days of the civil calendar, every fourth +# year without exception was a leap year. This way of +# reckoning leap years is the *Julian* *Calendar*. +# +# However, the solar year is marginally shorter than 365 1/4 +# days, and so the *Julian* *Calendar* gradually ran slow +# over the centuries. To correct this, every 100th year +# (but not every 400th year) was excluded as a leap year. +# This way of reckoning leap years, which we use today, is +# the *Gregorian* *Calendar*. +# +# The Gregorian Calendar was introduced at different times +# in different regions. The day on which it was introduced +# for a particular region is the *Day* *of* *Calendar* +# *Reform* for that region. This is abbreviated as +sg+ +# (for Start of Gregorian calendar) in the Date class. +# +# Two such days are of particular +# significance. The first is October 15, 1582, which was +# the Day of Calendar Reform for Italy and most Catholic +# countries. The second is September 14, 1752, which was +# the Day of Calendar Reform for England and its colonies +# (including what is now the United States). These two +# dates are available as the constants Date::ITALY and +# Date::ENGLAND, respectively. (By comparison, Germany and +# Holland, less Catholic than Italy but less stubborn than +# England, changed over in 1698; Sweden in 1753; Russia not +# till 1918, after the Revolution; and Greece in 1923. Many +# Orthodox churches still use the Julian Calendar. A complete +# list of Days of Calendar Reform can be found at +# http://www.polysyllabic.com/GregConv.html.) +# +# Switching from the Julian to the Gregorian calendar +# involved skipping a number of days to make up for the +# accumulated lag, and the later the switch was (or is) +# done, the more days need to be skipped. So in 1582 in Italy, +# 4th October was followed by 15th October, skipping 10 days; in 1752 +# in England, 2nd September was followed by 14th September, skipping +# 11 days; and if I decided to switch from Julian to Gregorian +# Calendar this midnight, I would go from 27th July 2003 (Julian) +# today to 10th August 2003 (Gregorian) tomorrow, skipping +# 13 days. The Date class is aware of this gap, and a supposed +# date that would fall in the middle of it is regarded as invalid. +# +# The Day of Calendar Reform is relevant to all date representations +# involving years. It is not relevant to the Julian Day Numbers, +# except for converting between them and year-based representations. +# +# In the Date and DateTime classes, the Day of Calendar Reform or +# +sg+ can be specified a number of ways. First, it can be as +# the Julian Day Number of the Day of Calendar Reform. Second, +# it can be using the constants Date::ITALY or Date::ENGLAND; these +# are in fact the Julian Day Numbers of the Day of Calendar Reform +# of the respective regions. Third, it can be as the constant +# Date::JULIAN, which means to always use the Julian Calendar. +# Finally, it can be as the constant Date::GREGORIAN, which means +# to always use the Gregorian Calendar. +# +# Note: in the Julian Calendar, New Years Day was March 25. The +# Date class does not follow this convention. +# +# === Time Zones +# +# DateTime objects support a simple representation +# of time zones. Time zones are represented as an offset +# from UTC, as a fraction of a day. This offset is the +# how much local time is later (or earlier) than UTC. +# UTC offset 0 is centred on England (also known as GMT). +# As you travel east, the offset increases until you +# reach the dateline in the middle of the Pacific Ocean; +# as you travel west, the offset decreases. This offset +# is abbreviated as +of+ in the Date class. +# +# This simple representation of time zones does not take +# into account the common practice of Daylight Savings +# Time or Summer Time. +# +# Most DateTime methods return the date and the +# time in local time. The two exceptions are +# #ajd() and #amjd(), which return the date and time +# in UTC time, including fractional days. +# +# The Date class does not support time zone offsets, in that +# there is no way to create a Date object with a time zone. +# However, methods of the Date class when used by a +# DateTime instance will use the time zone offset of this +# instance. +# +# == Examples of use +# +# === Print out the date of every Sunday between two dates. +# +# def print_sundays(d1, d2) +# d1 +=1 while (d1.wday != 0) +# d1.step(d2, 7) do |date| +# puts "#{Date::MONTHNAMES[date.mon]} #{date.day}" +# end +# end +# +# print_sundays(Date::civil(2003, 4, 8), Date::civil(2003, 5, 23)) +# +# === Calculate how many seconds to go till midnight on New Year's Day. +# +# def secs_to_new_year(now = DateTime::now()) +# new_year = DateTime.new(now.year + 1, 1, 1) +# dif = new_year - now +# hours, mins, secs, ignore_fractions = Date::day_fraction_to_time(dif) +# return hours * 60 * 60 + mins * 60 + secs +# end +# +# puts secs_to_new_year() + +require 'date/format' + +# Class representing a date. +# +# See the documentation to the file date.rb for an overview. +# +# Internally, the date is represented as an Astronomical +# Julian Day Number, +ajd+. The Day of Calendar Reform, +sg+, is +# also stored, for conversions to other date formats. (There +# is also an +of+ field for a time zone offset, but this +# is only for the use of the DateTime subclass.) +# +# A new Date object is created using one of the object creation +# class methods named after the corresponding date format, and the +# arguments appropriate to that date format; for instance, +# Date::civil() (aliased to Date::new()) with year, month, +# and day-of-month, or Date::ordinal() with year and day-of-year. +# All of these object creation class methods also take the +# Day of Calendar Reform as an optional argument. +# +# Date objects are immutable once created. +# +# Once a Date has been created, date values +# can be retrieved for the different date formats supported +# using instance methods. For instance, #mon() gives the +# Civil month, #cwday() gives the Commercial day of the week, +# and #yday() gives the Ordinal day of the year. Date values +# can be retrieved in any format, regardless of what format +# was used to create the Date instance. +# +# The Date class includes the Comparable module, allowing +# date objects to be compared and sorted, ranges of dates +# to be created, and so forth. +class Date + + include Comparable + + # Full month names, in English. Months count from 1 to 12; a + # month's numerical representation indexed into this array + # gives the name of that month (hence the first element is nil). + MONTHNAMES = [nil] + %w(January February March April May June July + August September October November December) + + # Full names of days of the week, in English. Days of the week + # count from 0 to 6 (except in the commercial week); a day's numerical + # representation indexed into this array gives the name of that day. + DAYNAMES = %w(Sunday Monday Tuesday Wednesday Thursday Friday Saturday) + + # Abbreviated month names, in English. + ABBR_MONTHNAMES = [nil] + %w(Jan Feb Mar Apr May Jun + Jul Aug Sep Oct Nov Dec) + + # Abbreviated day names, in English. + ABBR_DAYNAMES = %w(Sun Mon Tue Wed Thu Fri Sat) + + [MONTHNAMES, DAYNAMES, ABBR_MONTHNAMES, ABBR_DAYNAMES].each do |xs| + xs.each{|x| x.freeze unless x.nil?}.freeze + end + + JODA = org.joda.time + + class Infinity < Numeric # :nodoc: + + include Comparable + + def initialize(d=1) @d = d <=> 0 end + + def d() @d end + + protected :d + + def zero? () false end + def finite? () false end + def infinite? () d.nonzero? end + def nan? () d.zero? end + + def abs() self.class.new end + + def -@ () self.class.new(-d) end + def +@ () self.class.new(+d) end + + def <=> (other) + case other + when Infinity + d <=> other.d + when Numeric + d + else + begin + l, r = other.coerce(self) + l <=> r + rescue NoMethodError + nil + end + end + end + + def coerce(other) + case other + when Numeric + [-d, d] + else + super + end + end + + def to_s + @d == 1 ? "Inf" : "-Inf" + end + end + + # The Julian Day Number of the Day of Calendar Reform for Italy + # and the Catholic countries. + ITALY = 2299161 # 1582-10-15 + + # The Julian Day Number of the Day of Calendar Reform for England + # and her Colonies. + ENGLAND = 2361222 # 1752-09-14 + + # A constant used to indicate that a Date should always use the + # Julian calendar. + JULIAN = Infinity.new + + # A constant used to indicate that a Date should always use the + # Gregorian calendar. + GREGORIAN = -Infinity.new + + HALF_DAYS_IN_DAY = Rational(1, 2) # :nodoc: + HOURS_IN_DAY = Rational(1, 24) # :nodoc: + SECONDS_IN_DAY = Rational(1, 86400) # :nodoc: + + MJD_EPOCH_IN_AJD = Rational(4800001, 2) # 1858-11-17 # :nodoc: + UNIX_EPOCH_IN_AJD = Rational(4881175, 2) # 1970-01-01 # :nodoc: + MJD_EPOCH_IN_CJD = 2400001 # :nodoc: + UNIX_EPOCH_IN_CJD = 2440588 # :nodoc: + LD_EPOCH_IN_CJD = 2299160 # :nodoc: + + t = Module.new do + + private + + def find_fdoy(y, sg) # :nodoc: + j = nil + 1.upto(31) do |d| + break if j = _valid_civil?(y, 1, d, sg) + end + j + end + + def find_ldoy(y, sg) # :nodoc: + j = nil + 31.downto(1) do |d| + break if j = _valid_civil?(y, 12, d, sg) + end + j + end + + def find_fdom(y, m, sg) # :nodoc: + j = nil + 1.upto(31) do |d| + break if j = _valid_civil?(y, m, d, sg) + end + j + end + + def find_ldom(y, m, sg) # :nodoc: + j = nil + 31.downto(1) do |d| + break if j = _valid_civil?(y, m, d, sg) + end + j + end + + # Convert an Ordinal Date to a Julian Day Number. + # + # +y+ and +d+ are the year and day-of-year to convert. + # +sg+ specifies the Day of Calendar Reform. + # + # Returns the corresponding Julian Day Number. + def ordinal_to_jd(y, d, sg=GREGORIAN) # :nodoc: + find_fdoy(y, sg) + d - 1 + end + + # Convert a Julian Day Number to an Ordinal Date. + # + # +jd+ is the Julian Day Number to convert. + # +sg+ specifies the Day of Calendar Reform. + # + # Returns the corresponding Ordinal Date as + # [year, day_of_year] + def jd_to_ordinal(jd, sg=GREGORIAN) # :nodoc: + y = jd_to_civil(jd, sg)[0] + j = find_fdoy(y, sg) + doy = jd - j + 1 + return y, doy + end + + # Convert a Civil Date to a Julian Day Number. + # +y+, +m+, and +d+ are the year, month, and day of the + # month. +sg+ specifies the Day of Calendar Reform. + # + # Returns the corresponding Julian Day Number. + def civil_to_jd(y, m, d, sg=GREGORIAN) # :nodoc: + if m <= 2 + y -= 1 + m += 12 + end + a = (y / 100.0).floor + b = 2 - a + (a / 4.0).floor + jd = (365.25 * (y + 4716)).floor + + (30.6001 * (m + 1)).floor + + d + b - 1524 + if jd < sg + jd -= b + end + jd + end + + # Convert a Julian Day Number to a Civil Date. +jd+ is + # the Julian Day Number. +sg+ specifies the Day of + # Calendar Reform. + # + # Returns the corresponding [year, month, day_of_month] + # as a three-element array. + def jd_to_civil(jd, sg=GREGORIAN) # :nodoc: + if jd < sg + a = jd + else + x = ((jd - 1867216.25) / 36524.25).floor + a = jd + 1 + x - (x / 4.0).floor + end + b = a + 1524 + c = ((b - 122.1) / 365.25).floor + d = (365.25 * c).floor + e = ((b - d) / 30.6001).floor + dom = b - d - (30.6001 * e).floor + if e <= 13 + m = e - 1 + y = c - 4716 + else + m = e - 13 + y = c - 4715 + end + return y, m, dom + end + + # Convert a Commercial Date to a Julian Day Number. + # + # +y+, +w+, and +d+ are the (commercial) year, week of the year, + # and day of the week of the Commercial Date to convert. + # +sg+ specifies the Day of Calendar Reform. + def commercial_to_jd(y, w, d, sg=GREGORIAN) # :nodoc: + j = find_fdoy(y, sg) + 3 + (j - (((j - 1) + 1) % 7)) + + 7 * (w - 1) + + (d - 1) + end + + # Convert a Julian Day Number to a Commercial Date + # + # +jd+ is the Julian Day Number to convert. + # +sg+ specifies the Day of Calendar Reform. + # + # Returns the corresponding Commercial Date as + # [commercial_year, week_of_year, day_of_week] + def jd_to_commercial(jd, sg=GREGORIAN) # :nodoc: + a = jd_to_civil(jd - 3, sg)[0] + y = if jd >= commercial_to_jd(a + 1, 1, 1, sg) then a + 1 else a end + w = 1 + ((jd - commercial_to_jd(y, 1, 1, sg)) / 7).floor + d = (jd + 1) % 7 + d = 7 if d == 0 + return y, w, d + end + + def weeknum_to_jd(y, w, d, f=0, sg=GREGORIAN) # :nodoc: + a = find_fdoy(y, sg) + 6 + (a - ((a - f) + 1) % 7 - 7) + 7 * w + d + end + + def jd_to_weeknum(jd, f=0, sg=GREGORIAN) # :nodoc: + y, m, d = jd_to_civil(jd, sg) + a = find_fdoy(y, sg) + 6 + w, d = (jd - (a - ((a - f) + 1) % 7) + 7).divmod(7) + return y, w, d + end + + def nth_kday_to_jd(y, m, n, k, sg=GREGORIAN) # :nodoc: + j = if n > 0 + find_fdom(y, m, sg) - 1 + else + find_ldom(y, m, sg) + 7 + end + (j - (((j - k) + 1) % 7)) + 7 * n + end + + def jd_to_nth_kday(jd, sg=GREGORIAN) # :nodoc: + y, m, d = jd_to_civil(jd, sg) + j = find_fdom(y, m, sg) + return y, m, ((jd - j) / 7).floor + 1, jd_to_wday(jd) + end + + # Convert an Astronomical Julian Day Number to a (civil) Julian + # Day Number. + # + # +ajd+ is the Astronomical Julian Day Number to convert. + # +of+ is the offset from UTC as a fraction of a day (defaults to 0). + # + # Returns the (civil) Julian Day Number as [day_number, + # fraction] where +fraction+ is always 1/2. + def ajd_to_jd(ajd, of=0) (ajd + of + HALF_DAYS_IN_DAY).divmod(1) end # :nodoc: + + # Convert a (civil) Julian Day Number to an Astronomical Julian + # Day Number. + # + # +jd+ is the Julian Day Number to convert, and +fr+ is a + # fractional day. + # +of+ is the offset from UTC as a fraction of a day (defaults to 0). + # + # Returns the Astronomical Julian Day Number as a single + # numeric value. + def jd_to_ajd(jd, fr, of=0) jd + fr - of - HALF_DAYS_IN_DAY end # :nodoc: + + # Convert a fractional day +fr+ to [hours, minutes, seconds, + # fraction_of_a_second] + def day_fraction_to_time(fr) # :nodoc: + ss, fr = fr.divmod(SECONDS_IN_DAY) # 4p + h, ss = ss.divmod(3600) + min, s = ss.divmod(60) + return h, min, s, fr * 86400 + end + + # Convert an +h+ hour, +min+ minutes, +s+ seconds period + # to a fractional day. + def time_to_day_fraction(h, min, s) + Rational(h * 3600 + min * 60 + s, 86400) # 4p + end + + # Convert an Astronomical Modified Julian Day Number to an + # Astronomical Julian Day Number. + def amjd_to_ajd(amjd) amjd + MJD_EPOCH_IN_AJD end # :nodoc: + + # Convert an Astronomical Julian Day Number to an + # Astronomical Modified Julian Day Number. + def ajd_to_amjd(ajd) ajd - MJD_EPOCH_IN_AJD end # :nodoc: + + # Convert a Modified Julian Day Number to a Julian + # Day Number. + def mjd_to_jd(mjd) mjd + MJD_EPOCH_IN_CJD end # :nodoc: + + # Convert a Julian Day Number to a Modified Julian Day + # Number. + def jd_to_mjd(jd) jd - MJD_EPOCH_IN_CJD end # :nodoc: + + # Convert a count of the number of days since the adoption + # of the Gregorian Calendar (in Italy) to a Julian Day Number. + def ld_to_jd(ld) ld + LD_EPOCH_IN_CJD end # :nodoc: + + # Convert a Julian Day Number to the number of days since + # the adoption of the Gregorian Calendar (in Italy). + def jd_to_ld(jd) jd - LD_EPOCH_IN_CJD end # :nodoc: + + # Convert a Julian Day Number to the day of the week. + # + # Sunday is day-of-week 0; Saturday is day-of-week 6. + def jd_to_wday(jd) (jd + 1) % 7 end # :nodoc: + + # Is +jd+ a valid Julian Day Number? + # + # If it is, returns it. In fact, any value is treated as a valid + # Julian Day Number. + def _valid_jd? (jd, sg=GREGORIAN) jd end # :nodoc: + + # Do the year +y+ and day-of-year +d+ make a valid Ordinal Date? + # Returns the corresponding Julian Day Number if they do, or + # nil if they don't. + # + # +d+ can be a negative number, in which case it counts backwards + # from the end of the year (-1 being the last day of the year). + # No year wraparound is performed, however, so valid values of + # +d+ are -365 .. -1, 1 .. 365 on a non-leap-year, + # -366 .. -1, 1 .. 366 on a leap year. + # A date falling in the period skipped in the Day of Calendar Reform + # adjustment is not valid. + # + # +sg+ specifies the Day of Calendar Reform. + def _valid_ordinal? (y, d, sg=GREGORIAN) # :nodoc: + if d < 0 + return unless j = find_ldoy(y, sg) + ny, nd = jd_to_ordinal(j + d + 1, sg) + return unless ny == y + d = nd + end + jd = ordinal_to_jd(y, d, sg) + return unless [y, d] == jd_to_ordinal(jd, sg) + jd + end + + # Do year +y+, month +m+, and day-of-month +d+ make a + # valid Civil Date? Returns the corresponding Julian + # Day Number if they do, nil if they don't. + # + # +m+ and +d+ can be negative, in which case they count + # backwards from the end of the year and the end of the + # month respectively. No wraparound is performed, however, + # and invalid values cause an ArgumentError to be raised. + # A date falling in the period skipped in the Day of Calendar + # Reform adjustment is not valid. + # + # +sg+ specifies the Day of Calendar Reform. + def _valid_civil? (y, m, d, sg=GREGORIAN) # :nodoc: + if m < 0 + m += 13 + end + if d < 0 + return unless j = find_ldom(y, m, sg) + ny, nm, nd = jd_to_civil(j + d + 1, sg) + return unless [ny, nm] == [y, m] + d = nd + end + jd = civil_to_jd(y, m, d, sg) + return unless [y, m, d] == jd_to_civil(jd, sg) + jd + end + + # Do year +y+, week-of-year +w+, and day-of-week +d+ make a + # valid Commercial Date? Returns the corresponding Julian + # Day Number if they do, nil if they don't. + # + # Monday is day-of-week 1; Sunday is day-of-week 7. + # + # +w+ and +d+ can be negative, in which case they count + # backwards from the end of the year and the end of the + # week respectively. No wraparound is performed, however, + # and invalid values cause an ArgumentError to be raised. + # A date falling in the period skipped in the Day of Calendar + # Reform adjustment is not valid. + # + # +sg+ specifies the Day of Calendar Reform. + def _valid_commercial? (y, w, d, sg=GREGORIAN) # :nodoc: + if d < 0 + d += 8 + end + if w < 0 + ny, nw, nd = + jd_to_commercial(commercial_to_jd(y + 1, 1, 1, sg) + w * 7, sg) + return unless ny == y + w = nw + end + jd = commercial_to_jd(y, w, d, sg) + return unless [y, w, d] == jd_to_commercial(jd, sg) + jd + end + + def _valid_weeknum? (y, w, d, f, sg=GREGORIAN) # :nodoc: + if d < 0 + d += 7 + end + if w < 0 + ny, nw, nd, nf = + jd_to_weeknum(weeknum_to_jd(y + 1, 1, f, f, sg) + w * 7, f, sg) + return unless ny == y + w = nw + end + jd = weeknum_to_jd(y, w, d, f, sg) + return unless [y, w, d] == jd_to_weeknum(jd, f, sg) + jd + end + + def _valid_nth_kday? (y, m, n, k, sg=GREGORIAN) # :nodoc: + if k < 0 + k += 7 + end + if n < 0 + ny, nm = (y * 12 + m).divmod(12) + nm, = (nm + 1) .divmod(1) + ny, nm, nn, nk = + jd_to_nth_kday(nth_kday_to_jd(ny, nm, 1, k, sg) + n * 7, sg) + return unless [ny, nm] == [y, m] + n = nn + end + jd = nth_kday_to_jd(y, m, n, k, sg) + return unless [y, m, n, k] == jd_to_nth_kday(jd, sg) + jd + end + + # Do hour +h+, minute +min+, and second +s+ constitute a valid time? + # + # If they do, returns their value as a fraction of a day. If not, + # returns nil. + # + # The 24-hour clock is used. Negative values of +h+, +min+, and + # +sec+ are treating as counting backwards from the end of the + # next larger unit (e.g. a +min+ of -2 is treated as 58). No + # wraparound is performed. + def _valid_time? (h, min, s) # :nodoc: + h += 24 if h < 0 + min += 60 if min < 0 + s += 60 if s < 0 + return unless ((0...24) === h && + (0...60) === min && + (0...60) === s) || + (24 == h && + 0 == min && + 0 == s) + time_to_day_fraction(h, min, s) + end + + def chronology(sg, of=0) + tz = if JODA::DateTimeZone === of + of + elsif of == 0 + return CHRONO_ITALY_UTC if sg == ITALY + JODA::DateTimeZone::UTC + else + raise ArgumentError, "Invalid offset: #{of}" if of <= -1 or of >= 1 + JODA::DateTimeZone.forOffsetMillis((of * 86_400_000).round) + end + + chrono = if sg == ITALY + JODA.chrono::GJChronology + elsif sg == JULIAN + JODA.chrono::JulianChronology + elsif sg == GREGORIAN + JODA.chrono::GregorianChronology + end + + if chrono + chrono.getInstance(tz) + else + constructor = JODA::Instant.java_class.constructor(Java::long) + cutover = constructor.new_instance JODA::DateTimeUtils.fromJulianDay(jd_to_ajd(sg, 0)) + JODA.chrono::GJChronology.getInstance(tz, cutover) + end + end + end + + extend t + include t + + DEFAULT_TZ = Time.now.strftime('%z') + DEFAULT_DTZ = Time.now.strftime('%z') + CHRONO_ITALY_DEFAULT_DTZ = chronology(ITALY, DEFAULT_DTZ) + CHRONO_ITALY_UTC = JODA.chrono::GJChronology.getInstance(JODA::DateTimeZone::UTC) + + # Is a year a leap year in the Julian calendar? + # + # All years divisible by 4 are leap years in the Julian calendar. + def self.julian_leap? (y) y % 4 == 0 end + + # Is a year a leap year in the Gregorian calendar? + # + # All years divisible by 4 are leap years in the Gregorian calendar, + # except for years divisible by 100 and not by 400. + def self.gregorian_leap? (y) y % 4 == 0 && y % 100 != 0 || y % 400 == 0 end + class << self; alias_method :leap?, :gregorian_leap? end + + class << self; alias_method :new!, :new end + + def self.valid_jd? (jd, sg=ITALY) + !!_valid_jd?(jd, sg) + end + + def self.valid_ordinal? (y, d, sg=ITALY) + !!_valid_ordinal?(y, d, sg) + end + + def self.valid_civil? (y, m, d, sg=ITALY) + !!_valid_civil?(y, m, d, sg) + end + class << self; alias_method :valid_date?, :valid_civil? end + + def self.valid_commercial? (y, w, d, sg=ITALY) + !!_valid_commercial?(y, w, d, sg) + end + + def self.valid_weeknum? (y, w, d, f, sg=ITALY) # :nodoc: + !!_valid_weeknum?(y, w, d, f, sg) + end + private_class_method :valid_weeknum? + + def self.valid_nth_kday? (y, m, n, k, sg=ITALY) # :nodoc: + !!_valid_nth_kday?(y, m, n, k, sg) + end + private_class_method :valid_nth_kday? + + def self.valid_time? (h, min, s) # :nodoc: + !!_valid_time?(h, min, s) + end + private_class_method :valid_time? + + # Create a new Date object from a Julian Day Number. + # + # +jd+ is the Julian Day Number; if not specified, it defaults to + # 0. + # +sg+ specifies the Day of Calendar Reform. + def self.jd(jd=0, sg=ITALY) + jd = _valid_jd?(jd, sg) + new!(jd_to_ajd(jd, 0, 0), 0, sg) + end + + # Create a new Date object from an Ordinal Date, specified + # by year +y+ and day-of-year +d+. +d+ can be negative, + # in which it counts backwards from the end of the year. + # No year wraparound is performed, however. An invalid + # value for +d+ results in an ArgumentError being raised. + # + # +y+ defaults to -4712, and +d+ to 1; this is Julian Day + # Number day 0. + # + # +sg+ specifies the Day of Calendar Reform. + def self.ordinal(y=-4712, d=1, sg=ITALY) + unless jd = _valid_ordinal?(y, d, sg) + raise ArgumentError, 'invalid date' + end + new!(jd_to_ajd(jd, 0, 0), 0, sg) + end + + # Create a new Date object for the Civil Date specified by + # year +y+, month +m+, and day-of-month +d+. + # + # +m+ and +d+ can be negative, in which case they count + # backwards from the end of the year and the end of the + # month respectively. No wraparound is performed, however, + # and invalid values cause an ArgumentError to be raised. + # can be negative + # + # +y+ defaults to -4712, +m+ to 1, and +d+ to 1; this is + # Julian Day Number day 0. + # + # +sg+ specifies the Day of Calendar Reform. + def self.civil(y=-4712, m=1, d=1, sg=ITALY) + if Fixnum === y and Fixnum === m and Fixnum === d and d > 0 + m += 13 if m < 0 + y -= 1 if y <= 0 and sg > 0 # TODO + begin + dt = JODA::DateTime.new(y, m, d, 0, 0, 0, chronology(sg)) + rescue JODA::IllegalFieldValueException, Java::JavaLang::IllegalArgumentException + raise ArgumentError, 'invalid date' + end + new!(dt, 0, sg) + else + unless jd = _valid_civil?(y, m, d, sg) + raise ArgumentError, 'invalid date' + end + new!(jd_to_ajd(jd, 0, 0), 0, sg) + end + end + class << self; alias_method :new, :civil end + + # Create a new Date object for the Commercial Date specified by + # year +y+, week-of-year +w+, and day-of-week +d+. + # + # Monday is day-of-week 1; Sunday is day-of-week 7. + # + # +w+ and +d+ can be negative, in which case they count + # backwards from the end of the year and the end of the + # week respectively. No wraparound is performed, however, + # and invalid values cause an ArgumentError to be raised. + # + # +y+ defaults to -4712, +w+ to 1, and +d+ to 1; this is + # Julian Day Number day 0. + # + # +sg+ specifies the Day of Calendar Reform. + def self.commercial(y=-4712, w=1, d=1, sg=ITALY) + unless jd = _valid_commercial?(y, w, d, sg) + raise ArgumentError, 'invalid date' + end + new!(jd_to_ajd(jd, 0, 0), 0, sg) + end + + def self.weeknum(y=-4712, w=0, d=1, f=0, sg=ITALY) + unless jd = _valid_weeknum?(y, w, d, f, sg) + raise ArgumentError, 'invalid date' + end + new!(jd_to_ajd(jd, 0, 0), 0, sg) + end + private_class_method :weeknum + + def self.nth_kday(y=-4712, m=1, n=1, k=1, sg=ITALY) + unless jd = _valid_nth_kday?(y, m, n, k, sg) + raise ArgumentError, 'invalid date' + end + new!(jd_to_ajd(jd, 0, 0), 0, sg) + end + private_class_method :nth_kday + + def self.rewrite_frags(elem) # :nodoc: + elem ||= {} + if seconds = elem[:seconds] + d, fr = seconds.divmod(86400) + h, fr = fr.divmod(3600) + min, fr = fr.divmod(60) + s, fr = fr.divmod(1) + offset = elem[:offset] + unless offset.nil? + seconds += offset + end + elem[:jd] = UNIX_EPOCH_IN_CJD + d + elem[:hour] = h + elem[:min] = min + elem[:sec] = s + elem[:sec_fraction] = fr + elem.delete(:seconds) + end + elem + end + private_class_method :rewrite_frags + + COMPLETE_FRAGS = [ + [:time, []], + [nil, [:jd]], + [:ordinal, [:year, :yday]], + [:civil, [:year, :mon, :mday]], + [:commercial, [:cwyear, :cweek, :cwday]], + [:wday, [:wday, :__need_jd_filling]], + [:wnum0, [:year, :wnum0, :wday]], + [:wnum1, [:year, :wnum1, :wday]], + [nil, [:cwyear, :cweek, :wday]], + [nil, [:year, :wnum0, :cwday]], + [nil, [:year, :wnum1, :cwday]] + ] + + def self.complete_frags(elem) # :nodoc: + g = COMPLETE_FRAGS.max_by { |kind, fields| + fields.count { |field| elem.key? field } + } + c = g[1].count { |field| elem.key? field } + + if c == 0 and [:hour, :min, :sec].none? { |field| elem.key? field } + g = nil + end + + if g && g[0] && g[1].size != c + d ||= Date.today + + case g[0] + when :ordinal + elem[:year] ||= d.year + elem[:yday] ||= 1 + when :civil + g[1].each do |e| + break if elem[e] + elem[e] = d.__send__(e) + end + elem[:mon] ||= 1 + elem[:mday] ||= 1 + when :commercial + g[1].each do |e| + break if elem[e] + elem[e] = d.__send__(e) + end + elem[:cweek] ||= 1 + elem[:cwday] ||= 1 + when :wday + elem[:jd] ||= (d - d.wday + elem[:wday]).jd + when :wnum0 + g[1].each do |e| + break if elem[e] + elem[e] = d.__send__(e) + end + elem[:wnum0] ||= 0 + elem[:wday] ||= 0 + when :wnum1 + g[1].each do |e| + break if elem[e] + elem[e] = d.__send__(e) + end + elem[:wnum1] ||= 0 + elem[:wday] ||= 1 + end + end + + if self <= DateTime + if g && g[0] == :time + d ||= Date.today + elem[:jd] ||= d.jd + end + + elem[:hour] ||= 0 + elem[:min] ||= 0 + elem[:sec] ||= 0 + # see [ruby-core:47226] and the "fix" + elem[:sec] = 59 if elem[:sec] == 60 + end + + elem + end + private_class_method :complete_frags + + def self.valid_date_frags?(elem, sg) # :nodoc: + if jd = elem[:jd] and + jd = _valid_jd?(jd, sg) + return jd + end + + year = elem[:year] + + if year and yday = elem[:yday] and + jd = _valid_ordinal?(year, yday, sg) + return jd + end + + if year and mon = elem[:mon] and mday = elem[:mday] and + jd = _valid_civil?(year, mon, mday, sg) + return jd + end + + if cwyear = elem[:cwyear] and cweek = elem[:cweek] and cwday = (elem[:cwday] || elem[:wday].nonzero? || 7) and + jd = _valid_commercial?(cwyear, cweek, cwday, sg) + return jd + end + + if year and wnum0 = elem[:wnum0] and wday = (elem[:wday] || (elem[:cwday] && elem[:cwday] % 7)) and + jd = _valid_weeknum?(year, wnum0, wday, 0, sg) + return jd + end + + if year and wnum1 = elem[:wnum1] and wday = ( + (elem[:wday] && (elem[:wday] - 1) % 7) || + (elem[:cwday] && (elem[:cwday] - 1) % 7) + ) and jd = _valid_weeknum?(year, wnum1, wday, 1, sg) + return jd + end + end + private_class_method :valid_date_frags? + + def self.valid_time_frags? (elem) # :nodoc: + h, min, s = elem.values_at(:hour, :min, :sec) + _valid_time?(h, min, s) + end + private_class_method :valid_time_frags? + + def self.new_by_frags(elem, sg) # :nodoc: + # fast path + if elem and !elem.key?(:jd) and !elem.key?(:yday) and + year = elem[:year] and mon = elem[:mon] and mday = elem[:mday] + return Date.civil(year, mon, mday, sg) + end + + elem = rewrite_frags(elem) + elem = complete_frags(elem) + unless jd = valid_date_frags?(elem, sg) + raise ArgumentError, 'invalid date' + end + new!(jd_to_ajd(jd, 0, 0), 0, sg) + end + private_class_method :new_by_frags + + # Create a new Date object by parsing from a String + # according to a specified format. + # + # +str+ is a String holding a date representation. + # +fmt+ is the format that the date is in. See + # date/format.rb for details on supported formats. + # + # The default +str+ is '-4712-01-01', and the default + # +fmt+ is '%F', which means Year-Month-Day_of_Month. + # This gives Julian Day Number day 0. + # + # +sg+ specifies the Day of Calendar Reform. + # + # An ArgumentError will be raised if +str+ cannot be + # parsed. + def self.strptime(str='-4712-01-01', fmt='%F', sg=ITALY) + elem = _strptime(str, fmt) + new_by_frags(elem, sg) + end + + # Create a new Date object by parsing from a String, + # without specifying the format. + # + # +str+ is a String holding a date representation. + # +comp+ specifies whether to interpret 2-digit years + # as 19XX (>= 69) or 20XX (< 69); the default is not to. + # The method will attempt to parse a date from the String + # using various heuristics; see #_parse in date/format.rb + # for more details. If parsing fails, an ArgumentError + # will be raised. + # + # The default +str+ is '-4712-01-01'; this is Julian + # Day Number day 0. + # + # +sg+ specifies the Day of Calendar Reform. + def self.parse(str='-4712-01-01', comp=true, sg=ITALY) + elem = _parse(str, comp) + new_by_frags(elem, sg) + end + + def self.iso8601(str='-4712-01-01', sg=ITALY) # :nodoc: + elem = _iso8601(str) + new_by_frags(elem, sg) + end + + def self.rfc3339(str='-4712-01-01T00:00:00+00:00', sg=ITALY) # :nodoc: + elem = _rfc3339(str) + new_by_frags(elem, sg) + end + + def self.xmlschema(str='-4712-01-01', sg=ITALY) # :nodoc: + elem = _xmlschema(str) + new_by_frags(elem, sg) + end + + def self.rfc2822(str='Mon, 1 Jan -4712 00:00:00 +0000', sg=ITALY) # :nodoc: + elem = _rfc2822(str) + new_by_frags(elem, sg) + end + class << self; alias_method :rfc822, :rfc2822 end + + def self.httpdate(str='Mon, 01 Jan -4712 00:00:00 GMT', sg=ITALY) # :nodoc: + elem = _httpdate(str) + new_by_frags(elem, sg) + end + + def self.jisx0301(str='-4712-01-01', sg=ITALY) # :nodoc: + elem = _jisx0301(str) + new_by_frags(elem, sg) + end + + # *NOTE* this is the documentation for the method new!(). If + # you are reading this as the documentation for new(), that is + # because rdoc doesn't fully support the aliasing of the + # initialize() method. + # new() is in + # fact an alias for #civil(): read the documentation for that + # method instead. + # + # Create a new Date object. + # + # +ajd+ is the Astronomical Julian Day Number. + # +of+ is the offset from UTC as a fraction of a day. + # Both default to 0. + # + # +sg+ specifies the Day of Calendar Reform to use for this + # Date object. + # + # Using one of the factory methods such as Date::civil is + # generally easier and safer. + def initialize(dt_or_ajd=0, of=0, sg=ITALY, sub_millis=0) + if Time === dt_or_ajd + @dt = dt_or_ajd + @sub_millis = sub_millis + of ||= Rational(@dt.getChronology.getZone.getOffset(@dt), 86_400_000) + else + # cannot use JODA::DateTimeUtils.fromJulianDay since we need to keep ajd as a Rational for precision + millis, @sub_millis = ((dt_or_ajd - UNIX_EPOCH_IN_AJD) * 86400000).divmod(1) + raise ArgumentError, "Date out of range: millis=#{millis} (#{millis.class})" unless Fixnum === millis + @dt = JODA::DateTime.new(millis, chronology(sg, of)) + end + + @of = of # offset + @sg = sg # start + end + + attr_reader :dt, :sub_millis + protected :dt, :sub_millis + + # Get the date as an Astronomical Julian Day Number. + def ajd + # Rational(@dt.getMillis + @sub_millis, 86400000) + 2440587.5 + Rational(210866760000000 + @dt.getMillis + @sub_millis, 86400000) + end + + # Get the date as an Astronomical Modified Julian Day Number. + def amjd + ajd_to_amjd(ajd) + end + + # Get the date as a Julian Day Number. + def jd + (JODA::DateTimeUtils.toJulianDay(@dt.getMillis) + @of.to_f + 0.5).floor + end + + # Get any fractional day part of the date. + def day_fraction + ms = ((hour * 60 + min) * 60 + sec) * 1000 + @dt.getMillisOfSecond + @sub_millis + Rational(ms, 86_400_000) + end + + # Get the date as a Modified Julian Day Number. + def mjd() jd_to_mjd(jd) end + + # Get the date as the number of days since the Day of Calendar + # Reform (in Italy and the Catholic countries). + def ld() jd_to_ld(jd) end + + def joda_year_to_date_year(year) + if year < 0 and julian? + # Joda-time returns -x for year x BC in JulianChronology (so there is no year 0), + # while date.rb returns -x+1, following astronomical year numbering (with year 0) + year + 1 + else + year + end + end + private :joda_year_to_date_year + + # Get the year of this date. + def year + joda_year_to_date_year(@dt.getYear) + end + + # Get the day-of-the-year of this date. + # + # January 1 is day-of-the-year 1 + def yday + @dt.getDayOfYear + end + + # Get the month of this date. + # + # January is month 1. + def mon + @dt.getMonthOfYear + end + alias_method :month, :mon + + # Get the day-of-the-month of this date. + def mday + @dt.getDayOfMonth + end + alias_method :day, :mday + + # Get the hour of this date. + def hour + @dt.getHourOfDay + end + + # Get the minute of this date. + def min + @dt.getMinuteOfHour + end + alias_method :minute, :min + + # Get the second of this date. + def sec + @dt.getSecondOfMinute + end + alias_method :second, :sec + + # Get the fraction-of-a-second of this date. + def sec_fraction + Rational(@dt.getMillisOfSecond + @sub_millis, 1000) + end + alias_method :second_fraction, :sec_fraction + + private :hour, :min, :sec, :sec_fraction, + :minute, :second, :second_fraction + + def zone() strftime('%:z') end + private :zone + + # Get the commercial year of this date. See *Commercial* *Date* + # in the introduction for how this differs from the normal year. + def cwyear + joda_year_to_date_year(@dt.getWeekyear) + end + + # Get the commercial week of the year of this date. + def cweek + @dt.getWeekOfWeekyear + end + + # Get the commercial day of the week of this date. Monday is + # commercial day-of-week 1; Sunday is commercial day-of-week 7. + def cwday + @dt.getDayOfWeek + end + + # Get the week day of this date. Sunday is day-of-week 0; + # Saturday is day-of-week 6. + def wday + @dt.getDayOfWeek % 7 + end + + DAYNAMES.each_with_index do |n, i| + define_method(n.downcase + '?'){wday == i} + end + + def nth_kday? (n, k) + k == wday && jd === nth_kday_to_jd(year, mon, n, k, start) + end + private :nth_kday? + + # Is the current date old-style (Julian Calendar)? + def julian? + jd < @sg + end + + # Is the current date new-style (Gregorian Calendar)? + def gregorian? () !julian? end + + def fix_style # :nodoc: + if julian? + then self.class::JULIAN + else self.class::GREGORIAN end + end + private :fix_style + + # Is this a leap year? + def leap? + julian? ? Date.julian_leap?(year) : Date.gregorian_leap?(year) + end + + # When is the Day of Calendar Reform for this Date object? + def start + case @dt.getChronology + when JODA.chrono::JulianChronology + JULIAN + when JODA.chrono::GregorianChronology + GREGORIAN + else + JODA::DateTimeUtils.toJulianDayNumber @dt.getChronology.getGregorianCutover.getMillis + end + end + + # Create a copy of this Date object using a new Day of Calendar Reform. + def new_start(sg=self.class::ITALY) + self.class.new!(@dt.withChronology(chronology(sg, @of)), @of, sg, @sub_millis) + end + + # Create a copy of this Date object that uses the Italian/Catholic + # Day of Calendar Reform. + def italy() new_start(self.class::ITALY) end + + # Create a copy of this Date object that uses the English/Colonial + # Day of Calendar Reform. + def england() new_start(self.class::ENGLAND) end + + # Create a copy of this Date object that always uses the Julian + # Calendar. + def julian() new_start(self.class::JULIAN) end + + # Create a copy of this Date object that always uses the Gregorian + # Calendar. + def gregorian() new_start(self.class::GREGORIAN) end + + def offset + Rational(@dt.getChronology.getZone.getOffset(@dt), 86_400_000) + end + + def new_offset(of=0) + if String === of + of = Rational(zone_to_diff(of) || 0, 86400) + end + self.class.new!(@dt.withChronology(chronology(@sg, of)), of, @sg, @sub_millis) + end + private :offset, :new_offset + + # Return a new Date object that is +n+ days later than the + # current one. + # + # +n+ may be a negative value, in which case the new Date + # is earlier than the current one; however, #-() might be + # more intuitive. + # + # If +n+ is not a Numeric, a TypeError will be thrown. In + # particular, two Dates cannot be added to each other. + def + (n) + case n + when Fixnum + self.class.new!(@dt.plusDays(n), @of, @sg, @sub_millis) + when Numeric + ms, sub = (n * 86_400_000).divmod(1) + sub = 0 if sub == 0 # avoid Rational(0, 1) + sub_millis = @sub_millis + sub + if sub_millis >= 1 + sub_millis -= 1 + ms += 1 + end + self.class.new!(@dt.plus(ms), @of, @sg, sub_millis) + else + raise TypeError, 'expected numeric' + end + end + + # If +x+ is a Numeric value, create a new Date object that is + # +x+ days earlier than the current one. + # + # If +x+ is a Date, return the number of days between the + # two dates; or, more precisely, how many days later the current + # date is than +x+. + # + # If +x+ is neither Numeric nor a Date, a TypeError is raised. + def - (x) + case x + when Numeric + self + (-x) + when Date + diff = @dt.getMillis - x.dt.getMillis + diff_sub = @sub_millis - x.sub_millis + diff += diff_sub if diff_sub != 0 + Rational(diff, 86_400_000) + else + raise TypeError, 'expected numeric or date' + end + end + + # Compare this date with another date. + # + # +other+ can also be a Numeric value, in which case it is + # interpreted as an Astronomical Julian Day Number. + # + # Comparison is by Astronomical Julian Day Number, including + # fractional days. This means that both the time and the + # timezone offset are taken into account when comparing + # two DateTime instances. When comparing a DateTime instance + # with a Date instance, the time of the latter will be + # considered as falling on midnight UTC. + class org::joda::time::DateTime + java_alias :compareDT, :compareTo, [org.joda.time.ReadableInstant] + end + def <=> (other) + if other.kind_of?(Date) + # The method compareTo doesn't compare the sub milliseconds so after compare the two dates + # then we have to compare the sub milliseconds to make sure that both are exactly equal. + @dt.compareDT(other.dt).nonzero? || @sub_millis <=> other.sub_millis + else + __internal_cmp(other) + end + end + + private def __internal_cmp(other) + if other.kind_of? Numeric + ajd <=> other + else + begin + l, r = other.coerce(self) + l <=> r + rescue NoMethodError + nil + end + end + end + + # The relationship operator for Date. + # + # Compares dates by Julian Day Number. When comparing + # two DateTime instances, or a DateTime with a Date, + # the instances will be regarded as equivalent if they + # fall on the same date in local time. + def === (other) + case other + when Numeric + jd == other + when Date + jd == other.jd + else + begin + l, r = other.coerce(self) + l === r + rescue NoMethodError + false + end + end + end + + def next_day(n=1) self + n end + def prev_day(n=1) self - n end + + # Return a new Date one day after this one. + def next() next_day end + alias_method :succ, :next + + # Return a new Date object that is +n+ months later than + # the current one. + # + # If the day-of-the-month of the current Date is greater + # than the last day of the target month, the day-of-the-month + # of the returned Date will be the last day of the target month. + def >> (n) + n = n.to_int rescue raise(TypeError, "n must be a Fixnum") + self.class.new!(@dt.plusMonths(n), @of, @sg, @sub_millis) + end + + # Return a new Date object that is +n+ months earlier than + # the current one. + # + # If the day-of-the-month of the current Date is greater + # than the last day of the target month, the day-of-the-month + # of the returned Date will be the last day of the target month. + def << (n) self >> -n end + + def next_month(n=1) self >> n end + def prev_month(n=1) self << n end + + def next_year(n=1) + self.class.new!(@dt.plusYears(n.to_i), @of, @sg, @sub_millis) + end + + def prev_year(n=1) + next_year(-n) + end + + # Step the current date forward +step+ days at a + # time (or backward, if +step+ is negative) until + # we reach +limit+ (inclusive), yielding the resultant + # date at each step. + def step(limit, step=1) # :yield: date +=begin + if step.zero? + raise ArgumentError, "step can't be 0" + end +=end + unless block_given? + return to_enum(:step, limit, step) + end + da = self + op = %w(- <= >=)[step <=> 0] + while da.__send__(op, limit) + yield da + da += step + end + self + end + + # Step forward one day at a time until we reach +max+ + # (inclusive), yielding each date as we go. + def upto(max, &block) # :yield: date + step(max, +1, &block) + end + + # Step backward one day at a time until we reach +min+ + # (inclusive), yielding each date as we go. + def downto(min, &block) # :yield: date + step(min, -1, &block) + end + + # Is this Date equal to +other+? + # + # +other+ must both be a Date object, and represent the same date. + def eql? (other) Date === other && self == other end + + # Calculate a hash value for this date. + def hash() @dt.getMillis end + + # Return internal object state as a programmer-readable string. + def inspect + s = (hour * 60 + min) * 60 + sec - (@of*86_400).to_i + ns = ((@dt.getMillisOfSecond + @sub_millis) * 1_000_000) + ns = ns.to_i if Rational === ns and ns.denominator == 1 + of = "%+d" % (@of * 86_400) + sg = Date::Infinity === @sg ? @sg.to_s : "%.0f" % @sg + "#<#{self.class}: #{to_s} ((#{jd}j,#{s}s,#{ns.inspect}n),#{of}s,#{sg}j)>" + end + + # Return the date as a human-readable string. + # + # The format used is YYYY-MM-DD. + def to_s() format('%.4d-%02d-%02d', year, mon, mday) end # 4p + + # Dump to Marshal format. + def marshal_dump() [ajd, @of, @sg] end + + # Load from Marshal format. + def marshal_load(a) + ajd, of, sg = nil + + case a.size + when 2 # 1.6.x + ajd = a[0] - HALF_DAYS_IN_DAY + of = 0 + sg = a[1] + sg = sg ? GREGORIAN : JULIAN unless Numeric === sg + when 3 # 1.8.x, 1.9.2 + ajd, of, sg = a + when 6 + _, jd, df, sf, of, sg = a + of = Rational(of, 86_400) + ajd = jd - HALF_DAYS_IN_DAY + ajd += Rational(df, 86_400) if df != 0 + ajd += Rational(sf, 86_400_000_000_000) if sf != 0 + else + raise TypeError, "invalid size" + end + + initialize(ajd, of, sg) + end + + def self._load(str) + ary = Marshal.load(str) + raise TypeError, "expected an array" unless Array === ary + obj = allocate + obj.marshal_load(ary) + obj + end + +end + +# Class representing a date and time. +# +# See the documentation to the file date.rb for an overview. +# +# DateTime objects are immutable once created. +# +# == Other methods. +# +# The following methods are defined in Date, but declared private +# there. They are made public in DateTime. They are documented +# here. +# +# === hour() +# +# Get the hour-of-the-day of the time. This is given +# using the 24-hour clock, counting from midnight. The first +# hour after midnight is hour 0; the last hour of the day is +# hour 23. +# +# === min() +# +# Get the minute-of-the-hour of the time. +# +# === sec() +# +# Get the second-of-the-minute of the time. +# +# === sec_fraction() +# +# Get the fraction of a second of the time. This is returned as +# a +Rational+. +# +# === zone() +# +# Get the time zone as a String. This is representation of the +# time offset such as "+1000", not the true time-zone name. +# +# === offset() +# +# Get the time zone offset as a fraction of a day. This is returned +# as a +Rational+. +# +# === new_offset(of=0) +# +# Create a new DateTime object, identical to the current one, except +# with a new time zone offset of +of+. +of+ is the new offset from +# UTC as a fraction of a day. +# +class DateTime < Date + + # Create a new DateTime object corresponding to the specified + # Julian Day Number +jd+ and hour +h+, minute +min+, second +s+. + # + # The 24-hour clock is used. Negative values of +h+, +min+, and + # +sec+ are treating as counting backwards from the end of the + # next larger unit (e.g. a +min+ of -2 is treated as 58). No + # wraparound is performed. If an invalid time portion is specified, + # an ArgumentError is raised. + # + # +of+ is the offset from UTC as a fraction of a day (defaults to 0). + # +sg+ specifies the Day of Calendar Reform. + # + # All day/time values default to 0. + def self.jd(jd=0, h=0, min=0, s=0, of=0, sg=ITALY) + unless (jd = _valid_jd?(jd, sg)) && + (fr = _valid_time?(h, min, s)) + raise ArgumentError, 'invalid date' + end + if String === of + of = Rational(zone_to_diff(of) || 0, 86400) + end + new!(jd_to_ajd(jd, fr, of), of, sg) + end + + # Create a new DateTime object corresponding to the specified + # Ordinal Date and hour +h+, minute +min+, second +s+. + # + # The 24-hour clock is used. Negative values of +h+, +min+, and + # +sec+ are treating as counting backwards from the end of the + # next larger unit (e.g. a +min+ of -2 is treated as 58). No + # wraparound is performed. If an invalid time portion is specified, + # an ArgumentError is raised. + # + # +of+ is the offset from UTC as a fraction of a day (defaults to 0). + # +sg+ specifies the Day of Calendar Reform. + # + # +y+ defaults to -4712, and +d+ to 1; this is Julian Day Number + # day 0. The time values default to 0. + def self.ordinal(y=-4712, d=1, h=0, min=0, s=0, of=0, sg=ITALY) + unless (jd = _valid_ordinal?(y, d, sg)) && + (fr = _valid_time?(h, min, s)) + raise ArgumentError, 'invalid date' + end + if String === of + of = Rational(zone_to_diff(of) || 0, 86400) + end + new!(jd_to_ajd(jd, fr, of), of, sg) + end + + # Create a new DateTime object corresponding to the specified + # Civil Date and hour +h+, minute +min+, second +s+. + # + # The 24-hour clock is used. Negative values of +h+, +min+, and + # +sec+ are treating as counting backwards from the end of the + # next larger unit (e.g. a +min+ of -2 is treated as 58). No + # wraparound is performed. If an invalid time portion is specified, + # an ArgumentError is raised. + # + # +of+ is the offset from UTC as a fraction of a day (defaults to 0). + # +sg+ specifies the Day of Calendar Reform. + # + # +y+ defaults to -4712, +m+ to 1, and +d+ to 1; this is Julian Day + # Number day 0. The time values default to 0. + def self.civil(y=-4712, m=1, d=1, h=0, min=0, s=0, of=0, sg=ITALY) + if String === of + of = Rational(zone_to_diff(of) || 0, 86400) + end + + if Fixnum === y and Fixnum === m and Fixnum === d and + Fixnum === h and Fixnum === min and + (Fixnum === s or (Rational === s and 1000 % s.denominator == 0)) and + m > 0 and d > 0 and h >= 0 and h < 24 and min >= 0 and s >= 0 + y -= 1 if y <= 0 and sg > 0 # TODO + ms = 0 + if Rational === s + s, ms = (s.numerator * 1000 / s.denominator).divmod(1000) + end + begin + dt = JODA::DateTime.new(y, m, d, h, min, s, ms, chronology(sg, of)) + rescue JODA::IllegalFieldValueException, Java::JavaLang::IllegalArgumentException + raise ArgumentError, 'invalid date' + end + new!(dt, of, sg) + else + unless (jd = _valid_civil?(y, m, d, sg)) && + (fr = _valid_time?(h, min, s)) + raise ArgumentError, 'invalid date' + end + new!(jd_to_ajd(jd, fr, of), of, sg) + end + end + class << self; alias_method :new, :civil end + + # Create a new DateTime object corresponding to the specified + # Commercial Date and hour +h+, minute +min+, second +s+. + # + # The 24-hour clock is used. Negative values of +h+, +min+, and + # +sec+ are treating as counting backwards from the end of the + # next larger unit (e.g. a +min+ of -2 is treated as 58). No + # wraparound is performed. If an invalid time portion is specified, + # an ArgumentError is raised. + # + # +of+ is the offset from UTC as a fraction of a day (defaults to 0). + # +sg+ specifies the Day of Calendar Reform. + # + # +y+ defaults to -4712, +w+ to 1, and +d+ to 1; this is + # Julian Day Number day 0. + # The time values default to 0. + def self.commercial(y=-4712, w=1, d=1, h=0, min=0, s=0, of=0, sg=ITALY) + unless (jd = _valid_commercial?(y, w, d, sg)) && + (fr = _valid_time?(h, min, s)) + raise ArgumentError, 'invalid date' + end + if String === of + of = Rational(zone_to_diff(of) || 0, 86400) + end + new!(jd_to_ajd(jd, fr, of), of, sg) + end + + def self.weeknum(y=-4712, w=0, d=1, f=0, h=0, min=0, s=0, of=0, sg=ITALY) # :nodoc: + unless (jd = _valid_weeknum?(y, w, d, f, sg)) && + (fr = _valid_time?(h, min, s)) + raise ArgumentError, 'invalid date' + end + if String === of + of = Rational(zone_to_diff(of) || 0, 86400) + end + new!(jd_to_ajd(jd, fr, of), of, sg) + end + private_class_method :weeknum + + def self.nth_kday(y=-4712, m=1, n=1, k=1, h=0, min=0, s=0, of=0, sg=ITALY) # :nodoc: + unless (jd = _valid_nth_kday?(y, m, n, k, sg)) && + (fr = _valid_time?(h, min, s)) + raise ArgumentError, 'invalid date' + end + if String === of + of = Rational(zone_to_diff(of) || 0, 86400) + end + new!(jd_to_ajd(jd, fr, of), of, sg) + end + private_class_method :nth_kday + + def self.new_by_frags(elem, sg) # :nodoc: + elem = rewrite_frags(elem) + elem = complete_frags(elem) + unless (jd = valid_date_frags?(elem, sg)) && + (fr = valid_time_frags?(elem)) + raise ArgumentError, 'invalid date' + end + fr += (elem[:sec_fraction] || 0) / 86400 + of = Rational(elem[:offset] || 0, 86400) + if of < -1 || of > 1 + of = 0 + warn "invalid offset is ignored" if $VERBOSE + end + new!(jd_to_ajd(jd, fr, of), of, sg) + end + private_class_method :new_by_frags + + # Create a new DateTime object by parsing from a String + # according to a specified format. + # + # +str+ is a String holding a date-time representation. + # +fmt+ is the format that the date-time is in. See + # date/format.rb for details on supported formats. + # + # The default +str+ is '-4712-01-01T00:00:00+00:00', and the default + # +fmt+ is '%FT%T%z'. This gives midnight on Julian Day Number day 0. + # + # +sg+ specifies the Day of Calendar Reform. + # + # An ArgumentError will be raised if +str+ cannot be + # parsed. + def self.strptime(str='-4712-01-01T00:00:00+00:00', fmt='%FT%T%z', sg=ITALY) + elem = _strptime(str, fmt) + new_by_frags(elem, sg) + end + + # Create a new DateTime object by parsing from a String, + # without specifying the format. + # + # +str+ is a String holding a date-time representation. + # +comp+ specifies whether to interpret 2-digit years + # as 19XX (>= 69) or 20XX (< 69); the default is not to. + # The method will attempt to parse a date-time from the String + # using various heuristics; see #_parse in date/format.rb + # for more details. If parsing fails, an ArgumentError + # will be raised. + # + # The default +str+ is '-4712-01-01T00:00:00+00:00'; this is Julian + # Day Number day 0. + # + # +sg+ specifies the Day of Calendar Reform. + def self.parse(str='-4712-01-01T00:00:00+00:00', comp=true, sg=ITALY) + elem = _parse(str, comp) + new_by_frags(elem, sg) + end + + def self.iso8601(str='-4712-01-01T00:00:00+00:00', sg=ITALY) # :nodoc: + elem = _iso8601(str) + new_by_frags(elem, sg) + end + + def self.rfc3339(str='-4712-01-01T00:00:00+00:00', sg=ITALY) # :nodoc: + elem = _rfc3339(str) + new_by_frags(elem, sg) + end + + def self.xmlschema(str='-4712-01-01T00:00:00+00:00', sg=ITALY) # :nodoc: + elem = _xmlschema(str) + new_by_frags(elem, sg) + end + + def self.rfc2822(str='Mon, 1 Jan -4712 00:00:00 +0000', sg=ITALY) # :nodoc: + elem = _rfc2822(str) + new_by_frags(elem, sg) + end + class << self; alias_method :rfc822, :rfc2822 end + + def self.httpdate(str='Mon, 01 Jan -4712 00:00:00 GMT', sg=ITALY) # :nodoc: + elem = _httpdate(str) + new_by_frags(elem, sg) + end + + def self.jisx0301(str='-4712-01-01T00:00:00+00:00', sg=ITALY) # :nodoc: + elem = _jisx0301(str) + new_by_frags(elem, sg) + end + + public :hour, :min, :sec, :sec_fraction, :zone, :offset, :new_offset, + :minute, :second, :second_fraction + + def to_s # 4p + format('%.4d-%02d-%02dT%02d:%02d:%02d%s', + year, mon, mday, hour, min, sec, zone) + end + +end + +class Time + + def to_time + getlocal + end + + def to_date + Date.civil(year, mon, mday, Date::GREGORIAN) + end + + def to_datetime(sg = Date::ITALY, klass = DateTime) + of = Rational(utc_offset, 86400) + s = [sec, 59].min + ms, sub_millis = nsec.divmod(1_000_000) # expects ns precision for Time + sub_millis = Rational(sub_millis, 1_000_000) if sub_millis != 0 + dt = Date::JODA::DateTime.new(1000 * to_i + ms, Date.send(:chronology, sg, of)) + klass.new!(dt, of, sg, sub_millis) + end + +end + +class Date + + def to_time + Time.local(year, mon, mday) + end + + def to_date + self + end + + def to_datetime + DateTime.new!(@dt.withTimeAtStartOfDay, @of, @sg) + end + + # Create a new Date object representing today. + # + # +sg+ specifies the Day of Calendar Reform. + def self.today(sg=ITALY) + t = Time.now + civil(t.year, t.mon, t.mday, sg) + end + + # Create a new DateTime object representing the current time. + # + # +sg+ specifies the Day of Calendar Reform. + def self.now(sg=ITALY) + dtz = (ENV['TZ'] == DEFAULT_TZ) ? DEFAULT_DTZ : org.jruby::RubyTime.getLocalTimeZone(JRuby.runtime) + + if dtz == DEFAULT_DTZ and sg == ITALY + new!(JODA::DateTime.new(CHRONO_ITALY_DEFAULT_DTZ), nil, sg) + else + new!(JODA::DateTime.new(chronology(sg, dtz)), nil, sg) + end + end + private_class_method :now + +end + +class DateTime < Date + + def to_time + Time.new(year, mon, mday, hour, min, sec + sec_fraction, (@of * 86400).to_i).getlocal + end + + def to_date + Date.civil(year, mon, mday, @sg) + end + + def to_datetime + self + end + + private_class_method :today + public_class_method :now + +end +# \ No newline at end of file diff --git a/gxg/web/gxg_elements.rb b/gxg/web/gxg_elements.rb new file mode 100644 index 0000000..8f50606 --- /dev/null +++ b/gxg/web/gxg_elements.rb @@ -0,0 +1,1887 @@ +# +module GxG + # + class ByteArray + # make as close to array as possible with one exception: only works-with/accepts Integer between 0 and 255 as elements. + # Also: ByteArray is one-dimensional/flat --> cannot encapsulate other arrays, just Integers between 0 and 255. + # TODO: ByteArray : fill in missing methods compared to standard Array class. + # GxG::ByteArray::missing_methods(Array) + private + def array_to_ascii(the_array=nil) + result = "" + if the_array.is_a?(::Buffer::Array) + unsigned_array = `new Uint8Array(#{the_array.to_a})` + # array_size = `#{the_array}.byteLength` + # an_array = [] + # (0..(array_size - 1)).each do |index| + # an_array[(index)] = `#{the_array.to_a}[#{index}]`.to_i + # end + # unsigned_array = `new Uin8Array(#{an_array})` + # result = `new window.TextDecoder("ascii").decode(#{unsigned_array})`.to_s + result = `new window.TextDecoder('ascii').decode(#{unsigned_array})` + end + result + end + # + def array_to_string(the_array=nil) + result = "" + if the_array + result = `new window.TextDecoder().decode(new Uint8Array(#{the_array.to_a}))`.to_s + end + result + end + # + def string_to_array(the_string="") + # "Sample 测 Text" + array = `new window.TextEncoder().encode(#{the_string}.toString())` + array_size = `#{array}.byteLength` + buffer = Buffer.new(array_size).to_a + (0..(array_size - 1)).each do |index| + buffer[(index)] = `#{array}[(#{index})]` + end + buffer + end + # + def join_arrays(*args) + overall_size = 0 + indexer = 0 + args.each do |entry| + overall_size += entry.bytesize + end + new_array = Buffer.new(overall_size).to_a + args.each do |entry| + the_size = entry.bytesize + if the_size > 0 + (0..(the_size - 1)).each do |the_index| + new_array[(indexer)] = entry[(the_index)] + indexer += 1 + end + end + end + # + new_array + end + # + protected + # + def filter_data(the_data="") + # + error_message = ("ByteArray.new expects a String, a Integer or Float between 0 and 255, a nil ( counted as padvalue() ), a ByteArray, or an Array of any of those; you provided #{the_data.inspect}") + # + unless the_data.is_any?(::NilClass,::String,::Numeric, ::Array, ::GxG::ByteArray, Buffer::Array) + raise Exception.new(error_message) + end + # ???? + if the_data.is_a?(Buffer::Array) + the_data = array_to_ascii(the_data) + end + if the_data.is_a?(::String) + the_data = string_to_array(the_data) + end + if the_data.is_a?(::Numeric) + the_data = the_data.to_i + if (0..255).include?(the_data) + the_data = string_to_array(the_data.chr) + else + raise Exception.new(error_message) + end + end + if the_data.is_a?(::GxG::ByteArray) + the_data = string_to_array(array_to_string(the_data.data())) + end + if the_data.is_a?(::Array) + accumulator = [] + the_data.flatten.to_enum.each do |element| + accumulator << self.filter_data(element) + end + if accumulator.size > 0 + the_data = join_arrays(*accumulator) + else + the_data = Buffer.new(0).to_a + end + end + unless the_data + the_data = Buffer.new(1).to_a + the_data[0] = @pad_value.to_i + end + # + the_data + end + # + def filter_parameters(*args) + the_range = nil + if args[0].is_any?(::Numeric, ::Integer, ::Float, ::BigDecimal) + args[0] = args[0].to_i + if args[0] < 0 + args[0] = @data.bytesize + args[0] + end + the_range = ((args[0])..(args[0])) + if args[1].is_a?(Numeric) + args[1] = args[1].to_i + if args[1] < 1 + raise ArgumentError.new("Second parameter needs to be a Numeric greater than 0, you provided #{args[1].inspect}") + else + the_range = ((args[0])..(args[0] + (args[1] - 1))) + end + end + else + if args[0].is_a?(Range) + if args[0].first < 0 + args[1] = @data.bytesize + args[0].first + else + args[1] = args[0].first + end + if args[0].last < 0 + args[2] = @data.bytesize + args[0].last + else + args[2] = args[0].last + end + if args[1] <= args[2] + the_range = ((args[1])..(args[2])) + else + the_range = ((args[2])..(args[1])) + end + else + unless the_range + raise ArgumentError.new("First parameter needs to be a Numeric or a Range, you provided #{args[0].inspect}") + end + end + end + the_range + end + # + public + # + def self.try_convert(*args) + begin + GxG::ByteArray.new(1,[(args)]) + rescue Exception + nil + end + end + # + def self.[](*args) + GxG::ByteArray.new(*args) + end + # + def self.process(the_array=GxG::ByteArray.new,&block) + new_array = GxG::ByteArray.new + if block.respond_to?(:call) + if the_array.is_any?(::Array, ::Hash, ::GxG::ByteArray, ::Set, ::Struct) + new_array = the_array.process(&block) + else + raise ArgumentError, "you must pass an Array, a ByteArray, or a Hash" + end + end + new_array + end + # + def self.search(the_array=GxG::ByteArray.new,&block) + new_array = GxG::ByteArray.new + if block.respond_to?(:call) + if the_array.is_any?(::Array, ::Hash, ::GxG::ByteArray, ::Set, ::Struct) + new_array = the_array.search(&block) + else + raise ArgumentError, "you must pass an Array, a ByteArray, or a Hash" + end + end + new_array + end + # + def data() + @data + end + # + # instance methods: + def initialize(*args,&block) + # Integer(how_many), , , , + # --OR-- + # Integer(how_many), , , &block + @data = Buffer.new(0).to_a + # + memory_limits = {:process => (18446744073709551616..18446744073709551616)} + @max_size = nil + @pad_value = 0 + @outer = nil + # *should* create a 'self' to call upon. + super() + # + if args.size > 0 + if block.respond_to?(:call) + if args[0].is_a?(Numeric) + args[0] = args[0].to_i + if args[0] >= 0 + if args[0] > memory_limits[:process].first + args[0] = memory_limits[:process].first + end + if args[1].is_a?(Numeric) + self.maxsize = args[1].to_i + end + if args[2].is_a?(Numeric) + self.padvalue = args[1].to_i + end + if args[3].is_a?(Numeric) + self.outervalue = args[1].to_i + end + # + (0..(args[0] - 1)).to_enum.each do |index| + raw_data_array = [(block.call(index))].flatten + raw_data_array.to_enum.each do |raw_data| + # + raw_data = (self.filter_data(raw_data) || Buffer.new(0).to_a) + # + if raw_data.bytesize > 0 + if (args[0] * raw_data.bytesize) > memory_limits[:process].first + # Attempts to project whether or not maximum memory limits will be exceeded. + # LATER: ByteArray.new: Might get false positives on variable return data from block (re-think this) + raise NoMemoryError.new("This operation will exceed available per-process memory limits") + end + @data = join_arrays(@data,raw_data) + end + end + # + end + # + if @max_size + if @data.bytesize > @max_size + @max_size = @data.bytesize + end + end + # + end + end + else + # + if args[0].is_a?(Numeric) + how_many = args[0].to_i + if args[1] + raw_data_array = [(args[1])].flatten + else + raw_data_array = [(@pad_value.to_i.chr)] + end + if args[2].is_a?(Numeric) + self.maxsize = args[2].to_i + end + if args[3].is_a?(Numeric) + self.padvalue = args[3].to_i + end + if args[4].is_a?(Numeric) + self.outervalue = args[4].to_i + end + else + how_many = 1 + raw_data_array = [(args[0])].flatten + if args[1].is_a?(Numeric) + self.maxsize = args[1].to_i + end + if args[2].is_a?(Numeric) + self.padvalue = args[2].to_i + end + if args[3].is_a?(Numeric) + self.outervalue = args[3].to_i + end + end + # + new_data = [] + raw_data_array.to_enum.each do |raw_data| + new_data << (self.filter_data(raw_data) || Buffer.new(0).to_a) + end + new_data = join_arrays(*new_data) + if new_data.bytesize > 0 + if (how_many * new_data.bytesize) > memory_limits[:process].first + raise NoMemoryError.new("This operation will exceed available per-process memory limits") + end + how_many.times do + @data = join_arrays(@data,new_data) + end + end + # + if @max_size + if @data.bytesize > @max_size + @max_size = @data.bytesize + end + end + # + end + # + end + self + end + # + # def mime_type() + # # array_to_string(@data).mime_type() + # nil + # end + # + def process(&block) + result = self.clone + if block.respond_to?(:call) + result.each_with_index do |entry,index| + block.call(entry, index, self) + end + end + result + end + # + def process!(&block) + if block.respond_to?(:call) + self.replace(array_to_ascii(self.process(&block).data())) + end + self + end + # + def search(&block) + result = [] + if block.respond_to?(:call) + self.each_with_index do |entry,index| + raw_result = block.call(entry,index,self) + if raw_result + result << raw_result + end + end + end + result + end + # + def pattern_range(the_pattern="", initial_offset=0) + result = nil + the_pattern = array_to_ascii(GxG::ByteArray.new(the_pattern).data) + found_index = array_to_ascii(self.data).index(the_pattern,initial_offset) + if found_index + result = ((found_index)..(found_index + (the_pattern.size - 1))) + end + result + end + # + def pattern_ranges(the_pattern="") + result = [] + offset = 0 + if the_pattern.is_a?(GxG::ByteArray) + the_pattern = array_to_ascii(the_pattern.data) + end + last_found = self.pattern_range(the_pattern,offset) + while last_found != nil do + if last_found.is_a?(::Range) + result << last_found + if (last_found.last() + 1) <= @data.bytesize + offset = (last_found.last() + 1) + else + break + end + end + last_found = self.pattern_range(the_pattern,offset) + end + result + end + # + def padvalue() + @pad_value.clone + end + # + def padvalue=(*args) + if args.size > 0 + if args[0].is_a?(Float) + args[0] = args[0].to_i + end + if args[0].is_a?(Integer) + if (0..255).include?(args[0]) + @pad_value = args[0] + end + else + raise TypeError.new("Value type: expected Float or Integer. You provided #{args[0].class}") + end + end + @pad_value.clone + end + # + def match(regular_expression) + array_to_ascii(@data).match(regular_expression) + end + # + def maxsize() + @max_size.clone + end + # + def maxsize=(*args) + # Set @maxsize: Warning: setting @maxsize lower than the @data.bytesize will truncate the data + if args.size > 0 + if args[0] + if args[0].is_a?(Numeric) + args[0] = args[0].to_i + end + # + if args[0].is_a?(Integer) + if args[0] > 18446744073709551616 + raise NoMemoryError.new("Setting maxsize this high exceeds per-process memory limits") + end + if args[0] > @max_size.to_i + @max_size = args[0] + else + if args[0] < @max_size.to_i + # truncate data if @max_size lowered below @data.bytesize + @max_size = args[0] + if @data.bytesize > args[0] + @data.replace(@data[0..(args[0] - 1)]) + end + end + end + else + raise ArgumentError.new("Expected NilClass or Numeric class. You provided #{args[0].class}") + end + else + @max_size = nil + end + else + @max_size = nil + end + @max_size.clone + end + # + def outervalue() + @outer.clone + end + # + def outervalue=(*args) + if args.size > 0 + if args[0] + if args[0].is_a?(Float) + args[0] = args[0].to_i + end + if args[0].is_a?(Integer) + if (0..255).include?(args[0]) + @outer = args[0] + end + else + raise TypeError.new("Expected a NilClass, a Float, or a Integer. You provided #{args[0].class}") + end + else + @outer = nil + end + else + @outer = nil + end + self.outervalue() + end + # + def dclone() + result = GxG::ByteArray.new(array_to_ascii(@data)) + result.padvalue = (self.padvalue()) + result.maxsize = (self.maxsize()) + result.outervalue = (self.outervalue()) + result + end + # + def initialize_clone() + result = self.dclone + if self.frozen? + result.freeze + end + result + end + # + def initialize_dup() + # ByteArry is one-dimensional anyhow + self.dclone + end + alias :dup :initialize_dup + alias :initialize_copy :initialize_dup + def clone() + initialize_dup() + end + # + def [](indexer=0, length=nil) + self.slice(indexer,length) + end + # + def []=(indexer=0, length=nil, value=nil) + # + if length + unless value + value = length + length = nil + end + end + value = self.filter_data(value) + error = nil + result = nil + # + if value + if value.bytesize > 0 + # + the_range = self.filter_parameters(indexer,length) + # + unless the_range + if length + error = ArgumentError.new("#{indexer.inspect},#{length.inspect} could not be fashioned into a valid Indexer") + else + error = ArgumentError.new("#{indexer.inspect} could not be fashioned into a valid Indexer") + end + end + if error + raise error + end + # check overall compliance with maxsize + if @max_size + if (the_range.count > @max_size || the_range.first > (@max_size - 1) || the_range.last > (@max_size - 1)) + error = IndexError.new("Index exceeds #{@max_size} limit. You might need to adjust maxsize.") + end + if (the_range.first + (value.bytesize - 1)) > (@max_size - 1) + error = RangeError.new("Data size at specified Index exceeds #{@max_size} limit. You might need to adjust maxsize.") + end + # + if indexer.is_a?(Integer) + if length + # replace operation + if (the_range.first + (the_range.count - 1)) > (@max_size - 1) + error = RangeError.new("Range exceeds #{@max_size} limit. You might need to adjust maxsize.") + else + if the_range.count < value.bytesize and (the_range.first + (the_range.count - 1) + ((value.bytesize - 1) - (the_range.count - 1))) > (@max_size - 1) + error = RangeError.new("Value size exceeds #{@max_size} limit. You might need to adjust maxsize.") + end + end + else + # insert operation + if (@data.size - 1 + value.bytesize) > @max_size + error = RangeError.new("Operation exceeds #{@max_size} limit. You might need to adjust maxsize.") + end + if (the_range.first + (value.bytesize - 1)) > (@max_size - 1) + error = RangeError.new("Value size exceeds #{@max_size} limit. You might need to adjust maxsize.") + end + end + else + if the_range.count > @max_size + error = RangeError.new("Range exceeds #{@max_size} limit. You might need to adjust maxsize.") + else + if the_range.count < value.bytesize and (the_range.first + (the_range.count - 1) + ((value.bytesize - 1) - (the_range.count - 1))) > (@max_size - 1) + error = RangeError.new("Value size exceeds #{@max_size} limit. You might need to adjust maxsize.") + end + end + end + if error + raise error + end + end + # + if the_range.first > (@data.bytesize - 1) + (the_range.first - (@data.bytesize - 1)).times do + self << @pad_value.chr + end + end + # + if length + @data[(the_range.first),(the_range.count)] = value + else + if indexer.is_a?(Numeric) + if value.bytesize == 1 + @data[(the_range.first)] = value[0] + else + start_index = the_range.first + (0..(value.bytesize - 1)).each do |the_indexer| + if the_indexer > (@data.bytesize - 1) + self << value[(the_indexer)] + else + @data[(start_index + the_indexer)] = value[(the_indexer)] + end + end + end + else + @data[(the_range)] = value + end + end + # + end + result = GxG::ByteArray.new(value) + else + # err - invalid value + end + result + end + # + def |(*args) + scrap = GxG::ByteArray::try_convert(args) + result = self.clone + if scrap + scrap.each do |byte| + unless result.include?(byte) + result << byte + end + end + end + result + end + # + def &(*args) + result = GxG::ByteArray.new + if args.size > 0 + other = GxG::ByteArray::try_convert(args) + if other + self.each do |the_byte| + if other.include?(the_byte) + result << the_byte + end + end + end + end + result + end + # + def *(*args) + result = GxG::ByteArray.new + if self.size > 0 + if args.size > 0 + if args[0].is_a?(::String) + result = self.join(args[0]) + else + if args[0].is_a?(Numeric) + args[0] = args[0].to_i + if (args[0] * self.size) <= 18446744073709551616 + args[0].times do + result << self.clone + end + else + raise NoMemoryError.new("This operation will exceed per-process memory limits") + end + else + raise ArgumentError.new("a String or Numeric is required, you provied #{args[0].inspect}") + end + end + else + raise TypeError.new("Can't convert nil to ByteArray") + end + end + result + end + # + def +(*args) + result = GxG::ByteArray.new + if args.size > 0 + copy = self.clone + other = GxG::ByteArray::try_convert(args) + if other + if other.size > 0 + result = GxG::ByteArray.new(1,[(array_to_ascii(copy.data)),(array_to_ascii(other.data))]) + end + end + else + raise TypeError.new("Can't convert nil to ByteArray") + end + result + end + # + def -(*args) + result = GxG::ByteArray.new + if args.size > 0 + scrap = GxG::ByteArray::try_convert(args) + if scrap + self.each do |the_byte| + unless scrap.include?(the_byte) + result << the_byte + end + end + end + end + result + end + # + def <<(*args) + if args.size > 0 + value = GxG::ByteArray::try_convert(args) + if value + if @max_size + if (@data.bytesize + value.size) > @max_size + raise RangeError.new("Operation exceeds #{@max_size} limit. You might need to adjust maxsize.") + end + end + @data = join_arrays(@data,value.data()) + end + end + self + end + # + def <=>(*args) + result = 1 + if args.size > 0 + if args[0].is_a?(::GxG::ByteArray) + result = (self.data() <=> args[0].data()) + else + # Allows sorting against std. Array or String that *would* sort normal if it were a ByteArray (byte comparison) + other = GxG::ByteArray::try_convert(args) + if other + result = (self.data() <=> other.data()) + end + end + end + result + end + # + def ==(*args) + result = false + if args.size > 0 + if args[0].is_a?(::GxG::ByteArray) + result = (self.data() == args[0].data()) + else + # kinda cool, but might be a bridge too far for strict equivalence. + # other = GxG::ByteArray::try_convert(args) + # if other + # result = (self.data() == other.data()) + #end + end + end + result + end + # + def at(*args) + if args[0].is_a?(Float) + args[0] = args[0].to_i + end + if args[0].is_a?(Integer) + self[(args[0])] + else + raise TypeError.new("Expected Integer or Float. You provided #{args[0].class}") + end + end + # + def clear() + @data = Buffer.new(0).to_a + self + end + # + def collect(&block) + result = GxG::ByteArray.new + if block.respond_to?(:call) + self.each do |the_byte| + result << array_to_ascii(self.filter_data(block.call(the_byte))) + end + else + result = self.to_enum(:collect) + end + result + end + # + def collect!(&block) + if block.respond_to?(:call) + @data = self.collect(&block).data() + self + else + self.to_enum(:collect!) + end + end + # + def combination(*args,&block) + # TODO: port c code to ruby (true-enumeration-as-bytearray support) + enumerator = self.to_a.to_enum(:combination,*args) + if block.respond_to?(:call) + enumerator.each do |the_combination| + block.call(GxG::ByteArray.new(the_combination)) + end + self + else + # Note: this form does not put bytearrays of each combination into the block, just arrays (please fix) + enumerator + end + end + # + def repeated_combination(*args,&block) + # TODO: port c code to ruby (true-enumeration-as-bytearray support) + enumerator = self.to_a.to_enum(:repeated_combination,*args) + if block.respond_to?(:call) + enumerator.each do |the_combination| + block.call(GxG::ByteArray.new(the_combination)) + end + self + else + enumerator + end + end + # + def compact() + self + end + # + def concat(*args) + result = self + if args.size > 0 + other = GxG::ByteArray::try_convert(args) + if other + @data = join_arrays(@data,other.data) + else + raise Exception.new("Could not convert #{args.inspect} to ByteArray") + end + end + result + end + # + def count(match_value=nil,&block) + result = 0 + if match_value.is_a?(Numeric) + self.each do |the_byte| + if the_byte == match_value.to_i + result += 1 + end + end + else + if block.respond_to?(:call) + self.each do |the_byte| + if block.call(the_byte) == true + result += 1 + end + end + else + result = @data.bytesize + end + end + result + end + # + def cycle(iterations=nil,&block) + if @data.bytesize > 0 + unless iterations.to_i < 0 + if block.respond_to?(:call) + if iterations + iterations.to_i.times do + self.each do |the_byte| + block.call(the_byte) + end + end + nil + else + # I really don't like this + while true do + self.each do |the_byte| + block.call(the_byte) + end + end + end + else + self.to_a.to_enum(:cycle,iterations) + end + end + end + end + # + def delete(*args,&block) + result = nil + if args[0].is_a?(Numeric) + comparitor = self.filter_data(args[0].to_i) + if comparitor.bytesize > 1 + comparitor = comparitor[0] + end + scrap = @data.delete(comparitor) + if scrap == @data + if block.respond_to?(:call) + result = block.call() + end + else + @data.replace(scrap) + result = args[0] + end + end + result + end + # + def delete_at(*args) + result = nil + if args.size > 0 && self.size > 0 + if args[0].is_a?(::Numeric) + index = args[0].to_i + if index < self.size + result = @data[(index)] + if index == 0 + ranges = [(1..(@data.bytesize - 1))] + else + ranges = [(0..(index - 1))] + unless index == (@data.bytesize - 1) + ranges << ((index + 1)..(@data.bytesize - 1)) + end + end + new_size = 0 + ranges.each do |the_range| + new_size += the_range.size + end + buffer = Buffer.new(new_size).to_a + indexer = 0 + ranges.each do |the_range| + the_range.each do |the_index| + buffer[(indexer)] = @data[(the_index)] + indexer += 1 + end + end + @data = buffer + end + end + # + end + result + end + # + def delete_if(&block) + self.reject!(&block) + end + # + def drop(length) + self.slice((length.to_i - 1)..-1) + end + # + def drop_while(&block) + accumulator = GxG::ByteArray.new + if block.respond_to?(:call) + self.each do |the_byte| + result = block.call(the_byte) + unless result + accumulator << the_byte + end + end + accumulator + else + # TODO: 5xMem efficiency fix for various engine run conditions + self.to_a.to_enum(:drop_while) + end + end + # + # TODO: GxG::ByteArray.each_index/each_with_index: improve memory efficiency and speed significantly (!) + # + def each(&block) + if block.respond_to?(:call) + if @data.bytesize > 0 + # array_to_ascii(@data).each {|the_char| block.call(the_char.ord)} + @data.to_a.each {|the_char| block.call(the_char.ord)} + end + self + else + self.to_enum(:each) + end + end + # + def each_index(&block) + if block.respond_to?(:call) + if @data.bytesize > 0 + (0..(@data.bytesize - 1)).each {|index| block.call(index)} + end + self + else + self.to_enum(:each_index) + end + end + # + def each_with_index(offset=0,&block) + if block.respond_to?(:call) + if @data.bytesize > 0 + # bulky, but required to support self.reject!, etc : + @data.to_a.with_index(offset).each {|value,index| block.call(value.ord,index)} + # array_to_ascii(@data).with_index(offset).each {|value,index| block.call(value.ord,index)} + #old : (0..(@data.bytesize - 1)).to_enum.each {|index| block.call(@data[(index)].ord,index)} + end + self + else + self.to_enum(:each_with_index,offset) + end + end + # + def empty?() + @data.bytesize == 0 + end + # + def eql?(*args) + if args.size > 0 + self == args[0] + else + false + end + end + # + def fetch(*args,&block) + #fetch(index) → obj + #fetch(index, default ) → obj + #fetch(index) {|index| block } → obj + # + #Tries to return the element at position index. If the index lies outside the array, the first form throws an IndexError exception, + # the second form returns default, and the third form returns the value of invoking the block, passing in the index. + # Negative values of index count from the end of the array. + # LATER: ByteArray.fetch: check how to incorporate 'padded-edges' feature + if args.size > 0 + if args[0].is_a?(Numeric) + args[0] = self.filter_parameters(args[0].to_i).first + if args[0] >= self.size + if args[1] + if block.respond_to?(:call) + block.call(args[0]) + else + args[1] + end + else + raise IndexError.new("Index specified lies outside the ByteArray") + end + else + self[(args[0])] + end + end + end + end + # + def fill(*args,&block) + #fill(obj) → ary click to toggle source + #fill(obj, start [, length]) → ary + #fill(obj, range ) → ary + #fill {|index| block } → ary + #fill(start [, length] ) {|index| block } → ary + #fill(range) {|index| block } → ary + # + #The first three forms set the selected elements of self (which may be the entire array) to obj. A start of nil is equivalent to zero. + #A length of nil is equivalent to self.length. The last three forms fill the array with the value of the block. + #The block is passed the absolute index of each element to be filled. Negative values of start count from the end of the array. + #a = [ "a", "b", "c", "d" ] + #a.fill("x") #=> ["x", "x", "x", "x"] + #a.fill("z", 2, 2) #=> ["x", "x", "z", "z"] + #a.fill("y", 0..1) #=> ["y", "y", "z", "z"] + #a.fill {|i| i*i} #=> [0, 1, 4, 9] + #a.fill(-2) {|i| i*i*i} #=> [0, 1, 8, 27] + # Note: if ByteArray is empty, it will be filled to @max_size, otherwise only the used portion or specified range. + result = nil + result_scrap = GxG::ByteArray.new + # + if block.respond_to?(:call) + if args.size > 0 + if args[0].is_a?(Numeric) + args[0] = args[0].to_i + if args[0] < 0 + if args[1].is_a?(Numeric) + if args[1].to_i > 0 + args = [((@data.bytesize + args[0])..(@data.bytesize + args[0] + (args[1].to_i - 1)))] + else + raise ArgumentError.new("length specified should be a positive Integer for Float") + end + else + args = [((@data.bytesize + args[0])..(@data.bytesize - 1))] + end + end + end + fillrange = self.filter_parameters(*args) + else + fillrange = (0..((@max_size || @data.bytesize) - 1)) + end + (0..(@data.bytesize - 1)).each do |index| + if fillrange.include?(index) + raw_value = self.filter_data(block.call(index)) + if raw_value.size > 0 + # Allow only single byte results to merge into fill + # since the concept is 'per-item' which in this case is a single byte only. + result_scrap << raw_value[0] + else + result_scrap << @pad_value + end + else + result_scrap << @data[(index)] + end + end + else + if args.size > 0 + value = nil + case args.size + when 1 + fillrange = (0..((@max_size || @data.bytesize) - 1)) + value = self.filter_data(args[0]) + when 2 + fillrange = self.filter_parameters(args[1]) + value = self.filter_data(args[0]) + when 3 + fillrange = self.filter_parameters(args[1],args[2]) + value = self.filter_data(args[0]) + else + fillrange = self.filter_parameters(args[1],args[2]) + value = self.filter_data(args[0]) + end + value = (value || @pad_value) + if value.is_a?(::Array) + value = (value[0] || @pad_value) + end + (0..(@data.bytesize - 1)).to_enum.each do |index| + if fillrange.include?(index) + result_scrap << value + else + result_scrap << @data[(index)] + end + end + else + raise ArgumentError.new("wrong number of arguments (0 for 1..3)") + end + end + if result_scrap.size > 0 + @data = result_scrap.data + end + self + end + # + def first(*args) + if args.size > 0 + if args[0].is_a?(Float) + args[0] = args[0].to_i + end + if args[0].is_a?(Integer) + if args[0] > 0 + if @max_size + unless args[0] <= @max_size + raise RangeError.new("#{args[0]} is out of range (1 - #{@max_size}).") + end + end + self[(0..(args[0] - 1))] + end + else + raise TypeError.new("Expected Integer. You provided #{args[0].class}") + end + else + self[0] + end + end + # + def flatten() + # ByteArray is always flat anyhow + self.dclone + end + # + def flatten!() + # ByteArray is always flat anyhow + self + end + # + def freeze() + @data.freeze + @max_size.freeze + @pad_value.freeze + @outer.freeze + super() + end + # + def hash() + @data.hash + end + # + def include?(*args) + if args.size > 0 + if args[0].is_a?(Float) + args[0] = args[0].to_i + end + if args[0].is_a?(Integer) + if (0..255).include?(args[0]) + @data.include?(args[0].chr) + else + raise RangeError.new("Value out of range: range is 0 to 255. You provided #{args[0]}") + end + else + raise TypeError.new("Expected Integer or Float. You provided #{args[0].class}") + end + else + raise ArgumentError.new("Wrong number of arguments (#{args.size} for 1).") + end + end + # + def find_index(*args, &block) + result = nil + if block.respond_to?(:call) + found_at = 0 + self.each do |the_byte| + if block.call(the_byte) == true + result = found_at + break + end + found_at += 1 + end + else + if args.size > 0 + if args[0].is_a?(Float) + args[0] = args[0].to_i + end + if args[0].is_a?(Integer) + if (0..255).include?(args[0]) + if args[1] + # offset specified (addtional option vs standard Array) + result = array_to_ascii(@data).index(args[0].chr,args[1]) + else + result = array_to_ascii(@data).index(args[0].chr) + end + end + else + raise TypeError.new("Expected Integer or Float. You provided #{args[0].class}") + end + else + result = @data.to_enum(:bytes) + end + end + result + end + # + def insert(*args) + if args.size > 1 + if args[0].is_a?(Float) + args[0] = args[0].to_i + end + if args[0].is_a?(Integer) + value = self.filter_data(args[(1..(args.size - 1))]) + if @max_size + unless (@data.bytesize + value.bytesize) <= @max_size + raise RangeError.new("Operation exceeds #{@max_size} limit. You need to adjust maxsize.") + end + end + begin + index = args[0] + accumulator = [] + if index >= 0 && index < @data.bytesize + if index == 0 + accumulator << value + accumulator << @data + else + if index == (@data.bytesize - 1) + accumulator << (self.slice((0..(index - 1))).data) + accumulator << value + tail = Buffer.new(1).to_a + tail[0] = @data[(@data.bytesize - 1)] + accumulator << tail + else + accumulator << (self.slice((0..(index - 1))).data) + accumulator << value + accumulator << (self.slice(((index)..(@data.bytesize - 1))).data) + end + end + else + # range err + end + if accumulator.size > 0 + @data = join_arrays(*accumulator) + end + rescue Exception => error + raise error.exception(error.message.gsub("string",(self.class.to_s))) + end + else + raise TypeError.new("Expected Integer or Float. You provided #{args[0].class}") + end + else + raise ArgumentError.new("Wrong number of arguments (#{args.size} for 2).") + end + self + end + # + def inspect() + result = "[" + if @data.bytesize > 0 + (0..(@data.bytesize - 1)).each do |index| + if result.size > 1 + result = (result + ", ") + end + the_byte = @data[(index)] + if the_byte > 15 + result = (result + "0x#{the_byte.to_s(16).upcase}") + else + result = (result + "0x0#{the_byte.to_s(16).upcase}") + end + end + end + result = (result + "]") + result + end + # + def join(*args) + array_to_ascii(@data).split.join(*args) + end + # + def last(*args) + if args.size > 0 + if args[0].is_a?(Float) + args[0] = args[0].to_i + end + if args[0].is_a?(Integer) + if args[0] > 0 + the_range = ((@data.bytesize - 1 - (args[0] - 1))..(@data.bytesize - 1)) + if @max_size + unless args[0] <= @max_size + raise RangeError.new("#{args[0]} is out of range (1 - #{@max_size}).") + end + end + self[(the_range)] + else + raise RangeError.new("#{args[0]} is out of range (1 - #{@max_size}).") + end + else + raise TypeError.new("Expected Integer. You provided #{args[0].class}") + end + else + self[(@data.bytesize - 1)] + end + end + # + def pack(*args) + result = "" + if args.size >= 1 + args[0] = (args[0] || "") + if args[0].is_a?(String) + if args[1].is_a?(Encoding) + # Extra optional encoding vs standard Array + result.force_encoding(args[1]) + end + # LATER: ByteArray.pack: a more memory-efficient method is needed here long term. For now: leverage Array.pack. + # Each byte will take up 4 to 8 bytes depending just to get packed into a string ... gotta fix this. + result << self.to_a.pack(args[0]) + else + raise TypeError.new("Expected String. You provided #{args[0].class}") + end + else + raise ArgumentError.new("Wrong number of arguments (#{args.size} for 1+).") + end + result + end + # + def permutation(*args,&block) + # TODO: port c code to ruby (true-enumeration-as-bytearray support) + enumerator = array_to_ascii(@data).split.to_enum(:permutation,*args) + if block.respond_to?(:call) + enumerator.each do |the_permutation| + block.call(GxG::ByteArray.new(the_permutation)) + end + self + else + enumerator + end + end + # + def repeated_permutation(*args,&block) + # TODO: port c code to ruby (true-enumeration-as-bytearray support) + enumerator = array_to_ascii(@data).split.to_enum(:repeated_permutation,*args) + if block.respond_to?(:call) + enumerator.each do |the_permutation| + block.call(GxG::ByteArray.new(the_permutation)) + end + self + else + enumerator + end + end + # + def pop(how_many=nil) + # LIFO style stack operation + result = nil + if how_many.is_a?(Numeric) + unless how_many.to_i > 0 + raise ArgumentError.new("#{how_many} needs to be a positive Numeric") + end + result = ::GxG::ByteArray.new + how_many.times do + result << self.delete_at(-1) + end + else + result = self.delete_at(-1) + end + result + end + # + def product(*others,&block) + if @data.bytesize > 0 + others = GxG::ByteArray.new(others) + if block.respond_to?(:call) + self.each do |the_byte| + others.each do |other_byte| + block.call(GxG::ByteArray.new([(the_byte),(other_byte)])) + end + end + self + else + accumulator = GxG::ByteArray.new + self.each do |the_byte| + others.each do |other_byte| + accumulator << the_byte + accumulator << other_byte + end + end + accumulator + end + else + self + end + end + # + def push(*args) + # LIFO style stack operation + self << GxG::ByteArray.new(args) + end + # + def rassoc(key=nil) + nil + end + # + def reject(&block) + result = nil + if block.respond_to?(:call) + result = GxG::ByteArray.new + result.padvalue = (@pad_value) + if @max_size + result.maxsize = (@max_size) + end + self.each_with_index do |value, index| + unless block.call(value) == true + result << value + end + end + else + result = self.to_a.to_enum(:reject) + end + result + end + # + def reject!(&block) + result = nil + if block.respond_to?(:call) + value = self.reject(&block) + if value + @data = value.data + end + result = self + else + result = self.to_a.to_enum(:reject!) + end + result + end + # + def replace(*args) + # replace data payload while leaving max_size and pad_value untouched, fitting replacement data into in-place constraints if any. + if args.size > 0 + replacement = GxG::ByteArray::try_convert(args) + if replacement + if @max_size + if replacement.data.bytesize <= @max_size + @data = replacement.data + else + @data = replacement[(0..(@max_size - 1))].data + end + else + @data = replacement.data + end + else + raise Exception.new("Could not convert #{args[0].class} to ByteArray") + end + end + self + end + # + def reverse() + result = GxG::ByteArray.new(array_to_ascii(@data).reverse) + result.padvalue = (@pad_value.clone) + if @max_size + result.maxsize = (@max_size.clone) + end + result + end + # + def reverse!() + @data = self.reverse().data + self + end + # + def reverse_each(&block) + result = nil + if block.respond_to?(:call) + # use duplicate of self, do not preserve frozen state. + result = self.clone + result.clear + self.reverse().each {|the_byte| result << block.call(the_byte)} + else + # ballons mem footprint x2: *ouch* - don't know a better way yet. + result = self.reverse().to_enum(:each) + end + result + end + # + def rotate(vector=1) + unless vector.is_a?(Numeric) + raise ArgumentError.new("Expected a Numeric, you provided #{vector.class}") + end + # from array.c : rotate_count + if vector < 0 + vector = (self.size - (~vector % self.size) - 1).to_i + else + vector = (vector % self.size).to_i + end + scrap = [] + unless vector == 0 + the_range = self.filter_parameters(((vector)..-1)) + scrap << self[(the_range)] + if the_range.first > 0 + scrap << self[(0..(the_range.first - 1))] + end + end + if scrap.size > 0 + result = GxG::ByteArray.new(scrap) + else + result = self.clone + end + result + end + # + def rotate!(vector=1) + if vector.is_a?(Numeric) + if vector.to_i != 0 + @data = self.rotate(vector).data() + end + else + raise ArgumentError.new("Expected a Numeric, you provided #{vector.class}") + end + self + end + # + def sample(*args) + GxG::ByteArray.new(self.to_a.sample(*args)) + end + # + def select(&block) + result = GxG::ByteArray.new + result.maxsize = (@max_size) + result.padvalue = (@pad_value) + result.outervalue = (@outer) + if block.respond_to?(:call) + self.each do |value| + unless block.call(value) == false + result << value + end + end + else + result = self.to_enum(:select) + end + result + end + # + def select!(&block) + if block.respond_to?(:call) + state = @data.hash + @data = self.select(&block).data + if state == @data.hash + nil + else + self + end + else + self.to_enum(:select!) + end + end + # + def shift() + # FIFO style pop + self.delete_at(0) + end + # + def shuffle(*args) + GxG::ByteArray.new(self.to_a.shuffle(*args)) + end + # + def shuffle!(*args) + self.replace(self.shuffle(*args)) + self + end + # + def size() + @data.bytesize + end + # + def slice(indexer=0, length=nil) + the_range = self.filter_parameters(indexer,length) + unless the_range + if length + raise ArgumentError.new("#{indexer.inspect},#{length.inspect} could not be fashioned into a valid Indexer") + else + raise ArgumentError.new("#{indexer.inspect} could not be fashioned into a valid Indexer") + end + end + # check overall compliance with maxsize + if @max_size + if (the_range.count > @max_size || the_range.first > (@max_size -1) || the_range.last > (@max_size - 1)) + raise IndexError.new("Index exceeds #{@max_size} limit. You need to adjust maxsize.") + end + end + # + result = nil + # + if the_range + if the_range.first == the_range.last + result = @data[(the_range.first)] + else + buffer = Buffer.new(the_range.size).to_a + offsetter = 0 + the_range.each do |index| + buffer[(offsetter)] = @data[(index)] + offsetter += 1 + end + result = ::GxG::ByteArray.new(array_to_ascii(buffer)) + end + end + # + result + end + # + def slice!(indexer=0, length=nil) + replacement = self.slice(indexer,length) + if replacement.is_a?(Numeric) + self.replace(replacement.chr) + else + self.replace(replacement) + end + if self.size == 1 + self.slice(0) + else + self + end + end + # + # BETA: ByteArray : see about self.to_a replacement (memory efficiency) in sort methods. + # + def sort(&block) + if block.respond_to?(:call) + GxG::ByteArray.new(self.to_a.sort {|a,b| block.call(a,b)}) + else + GxG::ByteArray.new(self.to_a.sort) + end + end + # + def sort!(&block) + if block.respond_to?(:call) + self.replace(self.sort(&block).data()) + end + self + end + # + def sort_by!(&block) + if block.respond_to?(:call) + self.replace(GxG::ByteArray.new(self.to_a.sort_by! {|a,b| block.call(a,b)}).data()) + self + else + self.to_enum(:sort_by!) + end + end + # + def take(length) + self.slice(0..(length.to_i - 1)) + end + # + def take_while(&block) + if block.respond_to?(:call) + accumulator = GxG::ByteArray.new + self.each do |the_byte| + result = block.call(the_byte) + if result + accumulator << the_byte + else + break + end + end + accumulator + else + self.to_enum(:take_while) + end + end + # + def to_a() + result = [] + if @data.bytesize > 0 + (0..(@data.bytesize - 1)).each {|index| result << @data[(index)]} + end + result + end + # + def to_s() + array_to_ascii(@data) + end + # + def to_ascii() + array_to_ascii(@data) + end + # + def to_json() + ("binary:" + self.to_ascii.encode64) + end + # + def transpose() + self.clone + end + # + def uniq(&block) + if block.respond_to?(:call) + GxG::ByteArray.new(self.to_a.uniq(&block)) + else + self + end + end + # + def uniq!(&block) + scrap = self.uniq(&block) + if scrap.size == self.size + nil + else + self.replace(scrap) + self + end + end + # + def unshift(*args) + # FIFO style push + if args.size > 0 + self.insert(0,args) + end + self + end + # + def values_at(*args) + result = GxG::ByteArray.new + if args.size > 0 + args.to_enum.each do |selector| + if selector.is_a?(Numeric) or selector.is_a?(Range) + unless selector.is_a?(Range) + selector = selector.to_i + end + result << self[(selector)] + else + raise TypeError.new("Expected Integer, Float or Range. You provided #{selector.class}") + end + end + end + result + end + # + def zip(*args,&block) + # Converts any valid argument values to array, then merges elements of self with corresponding elements from each argument + # This generates a sequence of self.size n-element ByteArrays as step buffers, where n is one more that the count of arguments + # * Then flattened output as ByteArray * + # If the size of any argument is less than enumObj.size, @pad_value is supplied + # If a block given, it is invoked for each output array. + # If block returns an array or ByteArray, it replaces the provided buffer and is appended to output. + # + result = nil + if args.size > 0 + result = GxG::ByteArray.new + args.to_enum(:each_index).each do |index| + unless args[(index)].is_a?(GxG::ByteArray) + args[(index)] = GxG::ByteArray.new(args[(index)]) + end + end + self.each_with_index do |byte,index| + # + buffer = GxG::ByteArray.new + buffer << byte + args.to_enum(:each_index).each do |arg_index| + if index >= args[(arg_index)].size + buffer << @pad_value + else + buffer << args[(arg_index)].data()[(index)] + end + end + # + if block.respond_to?(:call) + # I want to differ from stock array.zip behavior: + # I want to have it possible to return the processed buffer back - + # to a result ByteArray if result of yield = Array/ByteArray. + raw_new = block.call(buffer) + if raw_new.is_a?(::Array) + raw_new = GxG::ByteArray.new(raw_new) + end + if raw_new.is_a?(::GxG::ByteArray) + buffer = raw_new + end + end + result << buffer + end + # + else + result = self.clone + end + result + end + + # Method Aliases + alias :assoc :rassoc + alias :map :collect + alias :map! :collect! + alias :compact! :compact + alias :indices :values_at + alias :indexes :values_at + alias :rindex :find_index + alias :index :find_index + alias :length :size + alias :nitems :size + alias :to_ary :to_a + alias :keep_if :select! + + # Byte Operations + + # GxG Convenience Methods: + def paths_to(object=nil,base_path="") + # new idea here: + search_results = [] + # TODO: if paths can use ranges, expand to use string or byte or int byte or byte-pattern matching to define returned valid path for get/set. + if object.is_a?(Numeric) + self.each_with_index do |entry,index| + if entry == object.to_i + search_results << (base_path + "/" + index.to_s) + end + end + end + search_results + end + # + def get_at_path(the_path="/") + # TODO: question: would it serve much to allow ranges as structure-path specifier? What would that involve? + # /^(?:[0-9])*[0-9](?:[0-9])*$/ = nil if an alpha present there, else 0 only numeric + # Attribution : http://stackoverflow.com/questions/1240674/regex-match-a-string-containing-numbers-and-letters-but-not-a-string-of-just-nu + # + # if ":" detected do: (str.gsub("%2f","/").to_sym) as key else (str.gsub("%2f","/")) + result = nil + if the_path == "/" + result = self + else + object_stack = [(self)] + path_stack = the_path.split("/") + path_stack.to_enum.each do |path_element| + element = nil + if path_element.size > 0 + if (path_element =~ /^(?:[0-9])*[0-9](?:[0-9])*$/) == 0 + element = path_element.to_i + end + end + if element + result = object_stack.first[(element)] + if result.is_a?(NilClass) + break + else + object_stack.unshift(result) + end + else + # ignore double slashes? '//' + # break + end + end + end + result + end + # + def set_at_path(the_path="/",the_value=nil) + result = nil + if the_path != "/" + container = self + if container + raw_selector = ::File::basename(the_path) + selector = nil + if raw_selector.size > 0 + if (raw_selector =~ /^(?:[0-9])*[0-9](?:[0-9])*$/) == 0 + selector = raw_selector.to_i + end + end + if selector + container[(selector)] = the_value + if container[(selector)] == the_value + result = true + end + else + # ignore double slashes? '//' + # break + end + # + end + end + result + end + end + # End ByteArray +end +# \ No newline at end of file diff --git a/gxg/web/gxg_events.rb b/gxg/web/gxg_events.rb new file mode 100644 index 0000000..8c963ce --- /dev/null +++ b/gxg/web/gxg_events.rb @@ -0,0 +1,567 @@ +# Events Section: +module GxG + module Events + # + class Message + # + def initialize(*args) + # MUST provide a hash with at least :sender message field set + # {:sender => , :body => , :post_process => , :error_handler => / } + unless args[0].is_a?(::Hash) + raise ArgumentError, "you must pass a Hash to create the message" + end + @data = {:sender => nil, :id => (::GxG.uuid_generate().to_s.to_sym), :subject => args[1], :body => nil, :on_success => nil, :on_fail => nil}.merge(args[0]) + unless @data[:sender] + raise ArgumentError, "you must set the :sender key in the argument Hash" + end + self + end + # + def inspect() + @data.inspect + end + # + def id() + @data[:id] + end + # + def sender() + @data[:sender] + end + # + def subject() + @data[:subject] + end + # + def body() + @data[:body] + end + # + def succeed(input=nil) + if @data[:on_success].respond_to?(:call) + @data[:on_success].call(input) + end + end + # + def fail(input=nil) + if @data[:on_fail].respond_to?(:call) + @data[:on_fail].call(input) + end + end + # + def on(event_type=nil,&block) + if [:success, :fail].include?(event_type) + if block.respond_to?(:call) + case event_type + when :success + @data[:on_success] = block + when :fail + @data[:on_fail] = block + end + else + raise ArgumentError, "You MUST provide a callable block." + end + else + raise ArgumentError, "Unknown event type: #{event_type}" + end + end + # Hash-like support methods: + def keys() + @data.keys() + end + def [](the_key) + @data[(the_key)] + end + def []=(the_key, the_value) + @data[(the_key)] = the_value + end + def process(&block) + @data.process(&block) + end + def process!(&block) + @data.process!(&block) + end + def search(&block) + @data.search(&block) + end + def paths_to(*args) + @data.paths_to(*args) + end + def get_at_path(*args) + @data.get_at_path(*args) + end + def set_at_path(*args) + @data.set_at_path(*args) + end + end + # + class LoggerDB + # + end + # + class LogRing + # + def initialize() + @uuid = ::GxG.uuid_generate().to_sym + @busy = false + @outlets = {} + @messages = [] + end + # + def uuid() + @uuid + end + # + def keys() + @outlets.keys() + end + # + def [](the_key) + @outlets[(the_key)] + end + # + def []=(the_key,the_logger) + if the_logger.is_any?(::Logger, ::GxG::Events::LoggerDB) + @outlets[(the_key)] = the_logger + the_logger + else + raise ArgumentError, "You must supply a Logger or GxG::Events::LoggerDB instance, you provided a #{the_logger.class}" + end + end + # + def process_messages() + unless @busy + @busy = true + # + the_message = @messages.shift + if the_message.is_a?(::GxG::Events::Message) + begin + if the_message[:subject] == :notification + if the_message[:body].is_a?(::Hash) + outlets = self.keys() + outlets.to_enum(:each).each do |the_outlet_key| + the_outlet = self[(the_outlet_key)] + if (the_message[:body][:severity] || ::Logger::UNKNOWN) >= the_outlet.level() + # output + if the_outlet.is_a?(::GxG::Events::LoggerDB) + # TODO: GxG::Events::LogRing : build out object-based event logging: + else + # conventional string based: + if (the_message[:body][:message].is_a?(::String) || the_message[:body][:message].is_a?(::Exception)) + the_outlet.add(the_message[:body][:severity], the_message[:body][:message], the_message[:body][:progname]) + else + if the_message[:body][:message].is_a?(::Hash) + # + if the_message[:body][:message][:error].is_a?(::Exception) + if the_message[:body][:message][:error].backtrace.is_a?(::Array) + if the_message[:body][:message][:parameters] + the_outlet.add(the_message[:body][:severity], ("\n #{the_message[:body][:message][:error].exception.class.to_s}: " + the_message[:body][:message][:error].to_s + "\n Parameters: " + the_message[:body][:message][:parameters].inspect + "\n Trace: " + the_message[:body][:message][:error].backtrace.join("\n") + "\n"), the_message[:body][:progname]) + else + the_outlet.add(the_message[:body][:severity], ("\n #{the_message[:body][:message][:error].exception.class.to_s}: " + the_message[:body][:message][:error].to_s + "\n Trace: " + the_message[:body][:message][:error].backtrace.join("\n") + "\n"), the_message[:body][:progname]) + end + else + if the_message[:body][:message][:parameters] + the_outlet.add(the_message[:body][:severity], ("\n #{the_message[:body][:message][:error].exception.class.to_s}: " + the_message[:body][:message][:error].to_s + "\n Parameters: " + the_message[:body][:message][:parameters].inspect + "\n"), the_message[:body][:progname]) + else + the_outlet.add(the_message[:body][:severity], ("\n #{the_message[:body][:message][:error].exception.class.to_s}: " + the_message[:body][:message][:error].to_s + "\n"), the_message[:body][:progname]) + end + end + else + the_outlet.add(the_message[:body][:severity], the_message[:body][:message].inspect, the_message[:body][:progname]) + end + else + the_outlet.add(the_message[:body][:severity], the_message[:body][:message].inspect, the_message[:body][:progname]) + end + end + end + end + # + end + end + end + rescue Exception => the_error + @busy = false + # log_error({:error => the_error, :parameters => {:message => the_message}}) + end + end + # + @busy = false + end + true + end + # + def add(severity = nil, message = nil, progname = nil, origin = nil, &block) + # + unless [::Logger::DEBUG, ::Logger::INFO, ::Logger::WARN, ::Logger::ERROR, ::Logger::FATAL, ::Logger::UNKNOWN].include?(severity) + severity = ::Logger::UNKNOWN + end + if progname + unless message + message = progname + progname = nil + end + end + begin + unless message + if block.respond_to?(:call) + message = block.call() + end + end + # self.mailbox << ::GxG::Events::Message.new({:sender => (origin || self), :severity => severity, :message => message, :progname => progname}) + the_message = new_message({:severity => severity, :message => message, :progname => progname}, :notification) + if origin + the_message[:sender] = origin + end + while @busy == true do + sleep 0.333 + end + @busy = true + @messages << the_message + @busy = false + true + rescue Exception => the_error + @busy = false + # log_error({:error => the_error, :parameters => {:severity => severity, :message => message, :progname => progname, :block => block}}) + false + end + end + # + def unknown?() + true + end + alias :trace? unknown? + def unknown(message = nil, progname = nil, origin = nil, &block) + self.add(::Logger::UNKNOWN, message, progname, (origin || self), &block) + end + alias :trace :unknown + def fatal?() + true + end + def fatal(message = nil, progname = nil, origin = nil, &block) + self.add(::Logger::FATAL, message, progname, (origin || self), &block) + end + def error?() + true + end + def error(message = nil, progname = nil, origin = nil, &block) + self.add(::Logger::ERROR, message, progname, (origin || self), &block) + end + def warn?() + true + end + alias :warning? :warn? + def warn(message = nil, progname = nil, origin = nil, &block) + self.add(::Logger::WARN, message, progname, (origin || self), &block) + end + alias :warning :warn + def info?() + true + end + def info(message = nil, progname = nil, origin = nil, &block) + self.add(::Logger::INFO, message, progname, (origin || self), &block) + end + def debug?() + true + end + def debug(message = nil, progname = nil, origin = nil, &block) + self.add(::Logger::DEBUG, message, progname, (origin || self), &block) + end + end + # + class EventDispatcher + def initialize(interval=0.333) + super() + @uuid = ::GxG.uuid_generate().to_sym + @event_queues = {:root => {:events => [], :settings => {:active => true, :portion => 100.0}}} + @running = false + @ticking = false + # Timer format: {:when => Time_Object.to_f, :what => event_frame, :interval => float, :id => uuid.sym} + @timers = [] + # JavaScript main pushing timer: + @js_interval = (interval.to_f * 1000.0).to_i + @host_timer = nil + # Register with global dispatcher pool: + self + end + # + def startup() + @running = true + unless @host_timer + @host_timer = `setInterval(#{method(:tick).to_proc}, #{@js_interval})` + end + if @host_timer + true + else + false + end + end + # + def shutdown() + @running = false + if @host_timer + if @host_timer.is_a?(Numeric) + `clearInterval(#{@host_timer})` + else + `clearInterval(#{@host_timer}._id)` + end + end + self.cancel_all_timers + @host_timer = nil + true + end + # + def tick() + if @running + unless @ticking + @ticking = true + events = [] + # + if @timers.size > 0 + triggered = [] + current = ::Time.now.to_f + @timers.each_with_index do |timer,indexer| + if current >= timer[:when] + events << timer[:what] + if timer[:interval] + @timers[(indexer)][:when] = (current + timer[:interval].to_f) + else + triggered.unshift(indexer) + end + end + end + triggered.each do |indexer| + @timers.delete_at(indexer) + end + end + # + total_slots = 100 + load_size = 0 + queues = {} + @event_queues.keys.each do |the_queue| + if (@event_queues[(the_queue)][:settings][:active] && @event_queues[(the_queue)][:events].size > 0) + queues[(the_queue)] = {:portion => ((@event_queues[(the_queue)][:settings][:portion].to_f / 100.0) * total_slots.to_f).to_i} + if queues[(the_queue)][:portion] == 0 + # Fudging a little here in case of real heavy loads and/or too many queues. + queues[(the_queue)][:portion] = 1 + end + queues[(the_queue)][:size] = @event_queues[(the_queue)][:events].size + load_size += queues[(the_queue)][:size] + end + end + queues.keys.each do |the_queue| + # TODO: devise apportioned event dispatch + queues[(the_queue)][:portion].times do + op = @event_queues[(the_queue)][:events].shift + if op.respond_to?(:call) + events << op + else + break + end + end + end + # + events.each do |the_event| + if the_event + begin + the_event.call() + rescue Exception => the_error + # + end + end + end + # + @ticking = false + end + end + end + # + def inspect_timers() + @timers + end + def inspect_queue(queue=:root) + @event_queues[(queue)] + end + # + def adjust_event_queue(queue=:root,settings={}) + # RESEARCH: GxG::Events::EventDispatcher.adjust_event_queue : refactor :portion of each queue algo needed. + unless queue.to_s.to_sym == :root + if @event_queues[(queue.to_sym)] + # LATER: Eventually add :portion as valid setting to accept. + [:active].each do |the_setting| + if settings[(the_setting.to_sym)] + new_setting = {} + # portion adjustment algo here + new_setting[(the_setting.to_sym)] = settings[(the_setting.to_sym)] + # + @event_queues[(queue.to_sym)][:settings].merge!(new_setting) + end + end + else + puts "Warning: queue #{queue.inspect} does not exist" + # For Now: just ignore references to non-existent queues. (reconsider later) + # raise ArgumentError, "Event Queue :#{queue} does not exist to alter" + end + end + end + # + def create_event_queue(settings={}) + unless settings.is_a?(Hash) + raise ArgumentError, "you must provide a Hash as a parameter set" + end + queue_name = settings.delete(:name).to_s.to_sym + if queue_name.to_s.size > 0 + if @event_queues[(queue_name)] + raise ArgumentError, "Event Queue #{queue_name.inspect} already exists" + else + @event_queues[(queue_name)] = {:events => [], :settings => {:active => false, :portion => 0.0}} + # For Now: simply evenly divide the portions equally, only :root can be 100%. + even_portion = (100.0 / @event_queues.keys.size.to_f) + @event_queues.keys.each do |the_queue| + @event_queues[(the_queue)][:settings][:portion] = (even_portion) + end + self.adjust_event_queue(queue_name, settings.merge({:active => true})) + true + end + else + raise ArgumentError, ":name must be provided for Event Queue" + end + end + # + def delete_event_queue(queue=nil) + if queue.to_s.to_sym == :root + false + else + @event_queues.delete(queue) + # For Now: simply evenly divide the portions equally, only :root can be 100%. + even_portion = (@event_queues.keys.size.to_f / 100.0) + @event_queues.keys.each do |the_queue| + @event_queues[(the_queue)][:settings][:portion] = (even_portion) + end + true + end + end + # + def pause_event_queue(queue=nil) + unless queue.to_s.to_sym == :root + self.adjust_event_queue(queue,{:active => true}) + end + end + # + def unpause_event_queue(queue=nil) + unless queue.to_s.to_sym == :root + self.adjust_event_queue(queue,{:active => false}) + end + end + # + def post_to_event_queue(queue=:root,the_event=nil) + if the_event.respond_to?(:call) + if @event_queues[(queue)] + @event_queues[(queue)][:events] << the_event + else + # issue a warning, and post to :root queue anyways (for now). (link to logger, and output) + @event_queues[:root][:events] << the_event + end + end + end + # + def post_event(queue_name=:root,&block) + unless @event_queues[(queue_name.to_sym)] + self.create_event_queue({:name => queue_name.to_sym}) + end + self.post_to_event_queue(queue_name.to_sym,block) + true + end + # + # Adding Timers + def add_timer(options={},&block) + what_time = options[:when] + what = (options[:do] || block) + # + if what_time.is_a?(::Numeric) + # ms offset + what_time = (::Time::now.to_f + what_time.to_f) + else + if what_time.is_a?(::Time) + what_time = what_time.to_f + else + raise ArgumentError, "you must specify a :when => millisecond (float) offset, or a Time for the timer" + end + end + # + unless what.respond_to?(:call) + raise ArgumentError, "you must provide a :do => callable object (Proc) for the timer, or a block" + end + the_id = ::GxG.uuid_generate().to_sym + @timers << {:when => what_time, :what => what, :id => the_id} + {:timer => the_id} + end + # + def add_periodic_timer(options={},&block) + interval = options[:interval] + what = (options[:do] || block) + # + what_time = ::Time::now.to_f + if interval.is_a?(::Numeric) + # offset + what_time += interval.to_f + else + raise ArgumentError, "you must specify an :interval => Float (as you would with sleep) offset interval for the periodic timer" + end + # + unless what.respond_to?(:call) + raise ArgumentError, "you must provide a :do => callable object (Proc) for the timer, or a block" + end + the_id = ::GxG.uuid_generate().to_sym + @timers << {:when => what_time, :what => what, :interval => interval.to_f, :id => the_id} + {:timer => the_id} + end + # + def cancel_timer(params={}) + the_id = params[:timer] + result = false + if the_id + @timers.each_index do |indexer| + if @timers[(indexer)][:id] == the_id + @timers.delete_at(indexer) + result = true + break + end + end + end + result + end + # + def cancel_all_timers() + while @ticking == true do + sleep 0.333 + end + @timers = [] + true + end + # + end + end +end +# Establish Console Logger for Applications: +module GxG + # setup LOG. + if const_defined?(:LOG) + remove_const(:LOG) + end + LOG = ::GxG::Events::LogRing.new() +end +::GxG::LOG[:default] = ::Logger.new(::Logger::WARN) +# Establish Dispatcher Service for Applications: +module GxG + # setup DISPATCHER & LOG timer. + if const_defined?(:DISPATCHER) + remove_const(:DISPATCHER) + end + DISPATCHER = ::GxG::Events::EventDispatcher.new(0.1) + DISPATCHER.startup + LOG_TIMER = DISPATCHER.add_periodic_timer({:interval => 1.0}) { ::GxG::LOG.process_messages } + def self.shutdown() + DISPATCHER.shutdown + end +end +# \ No newline at end of file diff --git a/gxg/web/gxg_ferro.rb b/gxg/web/gxg_ferro.rb new file mode 100644 index 0000000..026477a --- /dev/null +++ b/gxg/web/gxg_ferro.rb @@ -0,0 +1,8418 @@ +# +module GxG + DISPLAY_DETAILS = {:server_status => :running, :logged_in => false, :host_path => "", :use_ssl => false, :connection => nil, :socket => nil, :object => nil, :theme => "default", :query => {}, :article => nil, :mode => :browsing, :mouse_x => 0, :mouse_y => 0, :mousedown => false} + module Gui + # + module Css + @@CssCache = [] + @@CssRuleProperties = {} + # + def self.load_font(font_profile=nil) + if font_profile.is_any?(::Hash, ::GxG::Database::PersistedHash) + if font_profile.is_a?(::GxG::Database::PersistedHash) + font_profile = font_profile.unpersist() + end + if font_profile[:resource_type] == "font-face" + thefamily = (font_profile[:font_family] || "Unknown") + thestyle = (font_profile[:font_style] || "normal") + theweight = (font_profile[:font_weight] || 400).to_s + if font_profile[:format] + fontformat = font_profile[:format].to_s.downcase + else + case File.extname(font_profile[:source]).downcase + when ".ttf", ".otf" + fontformat = "truetype" + when ".eot" + fontformat = "embedded-opentype" + when ".svg" + fontformat = "svg" + when ".woff" + fontformat = "woff" + when ".woff2" + fontformat = "woff2" + else + fontformat = "unknown" + end + end + if ["sftp:","ftp:/","http:", "https"].include?(font_profile[:source][0..4].downcase) + # reach out over the internet for the resource + source_url = font_profile[:source] + else + # search local site at resource path + source_url = GxG::DISPLAY_DETAILS[:object].site_asset(font_profile[:source]) + end + # + %x{ + var family = #{thefamily.to_s}; + var sourceurl = "url('" + #{source_url} + "')"; + var theformat = "format('" + #{fontformat} + "')"; + if (theformat == "format('unknown')") { + var sourcestring = sourceurl; + } else { + var sourcestring = (sourceurl + " " + theformat); + }; + var theweight = #{theweight}; + var thestyle = #{thestyle}; + var thefont = new FontFace(family, sourceurl, {weight: theweight, style: thestyle}); + thefont.load().then(function(loaded_face) { + document.fonts.add(loaded_face); + }).catch(function(error) { + alert(error); + }); + } + true + else + log_warn("You attempted to load a non-font resource as a font: #{font_profile.inspect}") + false + end + else + log_warn("You attempted to load a font with the wrong value: #{font_profile.inspect}") + false + end + end + # + class CSSRule + # Provide EASY interface to native CSS Rule. + def initialize(the_native_rule) + @native_rule = the_native_rule + @name = (`#{@native_rule}.selectorText` || "").to_s.to_sym + end + # + def native_object() + @native_rule + end + # + def inspect() + "" + end + # + def name() + @name + end + # + def keys() + GxG::Gui::Css::valid_properties() + end + # + def [](the_key) + `#{@native_rule}.style[#{the_key}]` + end + # + def []=(the_key, the_value) + # + `#{@native_rule}.style[#{the_key}] = #{(the_value || "")}` + the_value + end + # + def merge(properties={}) + properties.keys.each do |the_key| + self[(the_key)] = properties[(the_key)] + end + self + end + # + end + # + def self.gxg_style_sheet() + result = nil + sheets = `document.styleSheets.length`.to_i + (0..(sheets - 1)).each do |sheet_index| + the_sheet = `document.styleSheets[#{sheet_index}]` + if the_sheet + if `#{the_sheet}.href`.to_s.include?("/themes/page.css") + result = the_sheet + break + end + end + end + result + end + # + def self.refresh_css_cache() + the_style_sheet = GxG::Gui::Css::gxg_style_sheet() + if the_style_sheet + new_cache = [] + begin + ruleset = `#{the_style_sheet}.cssRules` + rescue Exception => the_error + ruleset = `#{the_style_sheet}.rules` + end + if ruleset + rules_count = (`#{ruleset}.length`.to_i) + if rules_count > 0 + end_index = ((`#{ruleset}.length`.to_i) - 1) + else + end_index = 0 + end + (0..(end_index)).each do |indexer| + begin + native_rule = `#{the_style_sheet}.cssRules[#{indexer}]` + rescue Exception => the_error + native_rule = `#{the_style_sheet}.rules[#{indexer}]` + end + if native_rule + new_cache << GxG::Gui::Css::CSSRule.new(native_rule) + end + end + end + @@CssCache = new_cache + end + true + end + # + def self.valid_properties() + @@CssRuleProperties.keys + end + # + def self.get_property_name(the_key=nil) + result = nil + if the_key.is_a?(::Symbol) + result = @@CssRuleProperties[(the_key)] + end + result + end + # + def self.rules() + result = [] + @@CssCache.each do |entry| + result << entry.name + end + result + end + # + def self.find_rule(the_rule_selector=nil) + result = [] + @@CssCache.each do |entry| + if entry.name == (the_rule_selector.to_s.to_sym) + result << entry + end + end + result + end + # + def self.set_rule(the_rule_selector=nil, the_properties={}, options={:merge => true}) + # find existing + # remove existing + # create or re-create + the_style_sheet = GxG::Gui::Css::gxg_style_sheet() + if the_style_sheet + found = GxG::Gui::Css::find_rule(the_rule_selector) + if found.size > 0 + GxG::Gui::Css::remove_rule(the_rule_selector) + end + # create it + the_site_prefix = ::GxG::CONNECTION.relative_url() + rule_string = "{ " + the_properties.keys.each do |property_key| + the_value = the_properties[(property_key)].to_s + if the_value.include?("url(") + end_point = the_value.index(")") + the_path = the_value[4..(end_point - 1)] + unless the_path.include?("data:") || the_path.include?("http:") || the_path.include?("https:") + unless (the_path[(0..(the_site_prefix.size - 1))] == the_site_prefix) + the_value = ("url(" + File.expand_path(the_site_prefix + "/" + the_path) + ")") + end + end + end + rule_string = rule_string + ("#{property_key.to_s}: #{the_value}; ") + end + rule_string = rule_string + " }" + if `#{the_style_sheet}.insertRule(#{the_rule_selector.to_s + rule_string.to_s},(#{the_style_sheet}.cssRules.length))`.to_i == `(#{the_style_sheet}.cssRules.length - 1)`.to_i + begin + native_rule = `#{the_style_sheet}.cssRules[(#{the_style_sheet}.cssRules.length - 1)]` + rescue Exception => the_error + native_rule = `#{the_style_sheet}.rules[(#{the_style_sheet}.cssRules.length - 1)]` + end + @@CssCache << GxG::Gui::Css::CSSRule.new(native_rule) + true + else + log_warn("GxG Style Sheet Rule creation failed..") + false + end + else + log_warn("GxG Style Sheet not found.") + false + end + # + end + # + def self.remove_rule(the_rule_selector=nil) + found_indexes = [] + @@CssCache.each_with_index do |entry, index| + if entry.name == the_rule_selector.to_s.to_sym || entry.name == the_rule_selector.to_s + found_indexes << index + end + end + found = [] + found_indexes.reverse.each do |the_index| + found << @@CssCache.delete_at(the_index) + end + if found.size > 0 + found.each do |the_rule| + native_rule = the_rule.native_object() + the_sheet = GxG::Gui::Css::gxg_style_sheet() + if the_sheet + rule_count = `#{the_sheet}.cssRules.length`.to_i + if rule_count > 0 + (0..(rule_count - 1)).to_a.reverse.each do |rule_index| + other_native_rule = `#{the_sheet}.cssRules[#{rule_index}]` + if other_native_rule + if `#{native_rule}.selectorText == #{other_native_rule}.selectorText` + `#{the_sheet}.deleteRule(#{rule_index})` + end + end + end + end + end + end + true + else + false + end + end + # + end + # + # -------------------------------------------------------------------------------------------------------------------------------------- + module EventHandlers + # Constants: + EVENT_TYPES = [ + "abort", + "afterprint", + "animatedend", + "animationiteration", + "animationstart", + "beforeprint", + "beforeunload", + "blur", + "canplay", + "canplaythrough", + "change", + "click", + "contextmenu", + "copy", + "cut", + "dblclick", + "drag", + "dragend", + "dragcenter", + "dragleave", + "dragover", + "dragstart", + "drop", + "durationchange", + "ended", + "error", + "focus", + "focusin", + "focusout", + "fullscreenchange", + "fullscreenerror", + "hashchange", + "input", + "invalid", + "keydown", + "keypress", + "keyup", + "load", + "loadeddata", + "loadedmetadata", + "loadstart", + "message", + "mousedown", + "mouseenter", + "mouseleave", + "mousemove", + "mouseover", + "mouseup", + "offline", + "online", + "open", + "pagehide", + "pageshow", + "paste", + "play", + "playing", + "popstate", + "progress", + "ratechange", + "resize", + "reset", + "scroll", + "search", + "seeked", + "select", + "show", + "stalled", + "storage", + "submit", + "suspend", + "timeupdate", + "toggle", + "touchcancel", + "touchend", + "touchmove", + "touchstart", + "transitionend", + "unload", + "volumechange", + "waiting", + "wheel" + ] + # + EVENT_GENRES = { + :click => :mouse, + :contextmenu => :mouse, + :dblclick => :mouse, + :mouseenter => :mouse, + :mouseleave => :mouse, + :mouseover => :mouse, + :mouseup => :mouse, + :mousedown => :mouse, + :mousemove => :mouse, + :mouseout => :mouse, + :keydown => :keyboard, + :keypress => :keyboard, + :keyup => :keyboard, + :touchcancel => :touch, + :touchmove => :touch, + :touchstart => :touch, + :touchend => :touch, + :animationstart => :animation, + :animationend => :animation, + :animationiteration => :animation, + :animationpause => :animation, + :animationresume => :animation, + :animationstop => :animation, + :input => :input, + :drag => :drag, + :dragend => :drag, + :dragenter => :drag, + :dragleave => :drag, + :dragover => :drag, + :dragstart => :drag, + :drop => :drag, + :wheel => :wheel, + :abort => :ui, + :beforeunload => :ui, + :error => :ui, + :load => :ui, + :resize => :ui, + :scroll => :ui, + :select => :ui, + :unload => :ui, + :blur => :focus, + :focus => :focus, + :focusin => :focus, + :focusout => :focus, + :afterprint => :event, + :beforeprint => :event, + :canplay => :event, + :canplaythrough => :event, + :change => :event, + :fullscreenchange => :event, + :fullscreenerror => :event, + :invalid => :event, + :loadeddata => :event, + :loadedmetadata => :event, + :message => :event, + :offline => :event, + :online => :event, + :open => :event, + :close => :event, + :pause => :event, + :play => :event, + :playing => :event, + :progress => :event, + :changerate => :event, + :reset => :event, + :search => :event, + :seek => :event, + :seeking => :event, + :show => :event, + :hide => :event, + :stalled => :event, + :submit => :event, + :suspend => :event, + :timeupdate => :event, + :toggle => :event, + :waiting => :event, + :copy => :clipboard, + :cut => :clipboard, + :paste => :clipboard, + :pagehide => :transition, + :pageshow => :transition, + :popstate => :history, + :loadstart => :progress, + :storage => :storage, + :credentialchanged => :ui + } + #TODO: look into fully supporting touch screen gleaning functions. + EVENT_FIELDS = { + :mouse => [:altKey, :button, :buttons, :clientX, :clientY, :ctrlKey, :metaKey, :movementX, :movementY, :offsetX, :offsetY, :pageX, :pageY, :screenX, :screenY, :shiftKey, :which, :detail, :view, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :keyboard => [:altKey, :charCode, :code, :ctrlKey, :metaKey, :isComposing, :key, :keyCode, :location, :shift, :repeat, :shiftKey, :which, :detail, :view, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :touch => [:altKey, :ctrlKey, :metaKey, :shiftKey, :detail, :view, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :animation => [:animationName, :elapsedTime, :pseudoElement, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :input => [:data, :inputType, :isComposing, :detail, :view, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :drag => [:dataTranser, :altKey, :button, :buttons, :clientX, :clientY, :ctrlKey, :metaKey, :movementX, :movementY, :offsetX, :offsetY, :pageX, :pageY, :screenX, :screenY, :shiftKey, :which, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :wheel => [:deltaX, :deltaY, :deltaZ, :deltaMode, :altKey, :button, :buttons, :clientX, :clientY, :ctrlKey, :metaKey, :movementX, :movementY, :offsetX, :offsetY, :pageX, :pageY, :screenX, :screenY, :shiftKey, :which, :detail, :view, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :ui => [:detail, :view,:bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :focus => [:relatedTarget, :detail, :view, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :event => [:bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :transition => [:persisted, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :history => [:state, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :progress => [:lengthComputable, :loaded, :total, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :storage => [:key, :newValue, :oldValue, :storageArea, :url, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :clipboard => [:clipboardData, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp] + } + # Internal method on + def on(the_event_type=nil, captured=false, &block) + unless @event_handlers.is_a?(::Hash) + @event_handlers = {} + @event_listeners = {} + end + if the_event_type.is_any?(::String, ::Symbol) + if block.respond_to?(:call) + @event_handlers[(the_event_type.to_s.downcase.to_sym)] = block + @event_listeners[(the_event_type.to_s.downcase.to_sym)] = `#{self.method(:event_handler).to_proc}` + `#{self.element}.addEventListener(#{the_event_type.to_s.downcase}, #{@event_listeners[(the_event_type.to_s.downcase.to_sym)]}, #{captured})` + end + end + end + # + def remove_listener(the_event_type=nil) + if @event_handlers[(the_event_type.to_s.downcase.to_sym)] && @event_listeners[(the_event_type.to_s.downcase.to_sym)] + `#{self.element}.removeEventListener(#{the_event_type.to_s.downcase},#{@event_listeners[(the_event_type.to_s.downcase.to_sym)]});` + @event_handlers.delete(the_event_type.to_s.downcase.to_sym) + @event_listeners.delete(the_event_type.to_s.downcase.to_sym) + end + true + end + # + def event_handler(the_event) + # format the event record for travel in the system. + # See: https://www.w3schools.com/jsref/dom_obj_event.asp + # Setup event type groupings to glean appropriate details from the event. + the_type = `#{the_event}.type`.to_s.downcase.to_sym + event_record = {} + if @event_handlers[(the_type)] + genre = (GxG::Gui::EventHandlers::EVENT_GENRES[(the_type)] || :event) + if genre + fields = GxG::Gui::EventHandlers::EVENT_FIELDS[(genre)] + if fields + # + %x{ + serializer = function thisOne (e) { + if (e) { + var o = { + eventName: e.toString(), + altKey: e.altKey, + bubbles: e.bubbles, + button: e.button, + buttons: e.buttons, + cancelBubble: e.cancelBubble, + cancelable: e.cancelable, + clientX: e.clientX, + clientY: e.clientY, + composed: e.composed, + ctrlKey: e.ctrlKey, + currentTarget: e.currentTarget ? e.currentTarget.outerHTML : null, + defaultPrevented: e.defaultPrevented, + detail: e.detail, + eventPhase: e.eventPhase, + fromElement: e.fromElement ? e.fromElement.outerHTML : null, + isTrusted: e.isTrusted, + layerX: e.layerX, + layerY: e.layerY, + metaKey: e.metaKey, + movementX: e.movementX, + movementY: e.movementY, + offsetX: e.offsetX, + offsetY: e.offsetY, + pageX: e.pageX, + pageY: e.pageY, + relatedTarget: e.relatedTarget ? e.relatedTarget.outerHTML : null, + returnValue: e.returnValue, + screenX: e.screenX, + screenY: e.screenY, + shiftKey: e.shiftKey, + sourceCapabilities: e.sourceCapabilities ? e.sourceCapabilities.toString() : null, + target: e.target ? e.target.outerHTML : null, + timeStamp: e.timeStamp, + toElement: e.toElement ? e.toElement.outerHTML : null, + type: e.type, + view: e.view ? e.view.toString() : null, + which: e.which, + x: e.x, + y: e.y + }; + return o; + }; + }; + event_record = JSON.stringify(serializer(#{the_event})); + } + event_record = ::JSON::parse(event_record.to_s, {:symbolize_names => true}) + # puts "Got: #{event_record.inspect}" + end + end + # + @event_handlers[(the_type)].call(event_record) + # + end + end + # + def set_script(the_script_body="") + result = false + if the_script_body.size > 0 + # + begin + # + if the_script_body.base64? + the_script_body = the_script_body.decode64 + end + # + instance_eval(the_script_body) + result = true + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:script => the_script_body}}) + end + end + result + end + # + end + # -------------------------------------------------------------------------------------------------------------------------------------- + # Animation Support: + class Animation + # + def initialize(details={}, the_uuid=nil) + @event_handlers = {} + if details.is_any?(::Hash, ::GxG::Database::PersistedHash) + if details[:origin] + if details[:origin].is_a?(::GxG::Database::PersistedHash) + @origin = details[:origin].unpersist + else + @origin = details[:origin] + end + else + @origin = nil + end + # + unless details[:destination] + raise ArgumentError, "You MUST provide a :destination state for the object." + end + # + if details[:destination].is_a?(::GxG::Database::PersistedHash) + @destination = details[:destination].unpersist + else + @destination = details[:destination] + end + # + if details[:options] + @options = {} + if details[:options].is_a?(::GxG::Database::PersistedHash) + the_options = details[:options].unpersist + else + the_options = details[:options] + end + callback_map = {:animationstart => :start, :animationend => :complete, :animationiteration => :update, :animationstop => :stop, :animationpause => :pause, :animationresume => :resume} + the_options.each_pair do |the_key, the_value| + if callback_map.keys.include?(the_key) + if the_value.respond_to?(:call) + # Expansion: compile formatted hashes of script records ?? + @event_handlers[(the_key)] = the_value + @options[(the_key)] = the_value + else + log_warn("Attempted to pass an invalid object as an event handler. #{the_key.inspect} --> #{the_value.inspect}") + end + else + @options[(the_key)] = the_value + end + end + else + @options = nil + end + # + unless details[:targets] + raise ArgumentError, "You MUST provide a one or more :targets." + end + # + if details[:targets].is_a?(::GxG::Database::PersistedHash) + @target_list = details[:targets].unpersist + else + @target_list = details[:targets] + end + end + @uuid = (the_uuid || ::GxG::uuid_generate).to_s.to_sym + @targets = nil + @tweens = [] + # + self + end + # + def update_targets(data=nil) + result = false + if data.is_any?(::Hash, ::GxG::Database::PersistedHash) + if data.is_a?(::GxG::Database::PersistedHash) + @target_list = data.unpersist + else + @target_list = data + end + result = true + end + result + end + # + def update_origin(data=nil) + result = false + if data.is_any?(::Hash, ::GxG::Database::PersistedHash) + if data.is_a?(::GxG::Database::PersistedHash) + @origin = data.unpersist + else + @origin = data + end + result = true + end + result + end + # + def update_destination(data=nil) + result = false + if data.is_any?(::Hash, ::GxG::Database::PersistedHash) + if data.is_a?(::GxG::Database::PersistedHash) + @destination = data.unpersist + else + @destination = data + end + result = true + end + result + end + # + def update_options(data=nil) + result = false + if data.is_any?(::Hash, ::GxG::Database::PersistedHash) + if data.is_a?(::GxG::Database::PersistedHash) + the_options = data.unpersist + else + the_options = data + end + @options = {} + callback_map = {:animationstart => :start, :animationend => :complete, :animationiteration => :update, :animationstop => :stop, :animationpause => :pause, :animationresume => :resume} + the_options.each_pair do |the_key, the_value| + if callback_map.keys.include?(the_key) + if the_value.respond_to?(:call) + # Expansion: compile formatted hashes of script records ?? + @event_handlers[(the_key)] = the_value + @options[(the_key)] = the_value + else + log_warn("Attempted to pass an invalid object as an event handler. #{the_key.inspect} --> #{the_value.inspect}") + end + else + @options[(the_key)] = the_value + end + end + result = true + end + result + end + # + def uuid() + @uuid + end + # + def on(the_event_type=nil, captured=false,&block) + unless @event_handlers.is_a?(::Hash) + @event_handlers = {} + end + if the_event_type.is_any?(::String, ::Symbol) + if block.respond_to?(:call) + @event_handlers[(the_event_type.to_s.downcase.to_sym)] = block + end + end + end + # + def event_handler(the_event) + # format the event record for travel in the system. + # See: https://www.w3schools.com/jsref/dom_obj_event.asp + # Setup event type groupings to glean appropriate details from the event. + the_type = `#{the_event}.type`.to_s.downcase.to_sym + event_record = {} + if @event_handlers[(the_type)] + genre = (GxG::Gui::EventHandlers::EVENT_GENRES[(the_type)] || :event) + if genre + fields = GxG::Gui::EventHandlers::EVENT_FIELDS[(genre)] + if fields + # See: https://stackoverflow.com/questions/11547672/how-to-stringify-event-object + %x{ + serializer = function thisOne (e) { + if (e) { + var o = { + eventName: e.toString(), + altKey: e.altKey, + bubbles: e.bubbles, + button: e.button, + buttons: e.buttons, + cancelBubble: e.cancelBubble, + cancelable: e.cancelable, + clientX: e.clientX, + clientY: e.clientY, + composed: e.composed, + ctrlKey: e.ctrlKey, + currentTarget: e.currentTarget ? e.currentTarget.outerHTML : null, + defaultPrevented: e.defaultPrevented, + detail: e.detail, + eventPhase: e.eventPhase, + fromElement: e.fromElement ? e.fromElement.outerHTML : null, + isTrusted: e.isTrusted, + layerX: e.layerX, + layerY: e.layerY, + metaKey: e.metaKey, + movementX: e.movementX, + movementY: e.movementY, + offsetX: e.offsetX, + offsetY: e.offsetY, + pageX: e.pageX, + pageY: e.pageY, + relatedTarget: e.relatedTarget ? e.relatedTarget.outerHTML : null, + returnValue: e.returnValue, + screenX: e.screenX, + screenY: e.screenY, + shiftKey: e.shiftKey, + sourceCapabilities: e.sourceCapabilities ? e.sourceCapabilities.toString() : null, + target: e.target ? e.target.outerHTML : null, + timeStamp: e.timeStamp, + toElement: e.toElement ? e.toElement.outerHTML : null, + type: e.type, + view: e.view ? e.view.toString() : null, + which: e.which, + x: e.x, + y: e.y + }; + return o; + }; + }; + event_record = JSON.stringify(serializer(#{the_event})); + } + event_record = ::JSON::parse(event_record.to_s, {:symbolize_names => true}) + # puts "Got: #{event_record.inspect}" + end + end + # + @event_handlers[(the_type)].call(event_record) + # + end + end + # + def set_script(the_script_body="") + result = false + if the_script_body.size > 0 + # + begin + # + if the_script_body.base64? + the_script_body = the_script_body.decode64 + end + # + instance_eval(the_script_body) + result = true + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:script => the_script_body}}) + end + end + result + end + # + def compile() + result = false + begin + @targets = [] + native_targets = `[]` + @target_list.keys.each do |the_method| + if the_method == :class + unless @target_list[:class].is_a?(::String) + raise Exception, "You MUST provide :class target as a String, not: #{@target_list[:class].class.inspect}" + end + @target_list[:class].split(" ").each do |the_classname| + if the_classname.size > 0 + found_list = `document.getElementsByClassName(#{the_classname})` + found_length = `#{found_list}.length`.to_i + if found_length > 0 + (0..(found_length - 1)).each do |the_index| + the_element = `#{found_list}.item(#{the_index})` + unless @targets.include?(the_element) + @targets << the_element + end + end + end + end + end + end + # + if the_method == :objects + # provide a list of UUIDs of components to include as targets + unless @target_list[:objects].is_a?(::Array) + raise Exception, "You MUST provide :object target list as an Array, not: #{@target_list[:objects].class.inspect}" + end + @target_list[:objects].each do |the_reference| + found_object = GxG::DISPLAY_DETAILS[:object].find_object(the_reference.to_s.to_sym) + if found_object + element_id = `#{found_object.element}.attributes.getNamedItem('id') || 0` + unless `#{element_id} == 0` + found_element = `document.getElementById(#{element_id})` + if found_element + unless @targets.include?(found_element) + @targets << found_element + end + end + end + end + end + end + if the_method == :elements + # provide a list of native elements to include as targets + unless @target_list[:elements].is_a?(::Array) + raise Exception, "You MUST provide :elements target list as an Array, not: #{@target_list[:elements].class.inspect}" + end + @target_list[:elements].each do |the_element| + element_id = `#{the_element}.attributes.getNamedItem('id') || 0` + unless `#{element_id} == 0` + element_id = `#{element_id}.value` + found_element = `document.getElementById(#{element_id})` + if found_element + unless @targets.include?(found_element) + @targets << found_element + end + end + end + end + end + if the_method == :assets + # Future expansion + end + end + # DEBUG: + unless @targets.size > 0 + puts "No valid targets resulted." + end + @targets.each do |the_element| + `#{native_targets}.push(#{the_element})` + end + # + if @origin.is_a?(::Hash) + if @origin.size > 0 + from_state = {} + @origin.each_pair do |the_key, the_value| + # key translations: + if the_key == :attribue || the_key == "attribute" + new_key = :attr + else + new_key = the_key + end + # + from_state[(new_key)] = the_value + end + else + from_state = nil + end + else + from_state = nil + end + # + if @destination.size > 0 + to_state = {} + @destination.each_pair do |the_key, the_value| + # key translations: + if the_key == :attribue || the_key == "attribute" + new_key = :attr + else + new_key = the_key + end + # + to_state[(new_key)] = the_value + end + else + raise Exception, "You MUST define a valid :destination state. not: #{@destination.inspect}" + end + # + if @options.is_a?(::Hash) + if @options.size > 0 + options = {} + callback_map = {:animationstart => :start, :animationend => :complete, :animationiteration => :update, :animationstop => :stop, :animationpause => :pause, :animationresume => :resume} + @options.each_pair do |the_key, the_value| + # Key translations: + if callback_map.keys.include?(the_key) + # include handlers + new_key = callback_map[(the_key)] + # + if @event_handlers[(the_key)].respond_to?(:call) + # FIXME + # puts "Setting callback for: #{new_key.inspect}" + options[(new_key)] = @event_handlers[(the_key)] + end + else + unless [:start_time].include?(the_key) + # TODO: research ALL options and only allow valid options to be set. + options[(the_key)] = the_value + end + end + end + else + options = nil + end + else + options = nil + end + # + @tweens = [] + # FIXME: + # puts "Compiling Options: #{options.inspect}" + @targets.each do |the_target| + if from_state + if options + @tweens << `KUTE.fromTo(#{the_target}, #{from_state.to_n}, #{to_state.to_n}, #{options.to_n})` + else + @tweens << `KUTE.fromTo(#{the_target}, #{from_state.to_n}, #{to_state.to_n})` + end + else + if options + @tweens << `KUTE.to(#{the_target},#{to_state.to_n}, #{options.to_n})` + else + @tweens << `KUTE.to(#{the_target},#{to_state.to_n})` + end + end + end + # + result = true + rescue Exception => the_error + log_error({:error => the_error}) + @targets = nil + @tweens = [] + end + result + end + # + def start(the_time=nil) + unless @tweens.size > 0 + self.compile + end + unless the_time.is_a?(::Numeric) + the_time = `window.performance.now()` + end + if @tweens.size > 0 + @tweens.each do |the_tween| + `#{the_tween}.start(#{the_time})` + end + end + end + # + def stop() + if @tweens.size > 0 + @tweens.each do |the_tween| + `#{the_tween}.stop()` + end + end + end + # + def pause() + if @tweens.size > 0 + @tweens.each do |the_tween| + `#{the_tween}.pause()` + end + end + end + # + def resume() + if @tweens.size > 0 + @tweens.each do |the_tween| + `#{the_tween}.resume()` + end + end + end + # + def included() + # return a list of all page components included in this animation + result = [] + if @targets + @targets.each do |the_element| + element_id = `#{the_element}.attributes.getNamedItem('id') || 0` + unless `#{element_id} == 0` + element_id = `#{element_id}.value` + found_object = GxG::DISPLAY_DETAILS[:object].find_object_by_id(element_id.to_s) + if found_object + result << found_object + end + end + end + end + result + end + # + def each_included(&block) + if block.respond_to?(:call) + self.included.each do |the_object| + block.call(the_object) + end + else + self.included.each + end + end + # send events to all target components (not their elements) + def broadcast(the_method=nil, the_parameters=nil) + self.each_included do |the_object| + the_object.send(the_method, the_parameters) + end + end + # + end + # + module AnimationSupport + # + def animate(animation_parameters=nil) + if animation_parameters[:destination].is_any?(::Hash, ::GxG::Database::PersistedHash) + the_animation = ::GxG::Gui::Animation.new({:targets => {:elements => [(self.element)]}, :origin => animation_parameters[:origin], :destination => animation_parameters[:destination], :options => animation_parameters[:options]}) + start_time = `window.performance.now()` + if animation_parameters[:options].is_any?(::Hash, ::GxG::Database::PersistedHash) + start_time = animation_parameters[:options][:start_time] + end + # FIXME: + # GxG::ANIMATION[:animations][(the_animation.uuid)] = the_animation + # + the_animation.compile + the_animation.start(start_time) + true + else + log_warn("Ignoring invalid animation: NO valid :destination state specified (required). not: #{animation_parameters[:destination].inspect}") + false + end + end + # ------------------------------------------------------------------------------ + def show(transition_parameters=nil,&block) + if transition_parameters.is_any?(::Hash, ::GxG::Database::PersistedHash) + if transition_parameters.is_a?(::GxG::Database::PersistedHash) + transition_parameters = transition_parameters.unpersist + end + # + if transition_parameters[:destination].is_a?(::Hash) + if transition_parameters[:destination][:opacity].is_a?(::Numeric) + if transition_parameters[:origin].is_a?(::Hash) + if transition_parameters[:origin][:opacity].is_a?(::Numeric) + self.gxg_merge_style({:opacity => transition_parameters[:origin][:opacity]}) + else + self.gxg_merge_style({:opacity => 0}) + end + else + self.gxg_merge_style({:opacity => 0}) + end + end + end + self.gxg_set_state(:hidden, false) + if block.respond_to?(:call) + unless transition_parameters[:options] + transition_parameters[:options] = {} + end + transition_parameters[:options][:animationend] = Proc.new do + block.call() + end + end + # + self.animate(transition_parameters) + else + self.gxg_set_state(:hidden, false) + if block.respond_to?(:call) + block.call() + end + end + # GxG::DISPLAY_DETAILS[:object].layout_refresh + true + end + # + def hide(transition_parameters=nil,&block) + if transition_parameters.is_any?(::Hash, ::GxG::Database::PersistedHash) + if transition_parameters.is_a?(::GxG::Database::PersistedHash) + transition_parameters = transition_parameters.unpersist + end + if block.respond_to?(:call) + unless transition_parameters[:options] + transition_parameters[:options] = {} + end + transition_parameters[:options][:animationend] = Proc.new do + self.gxg_set_state(:hidden, true) + block.call() + end + else + set_state = nil + if transition_parameters[:options] + if transition_parameters[:options][:animationend].respond_to?(:call) + old_complete = transition_parameters[:options][:animationend] + set_state = Proc.new do + old_complete.call() + self.gxg_set_state(:hidden, true) + end + else + set_state = Proc.new do + self.gxg_set_state(:hidden, true) + end + end + else + transition_parameters[:options] = {} + set_state = Proc.new do + self.gxg_set_state(:hidden, true) + end + end + if set_state + unless transition_parameters[:options].is_a?(::Hash) + transition_parameters[:options] = {} + end + transition_parameters[:options][:animationend] = set_state + end + end + # + self.animate(transition_parameters) + else + self.gxg_set_state(:hidden, true) + if block.respond_to?(:call) + block.call() + end + end + # GxG::DISPLAY_DETAILS[:object].layout_refresh + true + end + # + end + # -------------------------------------------------------------------------------------------------------------------------------------- + module ApplicationSupport + # + def find_parent_type(the_type=nil) + # return the first parent of type: the_type (Class, String, Symbol) + result = nil + if the_type.is_any?(::Class, ::String, ::Symbol) + if the_type.is_any?(::String, ::Symbol) + the_type = GxG::Gui::component_class(the_type.to_s.to_sym) + end + if the_type.is_a?(::Class) + search_queue = [(self)] + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.is_a?(the_type) + result = entry + break + end + search_queue << entry.parent + end + end + end + end + result + end + # deprecated - switch over to find_parent_type (eventually) + def viewport() + # find nearest viewport in object heirarchy. + self.find_parent_type(::GxG::Gui::ApplicationViewport) + end + # + def tree_node() + # find nearest :tree_node parent in object heirarchy. + self.find_parent_type(::GxG::Gui::TreeNode) + end + # + def tree() + # find nearest :tree parent in object heirarchy. + self.find_parent_type(::GxG::Gui::Tree) + end + # + def window() + # find nearest window in object heirarchy. + self.find_parent_type(::GxG::Gui::Window) + end + # + def find_parent(the_reference=nil) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + search_queue = [(self.parent)] + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + search_queue << entry.parent + end + end + end + result + end + # + def find_child_type(the_type=nil) + # return the first parent of type: the_type (Class, String, Symbol) + result = nil + if the_type.is_any?(::Class, ::String, ::Symbol) + if the_type.is_any?(::String, ::Symbol) + the_type = GxG::Gui::component_class(the_type.to_s.to_sym) + end + if the_type.is_a?(::Class) + search_queue = self.children.values + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.is_a?(the_type) + result = entry + break + end + end + end + end + end + result + end + # + def find_child(the_reference=nil) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + search_queue = self.children.values + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def build_interior_components(build_list=[]) + # You get ONE @content 'internal_component' (for now). + build_queue = [{:parent => nil, :record => {:content => build_list}, :element => self}] + while build_queue.size > 0 do + entry = build_queue.shift + if entry + if entry[:record][:content].size > 0 + entry[:record][:content].each do |sub_component| + element_class = GxG::Gui::component_class(sub_component[:component].to_s.to_sym) + if element_class + if sub_component.is_a?(::GxG::Database::PersistedHash) + the_options = sub_component[:options].unpersist() + the_uuid = sub_component.uuid.to_s.to_sym + if the_options[:title] + the_title = the_options.delete(:title) + else + the_title = sub_component.title.to_s + end + else + the_options = sub_component[:options].clone + the_uuid = ::GxG::uuid_generate.to_sym + if the_options[:title] + the_title = the_options.delete(:title) + else + the_title = "Untitled Component #{the_uuid.to_s}" + end + end + # + the_style = the_options.delete(:style) + the_states = (the_options.delete(:states) || {}) + the_options[:uuid] = the_uuid + if the_options[:track] + tracking = the_options.delete(:track) + else + tracking = nil + end + if entry[:element] == self + if entry[:element].respond_to?(:original_add_child) + new_entry = {:parent => entry[:element], :record => sub_component, :element => entry[:element].original_add_child((the_uuid), element_class, the_options)} + else + new_entry = {:parent => entry[:element], :record => sub_component, :element => entry[:element].add_child((the_uuid), element_class, the_options)} + end + else + new_entry = {:parent => entry[:element], :record => sub_component, :element => entry[:element].add_child((the_uuid), element_class, the_options)} + end + if the_title == "interior_component" + @content = new_entry[:element] + else + new_entry[:element].set_title(the_title) + end + # Window component tracking: + if tracking + the_window = new_entry[:element].window() + if the_window + the_window.track_deltas(the_uuid,new_entry[:element],tracking) + end + end + # Set states: + unless the_states.keys.include?(:hidden) + the_states[:hidden] = false + end + new_entry[:element].gxg_set_states(the_states) + # process style info: + if the_style.is_a?(::Hash) + new_entry[:element].gxg_set_style(the_style) + end + # + # Set Script if any is provided + if sub_component[:script].size > 0 + new_entry[:element].set_script(sub_component[:script].to_s) + end + build_queue << new_entry + end + end + end + end + end + # + end + # + end + # -------------------------------------------------------------------------------------------------------------------------------------- + module ThemeSupport + def page() + GxG::DISPLAY_DETAILS[:object] + end + def theme_icon(image_name=nil) + GxG::DISPLAY_DETAILS[:object].theme_icon(image_name) + end + # + def theme_background(image_name=nil) + GxG::DISPLAY_DETAILS[:object].theme_background(image_name) + end + # + def theme_widget(image_name=nil) + GxG::DISPLAY_DETAILS[:object].theme_widget(image_name) + end + # + def theme_font(font_name=nil) + GxG::DISPLAY_DETAILS[:object].theme_font(font_name) + end + # Asset retrieval support: + def site_asset(asset_path=nil) + GxG::DISPLAY_DETAILS[:object].site_asset(asset_path) + end + end + # -------------------------------------------------------------------------------------------------------------------------------------- + # Page objects and components: + # ### Components + # Universal to all components and elements: + # + # Override this method to return a Hash of styles. + # Hash-key is the CSS style name, hash-value is the CSS style value. + # def style;end + class Component < ::Ferro::Component::Base + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Header < ::Ferro::Component::Header + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Navigator < ::Ferro::Component::Navigation + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Section < ::Ferro::Component::Section + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Article < ::Ferro::Component::Article + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Aside < ::Ferro::Component::Aside + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Footer < ::Ferro::Component::Footer + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # ### Forms + # + class Form < ::Ferro::Form::Base + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + def form_data() + result = {} + search_queue = self.children.values + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.domtype == :input + result[(entry.title.to_s.to_sym)] = entry.value() + else + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + end + # + class Clickable < ::Ferro::Form::Clickable + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + ## Override this method to define what happens after + # element has been clicked. + # def clicked;end + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + end + # + class Fieldset < ::Ferro::Form::Fieldset + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Input < ::Ferro::Form::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + end + # + def uuid() + @uuid + end + # + # Creates an input element. + # In the DOM creates a: . + # Specify option :type to set the location. + # Use one of these values: + # :text, :password, :reset, :radio, :checkbox, :color, + # :date, :datetime-local, :email, :month, :number, + # :range, :search, :tel, :time, :url, :week. + # Or leave blank to create a :text input. + # def _before_create + # @options[:type] = :text + # super + # end + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + end + # + class TextInput < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :text + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class PasswordInput < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :password + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class ResetInput < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :reset + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class RadioButton < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :radio + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class ColorPicker < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :color + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class DatePicker < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :date + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class DateTimeLocal < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :"datetime-local" + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class EmailInput < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :email + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class MonthPicker < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :month + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class NumberInput < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :number + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class RangeInput < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :range + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class SearchInput < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :search + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class PhoneInput < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :tel + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class TimePicker < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :time + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class UrlInput < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :url + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class WeekPicker < ::GxG::Gui::Input + # + def _before_create + super() + @domtype = :input + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @options[:type] = :week + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + # + end + # + class Label < ::Ferro::Form::Label + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class TextArea < ::Ferro::Form::Textarea + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Output < ::Ferro::Form::Output + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class ButtonInput < ::Ferro::Form::Button + # + def disable() + @enabled = false + self.gxg_set_attribute(:disabled) + end + # + def enable() + @enabled = true + self.gxg_clear_attribute(:disabled) + end + # + def enabled?() + @enabled || false + end + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @enabled = @options.delete(:enabled) + unless @enabled == false + @enabled = true + end + super() + end + # + def _after_create + if @enabled == true + self.enable + else + self.disable + end + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class SubmitButton < ::Ferro::Form::Submit + # + def disable() + @enabled = false + self.gxg_set_attribute(:disabled) + end + # + def enable() + @enabled = true + self.gxg_clear_attribute(:disabled) + end + # + def enabled?() + @enabled || false + end + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @enabled = @options.delete(:enabled) + unless @enabled == false + @enabled = true + end + super() + end + # + def _after_create + if @enabled == true + self.enable + else + self.disable + end + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class ClickBlock < ::Ferro::Form::Block + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + ## Override this method to define what happens after + # element has been clicked. + # def clicked;end + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + end + # + class CheckBox < ::Ferro::Form::CheckBox + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + def is_set?() + # sleep 0.5 + if (`#{self.element}.checked == true`) + true + else + false + end + end + # + def set(the_state=false) + if the_state == true + `#{self.element}.checked = true` + else + `#{self.element}.checked = false` + end + self.is_set?() + end + # + end + # + class Selector < ::Ferro::Form::Select + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # ### Inline Elements + # + class Block < ::Ferro::Element::Block + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Text < ::Ferro::Element::Text + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class List < ::Ferro::Element::List + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + def initialize(the_name,the_class,the_options={}) + # + @items = [] + # Item: {:icon => "", :icon_width => 32, :icon_height => 32, :label => "", :uuid => ""} + @selection = nil + @select_responder = nil + @open_responder = nil + # + super(the_name, the_class, the_options) + self + end + # + def set_selection(uuid=nil) + @selection = uuid + self.children.values.each do |the_item| + if the_item.gxg_get_attribute('item-uuid') == uuid + the_item.select + else + the_item.unselect + end + end + uuid + end + # + def selection() + @selection + end + # + def set_select_responder(&block) + if block.respond_to?(:call) + @select_responder = block + end + end + # + def invoke_select_responder() + if @select_responder.respond_to?(:call) + @select_responder.call(@selection) + end + end + # + def set_open_responder(&block) + if block.respond_to?(:call) + @open_responder = block + end + end + # + def invoke_open_responder() + if @open_responder.respond_to?(:call) + @open_responder.call(@selection) + end + end + # + # + def add_list_item(item_details={}) + build_list = [] + # + if item_details[:row_style].is_a?(::Hash) + item_row = {:component=>"block_table_row", :options=>{:style => {:padding => "0px", :margin => "0px"}.merge(item_details[:row_style])}, :content=>[], :script=>""} + else + item_row = {:component=>"block_table_row", :options=>{:style => {:padding => "0px", :margin => "0px"}}, :content=>[], :script=>""} + end + if item_details[:icon] + icon_width = (item_details[:icon_width] || 32).to_i + icon_height = (item_details[:icon_height] || 32).to_i + item_icon = {:component=>"image", :options=>{:src=>(item_details[:icon]), :width=>icon_width, :height=>icon_height, :style => {:clear => "both"}}, :content=>[], :script=>""} + item_row[:content] << {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px", :"vertical-align" => "middle"}}, :content => [(item_icon)], :script => ""} + end + if item_details[:label].to_s.size > 0 + item_label = {:component=>"text", :options=> {:content => (item_details[:label].to_s), :style => {:padding => "0px 0px 2px 0px", :"font-size" => "16px"}}, :content => [], :script => ""} + if item_details[:label_cell_style].is_a?(::Hash) + item_row[:content] << {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px", :"vertical-align" => "middle"}.merge(item_details[:label_cell_style])}, :content => [(item_label)], :script => ""} + else + item_row[:content] << {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px", :"vertical-align" => "middle"}}, :content => [(item_label)], :script => ""} + end + end + if item_details[:content].is_any?(::Array, ::GxG::Database::PersistedArray) + if item_details[:content].size > 0 + item_details[:content].each do |the_component| + item_row[:content] << {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px"}}, :content => [(the_component)], :script => ""} + end + end + end + item_table = {:component=>"block_table", :options=>{:"item-uuid" => (item_details[:uuid].to_s), :style => {:padding => "0px", :margin => "0px"}}, :content=>[(item_row)], :script=>""} + item_table[:script] = " + def select() + @selected = true + self.gxg_merge_style({:'background-color' => '#87acd5'}) + end + def unselect() + @selected = false + self.gxg_merge_style({:'background-color' => '#f2f2f2'}) + end + on(:dblclick) do |event| + parent.set_selection(self.gxg_get_attribute('item-uuid')) + parent.invoke_open_responder() + end + on(:mouseup) do |event| + parent.set_selection(self.gxg_get_attribute('item-uuid')) + parent.invoke_select_responder() + end + " + # + build_list << item_table + # + @items << item_details + self.build_interior_components(build_list) + end + end + # + class OrderedList < ::Ferro::Element::List + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + @domtype = "ol" + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class ListItem < ::Ferro::Element::ListItem + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Anchor < ::Ferro::Element::Anchor + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class ExternalLink < ::Ferro::Element::ExternalLink + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Button < ::Ferro::Element::Button + # TODO: flesh out enable/disable methods for RadioButton and CheckBox as well. + def disable() + @enabled = false + self.gxg_set_attribute(:disabled) + end + # + def enable() + @enabled = true + self.gxg_clear_attribute(:disabled) + end + # + def enabled?() + @enabled || false + end + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + @enabled = @options.delete(:enabled) + unless @enabled == false + @enabled = true + end + super() + end + # + def _after_create + if @enabled == true + self.enable + else + self.disable + end + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # ### Misc. Elements + # + class Image < ::Ferro::Element::Image + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Video < ::Ferro::Element::Video + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Canvas < ::Ferro::Element::Canvas + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Table < ::Ferro::Component::Base + # + def _before_create + @domtype = :table + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class TableHeader < ::Ferro::Component::Base + def _before_create + @domtype = :th + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class TableRow < ::Ferro::Component::Base + def _before_create + @domtype = :tr + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class TableCell < ::Ferro::Component::Base + # + def _before_create + @domtype = :td + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class BlockTable < ::Ferro::Component::Base + # + def _before_create + @states = {:table => true} + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class BlockTableHeader < ::Ferro::Component::Base + # + def _before_create + @states = {:th => true} + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class BlockTableRow < ::Ferro::Component::Base + # + def _before_create + @states = {:tr => true} + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class BlockTableCell < ::Ferro::Component::Base + def _before_create + @states = {:td => true} + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + end + # + class Script < ::Ferro::Element::Script + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + # + end + # + class Tree < ::Ferro::Component::Base + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + alias :original_add_child :add_child + def add_child(name, element_class, options = {}) + if element_class == GxG::Gui::TreeNode + if @content + @content.add_child(name, element_class, options) + end + else + log_warn("Attempted to add a non-TreeNode object - ignoring: #{element_class.inspect}") + end + end + # + alias :original_children :children + def children() + if @content + @content.children + else + {} + end + end + # + def find_child(the_reference=nil, interior=false) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + if interior == true + search_queue = self.original_children.values + else + search_queue = self.children.values + end + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def initialize(the_name,the_class,the_options) + @content = nil + @selection = nil + @processors = {} + super(the_name, the_class, the_options) + self + end + # + def cascade() + # + self.build_interior_components([{:component=>"list", :options=>{:title => "interior_component", :style => {:"white-space" => "nowrap", :width => "auto", :height => "auto", :"list-style" => "none", :margin => "0px", :padding => "0px"}}, :content=>[], :script=>""}]) + # + end + # + def update_appearance() + # TODO ?? + end + # Selection / Processing: + def set_processor(the_type=nil, &block) + # :select, :expand, :collapse + if block.respond_to?(:call) + if [:select, :expand, :collapse].include?(the_type.to_s.to_sym) + @processors[(the_type.to_s.to_sym)] = block + else + log_warn("Unsupported processor type: #{the_type.to_s.to_sym.inspect}") + end + end + end + # + def node_at_path(the_path=nil) + result = nil + search_queue = self.children.values.clone + while search_queue.size > 0 + entry = search_queue.shift + if entry.is_a?(GxG::Gui::TreeNode) + if entry.node_path() == the_path + result = entry + break + else + entry.nodes.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def expand_path(the_path=nil,&block) + if the_path.is_a?(::String) + path_stack = [] + path_array = the_path.split("/") + path_array.each do |entry| + if entry.size > 0 + path_stack << entry + the_node = self.node_at_path(('/' + path_stack.join("/"))) + if the_node.is_a?(GxG::Gui::TreeNode) + if the_node.gxg_get_state(:expanded) == false + the_node.expand() + GxG::DISPATCHER.post_event(:display) do + self.expand_path(the_path,&block) + end + break + end + if the_node.node_path() == the_path + if block.respond_to?(:call) + block.call() + end + end + else + GxG::DISPATCHER.post_event(:display) do + self.expand_path(the_path,&block) + end + break + end + end + end + end + true + end + # + def selection() + @selection + end + # + def select(the_node=nil) + if the_node.is_a?(GxG::Gui::TreeNode) + if @selection != the_node + if @selection + @selection.unhighlight() + end + @selection = the_node + @selection.highlight() + # TODO: scroll to reveal highlighted node ?? + if @processors[:select].respond_to?(:call) + @processors[:select].call(@selection) + end + end + end + end + # + def expand(the_node=nil) + if the_node.is_a?(GxG::Gui::TreeNode) + if @processors[:expand].respond_to?(:call) + @processors[:expand].call(the_node) + end + end + end + # + def collapse(the_node=nil) + if the_node.is_a?(GxG::Gui::TreeNode) + if @processors[:collapse].respond_to?(:call) + @processors[:collapse].call(the_node) + end + end + end + end + # + class TreeNode < ::Ferro::Element::ListItem + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + if @data.is_a?(::Hash) + if @data[:title] + @title = @data[:title] + end + end + super() + end + # + def uuid() + @uuid + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + alias :original_add_child :add_child + def add_child(name, element_class, options = {}) + if element_class == GxG::Gui::TreeNode + if @content + @content.add_child(name, element_class, options) + end + else + self.original_add_child(name, element_class, options) + end + end + # + def nodes() + if @content + @content.children + else + {} + end + end + # + def node_path() + the_path = "" + lineage = [(self)] + the_parent = self.parent + while the_parent != nil do + if the_parent.is_a?(GxG::Gui::TreeNode) + lineage.unshift(the_parent) + end + the_parent = the_parent.parent + if the_parent.is_a?(GxG::Gui::Tree) + the_parent = nil + end + end + lineage.each do |the_node| + the_path = the_path + "/#{the_node.title}" + end + the_path + end + # + def find_child(the_reference=nil) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + search_queue = self.children.values + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def update_appearance() + if @appearance.is_any?(::Hash, ::GxG::Database::PersistedHash) + # + # TODO: update icon and label if different ?? + # + # @icon, @label component objects available. + true + else + false + end + end + # + def data() + @data + end + # + def appearance() + @appearance + end + # + def set_appearance(the_record=nil) + if the_record.is_any?(::Hash, ::GxG::Database::PersistedHash) + @appearance = the_record + # Look for :label, :icon w/in the hash record w/in the array. + self.update_appearance() + end + end + # + def initialize(the_name,the_class,the_options={}) + @content = nil + @appearance = {:icon => GxG::DISPLAY_DETAILS[:object].theme_icon("folder.svg"), :label => "Untitled"} + if the_options.is_any?(::Hash, ::GxG::Database::PersistedHash) + if the_options.is_a?(::GxG::Database::PersistedHash) + the_options = the_options.unpersist + else + the_options = the_options.clone + end + # Pass an Hash record containing at LEAST: :title, :type, and :content. + @data = (the_options.delete(:data) || {:title => "Untitled", :type => "virtual_directory", :content => {}}) + # select proper icon for @data[:type] value ?? (do in application!) + @appearance[:icon] = (the_options.delete(:icon) || GxG::DISPLAY_DETAILS[:object].theme_icon("folder.svg")).to_s + @appearance[:label] = (the_options.delete(:label) || @data[:title]).to_s + end + super(the_name, the_class, the_options) + self + end + # + def cascade() + # + node_frame = {:component=>"block_table", :options=>{:nodepath => (self.node_path()), :style => {}}, :content=>[], :script=>""} + # + frame_row_one = {:component=>"block_table_row", :options=>{}, :content=>[], :script=>""} + expander_object_title = "Object #{::GxG::uuid_generate.to_s}" + expander = {:component=>"image", :options=>{:title => (expander_object_title), :src=>(GxG::DISPLAY_DETAILS[:object].theme_widget("collapse.svg")), :width=>32, :height=>32, :style => {:clear => "both", :'vertical-align' => 'middle'}}, :content=>[], :script=>""} + expander[:script] = " + on(:mouseup) do |event| + the_node = self.tree_node() + if the_node + unless the_node.gxg_get_state(:disabled) == true + if self.gxg_get_state(:expanded) == true + self.gxg_set_attribute(:src, GxG::DISPLAY_DETAILS[:object].theme_widget('collapse.svg')) + self.gxg_set_state(:expanded,false) + the_node.collapse() + else + self.gxg_set_attribute(:src, GxG::DISPLAY_DETAILS[:object].theme_widget('expand.svg')) + self.gxg_set_state(:expanded,true) + the_node.expand() + end + end + end + end + " + node_expander_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :width => "32px", :'vertical-align' => 'middle'}}, :content=>[], :script=>""} + node_expander_cell[:content] = [(expander)] + # + icon_object_title = "Object #{::GxG::uuid_generate.to_s}" + icon = {:component=>"image", :options=>{:title => (icon_object_title), :src=>@appearance[:icon], :style => {:clear => "both", :width=>"32px", :height=>"32px", :'vertical-align' => 'middle'}}, :content=>[], :script=>""} + icon[:script] = " + on(:mouseup) do |event| + the_node = self.tree_node() + if the_node + the_tree = the_node.tree() + if the_tree + the_tree.select(the_node) + end + end + end + " + node_icon_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :width => "32px", :'vertical-align' => 'middle'}}, :content=>[], :script=>""} + node_icon_cell[:content] = [(icon)] + # + title_object_title = "Object #{::GxG::uuid_generate.to_s}" + title = {:component=>'label', :options=>{:title => (title_object_title), :content => @appearance[:label], :style => {:float => "left", :'font-size' => '16px', :'vertical-align' => 'middle', :'text-align' => 'left', :margin => "2px", :padding => "2px"}}, :content=>[], :script=>''} + title[:script] = " + on(:mouseup) do |event| + the_node = self.tree_node() + if the_node + the_tree = the_node.tree() + if the_tree + the_tree.select(the_node) + end + end + end + " + node_label_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :'vertical-align' => 'middle'}}, :content=>[], :script=>""} + node_label_cell[:content] = [(title)] + # + # frame_row_one[:content] = [(node_indent_cell),(node_expander_cell), (node_icon_cell), (node_label_cell)] + frame_row_one[:content] = [(node_expander_cell), (node_icon_cell), (node_label_cell)] + frame_row_one[:script] = " + on(:mouseenter) do |event| + the_node = self.tree_node() + if the_node + the_tree = the_node.highlight() + end + end + on(:mouseleave) do |event| + the_node = self.tree_node() + if the_node + the_tree = the_node.unhighlight() + end + end + " + # + sublist_object_title = "Object #{::GxG::uuid_generate.to_s}" + sublist = {:component=>"list", :options=>{:title => "interior_component", :width => "100%", :height => "100%", :style => {:"list-style" => "none"}}, :content=>[], :script=>""} + # + subnodes_frame_cell = {:component=>"block_table_cell", :options=>{}, :content=>[], :script=>""} + frame_row_two = {:component=>"block_table_row", :options=>{}, :content=>[], :script=>""} + subnodes_frame_cell[:content] = [(sublist)] + # frame_row_two[:content] = [(subnodes_indent_cell),(subnodes_frame_cell)] + frame_row_two[:content] = [(subnodes_frame_cell)] + # + node_frame[:content] = [(frame_row_one),(frame_row_two)] + # + self.build_interior_components([(node_frame)]) + # + # Review: content or sublist ?? + # @content = self.find_child(sublist_object_title) + @expander = self.find_child(expander_object_title) + @icon = self.find_child(icon_object_title) + @label = self.find_child(title_object_title) + # + end + # TODO: Add animation / transition effects. + def highlight(transition_parameters=nil) + if @label + @label.gxg_merge_style({:'background-color' => '#87acd5'}) + end + end + # + def unhighlight(transition_parameters=nil) + if @label + @label.gxg_merge_style({:'background-color' => '#f2f2f2'}) + end + end + # + def expand(transition_parameters=nil) + @content.show(transition_parameters) + self.gxg_set_state(:expanded, true) + if @expander + @expander.gxg_set_attribute(:src,GxG::DISPLAY_DETAILS[:object].theme_widget("expand.svg")) + end + the_tree = self.tree() + if the_tree + the_tree.expand(self) + end + end + # + def collapse(transition_parameters=nil) + @content.hide(transition_parameters) + self.gxg_set_state(:expanded, false) + if @expander + @expander.gxg_set_attribute(:src,GxG::DISPLAY_DETAILS[:object].theme_widget("collapse.svg")) + end + the_tree = self.tree() + if the_tree + the_tree.collapse(self) + end + end + # + def enable(transition_parameters=nil) + if transition_parameters.is_any?(::Hash, ::GxG::Database::PersistedHash) + if transition_parameters.is_a?(::GxG::Database::PersistedHash) + transition_parameters = transition_parameters.unpersist + end + else + transition_parameters = {:origin => {:opacity => 0.5}, :destination => {:opacity => 1.0}} + end + self.children.each_pair do |name, object| + unless name == :interior_component + object.gxg_set_state(:disabled, false) + object.animate(transition_parameters) + end + end + end + # + def disable(transition_parameters=nil) + if transition_parameters.is_any?(::Hash, ::GxG::Database::PersistedHash) + if transition_parameters.is_a?(::GxG::Database::PersistedHash) + transition_parameters = transition_parameters.unpersist + end + else + transition_parameters = {:origin => {:opacity => 1.0}, :destination => {:opacity => 0.5}} + end + self.children.each_pair do |name, object| + unless name == :interior_component + object.animate(transition_parameters) + object.gxg_set_state(:disabled, true) + end + end + end + # + end + # + class Panel < ::Ferro::Component::Base + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # + def cascade + super() + GxG::DISPLAY_DETAILS[:object].layout_refresh + end + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # TODO: Add auto-hide/hide/roll up-left-down-right capabilites with animations. + # ???? + end + # + class ApplicationViewport < ::Ferro::Component::Base + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + # + def uuid() + @uuid + end + # + def set_application(the_application) + @application = the_application + end + # + def application() + @application + end + # + def find_child(the_reference=nil) + result = nil + if the_reference + search_queue = [(self)] + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if GxG::valid_uuid?(the_reference) + if entry.children[(object_source.uuid())] + result = entry.children[(object_source.uuid())] + break + else + entry.gxg_each_child do |the_child| + search_queue << the_child + end + end + else + entry.gxg_each_child do |the_child| + if the_child.title == the_reference + result = the_child + break + else + search_queue << the_child + end + end + end + end + end + end + result + end + end + # + class Window < ::Ferro::Component::Base + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + def uuid() + @uuid + end + # Methods to override in window's script + def before_close(data=nil) + end + # + def close(data=nil) + end + # + def after_close(data=nil) + end + # + def before_open(data=nil) + end + # + def open(data=nil) + end + # + def after_open(data=nil) + end + # + def set_application(the_application) + @application = the_application + end + # + def application() + @application + end + # + # Support methods: + def commit_settings() + # conform to layout restrictions: + bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + # convert to % ?? + top_percent = (@top.to_f / bounds[:page_height].to_f) * 100.0 + left_percent = (@left.to_f / bounds[:page_width].to_f) * 100.0 + height_percent = (self.height.to_f / bounds[:page_height].to_f) * 100.0 + width_percent = (self.width.to_f / bounds[:page_width].to_f) * 100.0 + # + setting = {} + # + setting[:position] = "absolute" + setting[:display] = "block" + # setting[:overflow] = "hidden" + setting[:tabindex] = -1 + setting[:top] = "#{top_percent}%" + setting[:left] = "#{left_percent}%" + setting[:height] = "#{height_percent}%" + setting[:width] = "#{width_percent}%" + # + self.gxg_merge_style(setting) + if @title_object + @title_object.parent.gxg_merge_style({:padding => "0px 0px 0px #{((self.width() / 2) - ((@window_title.size / 2) * 8)) - 60}px"}) + end + if @content + @content.gxg_merge_style({:height => "#{self.height - (@title_margin + @menu_margin + @resize_margin)}px", :width => "#{self.width - (@resize_margin * 2)}px"}) + # update tracking objects + @tracking.each_pair do |selector, record| + new_style = {} + # + record[:details].each_pair do |op,value| + case op.to_s.to_sym + when :width + if value.to_s.include?("%") + # convert this % to px value width in new_style + new_style[:width] = (((value.to_s.gsub("%","").to_f / 100.0) * (self.width - (@resize_margin * 2)).to_f).to_i.to_s + "px") + else + # assume numeric px value + new_style[:width] = "#{value.to_s}px" + end + when :height + if value.to_s.include?("%") + # convert this % to px value width in new_style + new_style[:height] = (((value.to_s.gsub("%","").to_f / 100.0) * (self.height - (@title_margin + @menu_margin + @resize_margin)).to_f).to_i.to_s + "px") + else + # assume numeric px value + new_style[:height] = "#{value.to_s}px" + end + end + end + # + record[:object].gxg_merge_style(new_style) + end + end + if @menu_bar + @menu_area.gxg_merge_style({:width => "#{self.width - (@resize_margin * 2)}px"}) + # @menu_bar.gxg_merge_style({:width => "100%"}) + if @menu_bar.respond_to?(:update_appearance) + @menu_bar.update_appearance() + end + end + end + # + def track_deltas(the_reference=nil, the_object=nil, details={}) + if the_reference && the_object && details.is_any?(::Hash, ::GxG::Database::PersistedHash) + @tracking[(the_reference)] = {:object => the_object, :details => details} + true + else + false + end + end + def untrack_deltas(the_reference=nil) + if the_reference + @tracking.delete(the_reference) + true + else + false + end + end + # + def content() + @content + end + # + def content_width() + if @content + (`#{@content.element}.getBoundingClientRect().right - #{@content.element}.getBoundingClientRect().left`).to_i + else + 0 + end + end + # + def content_height() + if @content + (`#{@content.element}.getBoundingClientRect().bottom - #{@content.element}.getBoundingClientRect().top`).to_i + else + 0 + end + end + # + def set_top(the_top=nil) + if the_top.is_a?(::Numeric) + if @bottom - the_top >= 100 + if the_top >= GxG::DISPLAY_DETAILS[:object].layout_content_area()[:top] + @top = the_top + self.fit_within_boundaries() + commit_settings + end + end + end + end + # + def top() + @top + end + # + def set_left(the_left=nil) + if the_left.is_a?(::Numeric) + if @right - the_left >= 200 + if the_left >= GxG::DISPLAY_DETAILS[:object].layout_content_area()[:left] + @left = the_left + self.fit_within_boundaries() + commit_settings + end + end + end + end + # + def left() + @left + end + # + def set_right(the_right=nil) + if the_right.is_a?(::Numeric) + if the_right - @left >= 200 + if the_right <= GxG::DISPLAY_DETAILS[:object].layout_content_area()[:right] + @right = the_right + self.fit_within_boundaries() + commit_settings + end + end + end + end + # + def right() + @right + end + # + def set_bottom(the_bottom=nil) + if the_bottom.is_a?(::Numeric) + if the_bottom - @top >= 100 + if the_bottom <= GxG::DISPLAY_DETAILS[:object].layout_content_area()[:bottom] + @bottom = the_bottom + self.fit_within_boundaries() + commit_settings + end + end + end + end + # + def bottom() + @bottom + end + # + def set_width(the_width=nil) + if the_width.is_a?(::Numeric) + if the_width >= 200 + if (@left + the_width) <= GxG::DISPLAY_DETAILS[:object].layout_content_area()[:right] + @right = (@left + the_width) + self.fit_within_boundaries() + commit_settings + end + end + end + end + # + def width() + (@right - @left) + end + # + def set_height(the_height=nil) + if the_height.is_a?(::Numeric) + if the_height >= 100 + if (@top + the_height) <= GxG::DISPLAY_DETAILS[:object].layout_content_area()[:bottom] + @bottom = (@top + the_height) + self.fit_within_boundaries() + commit_settings + end + end + end + end + # + def height() + (@bottom - @top) + end + # + def set_position(the_x=nil,the_y=nil) + the_width = self.width + the_height = self.height + bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + if ((bounds[:top])..(bounds[:bottom])).include?(the_y.to_i) && ((bounds[:top])..(bounds[:bottom])).include?((the_y.to_i + the_height)) + @top = (the_y.to_i) + end + if ((bounds[:left])..(bounds[:right])).include?(the_x.to_i) && ((bounds[:left])..(bounds[:right])).include?((the_x.to_i + the_width)) + @left = (the_x.to_i) + end + @bottom = (@top + the_height) + @right = (@left + the_width) + self.fit_within_boundaries() + # commit_settings + # convert to % ?? + # top_percent = (@top.to_f / bounds[:page_height].to_f) * 100.0 + # left_percent = (@left.to_f / bounds[:page_width].to_f) * 100.0 + # self.gxg_merge_style({:left => "#{left_percent}%", :top => "#{top_percent}%"}) + self.commit_settings + end + # + def set_frame(settings=nil) + if settings.is_a?(::Hash) + @top = (settings[:top] || @top) + @left = (settings[:left] || @left) + @right = (settings[:right] || @right) + @bottom = (settings[:bottom] || @bottom) + self.fit_within_boundaries() + self.commit_settings + end + end + # + def position() + {:x => @left, :y => @top} + end + # + def fit_within_boundaries() + the_width = self.width + the_height = self.height + bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + if @top < bounds[:top] + @top = bounds[:top] + end + if @left < bounds[:left] + @left = bounds[:left] + end + @right = @left + the_width + @bottom = @top + the_height + if @bottom > bounds[:bottom] + @bottom = bounds[:bottom] + end + if @right > bounds[:right] + @right = bounds[:right] + end + end + # + def set_window_title(the_title=nil) + if @title_object + if the_title.is_a?(::String) + @window_title = the_title + @title_object.set_text(@window_title) + @title_object.parent.gxg_merge_style({:padding => "0px 0px 0px #{((self.width() / 2) - ((@window_title.size / 2) * 8)) - 60}px"}) + end + end + end + # + def window_title() + if @title_object + @window_title + else + nil + end + end + # + def special_state() + @special_state + end + # + def set_special_state(settings=nil) + if settings.is_a?(::Hash) + @special_state = settings + @top = (settings[:top] || @top) + @left = (settings[:left] || @left) + @right = (settings[:right] || @right) + @bottom = (settings[:bottom] || @bottom) + self.fit_within_boundaries() + self.commit_settings + end + end + # + def clear_special_state(settings=nil) + @special_state = nil + if settings.is_a?(::Hash) + self.set_frame(settings) + end + end + # Initialization and Setup + def initialize(the_name,the_class,the_options) + # FIXME: work around for strange bug in layout_refresh + ::GxG::DISPLAY_DETAILS[:object].layout_refresh + bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + # + @special_state = nil + @content = nil + # Process the_options + unless the_options.is_a?(::Hash) + the_options = {} + end + # So, at this point I've decided to use pixels internally, but percentages externally. (interface) + # You can pass Integer (as Pixels), Float, or String Percentage + if the_options[:top].is_a?(::String) || the_options[:top].to_s.include?(".") + if the_options[:top].is_a?(::String) + @top = (bounds[:page_height] * (the_options.delete(:top).gsub("%","").to_f / 100.0)) + else + @top = (bounds[:page_height] * (the_options.delete(:top) / 100.0)) + end + else + @top = (the_options.delete(:top) || ((`window.innerHeight`.to_i / 2) - 50)) + end + if the_options[:left].is_a?(::String) || the_options[:left].to_s.include?(".") + if the_options[:left].is_a?(::String) + @left = (bounds[:page_width] * (the_options.delete(:left).gsub("%","").to_f / 100.0)) + else + @left = (bounds[:page_width] * (the_options.delete(:left) / 100.0)) + end + else + @left = (the_options.delete(:left) || ((`window.innerWidth`.to_i / 2) - 100)) + end + if the_options[:right].is_a?(::String) || the_options[:right].to_s.include?(".") + if the_options[:right].is_a?(::String) + @right = (bounds[:page_width] * (the_options.delete(:right).gsub("%","").to_f / 100.0)) + else + @right = (bounds[:page_width] * (the_options.delete(:right) / 100.0)) + end + else + @right = (the_options.delete(:right) || ((`window.innerWidth`.to_i / 2) + 100)) + end + if the_options[:bottom].is_a?(::String) || the_options[:bottom].to_s.include?(".") + if the_options[:bottom].is_a?(::String) + @bottom = (bounds[:page_height] * (the_options.delete(:bottom).gsub("%","").to_f / 100.0)) + else + @bottom = (bounds[:page_height] * (the_options.delete(:bottom) / 100.0)) + end + else + @bottom = (the_options.delete(:bottom) || ((`window.innerHeight`.to_i / 2) + 50)) + end + # + @window_title = the_options[:window_title] || "Untitled" + @title_object = nil + # + if the_options[:width].is_a?(::String) || the_options[:width].to_s.include?(".") + if the_options[:width].is_a?(::String) + @right = @left + ((bounds[:right] - bounds[:left]) * (the_options.delete(:width).gsub("%","").to_f / 100.0)) + else + @right = @left + ((bounds[:right] - bounds[:left]) * (the_options.delete(:width) / 100.0)) + end + else + if the_options[:width].is_a?(::Numeric) + @right = (@left + the_options.delete(:width)) + end + end + if the_options[:height].is_a?(::String) || the_options[:height].to_s.include?(".") + if the_options[:height].is_a?(::String) + @bottom = @top + ((bounds[:bottom] - bounds[:top]) * (the_options.delete(:height).gsub("%","").to_f / 100.0)) + else + @bottom = @top + ((bounds[:bottom] - bounds[:top]) * (the_options.delete(:height) / 100.0)) + end + else + if the_options[:height].is_a?(::Numeric) + @bottom = (@top + the_options.delete(:height)) + end + end + # Bounds check top, left, right, and bottom + self.fit_within_boundaries() + # + @modal = (the_options[:modal] || false) + @scrolling = the_options.delete(:scroll) + @title_margin = 24 + if @modal == true + @resize = false + @resize_margin = 0 + else + @resize = (the_options[:resize] || true) + @resize_margin = 2 + @title_margin = 26 + end + if @modal == true + @closebox = false + @minimizebox = false + @maximizebox = false + else + if the_options[:close] == false + @closebox = false + else + @closebox = (the_options[:close] || :enabled).to_s.to_sym + end + if the_options[:minimize] == false + @minimizebox = false + else + @minimizebox = (the_options[:minimize] || :enabled).to_s.to_sym + end + if the_options[:maximize] == false + @maximizebox = false + else + @maximizebox = (the_options[:maximize] || :enabled).to_s.to_sym + end + end + # + @tracking = {} + # + @menu_reference = the_options.delete(:menu) + @menu_area = nil + @menu_bar = nil + if @menu_reference + @menu_margin = 16 + else + @menu_margin = 0 + end + # + super(the_name,the_class,the_options) + # + # self.gxg_set_attribute(:draggable,true) + # + self + end + # + def menu_reference() + @menu_reference + end + # + def menu() + @menu_bar + end + # + def set_menu(the_menu_source=nil) + if the_menu_source.is_any?(::Hash, ::GxG::Database::PersistedHash) + if @menu_area + page.build_components([{:parent => @menu_area.parent, :record => {:content => [(the_menu_source)]}, :element => @menu_area}]) + # + @menu_margin = (`#{@menu_area.element}.getBoundingClientRect().bottom - #{@menu_area.element}.getBoundingClientRect().top`.to_i) + # + @menu_bar = self.find_child(@menu_reference,true) + # + if @menu_bar + self.commit_settings + true + else + log_warn("Unable to properly initialize window menu #{@menu_reference.inspect} .") + false + end + else + false + end + else + false + end + end + # + alias :original_add_child :add_child + def add_child(name, element_class, options = {}) + if @content + @content.add_child(name, element_class, options) + end + end + # + alias :original_children :children + def children() + if @content + @content.children + else + {} + end + end + # + def find_child(the_reference=nil, interior=false) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + if interior == true + search_queue = self.original_children.values + else + search_queue = self.children.values + end + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def cascade() + commit_settings + # + build_list = [] + # + # Define Title Bar + # FIXME: CSS override of image :src does NOT work -- research. + # title_object_title = "Object #{::GxG::uuid_generate.to_s}" + the_title = {:component=>"label", :options=>{:title => "title_object", :content => @window_title, :style => {:width => "100%", :"font-size" => "16px", :"text-align" => "center"}}, :content=>[], :script=>""} + # + close_box = {:component=>"image", :options=>{:src => theme_widget("close.png"), :width=>20, :height=>20, :style => {:padding => "2px"}}, :content=>[], :script=>""} + close_box[:script] = " + on(:mouseup) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('close.png')) + the_window = self.window + if the_window + if the_window.application + the_window.application.window_close({:window => the_window.uuid}) + else + GxG::DISPLAY_DETAILS[:object].window_close(the_window.uuid) + end + else + log_warn('Window Not Found') + end + end + end + on(:mousedown) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('close_mousedown.png')) + end + end + on(:mouseenter) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('close_mouseenter.png')) + end + end + on(:mouseleave) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('close.png')) + end + end + " + minimize_box = {:component=>"image", :options=>{:src => theme_widget("minimize.png"), :width=>20, :height=>20, :style => {:padding => "2px"}}, :content=>[], :script=>""} + minimize_box[:script] = " + on(:mouseup) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('minimize.png')) + the_window = self.window + if the_window + special = the_window.special_state() + if special.is_a?(::Hash) + if special[:action] == :minimize + GxG::DISPLAY_DETAILS[:object].window_restore(the_window.uuid) + else + GxG::DISPLAY_DETAILS[:object].window_minimize(the_window.uuid,{:top => the_window.top, :left => the_window.left, :right => the_window.right, :bottom => the_window.bottom}) + end + else + GxG::DISPLAY_DETAILS[:object].window_minimize(the_window.uuid,{:top => the_window.top, :left => the_window.left, :right => the_window.right, :bottom => the_window.bottom}) + end + end + end + end + on(:mousedown) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('minimize_mousedown.png')) + end + end + on(:mouseenter) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('minimize_mouseenter.png')) + end + end + on(:mouseleave) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('minimize.png')) + end + end + " + maximize_box = {:component=>"image", :options=>{:src => theme_widget("maximize.png"), :width=>20, :height=>20, :style => {:padding => "2px"}}, :content=>[], :script=>""} + maximize_box[:script] = " + on(:mouseup) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('maximize.png')) + the_window = self.window + if the_window + special = the_window.special_state() + if special.is_a?(::Hash) + if special[:action] == :maximize + GxG::DISPLAY_DETAILS[:object].window_restore(the_window.uuid) + else + GxG::DISPLAY_DETAILS[:object].window_maximize(the_window.uuid,{:top => the_window.top, :left => the_window.left, :right => the_window.right, :bottom => the_window.bottom}) + end + else + GxG::DISPLAY_DETAILS[:object].window_maximize(the_window.uuid,{:top => the_window.top, :left => the_window.left, :right => the_window.right, :bottom => the_window.bottom}) + end + end + end + end + on(:mousedown) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('maximize_mousedown.png')) + end + end + on(:mouseenter) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('maximize_mouseenter.png')) + end + end + on(:mouseleave) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('maximize.png')) + end + end + " + table = {:component=>"block_table", :options=>{:style => {:clear => "both", :height=>"24px", :padding => "0px", :margin => "0px", :"border-radius" => "5px 5px 0px 0px", :"border-bottom" => "1px solid #c2c2c2"}, :states => {:"default-background-color" => true}}, :content=>[], :script=>""} + row = {:component=>"block_table_row", :options=>{:style => {:padding => "0px", :margin => "0px", :"vertical-align" => "middle", :"background-color" => "transparent"}}, :content=>[], :script=>""} + cell_one = {:component=>"block_table_cell", :options=>{:style => {:padding => "0px", :margin => "0px", :"vertical-align" => "middle", :"background-color" => "transparent"}}, :content=>[], :script=>""} + cell_two = {:component=>"block_table_cell", :options=>{:style => {:width => "100%",:padding => "0px 0px 0px #{((self.width() / 2) - ((@window_title.size / 2) * 8)) - 60}px", :"vertical-align" => "middle", :margin => "0px", :"background-color" => "transparent"}}, :content=>[], :script=>""} + cell_three = {:component=>"block_table_cell", :options=>{:style => {:float => "right", :padding => "0px", :"vertical-align" => "middle", :margin => "0px", :"background-color" => "transparent"}}, :content=>[], :script=>""} + # link pieces + if @closebox + if @closebox == :disabled + close_box[:options][:src] = theme_widget("close_disable.png") + close_box[:options][:states][:disabled] = true + end + cell_one[:content] << close_box + end + if @minimizebox + if @minimizebox == :disabled + minimize_box[:options][:src] = theme_widget("minimize_disable.png") + minimize_box[:options][:states][:disabled] = true + end + cell_one[:content] << minimize_box + end + if @maximizebox + if @maximizebox == :disabled + maximize_box[:options][:src] = theme_widget("maximize_disable.png") + maximize_box[:options][:states][:disabled] = true + end + cell_one[:content] << maximize_box + end + cell_two[:content] = [(the_title)] + cell_two[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + # + if @window_handle + unless @last_mouse_x + @last_mouse_x = event[:pageX] + end + unless @last_mouse_y + @last_mouse_y = event[:pageY] + end + offset_x = (event[:pageX] - @window_handle.left) + if event[:pageY] < @window_handle.top + offset_y = (@window_handle.top - event[:pageY]) + else + offset_y = (event[:pageY] - @window_handle.top) + end + new_x = ((event[:pageX] - offset_x) + (event[:pageX] - @last_mouse_x)) + new_y = ((event[:pageY] - offset_y) + (event[:pageY] - @last_mouse_y)) + @window_handle.set_position(new_x,new_y) + @last_mouse_x = event[:pageX] + @last_mouse_y = event[:pageY] + end + end + end + on(:dblclick) do |event| + the_window = self.window + if the_window + special = the_window.special_state() + if special.is_a?(::Hash) + if special[:action] == :rollup + GxG::DISPLAY_DETAILS[:object].window_restore(the_window.uuid) + else + GxG::DISPLAY_DETAILS[:object].window_rollup(the_window.uuid,{:top => the_window.top, :left => the_window.left, :right => the_window.right, :bottom => the_window.bottom}) + end + else + GxG::DISPLAY_DETAILS[:object].window_rollup(the_window.uuid,{:top => the_window.top, :left => the_window.left, :right => the_window.right, :bottom => the_window.bottom}) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousemove) do |event| + @window_handle = self.window + if ((event[:buttons] & 1) == 1) + @mode = :drag + do_drag(event) + else + @mode = :hover + end + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + row[:content] = [(cell_one), (cell_two), (cell_three)] + table[:content] = [(row)] + # + build_list << table + # + interior_object_title = "Object #{::GxG::uuid_generate.to_s}" + interior_component = {:component=>"block", :options=>{:title => "interior_component", :style => {:clear => "both", :padding => "0px", :margin => "0px", :border => "0px", :width => "#{(self.width - (@resize_margin * 2))}px", :height => "#{self.height - (@title_margin + @menu_margin + @resize_margin)}px"}, :states => {:"default-background-color" => true}}, :content=>[], :script=>""} + # + if @scrolling + if @scrolling == true + interior_component[:options][:style][:overflow] = "scroll" + else + if @scrolling == "vertical" + interior_component[:options][:style][:"overflow-y"] = "scroll" + end + if @scrolling == "horizontal" + interior_component[:options][:style][:"overflow-x"] = "scroll" + end + end + else + interior_component[:options][:style][:overflow] = "hidden" + end + # + left_resizer = {:component => "block_table_cell", :options => {:style => {:width => "2px", :cursor => "w-resize"}}, :content => [], :script => ""} + left_resizer[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + window_handle = self.window + if window_handle + window_handle.set_left(event[:pageX]) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + right_resizer = {:component => "block_table_cell", :options => {:style => {:width => "2px", :cursor => "e-resize"}}, :content => [], :script => ""} + right_resizer[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + window_handle = self.window + if window_handle + window_handle.set_right(event[:pageX] + 1) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + bottom_resizer = {:component => "block_table_cell", :options => {:style => {:height => "2px", :cursor => "s-resize"}}, :content => [], :script => ""} + bottom_resizer[:script] = " + def do_drag(event) + window_handle = self.window + if window_handle + if ((event[:buttons] & 1) == 1) + window_handle.set_bottom(event[:pageY] + 4) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + sw_resizer = {:component => "block_table_cell", :options => {:style => {:width => "2px", :height => "2px", :cursor => "sw-resize"}}, :content => [], :script => ""} + sw_resizer[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + window_handle = self.window + if window_handle + window_handle.set_left(event[:pageX] - 2) + window_handle.set_bottom(event[:pageY] + 4) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + se_resizer = {:component => "block_table_cell", :options => {:style => {:width => "2px", :height => "2px", :cursor => "se-resize"}}, :content => [], :script => ""} + se_resizer[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + window_handle = self.window + if window_handle + window_handle.set_right(event[:pageX] + 2) + window_handle.set_bottom(event[:pageY] + 4) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + left_menu_resize = {:component => "block_table_cell", :options => {:style => {:width => "2px", :cursor => "w-resize"}}, :content => [], :script => ""} + left_menu_resize[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + window_handle = self.window + if window_handle + window_handle.set_left(event[:pageX]) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + right_menu_resize = {:component => "block_table_cell", :options => {:style => {:width => "2px", :cursor => "e-resize"}}, :content => [], :script => ""} + right_menu_resize[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + window_handle = self.window + if window_handle + window_handle.set_right(event[:pageX] + 1) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + # menu_object_title = "Object #{::GxG::uuid_generate.to_s}" + menu_cell = {:component => "block_table_cell", :options => {:title => "menu_area", :style => {:overflow => "hidden", :height=>"16px",:padding => "0px", :margin => "0px"}}, :content => [], :script => ""} + # menu_row_object_title = "Object #{::GxG::uuid_generate.to_s}" + menu_row = {:component=>"block_table_row", :options=>{:title => "menu_area_row", :style => {:overflow => "hidden", :clear => "both", :padding => "0px", :width => "100%", :height=>"16px", :margin => "0px", :"vertical-align" => "middle", :"background-color" => "transparent", :"border-bottom" => "1px solid #c2c2c2"}}, :content => [(left_menu_resize),(menu_cell),(right_menu_resize)], :script=>""} + content_area = {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px", :border => "0px"}}, :content => [], :script => ""} + row_one = {:component => "block_table_row", :options => {}, :content => [], :script => ""} + row_two = {:component => "block_table_row", :options => {}, :content => [], :script => ""} + # window_body_title = "Object #{::GxG::uuid_generate.to_s}" + resizing_areas = {:component => "block_table", :options => {:title => "window_body", :style => {:width => "100%"}, :states => {:"default-background-color" => true}}, :content => [], :script => ""} + # link pieces + content_area[:content] = [(interior_component)] + if @resize + row_one[:content] = [(left_resizer),(content_area),(right_resizer)] + row_two[:content] = [(sw_resizer),(bottom_resizer),(se_resizer)] + if @menu_reference + resizing_areas[:content] = [(menu_row),(row_one),(row_two)] + else + resizing_areas[:content] = [(row_one),(row_two)] + end + else + if @menu_reference + row_one[:content] = [(content_area)] + resizing_areas[:content] = [(menu_row),(row_one)] + else + row_one[:content] = [(content_area)] + resizing_areas[:content] = [(row_one)] + end + end + # + build_list << resizing_areas + # + self.build_interior_components(build_list) + # + # the_object = self.find_child(interior_object_title,true) + # if the_object + # @content = the_object + # end + # + # the_object = self.find_child(window_body_title,true) + # if the_object + # @window_body = the_object + # end + # + the_object = self.find_child("title_object",true) + if the_object + @title_object = the_object + end + # + the_object = self.find_child("menu_area",true) + if the_object + @menu_area = the_object + @menu_margin = (`#{@menu_area.element}.getBoundingClientRect().bottom - #{@menu_area.element}.getBoundingClientRect().top`.to_i) + # border-collapse: collapse + if @menu_area + @menu_area.parent.parent.gxg_merge_style({:"border-collapse" => "collapse"}) + end + else + # test: log_warn("Unable to establish Menu Area link internally.") + end + # FIXME: does not set correct size of content by default (!) (workaround here:) + # self.set_bottom(self.bottom()) + # (result: no effect) + # other attempt: (THIS worked!) + GxG::DISPATCHER.post_event do + self.commit_settings + end + self + end + end + # + class DialogBox < ::Ferro::Component::Base + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + include GxG::Gui::EventHandlers + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + # + def uuid() + @uuid + end + # Methods to override in window's script + def before_close(data=nil) + # override in window's script + end + # + def after_close(data=nil) + # override in window's script + end + # + def before_open(data=nil) + # override in window's script + end + # + def after_open(data=nil) + # override in window's script + end + # + def set_application(the_application) + @application = the_application + end + # + def application() + @application + end + # + def set_window_title(the_title=nil) + if @title_object + if the_title.is_a?(::String) + @window_title = the_title + @title_object.set_text(@window_title) + end + end + end + # + def window_title() + if @title_object + @window_title + else + nil + end + end + # + def set_data(data={}) + @data = data + end + # + def data() + @data + end + # + def set_responder(block=nil) + if block.respond_to?(:call) + @responder = block + end + end + # + def respond(data=nil) + self.page.dialog_close() + if @responder.respond_to?(:call) + @responder.call(data) + end + end + # Geometry details: + def top() + @top + end + # + def left() + @left + end + # + def right() + @right + end + # + def bottom() + @bottom + end + # + def width() + (@right - @left) + end + # + def height() + (@bottom - @top) + end + # + def position() + {:x => @left, :y => @top} + end + # + def layer() + @layer + end + # + # Initialization and Setup + def initialize(the_name,the_class,the_options) + # FIXME: work around for strange bug in layout_refresh + # ::GxG::DISPLAY_DETAILS[:object].layout_refresh + # bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + # + @special_state = nil + @content = nil + # Process the_options + unless the_options.is_a?(::Hash) + the_options = {} + end + # So, at this point I've decided to use pixels internally, but percentages externally. (interface) + # You can pass Integer (as Pixels), Float, or String Percentage + the_width = nil + if the_options[:width].is_a?(::String) || the_options[:width].to_s.include?(".") + if the_options[:width].is_a?(::String) + the_width = ((`window.innerWidth`.to_f) * (the_options.delete(:width).gsub("%","").to_f / 100.0)) + else + the_width = ((`window.innerWidth`.to_f) * (the_options.delete(:width) / 100.0)) + end + else + if the_options[:width].is_a?(::Numeric) + the_width = the_options.delete(:width).to_f + end + end + if the_width + @left = ((`window.innerWidth`.to_f / 2.0) - (the_width / 2.0)) + @right = ((`window.innerWidth`.to_f / 2.0) + (the_width / 2.0)) + end + # + the_height = nil + if the_options[:height].is_a?(::String) || the_options[:height].to_s.include?(".") + if the_options[:height].is_a?(::String) + the_height = ((`window.innerHeight`.to_f) * (the_options.delete(:height).gsub("%","").to_f / 100.0)) + else + the_height = ((`window.innerHeight`.to_f) * (the_options.delete(:height) / 100.0)) + end + else + if the_options[:height].is_a?(::Numeric) + the_height = the_options.delete(:height).to_f + end + end + if the_height + @top = ((`window.innerHeight`.to_f / 2.0) - (the_height / 2.0)) + @bottom = ((`window.innerHeight`.to_f / 2.0) + (the_height / 2.0)) + end + # + if the_options[:top].is_a?(::String) || the_options[:top].to_s.include?(".") + if the_options[:top].is_a?(::String) + @top = (`window.innerHeight`.to_f * (the_options.delete(:top).gsub("%","").to_f / 100.0)) + else + @top = (`window.innerHeight`.to_f * (the_options.delete(:top) / 100.0)) + end + else + unless the_height + @top = (the_options.delete(:top) || ((`window.innerHeight`.to_i / 2) - 150)) + end + end + if the_options[:left].is_a?(::String) || the_options[:left].to_s.include?(".") + if the_options[:left].is_a?(::String) + @left = (`window.innerWidth`.to_f * (the_options.delete(:left).gsub("%","").to_f / 100.0)) + else + @left = (`window.innerWidth`.to_f * (the_options.delete(:left) / 100.0)) + end + else + unless the_width + @left = (the_options.delete(:left) || ((`window.innerWidth`.to_i / 2) - 300)) + end + end + if the_options[:right].is_a?(::String) || the_options[:right].to_s.include?(".") + if the_options[:right].is_a?(::String) + @right = (`window.innerWidth`.to_f * (the_options.delete(:right).gsub("%","").to_f / 100.0)) + else + @right = (`window.innerWidth`.to_f * (the_options.delete(:right) / 100.0)) + end + else + unless the_width + @right = (the_options.delete(:right) || ((`window.innerWidth`.to_i / 2) + 300)) + end + end + if the_options[:bottom].is_a?(::String) || the_options[:bottom].to_s.include?(".") + if the_options[:bottom].is_a?(::String) + @bottom = (`window.innerHeight`.to_f * (the_options.delete(:bottom).gsub("%","").to_f / 100.0)) + else + @bottom = (`window.innerHeight`.to_f * (the_options.delete(:bottom) / 100.0)) + end + else + unless the_height + @bottom = (the_options.delete(:bottom) || ((`window.innerHeight`.to_i / 2) + 150)) + end + end + # + @window_title = the_options[:window_title] || "Untitled" + @title_object = nil + # + @opacity = (the_options.delete(:opacity) || 0.8) + @background_color = (the_options.delete(:"background-color") || "rgba(0,0,0,#{@opacity})") + @layer = 2000 + # + @tracking = {} + @responder = nil + @data = {} + # + super(the_name,the_class,the_options) + # + self.gxg_set_attribute(:draggable,false) + # + self + end + # Slight of hand: + alias :original_add_child :add_child + def add_child(name, element_class, options = {}) + if @content + @content.add_child(name, element_class, options) + end + end + # + alias :original_children :children + def children() + if @content + @content.children + else + {} + end + end + # + def find_child(the_reference=nil, interior=false) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + if interior == true + search_queue = self.original_children.values + else + search_queue = self.children.values + end + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + # Construction + def cascade() + # Initialize Style Settings: + self.gxg_merge_style({:position => "absolute", :top => "0px", :left => "0px", :width => "100%", :height => "100%", :"background-color" => @background_color, :"z-index" => @layer}) + mouse_event_capture = " + on(:mouseup,true) do |event| + end + on(:mousedown,true) do |event| + end + on(:mousemove,true) do |event| + end + on(:mouseenter,true) do |event| + end + on(:mouseleave,true) do |event| + end + " + self.set_script(mouse_event_capture) + # Prep build_list + build_list = [] + # + modal_frame = {:component=>"block_table", :options=>{:style => {:opacity => 1.0, :clear => "both", :overflow => "hidden", :position => "absolute", :top => "#{@top}px", :left => "#{@left}px", :width => "#{self.width}px", :height=>"#{self.height}px", :padding => "0px", :margin => "0px", :"border-radius" => "5px 5px 5px 5px", :"z-index" => (@layer + 100)}, :states => {:"default-background-color" => true}}, :content=>[], :script=>""} + title_object_title = "Object #{::GxG::uuid_generate.to_s}" + the_title = {:component=>"label", :options=>{:title => title_object_title, :content => @window_title, :style => {:opacity => 1.0, :width => "100%", :"font-size" => "16px", :"text-align" => "center"}}, :content=>[], :script=>""} + title_cell = {:component=>"block_table_cell", :options=>{:style => {:opacity => 1.0, :width => "100%", :padding => "0px 0px 0px #{((self.width() / 2) - ((@window_title.size / 2) * 8))}px", :margin => "0px", :"vertical-align" => "middle", :"background-color" => "transparent"}}, :content=>[(the_title)], :script=>""} + title_row = {:component=>"block_table_row", :options=>{:style => {:opacity => 1.0, :clear => "both", :padding => "0px", :width => "100%", :height=>"24px", :margin => "0px", :"vertical-align" => "middle", :"background-color" => "#ffffff", :"border-bottom" => "1px solid #c2c2c2"}}, :content=>[(title_cell)], :script=>""} + # + interior_object_title = "Object #{::GxG::uuid_generate.to_s}" + interior_component = {:component=>"block_table_cell", :options=>{:title => "interior_component", :style => {:opacity => 1.0, :clear => "both", :padding => "0px", :margin => "0px", :border => "0px", :width => "#{self.width}px", :height => "#{self.height}px"}, :states => {:"default-background-color" => true}}, :content=>[], :script=>""} + content_row = { + :component => "block_table_row", + :options => { + :style => { + :opacity => 1.0, + :width => "#{self.width}px", + :height => "#{self.height - 24}px", + :padding => "0px", + :margin => "0px", + :border => "0px" + } + }, + :content => [(interior_component)], + :script => "" + } + # + modal_frame[:content] = [(title_row),(content_row)] + build_list << modal_frame + # + self.build_interior_components(build_list) + # + # the_object = self.find_child(interior_object_title,true) + # if the_object + # @content = the_object + # end + # + the_object = self.find_child(title_object_title,true) + if the_object + @title_object = the_object + end + # + end + # Hooks for open/close events + def open(details={}) + # override in object script + end + # + def close(details={}) + # override in object script + end + end + # Menu Supports + class MenuItem < ::GxG::Gui::Block + def _after_create() + if @enabled == true + self.enable + else + self.disable + end + super() + end + # + # Structure: + def opened?() + false + end + # + def collapse() + self.unhighlight + if @the_parent.respond_to?(:collapse) + @the_parent.collapse() + end + end + # + def collapse_down() + self.unhighlight + end + # Visuals: + def enabled?() + @enabled + end + def enable() + @enabled = true + self.gxg_merge_style(:opacity => 1.0) + end + def disable() + @enabled = false + self.unhighlight + self.gxg_merge_style(:opacity => 0.5) + end + def highlight() + self.gxg_merge_style({:'background-color' => '#87acd5'}) + end + def unhighlight() + self.gxg_merge_style({:'background-color' => '#f2f2f2'}) + end + def update_appearance() + # override in object script + end + # + def initialize(the_name,the_class,the_options) + unless @title + @title = the_options.delete(:title) + end + @menu = the_options.delete(:menu) + @the_parent = the_options.delete(:parent) + @orientation = (the_options.delete(:orientation) || "vertical").to_s.to_sym + if @orientation == :horizontal + @zone = (the_options.delete(:zone) || "top").to_s.to_sym + else + @zone = (the_options.delete(:zone) || "left").to_s.to_sym + end + @enabled = the_options.delete(:enabled) + unless @enabled == false + @enabled = true + end + # presentation supports: + @icon = the_options.delete(:icon) + @icon_width = (the_options.delete(:icon_width) || 32) + @icon_height = (the_options.delete(:icon_height) || 32) + @label = the_options.delete(:content) + @key = the_options.delete(:key) + # Data payload: + @data = the_options.delete(:data) + # + super(the_name, the_class, the_options) + # + # Mouse Events: + on(:mouseup) do |event| + if self.enabled? + if @menu + self.collapse() + @menu.select_item(self) + end + end + end + on(:mouseenter) do |event| + if self.enabled? + self.highlight + end + end + on(:mouseleave) do |event| + if self.enabled? + self.unhighlight + end + end + self + end + # Payload: + def data() + @data + end + # + def cascade() + build_list = [] + # + spacer = {:component=>"block", :options=>{:style => {:clear => "both", :width=>"32px", :height=>"32px"}}, :content=>[], :script=>""} + # + icon = nil + if @icon + icon = {:component=>"image", :options=>{:src=>@icon, :width=>@icon_width, :height=>@icon_height, :style => {:clear => "both"}}, :content=>[], :script=>""} + end + # + label = nil + if @label + label = {:component=>"label", :options=>{:content => @label, :style => {:padding => "0px 0px 2px 0px", :"font-size" => "16px"}}, :content=>[], :script=>""} + end + # + key = nil + if @key + key = {:component=>"label", :options=>{:content => @key, :style => {:"font-size" => "16px"}}, :content=>[], :script=>""} + end + # grid structure + left_spacer_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :padding => "0px", :margin => "0px"}}, :content=>[(spacer)], :script=>""} + right_spacer_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "right", :padding => "0px", :margin => "0px"}}, :content=>[(spacer)], :script=>""} + # + icon_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :padding => "0px", :margin => "0px", :'vertical-align' => 'middle'}}, :content=>[(icon)], :script=>""} + label_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :padding => "0px 0px 0px 5px", :margin => "0px", :'vertical-align' => 'middle'}}, :content=>[(label)], :script=>""} + key_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "right", :padding => "0px", :margin => "0px", :'vertical-align' => 'middle'}}, :content=>[(key)], :script=>""} + entry_row = {:component=>"block_table_row", :options=>{:style => {:clear => "both", :padding => "0px", :margin => "0px"}}, :content=>[], :script=>""} + # + if @zone == :right + entry_row[:content] << left_spacer_cell + end + # + if icon + entry_row[:content] << icon_cell + end + # + if label + entry_row[:content] << label_cell + end + # + if key + entry_row[:content] << key_cell + end + # + if @zone != :right + entry_row[:content] << right_spacer_cell + end + # + entry_table = {:component=>"block_table", :options=>{:style => {:padding => "0px", :margin => "0px"}}, :content=>[(entry_row)], :script=>""} + # + build_list << entry_table + # + self.build_interior_components(build_list) + end + # + end + # + class MenuEntry < ::GxG::Gui::Block + def _after_create() + if @enabled == true + self.enable + else + self.disable + end + super() + end + # + def initialize(the_name,the_class,the_options) + @opened = false + unless @title + @title = the_options.delete(:title) + end + @menu = the_options.delete(:menu) + @the_parent = the_options.delete(:parent) + @orientation = (the_options.delete(:orientation) || "vertical").to_s.to_sym + if @orientation == :horizontal + @zone = (the_options.delete(:zone) || "top").to_s.to_sym + else + @zone = (the_options.delete(:zone) || "left").to_s.to_sym + end + if @the_parent.is_a?(::GxG::Gui::MenuBar) + @submenu = false + else + @submenu = true + end + @enabled = the_options.delete(:enabled) + unless @enabled == false + @enabled = true + end + # presentation supports: + @icon = the_options.delete(:icon) + @icon_width = (the_options.delete(:icon_width) || 32) + @icon_height = (the_options.delete(:icon_height) || 32) + @label = the_options.delete(:content) + @label_size = (the_options.delete(:content_size) || 16).to_s.gsub("px","").to_i + @key = the_options.delete(:key) + @layer = (the_options.delete(:layer) || 1000) + @menu_item_width = [200,(@icon_width)].max + # Review: adjust for border, padding, and margin + @menu_item_height = 32 + # if @icon + # @menu_item_height = [(@label_size),(@icon_height)].max + # else + # @menu_item_height = @label_size + 2 + # end + # + super(the_name, the_class, the_options) + # Create off-entry block body to attach items/etc to. + the_uuid = GxG::uuid_generate.to_sym + the_title = "Untitled Component #{the_uuid}" + @body = GxG::DISPLAY_DETAILS[:object].add_child(the_uuid, ::GxG::Gui::Block, {:uuid => the_uuid}) + @body.set_title(the_title) + GxG::DISPLAY_DETAILS[:object].register_object(the_title, @body) + # Set @body base-state + @body.gxg_set_state(:hidden, true) + # FIXME: menu background is errantly too short. + # @body.gxg_set_state(:"default-background-color", true) + @body.gxg_merge_style({:position => "absolute", :"background-color" => "#f2f2f2", :border => "1px solid #c2c2c2"}) + @body.on(:mouseleave) do |event| + hold_open = false + search_queue = @body.children.values + while search_queue.size > 0 do + the_child = search_queue.shift + if the_child.is_any?(::GxG::Gui::MenuEntry, ::GxG::Gui::MenuItem) + if the_child.opened? + hold_open = true + break + end + else + the_child.children.values.each do |sub_child| + search_queue << sub_child + end + end + end + unless hold_open == true + @body.hide + @opened = false + end + end + # + the_uuid = GxG::uuid_generate.to_sym + the_title = "Untitled Component #{the_uuid}" + @body_content = @body.add_child(the_uuid, ::GxG::Gui::BlockTable, {:uuid => the_uuid}) + @body_content.set_title(the_title) + GxG::DISPLAY_DETAILS[:object].register_object(the_title, @body_content) + # Mouse Events: + on(:mouseup) do |event| + if self.enabled? + if @body + if @body.gxg_get_state(:hidden) == true + self.update_appearance + @body.show + @opened = true + else + self.collapse_down + @body.hide + @opened = false + end + end + end + end + on(:mouseenter) do |event| + if self.enabled? + self.highlight + end + end + on(:mouseleave) do |event| + if self.enabled? + self.unhighlight + end + end + # + self + end + # Slight of hand: + alias :original_children :children + def children() + if @body + @body_content.children + else + {} + end + end + # + alias :original_add_child :add_child + def add_child(name, element_class, options = {}) + the_object = nil + if element_class == GxG::Gui::MenuEntry || element_class == GxG::Gui::MenuItem + if @body + # + if @orientation == :horizontal + the_row = @body_content.find_child_type(::GxG::Gui::BlockTableRow) + unless the_row + the_uuid = GxG::uuid_generate.to_sym + the_title = "Untitled Component #{the_uuid}" + the_row = @body_content.add_child(the_uuid, ::GxG::Gui::BlockTableRow, {:uuid => the_uuid}) + the_row.set_title(the_title) + GxG::DISPLAY_DETAILS[:object].register_object(the_title, the_row) + end + if the_row + the_uuid = GxG::uuid_generate.to_sym + the_title = "Untitled Component #{the_uuid}" + the_cell = the_row.add_child(the_uuid, ::GxG::Gui::BlockTableCell, {:uuid => the_uuid}) + the_cell.set_title(the_title) + GxG::DISPLAY_DETAILS[:object].register_object(the_title, the_cell) + if the_cell + the_object = the_cell.add_child(name, element_class, {:orientation => @orientation, :zone => @zone, :menu => @menu, :parent => self}.merge(options)) + # Review: MenuEntry too short issue + self.update_appearance() + else + # err - cannot proceed. + end + else + # err - cannot proceed. + end + else + the_uuid = GxG::uuid_generate.to_sym + the_title = "Untitled Component #{the_uuid}" + the_row = @body_content.add_child(the_uuid, ::GxG::Gui::BlockTableRow, {:uuid => the_uuid}) + the_row.set_title(the_title) + GxG::DISPLAY_DETAILS[:object].register_object(the_title, the_row) + if the_row + the_uuid = GxG::uuid_generate.to_sym + the_title = "Untitled Component #{the_uuid}" + the_cell = the_row.add_child(the_uuid, ::GxG::Gui::BlockTableCell, {:uuid => the_uuid}) + the_cell.set_title(the_title) + GxG::DISPLAY_DETAILS[:object].register_object(the_title, the_cell) + if the_cell + the_object = the_cell.add_child(name, element_class, {:orientation => @orientation, :zone => @zone, :menu => @menu, :parent => self}.merge(options)) + # Review: MenuEntry too short issue + self.update_appearance() + else + # err - cannot proceed. + end + else + # err - cannot proceed. + end + end + else + # err - cannot proceed + end + else + # the_object = self.original_add_child(name, element_class, options) + log_warn("MenuEntry only accepts MenuEntry or MenuItem as children, not: #{element_class.inspect} --> Other errors will likely follow.") + end + if the_object && @menu + @menu.register_entry(the_object) + end + the_object + end + # + def entries() + results = [] + search_pool = self.children.values + while search_pool.size > 0 do + entry = search_pool.shift + if entry + if entry.is_any?(::GxG::Gui::MenuEntry, ::GxG::Gui::MenuItem) + results << entry + else + entry.children.values.each do |the_child| + search_pool << the_child + end + end + end + end + results + end + # Structure: + def submenu() + @submenu + end + # + def opened?() + @opened + end + # + def set_opened(state=false) + @opened = state + end + # + def collapse() + if @body + @body.hide + end + self.unhighlight + if @the_parent.respond_to?(:collapse) + @the_parent.collapse() + end + end + # + def collapse_down() + self.entries.each do |the_child| + the_child.collapse_down + end + if @body + @body.hide + end + self.unhighlight + end + # Visuals: + def enabled?() + @enabled + end + def enable() + @enabled = true + self.entries.each do |the_child| + the_child.enable + end + self.gxg_merge_style(:opacity => 1.0) + end + def disable() + @enabled = false + self.entries.each do |the_child| + the_child.disable + end + @body.hide + self.unhighlight + self.gxg_merge_style(:opacity => 0.5) + end + def highlight() + self.gxg_merge_style({:'background-color' => '#87acd5'}) + end + def unhighlight() + self.gxg_merge_style({:'background-color' => '#f2f2f2'}) + end + def update_appearance() + # Set @body position according to @orientation and @zone + rectangle = {:top => 0, :left => 0, :right => 0, :bottom => 0} + rectangle[:top] = `#{self.element}.getBoundingClientRect().top`.to_i + rectangle[:left] = `#{self.element}.getBoundingClientRect().left`.to_i + rectangle[:right] = `#{self.element}.getBoundingClientRect().right`.to_i + rectangle[:bottom] = `#{self.element}.getBoundingClientRect().bottom`.to_i + # Review: @menu_item_height * self.entries.size : adjust for border, padding and margin (@menu_item_height * self.entries.size) + border_size ?? + body_settings = {:top => "0px", :left => "0px", :width => "#{@menu_item_width}px", :height => "#{@menu_item_height * self.entries.size}px", :"z-index" => @layer} + if @submenu == true + case @zone + when :top, :"top-left", :"top-right" + if @orientation == :horizontal + body_settings[:top] = "#{(rectangle[:bottom] + 2)}px" + body_settings[:left] = "#{rectangle[:left] + @menu_item_width + 2}px" + else + body_settings[:top] = "#{(rectangle[:bottom] + 2)}px" + body_settings[:left] = "#{rectangle[:left] + @menu_item_width + 2}px" + end + when :left + if @orientation == :horizontal + body_settings[:top] = "#{rectangle[:top]}px" + body_settings[:left] = "#{(rectangle[:right] + 2)}px" + else + body_settings[:top] = "#{rectangle[:top]}px" + body_settings[:left] = "#{(rectangle[:right] + 2)}px" + end + when :right + if @orientation == :horizontal + body_settings[:top] = "#{rectangle[:top]}px" + body_settings[:left] = "#{(rectangle[:left] - 2 - body_settings[:width])}px" + else + body_settings[:top] = "#{rectangle[:top]}px" + body_settings[:left] = "#{(rectangle[:left] - 2 - body_settings[:width])}px" + end + when :bottom, :"bottom-left", :"bottom-right" + if @orientation == :horizontal + body_settings[:top] = "#{(rectangle[:top] - 2 - body_settings[:height])}px" + body_settings[:left] = "#{rectangle[:left]}px" + else + body_settings[:top] = "#{(rectangle[:top] - 2 - body_settings[:height])}px" + body_settings[:left] = "#{rectangle[:left]}px" + end + end + else + case @zone + when :top, :"top-left", :"top-right" + if @orientation == :horizontal + body_settings[:top] = "#{(rectangle[:bottom] + 2)}px" + body_settings[:left] = "#{rectangle[:left]}px" + else + body_settings[:top] = "#{(rectangle[:bottom] + 2)}px" + body_settings[:left] = "#{rectangle[:left]}px" + end + when :left + if @orientation == :horizontal + body_settings[:top] = "#{rectangle[:top]}px" + body_settings[:left] = "#{(rectangle[:right] + 2)}px" + else + body_settings[:top] = "#{rectangle[:top]}px" + body_settings[:left] = "#{(rectangle[:right] + 2)}px" + end + when :right + if @orientation == :horizontal + body_settings[:top] = "#{rectangle[:top]}px" + body_settings[:left] = "#{(rectangle[:left] - 2 - body_settings[:width])}px" + else + body_settings[:top] = "#{rectangle[:top]}px" + body_settings[:left] = "#{(rectangle[:left] - 2 - body_settings[:width])}px" + end + when :bottom, :"bottom-left", :"bottom-right" + if @orientation == :horizontal + body_settings[:top] = "#{(rectangle[:top] - 2 - body_settings[:height])}px" + body_settings[:left] = "#{rectangle[:left]}px" + else + body_settings[:top] = "#{(rectangle[:top] - 2 - body_settings[:height])}px" + body_settings[:left] = "#{rectangle[:left]}px" + end + end + end + @body.gxg_merge_style(body_settings) + # + self.entries.each do |the_child| + the_child.update_appearance + end + end + # Construction: + def cascade() + build_list = [] + # + left_expander = nil + right_expander = nil + if @submenu == true + case @zone + when :top + if @orientation == :horizontal + right_expander = {:component=>"image", :options=>{:src => self.theme_widget("expand_bottom.svg"), :width=>32, :height=>32, :style => {:clear => "both"}}, :content=>[], :script=>""} + else + right_expander = {:component=>"image", :options=>{:src => self.theme_widget("expand_right.svg"), :width=>32, :height=>32, :style => {:clear => "both"}}, :content=>[], :script=>""} + end + when :left, :"top-left", :"bottom-left" + right_expander = {:component=>"image", :options=>{:src => self.theme_widget("expand_right.svg"), :width=>32, :height=>32, :style => {:clear => "both"}}, :content=>[], :script=>""} + when :right, :"top-right", :"bottom-right" + left_expander = {:component=>"image", :options=>{:src => self.theme_widget("expand_left.svg"), :width=>32, :height=>32, :style => {:clear => "both"}}, :content=>[], :script=>""} + when :bottom + if @orientation == :horizontal + right_expander = {:component=>"image", :options=>{:src => self.theme_widget("expand_top.svg"), :width=>32, :height=>32, :style => {:clear => "both"}}, :content=>[], :script=>""} + else + right_expander = {:component=>"image", :options=>{:src => self.theme_widget("expand_right.svg"), :width=>32, :height=>32, :style => {:clear => "both"}}, :content=>[], :script=>""} + end + end + end + # + the_icon = nil + if @icon + the_icon = {:component=>"image", :options=>{:src=>@icon, :width=>@icon_width, :height=>@icon_height, :style => {:clear => "both"}}, :content=>[], :script=>""} + end + # + the_label = nil + if @label + the_label = {:component=>"label", :options=>{:content => @label, :style => {:padding => "0px 0px 2px 0px", :"font-size" => "16px"}}, :content=>[], :script=>""} + end + # + the_key = nil + if @key + the_key = {:component=>"label", :options=>{:content => @key, :style => {:padding => "0px 0px 2px 0px", :"font-size" => "16px"}}, :content=>[], :script=>""} + end + # grid structure vertical-align:middle; + left_expander_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :padding => "0px", :margin => "0px"}}, :content=>[(left_expander)], :script=>""} + right_expander_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "right", :padding => "0px", :margin => "0px"}}, :content=>[(right_expander)], :script=>""} + icon_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :padding => "0px", :margin => "0px", :'vertical-align' => 'middle'}}, :content=>[(the_icon)], :script=>""} + label_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :padding => "0px 0px 0px 5px", :margin => "0px", :'vertical-align' => 'middle'}}, :content=>[(the_label)], :script=>""} + key_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "right", :padding => "0px", :margin => "0px", :'vertical-align' => 'middle'}}, :content=>[(the_key)], :script=>""} + entry_row = {:component=>"block_table_row", :options=>{:style => {:clear => "both", :padding => "0px", :margin => "0px"}}, :content=>[], :script=>""} + # + if left_expander + entry_row[:content] << left_expander_cell + end + # + if the_icon + entry_row[:content] << icon_cell + end + # + if the_label + entry_row[:content] << label_cell + end + # + if the_key + entry_row[:content] << key_cell + end + # + if right_expander + entry_row[:content] << right_expander_cell + end + # + entry_table = {:component=>"block_table", :options=>{:style => {:padding => "0px", :margin => "0px"}}, :content=>[(entry_row)], :script=>""} + # + build_list << entry_table + self.build_interior_components(build_list) + # + GxG::DISPATCHER.post_event do + self.update_appearance + end + # + true + end + end + # + class MenuBar < ::GxG::Gui::BlockTable + def initialize(the_name,the_class,the_options) + @zone = (the_options.delete(:zone) || "top").to_s.to_sym + case @zone.clone + when :top + @orientation = (the_options.delete(:orientation) || "horizontal").to_s.to_sym + when :bottom + @orientation = (the_options.delete(:orientation) || "horizontal").to_s.to_sym + when :left, :"top-left", :"bottom-left" + @orientation = (the_options.delete(:orientation) || "vertical").to_s.to_sym + when :right, :"top-right", :"bottom-right" + @orientation = (the_options.delete(:orientation) || "vertical").to_s.to_sym + else + @orientation = (the_options.delete(:orientation) || "horizontal").to_s.to_sym + end + @menu_registry = {} + super(the_name, the_class, the_options) + end + # + def register_entry(the_entry=nil) + if the_entry.is_any?(::GxG::Gui::MenuEntry, ::GxG::Gui::MenuItem) + @menu_registry[(the_entry.uuid)] = the_entry + true + else + false + end + end + # + def unregister_entry(the_reference=nil) + if the_reference.is_any?(::GxG::Gui::MenuEntry, ::GxG::Gui::MenuItem) + @menu_registry.delete(the_reference.uuid) + true + else + if the_reference.is_any?(::String, ::Symbol) + if GxG::valid_uuid?(the_reference) + @menu_registry.delete(the_reference.to_sym) + true + else + @menu_registry.values.each do |the_object| + if the_object.title == the_reference + @menu_registry.delete(the_object.uuid) + break + end + end + true + end + else + false + end + end + end + # + def find_entry(the_reference=nil) + result = nil + if the_reference.is_any?(::String, ::Symbol) + if GxG::valid_uuid?(the_reference) + result = @menu_registry[(the_reference.to_sym)] + else + @menu_registry.values.each do |the_object| + if the_object.title == the_reference + result = the_object + break + end + end + end + end + result + end + # Slight of hand: + alias :original_add_child :add_child + def add_child(name, element_class, options={}) + the_entry = nil + if element_class == GxG::Gui::MenuEntry + if @orientation == :horizontal + # + the_row = self.find_child_type(::GxG::Gui::BlockTableRow) + unless the_row + the_uuid = GxG::uuid_generate.to_sym + the_title = "Untitled Component #{the_uuid}" + the_row = self.original_add_child(the_uuid, ::GxG::Gui::BlockTableRow, {:uuid => the_uuid}) + the_row.set_title(the_title) + GxG::DISPLAY_DETAILS[:object].register_object(the_title, the_row) + end + if the_row + the_uuid = GxG::uuid_generate.to_sym + the_title = "Untitled Component #{the_uuid}" + the_cell = the_row.add_child(the_uuid, ::GxG::Gui::BlockTableCell, {:uuid => the_uuid}) + the_cell.set_title(the_title) + GxG::DISPLAY_DETAILS[:object].register_object(the_title, the_cell) + if the_cell + the_entry = the_cell.add_child(name, element_class, {:orientation => @orientation, :zone => @zone, :parent => self, :menu => self}.merge(options)) + # Review : MenuEntry too short issue + the_entry.update_appearance() + else + # err - cannot proceed. + end + else + # err - cannot proceed. + end + else + the_uuid = GxG::uuid_generate.to_sym + the_title = "Untitled Component #{the_uuid}" + the_row = self.original_add_child(the_uuid, ::GxG::Gui::BlockTableRow, {:uuid => the_uuid}) + the_row.set_title(the_title) + GxG::DISPLAY_DETAILS[:object].register_object(the_title, the_row) + if the_row + the_uuid = GxG::uuid_generate.to_sym + the_title = "Untitled Component #{the_uuid}" + the_cell = the_row.add_child(the_uuid, ::GxG::Gui::BlockTableCell, {:uuid => the_uuid}) + the_cell.set_title(the_title) + GxG::DISPLAY_DETAILS[:object].register_object(the_title, the_cell) + if the_cell + the_entry = the_cell.add_child(name, element_class, {:orientation => @orientation, :zone => @zone, :parent => self, :menu => self}.merge(options)) + # Review : MenuEntry too short issue + the_entry.update_appearance() + else + # err - cannot proceed. + end + else + # err - cannot proceed. + end + end + else + # the_entry = self.original_add_child(name, element_class, options) + log_warn("MenuBar only accepts MenuEntry as children, not: #{element_class.inspect} --> Other errors will likely follow.") + end + if the_entry + self.register_entry(the_entry) + end + the_entry + end + # + def entries() + results = [] + search_pool = self.children.values + while search_pool.size > 0 do + entry = search_pool.shift + if entry + if entry.is_a?(::GxG::Gui::MenuEntry) + results << entry + else + unless entry.is_a?(::GxG::Gui::MenuItem) + entry.children.values.each do |the_child| + search_pool << the_child + end + end + end + end + end + results + end + # + def collapse_down() + self.entries.each do |the_child| + the_child.collapse_down + end + end + # + def update_appearance() + self.entries.each do |the_child| + the_child.update_appearance + end + end + # + def select_item(the_menu_item=nil) + log_warn("Override this method in object script") + end + end + # Tabbed Regions Support + class TabSet < ::GxG::Gui::BlockTable + # OK - basic idea: + # Row0: tab labels in cells. :mouseup = hide others, show only designated Tab object as body. + # Row1: Tab content objects + def initialize(the_name,the_class,the_options) + # tab show/hide :transitions: + @transition_show = nil + @transition_hide = nil + the_transitions = the_options.delete(:transition) + if the_transitions.is_a?(::Hash) + if the_transitions[:show].is_a?(::Hash) + @transition_show = the_transitions[:show] + end + if the_transitions[:hide].is_a?(::Hash) + @transition_hide = the_transitions[:hide] + end + end + # + @tabs = {} + @tab_row = nil + @content_row = nil + # + super(the_name, the_class, the_options) + end + # Slight of hand: + alias :original_add_child :add_child + def add_child(name, element_class, options={}) + result = nil + # Only accept ::GxG::Gui::Tab items as children + if element_class == ::GxG::Gui::Tab + if @tab_row && @content_row + # LabelCell + label_cell_uuid = ::GxG::uuid_generate.to_sym + label_cell_title = "Untitled Component #{label_cell_uuid.to_s}" + # Re-distribute :width of each label over total width: + label_cell = @tab_row.add_child(label_cell_uuid, ::GxG::Gui::BlockTableCell, {:uuid => label_cell_uuid}) + label_cell.set_title(label_cell_title) + # GxG::DISPLAY_DETAILS[:object].register_object(label_cell_title, label_cell) ?? + # ContentCell + content_cell_uuid = ::GxG::uuid_generate.to_sym + content_cell_title = "Untitled Component #{content_cell_uuid.to_s}" + content_cell = @content_row.add_child(content_cell_uuid, ::GxG::Gui::BlockTableCell, {:uuid => content_cell_uuid}) + content_cell.set_title(content_cell_title) + # GxG::DISPLAY_DETAILS[:object].register_object(content_cell_title, content_cell) ?? + content_cell.hide + # TabItem + # Needs: :tabset => obj, :label_cell => obj, :content_cell => obj + the_tab_item = label_cell.add_child(name, element_class, (options || {}).merge({:tabset => self, :label_cell => label_cell, :content_cell => content_cell})) + if the_tab_item + @tabs[(name.to_s.to_sym)] = the_tab_item + result = the_tab_item + # Review: where else to put this?? + if @tabs.keys.size > 1 + total_width = (`#{self.element}.getBoundingClientRect().right`.to_i - `#{self.element}.getBoundingClientRect().left`.to_i).to_f + the_width = "#{((total_width / @tabs.keys.size.to_f) / total_width) * 100.0}%" + else + the_width = "100.0%" + end + @tabs.values.each do |the_tab| + the_tab.width = the_width + end + # GxG::DISPLAY_DETAILS[:object].register_object(name, the_tab_item) (already done by build_components ??) + end + end + # + end + result + end + # + alias :original_children :children + def children() + if @tabs + @tabs + else + {} + end + end + # + def find_child(the_reference=nil, interior=false) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + if interior == true + search_queue = self.original_children.values + else + search_queue = self.children.values + end + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def size() + @tabs.keys.size + end + # + def reference_tab(specifier=nil) + result = nil + check = nil + if GxG::valid_uuid?(specifier) + check = :uuid + else + if specifier.is_any?(::String, ::Symbol) + check = :reference + else + if specifier.is_a?(::GxG::Gui::Tab) + check = :instance + else + if specifier.is_a?(::Numeric) + check = :index + end + end + end + end + case check + when :uuid + @tabs.each_pair do |the_ref, the_instance| + if the_instance.uuid.to_s.to_sym == specifier.to_s.to_sym + result = @tabs[(the_ref)] + break + end + end + when :reference + result = @tabs[(specifier.to_s.to_sym)] + when :index + result = @tabs[(@tabs.keys[(specifier.to_i)])] + when :instance + @tabs.each_pair do |the_ref, the_instance| + if the_instance == specifier + result = @tabs[(the_ref)] + break + end + end + end + result + end + # + def detach_tab(specifier=nil) + # This should only be called from a Tab instance. + result = nil + check = nil + if GxG::valid_uuid?(specifier) + check = :uuid + else + if specifier.is_any?(::String, ::Symbol) + check = :reference + else + if specifier.is_a?(::GxG::Gui::Tab) + check = :instance + else + if specifier.is_a?(::Numeric) + check = :index + end + end + end + end + case check + when :uuid + @tabs.each_pair do |the_ref, the_instance| + if the_instance.uuid.to_s.to_sym == specifier.to_s.to_sym + result = @tabs.delete(the_ref) + break + end + end + when :reference + result = @tabs.delete(specifier.to_s.to_sym) + when :index + result = @tabs.delete(@tabs.keys[(specifier.to_i)]) + when :instance + @tabs.each_pair do |the_ref, the_instance| + if the_instance == specifier + result = @tabs.delete(the_ref) + break + end + end + end + result + end + # + def cascade() + build_list = [] + tab_row_id = "Tabs Row" + content_row_id = "Content Row" + # + tab_row = {:component=>"block_table_row", :options=>{:title => tab_row_id, :style => {:clear => "both", :padding => "0px", :margin => "0px", :width => "100%", :height => "32px", :"background-color" => "transparent"}}, :content=>[], :script=>""} + tab_table = {:component=>"block_table", :options=>{:style => {:clear => "both", :padding => "0px", :margin => "0px", :width => "100%", :height => "32px", :"background-color" => "transparent"}}, :content=>[(tab_row)], :script=>""} + tab_area_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :padding => "0px", :margin => "0px", :'vertical-align' => 'middle', :"background-color" => "transparent"}}, :content=>[(tab_table)], :script=>""} + tab_area_row = {:component=>"block_table_row", :options=>{:style => {:clear => "both", :padding => "0px", :margin => "0px", :width => "100%", :height => "32px", :"background-color" => "transparent"}}, :content=>[(tab_area_cell)], :script=>""} + # + content_row = {:component=>"block_table_row", :options=>{:title => content_row_id, :style => {:clear => "both", :padding => "0px", :margin => "0px", :width => "100%", :height => "100%"}}, :content=>[], :script=>""} + content_table = {:component=>"block_table", :options=>{:style => {:clear => "both", :padding => "0px", :margin => "0px", :width => "100%", :height => "100%"}}, :content=>[(content_row)], :script=>""} + content_area_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :padding => "0px", :margin => "0px", :'vertical-align' => 'middle'}}, :content=>[(content_table)], :script=>""} + content_area_row = {:component=>"block_table_row", :options=>{:style => {:clear => "both", :padding => "0px", :margin => "0px", :width => "100%", :height => "100%"}}, :content=>[(content_area_cell)], :script=>""} + # + build_list << tab_area_row + build_list << content_area_row + # + self.build_interior_components(build_list) + # + @tab_row = self.find_child(tab_row_id, true) + @content_row = self.find_child(content_row_id, true) + true + end + # Selection + def select_tab(specifier=nil,&block) + the_tab = self.reference_tab(specifier) + if the_tab + @tabs.values.each do |a_tab| + if a_tab.visible? && (a_tab != the_tab) + a_tab.hide(@transition_hide) do + unless the_tab.visible? + the_tab.show(@transition_show,&block) + end + end + else + if a_tab == the_tab + unless the_tab.visible? + the_tab.show(@transition_show,&block) + end + end + end + end + end + end + # + end + # + class Tab < ::GxG::Gui::BlockTable + # goes within TabSet only + def initialize(the_name,the_class,the_options) + # {:tabset => self, :label_object => label_cell, :content_object => content_cell} + @tabset = the_options.delete(:tabset) + @label_area = the_options.delete(:label_cell) + @content_area = the_options.delete(:content_cell) + unless (@tabset && @label_area && @content_area) + raise Exception, "Improperly initialized Tab object: create by adding to a TabSet." + end + if the_options.delete(:closeable) == true + @closeable = true + else + @closeable = false + end + if the_options.delete(:enabled) == false + @enabled = false + else + @enabled = true + end + if the_options[:content].is_a?(::String) + @label_text = the_options.delete(:content) + else + @label_text = "Untitled" + end + @label_object = nil + @tab_height = 24 + @tab_width = "100%" + @visible = false + # + super(the_name, the_class, the_options) + end + # + def destroy() + if @tabset + if @tabset.detach_tab(self) + if @content_area + @content_area.hide() + @content_area.destroy() + @content_area = nil + end + if @label_area + @label_area.hide() + @label_area.destroy() + @label_area = nil + end + @tabset = nil + end + end + end + # + alias :original_children :children + def children() + if @content_area + @content_area.children() + else + {} + end + end + # + def find_child(the_reference=nil, interior=false) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + if interior == true + search_queue = self.original_children.values + else + search_queue = self.children.values + end + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def cascade() + build_list = [] + # Build up Label + tab_label_row = {:component=>"block_table_row", :options=>{:style => {:clear => "both", :padding => "0px", :margin => "0px", :width => "100%", :height => "100%"}}, :content=>[], :script=>""} + # Close Box + if @closeable == true + close_box = {:component=>"image", :options=>{:src => theme_widget("close.png"), :width=>20, :height=>20, :style => {:padding => "2px"}}, :content=>[], :script=>""} + close_box[:script] = " + on(:mouseup) do |event| + `alert('Closebox Selected')` + end + on(:mousedown) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('close_mousedown.png')) + end + end + on(:mouseenter) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('close_mouseenter.png')) + end + end + on(:mouseleave) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('close.png')) + end + end + " + tab_close_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :padding => "0px", :margin => "0px", :'vertical-align' => 'middle'}}, :content=>[(close_box)], :script=>""} + tab_label_row[:content] << tab_close_cell + end + # Label + # label_obj_id = ::GxG::uuid_generate() + the_label = {:component=>"label", :options=>{:title => "Tab Label Object", :content => @label_text, :style => {:width => "100%", :"font-size" => "16px", :"text-align" => "center"}}, :content=>[], :script=>""} + unless @enabled == true + the_label[:options][:style][:"font-color"] = "#c2c2c2" + end + the_label[:script] = " + on(:mouseup) do |event| + `alert('Label Selected')` + end + " + tab_label_cell = {:component=>"block_table_cell", :options=>{:style => {:float => "left", :padding => "0px", :margin => "0px", :'vertical-align' => 'middle', :overflow => "hidden"}}, :content=>[(the_label)], :script=>""} + tab_label_row[:content] << tab_label_cell + tab_label_row[:script] = " + on(:mouseup) do |event| + `alert('Row Selected')` + end + " + # + build_list << tab_label_row + # + self.gxg_merge_style({:clear => "both", :padding => "0px", :margin => "0px", :width => "100%", :height => "#{@tab_height}px", :"background-color" => "#f2f2f2", :"border-radius" => "5px 5px 0px 0px", :overflow => "hidden", :"border-bottom" => "1px solid #c2c2c2"}) + # + self.build_interior_components(build_list) + # + @label_object = self.find_child("Tab Label Object",true) + true + end + # Visuals: + def width() + if @label_area + @tab_width + end + end + def width=(the_width=nil) + if @label_area && the_width + # set/get width of @label_area (a cell) to control tab width. ???? + if the_width.is_a?(::Numeric) + the_width = "#{the_width}px" + end + @tab_width = the_width + @label_area.gxg_merge_style({:width => @tab_width}) + end + end + # + def visible?() + @visible + end + # + def show(transition=nil, &block) + self.gxg_merge_style({:"border-bottom" => "none"}) + @visible = true + @content_area.show(transition,&block) + end + # + def hide(transition=nil, &block) + @content_area.hide(transition) do + if block.respond_to?(:call) + block.call() + end + self.gxg_merge_style({:"border-bottom" => "1px solid #c2c2c2"}) + @visible = false + end + end + # State: + def enabled?() + @enabled + end + def enable() + @enabled = true + @label_object.gxg_merge_style({:"font-color" => "#000000"}) + end + def disable() + @enabled = false + @label_object.gxg_merge_style({:"font-color" => "#c2c2c2"}) + end + # Selection + def selected?() + self.visible? + end + def select() + if @enabled == true + @tabset.select_tab(self) + end + end + # Methods to override: + def before_open(options={}) + end + def open(options={}) + end + def after_open(options={}) + end + def before_close(options={}) + end + def close(options={}) + end + def after_close(options={}) + end + # + end + # + class Variable < ::Ferro::Element::Var + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # Creates any kind of element depending on option :domtype. + end + # ### Compound Items + # + class SearchForm < ::Ferro::Combo::Search + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # # Override this method to specify what happens when + # submit button is clicked or enter key is pressed. + # + # @param [String] value The value of the text input. + # def submitted(value);end + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + end + # + class PopUpMenu < ::Ferro::Combo::PullDown + # + def _before_create + if @options[:uuid] + @uuid = @options.delete(:uuid).to_s.to_sym + end + super() + end + # + def uuid() + @uuid + end + # Creates a simple pull-down menu. + # Specify option :title to set the menu title text. + # Specify option :items as an Array of Ferro classes. + # Each class should be something clickable, for instance a + # FormBlock. These classes will be instanciated by + # this element. + include GxG::Gui::AnimationSupport + include GxG::Gui::ApplicationSupport + include GxG::Gui::ThemeSupport + end + # ----------------------------------------------------------------------------- + COMPONENT_CLASSES = { + :header => ::GxG::Gui::Header, + :navigator => ::GxG::Gui::Navigator, + :section => ::GxG::Gui::Section, + :article => ::GxG::Gui::Article, + :aside => ::GxG::Gui::Aside, + :footer => ::GxG::Gui::Footer, + :form => ::GxG::Gui::Form, + :clickable => ::GxG::Gui::Clickable, + :fieldset => ::GxG::Gui::Fieldset, + :text_input => ::GxG::Gui::TextInput, + :password_input => ::GxG::Gui::PasswordInput, + :reset_input => ::GxG::Gui::ResetInput, + :radio_button => ::GxG::Gui::RadioButton, + :color_picker => ::GxG::Gui::ColorPicker, + :date_picker => ::GxG::Gui::DatePicker, + :datetime_local => ::GxG::Gui::DateTimeLocal, + :email_input => ::GxG::Gui::EmailInput, + :month_picker => ::GxG::Gui::MonthPicker, + :number_input =>::GxG::Gui::NumberInput, + :range_input => ::GxG::Gui::RangeInput, + :search_input => ::GxG::Gui::SearchInput, + :phone_input => ::GxG::Gui::PhoneInput, + :time_picker => ::GxG::Gui::TimePicker, + :url_input => ::GxG::Gui::UrlInput, + :week_picker => ::GxG::Gui::WeekPicker, + :label => ::GxG::Gui::Label, + :text_area => ::GxG::Gui::TextArea, + :output => ::GxG::Gui::Output, + :button_input => ::GxG::Gui::ButtonInput, + :submit_button => ::GxG::Gui::SubmitButton, + :click_block => ::GxG::Gui::ClickBlock, + :checkbox => ::GxG::Gui::CheckBox, + :selector => ::GxG::Gui::Selector, + :block => ::GxG::Gui::Block, + :text => ::GxG::Gui::Text, + :list => ::GxG::Gui::List, + :ordered_list => ::GxG::Gui::OrderedList, + :list_item => ::GxG::Gui::ListItem, + :anchor => ::GxG::Gui::Anchor, + :external_link => ::GxG::Gui::ExternalLink, + :button => ::GxG::Gui::Button, + :image => ::GxG::Gui::Image, + :video => ::GxG::Gui::Video, + :canvas => ::GxG::Gui::Canvas, + :script => ::GxG::Gui::Script, + :application_viewport => ::GxG::Gui::ApplicationViewport, + :search_form => ::GxG::Gui::SearchForm, + :popupmenu => ::GxG::Gui::PopUpMenu, + :table => ::GxG::Gui::Table, + :table_header => ::GxG::Gui::TableHeader, + :table_row => ::GxG::Gui::TableRow, + :table_cell => ::GxG::Gui::TableCell, + :block_table => ::GxG::Gui::BlockTable, + :block_table_header => ::GxG::Gui::BlockTableHeader, + :block_table_row => ::GxG::Gui::BlockTableRow, + :block_table_cell => ::GxG::Gui::BlockTableCell, + :window => ::GxG::Gui::Window, + :dialog_box => ::GxG::Gui::DialogBox, + :panel => ::GxG::Gui::Panel, + :tree => ::GxG::Gui::Tree, + :tree_node => ::GxG::Gui::TreeNode, + :menu_bar => ::GxG::Gui::MenuBar, + :menu_entry => ::GxG::Gui::MenuEntry, + :menu_item => ::GxG::Gui::MenuItem, + :tabset => ::GxG::Gui::TabSet, + :tab => ::GxG::Gui::Tab + } + # + def self.component_class(the_type_specifier=nil) + result = nil + if the_type_specifier + result = GxG::Gui::COMPONENT_CLASSES[(the_type_specifier.to_s.to_sym)] + end + result + end + # + def self.class_component(the_class_specifier=nil) + result = nil + if the_class_specifier + found_index = GxG::Gui::COMPONENT_CLASSES.values.index(the_class_specifier) + if found_index + result = GxG::Gui::COMPONENT_CLASSES.keys[(found_index)] + end + end + result + end + # + def self.component_registered?(the_specifier=nil) + if GxG::Gui::component_class(the_specifier) + true + else + false + end + end + # + def self.register_component_class(the_specifier=nil, the_constant=nil) + if the_specifier.is_any?(::String, ::Symbol) && the_constant.is_a?(::Class) + if GxG::Gui::component_registered(the_specifier.to_s.to_sym) + if GxG::Gui::component_class(the_specifier.to_s.to_sym) != the_constant + GxG::Gui::COMPONENT_CLASSES[(the_specifier.to_s.to_sym)] = the_constant + true + else + log_warn("Component Class #{the_specifier.to_s.to_sym.inspect} => #{the_constant.inspect} already defined.") + false + end + else + GxG::Gui::COMPONENT_CLASSES[(the_specifier.to_s.to_sym)] = the_constant + true + end + else + false + end + end + # Set the valid component classes for animations: + class Animation + # This is intentional - keep this modification (!) + def self.valid_components() + ::GxG::Gui::COMPONENT_CLASSES.values + end + end + # + # -------------------------------------------------------------------------------------------------------------------------------------- + # + class Page < Ferro::Document + # + # Override in your Page Script: + def run(data=nil) + end + # + include GxG::Gui::EventHandlers + # + def element() + `document.body` + end + # + def find_object_by_id(the_id=nil) + result = nil + if the_id.is_a?(::Numeric) + the_id = ("_" + the_id.to_i.to_s()) + else + the_id = the_id.to_s + end + search_queue = [(self)] + while search_queue.size > 0 do + entry = search_queue.shift + + attrib = `#{entry.element()}.attributes.getNamedItem('id') || 0` + unless `#{attrib} == 0` + attrib = `#{attrib}.value` + if `(#{attrib.to_s} == #{the_id.to_s})` + result = entry + break + end + end + if entry.respond_to?(:children) + entry.children.keys.each do |the_child_key| + attrib = `#{entry.children[(the_child_key)].element()}.attributes.getNamedItem('id') || 0` + unless `#{attrib} == 0` + attrib = `#{attrib}.value` + if `(#{attrib.to_s} == #{the_id.to_s})` + result = entry.children[(the_child_key)] + break + else + search_queue << entry.children[(the_child_key)] + end + end + end + end + end + result + end + # + def find_object(object_name=nil) + result = nil + search_queue = [(self)] + while search_queue.size > 0 do + entry = search_queue.shift + if entry.respond_to?(:children) + if entry.children[(object_name)] + result = entry.children[(object_name)] + break + else + entry.children.keys.each do |the_child| + search_queue << entry.children[(the_child)] + end + end + end + end + result + end + # + def find_object_by_title(the_title=nil) + result = nil + if GxG::valid_uuid?(the_title) + uuid_check = true + else + uuid_check = false + end + @title_registry.keys.each do |the_uuid| + if uuid_check == true + if @title_registry[(the_uuid)].to_s.include?(the_title.to_s) + result = self.find_object(the_uuid) + break + end + else + if the_title == @title_registry[(the_uuid)] + result = self.find_object(the_uuid) + break + end + end + end + result + end + # + def register_object(the_title=nil, the_object=nil) + if the_title && the_object + @title_registry[(the_object.uuid)] = the_title.to_s + true + else + false + end + end + # + def unregister_object(the_title=nil) + if the_title + the_object = self.find_object_by_title(the_title) + if the_object + @title_registry.delete(the_object.uuid) + end + true + else + false + end + end + # + def initialize(the_display_path=nil) + super() + @busy = false + @theme = "default" + # Review : prepend @theme_prefix with the page 'relative_url' value (see GxG::CONNECTION) + @theme_prefix = (::GxG::CONNECTION.relative_url() + "/themes/default") + @window_switcher = nil + @window_registry = {} + @dialog = nil + @dialog_queue = [] + @title_registry = {} + @style_data = {} + @layout = {:"top-left" => {}, :top => {}, :"top-right" => {}, :right => {}, :"bottom-right" => {}, :bottom => {}, :"bottom-left" => {}, :left => {}} + @layout_content_area = {:top => 0, :left => 0, :bottom => `window.innerHeight`.to_i, :right => `window.innerWidth`.to_i} + @uuid = nil + @display_path = the_display_path + # Supports windowing: + on(:mousemove) do |event| + if event + GxG::DISPLAY_DETAILS[:mouse_x] = event[:pageX] + GxG::DISPLAY_DETAILS[:mouse_y] = event[:pageY] + GxG::DISPLAY_DETAILS[:mousedown] = ((event[:buttons] & 1) == 1) + end + end + # + ::GxG::DISPATCHER.post_event(:display) do + if GxG::CONNECTION.open? + ::GxG::DISPLAY_DETAILS[:object].start(`window.location.pathname`) + end + end + # + self + end + # + def uuid() + @uuid + end + # Busy/Unbusy Supports: + def busy() + @busy + end + def set_busy(state=false) + if state == true + unless @busy == true + busy_overlay = {:component => "block", :options => {:title => "gxg_wait_overlay", :style => {:cursor => "wait", :opacity => 0.01, :"background-color" => "#ffffff", :position => "absolute", :top => "0px", :left => "0px", :width => "100%", :height => "100%", :"z-index" => 9999}}, :content => [], :script => ""} + busy_overlay[:script] = " + on(:mouseup,true) do |event| + end + on(:mousedown,true) do |event| + end + on(:mousemove,true) do |event| + end + on(:mouseenter,true) do |event| + end + on(:mouseleave,true) do |event| + end + " + self.build_components([{:parent => nil, :record => {:content => [(busy_overlay)]}, :element => self}]) + @busy = true + end + else + if @busy == true + busy_overlay = self.find_object_by_title("gxg_wait_overlay") + if busy_overlay + self.unregister_object("gxg_wait_overlay") + busy_overlay.destroy + end + @busy = false + end + end + @busy + end + # Theme Supports: + def site_asset(asset_path=nil) + if (`window.location['href']`).include?("https://") + host_prefix = ("https://" + `window.location['host']`) + else + host_prefix = ("http://" + `window.location['host']`) + end + if ::GxG::CONNECTION.relative_url()[0] != "/" + host_prefix = (host_prefix + "/") + end + if asset_path + (host_prefix + File.expand_path((::GxG::CONNECTION.relative_url() + "/" + asset_path.to_s))) + else + "" + end + end + # + def theme_icon(image_name=nil) + if image_name + (@theme_prefix + "/icons/" + image_name.to_s) + else + "" + end + end + # + def theme_background(image_name=nil) + if image_name + (@theme_prefix + "/backgrounds/" + image_name.to_s) + else + "" + end + end + # + def theme_widget(image_name=nil) + if image_name + (@theme_prefix + "/widgets/" + image_name.to_s) + else + "" + end + end + # + def theme_font(font_name=nil) + if font_name + (@theme_prefix + "/fonts/" + font_name.to_s) + else + "" + end + end + # + def theme_window_title_layout() + {:left => [:close, :minimize, :maximize], :center => [:title], :right => []} + end + # Layout System Supports: + def layout_refresh() + # get rectangle of item -> convert to page % offsets + # `window.innerHeight`.to_i + # `window.innerWidth`.to_i + # .getClientRects()[0].left + page_width = `window.innerWidth`.to_i + # Calculating proper page height, See: https://stackoverflow.com/questions/1145850/how-to-get-height-of-entire-document-with-javascript + body = `document.body` + html = `document.documentElement` + page_bottom = 0 + %x{ + page_bottom = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); + } + page_height = [(`window.innerHeight`.to_i),(page_bottom.to_i)].max + # bounding areas: + top_bound = 0.0 + left_bound = 0.0 + right_bound = 1.0 + bottom_bound = 1.0 + # Update object percentages + [:"top-left", :top, :"top-right", :right, :"bottom-right", :bottom, :"bottom-left", :left].each do |the_position| + @layout[(the_position)].each_pair do |the_uuid,the_record| + object = self.find_object(the_uuid) + if object + rectangle = {:top => 0, :left => 0, :right => 0, :bottom => 0} + rectangle[:top] = `#{object.element}.getBoundingClientRect().top`.to_i + rectangle[:left] = `#{object.element}.getBoundingClientRect().left`.to_i + rectangle[:right] = `#{object.element}.getBoundingClientRect().right`.to_i + rectangle[:bottom] = `#{object.element}.getBoundingClientRect().bottom`.to_i + percentages = {:top => 0.0, :left => 0.0, :right => 0.0, :bottom => 0.0} + percentages[:top] = (rectangle[:top].to_f / page_height.to_f) + percentages[:left] = (rectangle[:left].to_f / page_width.to_f) + percentages[:right] = (rectangle[:right].to_f / page_width.to_f) + percentages[:bottom] = (rectangle[:bottom].to_f / page_height.to_f) + @layout[(the_position.to_s.to_sym)][(the_uuid.to_s.to_sym)] = percentages + end + end + end + # + [:"top-left", :top, :"top-right", :right, :"bottom-right", :bottom, :"bottom-left", :left].each do |the_position| + @layout[(the_position)].values.each do |the_record| + case the_position + when :top + if the_record[:bottom] > top_bound + top_bound = the_record[:bottom] + end + when :left + if the_record[:right] > left_bound + left_bound = the_record[:right] + end + when :right + if the_record[:left] < right_bound + right_bound = the_record[:left] + end + when :bottom + if the_record[:top] < bottom_bound + bottom_bound = the_record[:top] + end + when :"top-left" + if the_record[:bottom] > top_bound + top_bound = the_record[:bottom] + end + if the_record[:right] > left_bound + left_bound = the_record[:right] + end + when :"top-right" + if the_record[:bottom] > top_bound + top_bound = the_record[:bottom] + end + if the_record[:left] < right_bound + right_bound = the_record[:left] + end + when :"bottom-right" + if the_record[:top] < bottom_bound + bottom_bound = the_record[:top] + end + if the_record[:left] < right_bound + right_bound = the_record[:left] + end + when :"bottom-left" + if the_record[:top] < bottom_bound + bottom_bound = the_record[:top] + end + if the_record[:right] > left_bound + left_bound = the_record[:right] + end + end + end + end + @layout_content_area = {:top => 0, :left => 0, :bottom => page_height, :right => page_width, :page_width => page_width, :page_height => page_height} + @layout_content_area[:top] = (top_bound * page_height.to_f).to_i + @layout_content_area[:top_percent] = top_bound * 100.0 + @layout_content_area[:left] = (left_bound * page_width.to_f).to_i + @layout_content_area[:left_percent] = left_bound * 100.0 + @layout_content_area[:right] = (right_bound * page_width.to_f).to_i + @layout_content_area[:right_percent] = right_bound * 100.0 + @layout_content_area[:bottom] = (bottom_bound * page_height.to_f).to_i + @layout_content_area[:bottom_percent] = bottom_bound * 100.0 + true + end + # + def layout_add_item(the_uuid=nil, the_position=nil, refresh=true) + # store by uuid in position's record + if [:"top-left", :top, :"top-right", :right, :"bottom-right", :bottom, :"bottom-left", :left].include?(the_position.to_s.to_sym) + # + @layout[(the_position.to_s.to_sym)][(the_uuid.to_s.to_sym)] = {:top => 0.0, :left => 0.0, :right => 0.0, :bottom => 0.0} + if refresh + self.layout_refresh + end + # + true + else + false + end + end + # + def layout_remove_item(the_uuid=nil) + [:"top-left", :top, :"top-right", :right, :"bottom-right", :bottom, :"bottom-left", :left].each do |the_position| + if @layout[(the_position.to_s.to_sym)][(the_uuid.to_s.to_sym)] + @layout[(the_position.to_s.to_sym)].delete((the_uuid.to_s.to_sym)) + break + end + end + self.layout_refresh + # + true + end + # + def layout_content_area() + @layout_content_area + end + # Window Manager: + # Dialog Box supports: + def dialog_open(build_source=nil, the_application=nil, details=nil, &block) + if build_source.is_any?(::Hash, GxG::Database::PersistedHash) + if build_source.is_a?(GxG::Database::PersistedHash) + the_uuid = build_source.uuid + the_options = build_source[:options].unpersist + if the_options[:title] + the_title = the_options.delete(:title) + else + the_title = build_source.title + end + else + the_uuid = GxG::uuid_generate.to_sym + the_options = build_source[:options].clone + the_title = the_options.delete(:title) + end + the_style = the_options.delete(:style) + the_states = (the_options.delete(:states) || {}) + new_window = GxG::DISPLAY_DETAILS[:object].add_child(the_uuid, GxG::Gui::DialogBox, the_options) + if new_window + new_window.set_title(the_title) + # Set states: + unless the_states.keys.include?(:hidden) + the_states[:hidden] = false + end + new_window.gxg_set_states(the_states) + # process style info: + if the_style.is_a?(::Hash) + new_window.gxg_set_style(the_style) + end + if the_application.is_a?(::GxG::Application) + new_window.set_application(the_application) + unless the_application.get_window(the_uuid) + the_application.link_window(new_window) + end + end + # Set details if provided + if details + new_window.set_data(details) + end + # Set Script if any is provided + if build_source[:script].size > 0 + new_window.set_script(build_source[:script].to_s) + end + # Set Callback + if block.respond_to?(:call) + new_window.set_responder(block) + end + # + if @dialog.is_a?(::GxG::Gui::DialogBox) + @dialog_queue << new_window + self.register_object(the_title, new_window) + self.build_components([{:parent => GxG::DISPLAY_DETAILS[:object], :record => build_source, :element => new_window}],the_application) + # + else + @dialog = new_window + self.register_object(the_title, new_window) + self.build_components([{:parent => GxG::DISPLAY_DETAILS[:object], :record => build_source, :element => new_window}],the_application) + new_window.before_open() + new_window.open() + new_window.show({:origin => {:opacity => 0.0}, :destination => {:opacity => 1.0}}) + new_window.after_open() + end + true + else + false + end + else + false + end + end + # + def dialog_close(options={}) + if @dialog.is_a?(::GxG::Gui::DialogBox) + @dialog.before_close() + close_procedure = Proc.new do + @dialog.close() + @dialog.after_close() + @dialog.gxg_every_child do |the_child| + if the_child.is_a?(::GxG::Gui::ApplicationViewport) + the_application = the_child.application + if the_application + the_application.unlink_viewport(the_child.title) + end + end + end + self.unregister_object(@dialog.title) + @dialog.destroy + @dialog = nil + if @dialog_queue.size > 0 + @dialog = @dialog_queue.shift + @dialog.before_open + @dialog.open + @dialog.show({:origin => {:opacity => 0}, :destination => {:opacity => 1}, :options => {:duration => 500}}) + @dialog.after_open + end + end + if options[:origin] && options[:destination] + @dialog.hide(options, &close_procedure) + else + @dialog.hide({:origin => {:opacity => 1}, :destination => {:opacity => 0}, :options => {:duration => 500}}, &close_procedure) + end + end + true + end + # Choice Dialog + def open_dialog(the_application=nil, details=nil, &block) + # details: {:type => :choose, :title => "Some Title", :banner => "Some Text Here"} + self.layout_refresh() + # Determine Dialog Details: + window_title = (details || {:title => "Choose"})[:title].to_s + banner_text = (details || {:banner => "(Please provide some text here)"})[:banner].to_s + one_text = (details || {:one => ""})[:one].to_s + two_text = (details || {:two => ""})[:two].to_s + three_text = (details || {:three => ""})[:three].to_s + default_text = (details || {:default => ""})[:default].to_s + dialog_type = ((details || {:type => :choose})[:type] || :choose) + # Define bounds: (in px) + total_width = (self.layout_content_area()[:page_width].to_f * 0.25).to_i + form_padding = 20 + form_margin = 5 + inner_width = (total_width - ((form_padding + form_margin) * 2)) + total_height = ((form_padding + form_margin) * 2) + # + banner_font_size = 16 + if banner_text.to_s.size > 40 + banner_height = ((banner_text.size.to_f / 40.0) * banner_font_size.to_f).round.to_i + else + banner_height = banner_font_size + end + input_font_size = 16 + # Define parts: + # + banner = {:component=>"text", :options=> {:content => banner_text, :style => {:"font-size" => "#{banner_font_size}px", :width => "100%"}}, :content => [], :script => ""} + banner_cell = {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px", :width => "100%", :height => (banner_height), :"vertical-align" => "middle"}}, :content => [(banner)], :script => ""} + # + text_input = {:component=>"text_input", :options=>{:title => "data", :content => default_text, :style => {:"background-color" => "#f2f2f2", :"font-size" => "#{input_font_size}px", :width => "100%"}}, :content=>[], :script=>""} + text_input_cell = {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px", :width => "100%", :"vertical-align" => "middle"}}, :content => [(text_input)], :script => ""} + # Buttons: + one_btn = {:component=>"button", :options=>{:content => one_text, :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + one_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.respond({:action => :one}) + end + end + " + one_btn_cell = {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px", :width => "40%", :"vertical-align" => "middle"}}, :content => [(one_btn)], :script => ""} + # + two_btn = {:component=>"button", :options=>{:content => two_text, :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + two_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.respond({:action => :two}) + end + end + " + two_btn_cell = {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px", :width => "40%", :"vertical-align" => "middle"}}, :content => [(two_btn)], :script => ""} + # + three_btn = {:component=>"button", :options=>{:content => three_text, :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + three_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.respond({:action => :three}) + end + end + " + three_btn_cell = {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px", :width => "40%", :"vertical-align" => "middle"}}, :content => [(three_btn)], :script => ""} + # + cancel_btn = {:component=>"button", :options=>{:content => "Cancel", :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + cancel_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.respond({:action => :cancel}) + end + end + " + cancel_cell = {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px", :"vertical-align" => "middle"}}, :content => [(cancel_btn)], :script => ""} + # + # Define Containers: + # Rows: + row_one = {:component=>"block_table_row", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "100%"}}, :content=>[(banner_cell)], :script=>""} + row_two = {:component=>"block_table_row", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "100%"}}, :content=>[(text_input_cell)], :script=>""} + row_three = {:component=>"block_table_row", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "100%"}}, :content=>[], :script=>""} + # Tables: + table_one = {:component=>"block_table", :options=>{:style => {:padding => "5px", :margin => "0px", :width => "100%"}}, :content=>[(row_one)], :script=>""} + table_two = {:component=>"block_table", :options=>{:style => {:padding => "5px", :margin => "0px", :width => "100%"}}, :content=>[(row_two)], :script=>""} + table_three = {:component=>"block_table", :options=>{:style => {:padding => "5px", :margin => "0px", :width => "100%"}}, :content=>[(row_three)], :script=>""} + # + # Form: + form = {:component=>"form", :options=>{:style => {:"background-color" => "#f2f2f2", :padding => "#{form_padding}px", :margin => "#{form_margin}px"}}, :content=>[], :script=>""} + # + if banner_text.size > 0 + form[:content] << table_one + end + # Build form: + case dialog_type + when :choose + # no :cancel; :one, :two, and :three buttons + if one_text.size > 0 + row_three[:content] << one_btn_cell + end + if two_text.size > 0 + row_three[:content] << two_btn_cell + end + if three_text.size > 0 + row_three[:content] << three_btn_cell + end + when :alert + # no cancel button + action_btn = {:component=>"button", :options=>{:content => "OK", :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + action_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.respond({:action => :ok}) + end + end + " + action_btn_cell = {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px"}}, :content => [(action_btn)], :script => ""} + # + row_three[:content] << action_btn_cell + when :input + # text input, cancel and submit buttons + row_three[:content] << cancel_cell + action_btn = {:component=>"button", :options=>{:content => "OK", :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + action_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + form = self.find_parent_type(:form) + if the_window && form + the_window.respond({:action => :ok, :form => form.form_data()}) + end + end + " + action_btn_cell = {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px"}}, :content => [(action_btn)], :script => ""} + # + row_three[:content] << action_btn_cell + # + form[:content] << table_two + end + form[:content] << table_three + # Build Dialog + ### Calculate dialog width & height from content: + # + form[:options][:style][:width] = "#{total_width}px" + ### Set banner height and other settings: ???? + if banner_text.to_s.size > 0 + form[:content][0][:options][:style][:width] = "#{inner_width}px" + form[:content][0][:options][:style][:height] = "#{banner_height}px" + form[:content][0][:options][:style][:padding] = "10px 0px 10px 0px" + total_height += (banner_height + 20) + end + if dialog_type == :input + form[:content][1][:options][:style][:width] = "#{inner_width}px" + form[:content][1][:options][:style][:height] = "#{input_font_size}px" + form[:content][1][:options][:style][:padding] = "10px 0px 10px 0px" + total_height += (input_font_size + 20) + form[:content][2][:options][:style][:width] = "#{inner_width}px" + form[:content][2][:options][:style][:height] = "32px" + form[:content][2][:options][:style][:padding] = "10px 0px 10px 0px" + total_height += 52 + else + # just buttons + form[:content][1][:options][:style][:width] = "#{inner_width}px" + form[:content][1][:options][:style][:height] = "32px" + form[:content][1][:options][:style][:padding] = "10px 0px 10px 0px" + total_height += 52 + end + # Review : cuts buttons off @ bottom on Firefox (shim-fix) + total_height += 32 + # + form[:options][:style][:height] = "#{(total_height - ((form_padding + form_margin) * 2))}px" + ### Calculate button horizontal spacing: inner_width + case row_three[:content].size + when 1 + row_three[:content][0][:options][:style][:padding] = "5px 0px 5px #{((inner_width / 2) - 25)}px" + when 2 + row_three[:content][0][:options][:style][:padding] = "5px 0px 5px #{((inner_width / 4) - 12)}px" + row_three[:content][1][:options][:style][:padding] = "5px 0px 5px #{((inner_width / 4) - 12)}px" + when 3 + row_three[:content][0][:options][:style][:padding] = "5px 0px 5px #{((inner_width / 6) - 6)}px" + row_three[:content][1][:options][:style][:padding] = "5px 0px 5px #{((inner_width / 6) - 6)}px" + row_three[:content][2][:options][:style][:padding] = "5px 0px 5px #{((inner_width / 6) - 6)}px" + end + # + dialog_source = {:component => "dialog_box", :options => {:window_title => window_title, :states => {:hidden => true}, :width => total_width, :height => total_height}, :script => "", :content => [(form)]} + dialog_source[:script] = " + on(:keyup) do |event| + if event[:which] == 27 + self.respond({:action => :cancel}) + end + end + " + # + begin + # log_info("Opening dialog ...") + self.dialog_open(dialog_source, the_application, details, &block) + true + rescue Exception => the_error + log_error({:error => the_error, :parameters => {}}) + false + end + end + # Mini Path Finder: + def vfs_dialog(the_application=nil, details=nil, &block) + # details: {:type => :folder, :path => "/path/to/somewhere"} + # Determine Dialog Type: + window_title = "Unsupported Dialog Type" + action_title = "Unsupported" + dialog_type = (details || {:type => :folder})[:type] + case dialog_type + when :save, "save" + window_title = "Save As ..." + action_title = "Save" + when :folder, "folder" + window_title = "Choose Folder" + action_title = "Choose" + when :object, "object" + window_title = "Select Object" + action_title = "Select" + end + # Path Menu: ???? + # `#{@element}.textContent = 'The_String'` || set_text("The_String") + menu_bar = {:component => "menu_bar", :options => {:title => "path_menu_bar"}, :content => [], :script => ""} + menu_bar[:script] = " + def update_appearance() + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + this_layer = the_window.layer() + unless @current_path + @current_path = (the_window.data()[:path] || '/') + end + build_list = [] + if @current_path == '/' + current = [''] + else + current = @current_path.split('/') + end + # + path_menu = {:component => 'menu_entry', :options => {:title => 'path_menu', :content => File.basename(@current_path), :layer => (this_layer + 100), :menu => self}, :content => [], :script => ''} + # + neg_index = (current.size * -1) + ((neg_index)..-1).to_a.reverse.each_with_index do |the_offset, indexer| + subpath = current[(0..(the_offset))].join('/') + if subpath == '' || subpath == '/' + path_title = '/' + else + path_title = File.basename(subpath) + end + path_menu[:content] << {:component => 'menu_item', :options => {:content => path_title, :data => {:path => subpath}}, :content => [], :script => ''} + end + build_list << path_menu + self.children.values.each do |the_child| + the_child.destroy + end + self.build_interior_components(build_list) + end + end + def path() + @current_path + end + def path=(the_path=nil) + @current_path = (the_path || '/') + # self.set_text(File.basename(@current_path)) + self.update_appearance + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + list = the_window.find_child('subitems') + if list + list.update_appearance() + end + end + end + # + def select_item(the_menu_item=nil) + if the_menu_item + self.path = the_menu_item.data()[:path] + end + end + " + menu_bar_cell = {:component => "block_table_cell", :options => {:style => {:border => "1px solid #c2c2c2", :padding => "0px", :margin => "0px", :width => "100%"}}, :content => [(menu_bar)], :script => ""} + # SubItem List: + subitems = {:component=>"list", :options=>{:title => "subitems", :style => {:clear => "both", :"list-style" => "none", :padding => "0px", :margin => "0px"}}, :content=>[], :script=>""} + # Various SubItems scripts (per dialog_type) + subitems[:script] = " + def manifest() + @manifest + end + # + def update_appearance() + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + dialog_type = (the_window.data()[:type] || :folder) + menu_bar = the_window.find_child('path_menu_bar') + if menu_bar + current_path = (menu_bar.path() || '/') + else + current_path = '/' + end + @selection = nil + @manifest = {} + self.children.values.each do |the_child| + the_child.destroy + end + GxG::CONNECTION.entries({:location => (current_path)}) do |response| + if response.is_a?(::Hash) + response[:result].each do |the_profile| + item_uuid = ::GxG::uuid_generate() + item_path = (current_path + '/' + the_profile[:title].to_s) + if dialog_type == :folder + if [:virtual_directory, :directory, :persisted_array].include?(the_profile[:type].to_s.to_sym) + @manifest[(item_uuid.to_s.to_sym)] = {:path => item_path, :profile => the_profile} + self.add_list_item({:icon => self.theme_icon('folder.svg'), :label => the_profile[:title].to_s, :uuid => item_uuid.to_s}) + end + else + @manifest[(item_uuid.to_s.to_sym)] = {:path => item_path, :profile => the_profile} + case (the_profile[:type].to_s.to_sym) + when :virtual_directory, :directory, :persisted_array + the_icon_path = self.theme_icon('folder.svg') + when :persisted_hash + the_icon_path = self.theme_icon('object.svg') + else + # Review: further qualify file type icons ?? + the_icon_path = self.theme_icon('file.svg') + end + self.add_list_item({:icon => the_icon_path, :label => the_profile[:title].to_s, :uuid => item_uuid.to_s}) + end + end + end + end + # + end + end + " + case dialog_type + when :save + subitems[:script] = (subitems[:script] + "\n" + " + self.set_select_responder do |the_uuid| + if @manifest[(the_uuid.to_s.to_sym)] + # nothing + end + end + self.set_open_responder do |the_uuid| + if @manifest[(the_uuid.to_s.to_sym)] + if [:virtual_directory, :directory, :persisted_array].include?(@manifest[(the_uuid.to_s.to_sym)][:profile][:type].to_s.to_sym) + # open folder + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + menu_bar = the_window.find_child('path_menu_bar') + if menu_bar + GxG::DISPATCHER.post_event(:root) do + menu_bar.path = @manifest[(the_uuid.to_s.to_sym)][:path] + end + end + end + end + end + end + ") + when :folder + subitems[:script] = (subitems[:script] + "\n" + " + self.set_select_responder do |the_uuid| + if @manifest[(the_uuid.to_s.to_sym)] + # nothing + end + end + self.set_open_responder do |the_uuid| + if @manifest[(the_uuid.to_s.to_sym)] + if [:virtual_directory, :directory, :persisted_array].include?(@manifest[(the_uuid.to_s.to_sym)][:profile][:type].to_s.to_sym) + # open folder + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + menu_bar = the_window.find_child('path_menu_bar') + if menu_bar + GxG::DISPATCHER.post_event(:root) do + menu_bar.path = @manifest[(the_uuid.to_s.to_sym)][:path] + end + end + end + end + end + end + ") + when :object + subitems[:script] = (subitems[:script] + "\n" + " + self.set_select_responder do |the_uuid| + if @manifest[(the_uuid.to_s.to_sym)] + # if object selected enable Select button else disable it. + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + action_button = the_window.find_child('action_button') + if action_button + if [:file, :persisted_hash].include?(@manifest[(the_uuid.to_s.to_sym)][:profile][:type].to_s.to_sym) + action_button.enable() + else + action_button.disable() + end + end + end + end + end + self.set_open_responder do |the_uuid| + if @manifest[(the_uuid.to_s.to_sym)] + if [:virtual_directory, :directory, :persisted_array].include?(@manifest[(the_uuid.to_s.to_sym)][:profile][:type].to_s.to_sym) + # open folder + # first, disable Select button + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + menu_bar = the_window.find_child('path_menu_bar') + if menu_bar + GxG::DISPATCHER.post_event(:root) do + menu_bar.path = @manifest[(the_uuid.to_s.to_sym)][:profile][:path] + end + end + end + end + end + end + ") + end + # + subitems_container = {:component=>"block", :options => {:style => {:width => "100%", :height => "192px", :"overflow-y" => "scroll"}}, :content => [(subitems)], :script => ""} + subitems_cell = {:component => "block_table_cell", :options => {:style => {:border => "1px solid #c2c2c2", :padding => "0px", :margin => "0px", :width => "100%"}}, :content => [(subitems_container)], :script => ""} + # Save As ... Field + save_name = {:component=>"text_input", :options=>{:title => "save_name", :style => {:"background-color" => "#f2f2f2", :"font-size" => "16px", :width => "100%"}}, :content=>[], :script=>""} + save_name_cell = {:component => "block_table_cell", :options => {:style => {:padding => "0px", :margin => "0px", :width => "100%"}}, :content => [(save_name)], :script => ""} + # Buttons: + action_btn = {:component=>"submit_button", :options=>{:title => "action_button", :content => action_title, :style => {:padding => "0px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + action_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + form = self.find_parent_type(:form) + if the_window && form + menu_bar = the_window.find_child('path_menu_bar') + list = the_window.find_child('subitems') + if menu_bar && list + if list.selection() + result_path = list.manifest()[(list.selection().to_s.to_sym)][:path] + else + result_path = menu_bar.path() + end + if the_window.data()[:type].to_s.to_sym == :save + result_path = (result_path + '/' + (form.form_data()[:save_name] || 'Untitled.data').to_s) + end + the_window.respond({:action => the_window.data()[:type].to_s.to_sym, :path => result_path}) + end + end + end + ".encode64 + action_cell = {:component => "block_table_cell", :options => {:style => {:padding => "10px 0px 0px 35px", :margin => "0px", :width => "50%"}}, :content => [(action_btn)], :script => ""} + cancel_btn = {:component=>"button", :options=>{:content => "Cancel", :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + cancel_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.respond({:action => :cancel}) + end + end + ".encode64 + cancel_cell = {:component => "block_table_cell", :options => {:style => {:padding => "10px 0px 0px 35px", :margin => "0px", :width => "50%"}}, :content => [(cancel_btn)], :script => ""} + # Rows: + row_one = {:component=>"block_table_row", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "100%"}}, :content=>[(menu_bar_cell)], :script=>""} + row_two = {:component=>"block_table_row", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "100%"}}, :content=>[(subitems_cell)], :script=>""} + row_three = {:component=>"block_table_row", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "100%"}}, :content=>[(save_name_cell)], :script=>""} + row_four = {:component=>"block_table_row", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "100%"}}, :content=>[(cancel_cell),(action_cell)], :script=>""} + # Tables: + table_one = {:component=>"block_table", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "300px", :height => "32px"}}, :content=>[(row_one)], :script=>""} + table_two = {:component=>"block_table", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "300px", :height => "192px"}}, :content=>[(row_two)], :script=>""} + table_three = {:component=>"block_table", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "300px", :height => "32px"}}, :content=>[(row_three)], :script=>""} + table_four = {:component=>"block_table", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "300px", :height => "32px"}}, :content=>[(row_four)], :script=>""} + # + # Main Build: + form = {:component=>"form", :options=>{:style => {:"background-color" => "#f2f2f2", :padding => "20px", :margin => "5px", :width => "100%"}}, :content=>[], :script=>""} + # + total_height = 330 + form[:content] << table_one + form[:content] << table_two + if dialog_type == :save || dialog_type == "save" + form[:content] << table_three + total_height += 32 + end + form[:content] << table_four + viewport = {:component=>"application_viewport", :options=>{:title => "selection_viewport", :style => {:overflow => "hidden", :width => "100%", :height => "100%"}}, :content=>[(form)], :script=>""} + dialog_source = {:component => "dialog_box", :options => {:window_title => window_title, :states => {:hidden => true}, :width => 350, :height => total_height}, :script => "", :content => [(viewport)]} + dialog_source[:script] = " + def after_open() + menu_bar = self.find_child('path_menu_bar') + if menu_bar + menu_bar.path = (self.data()[:path]) + end + end + " + # + begin + # log_info("Opening dialog ...") + self.dialog_open(dialog_source, the_application, details, &block) + true + rescue Exception => the_error + log_error({:error => the_error, :parameters => {}}) + false + end + end + # internal windowing tools: + def get_window_registration(the_reference=nil) + if the_reference.is_any?(::String, ::Symbol) + # {:window => nil, :layer => 0, :restore => nil} + @window_registry[(the_reference)] + else + nil + end + end + # + def set_window_switcher(the_switcher=nil) + if the_switcher + @window_switcher = the_switcher + end + end + def get_window_switcher() + @window_switcher + end + # z-index management, restore/min/max etc. + # @window_registry = (@window_registry.sort_by {|uuid,entry| entry[:layer]}).to_h + # interfacing with others: + def get_window(the_reference=nil) + if the_reference.is_any?(::String, ::Symbol) + registration = self.get_window_registration(the_reference) + if registration + registration[:window] + else + nil + end + else + nil + end + end + # + def window_list() + result = [] + @window_registry.keys.each do |reference| + result << {:uuid => reference, :title => @window_registry[(reference)][:window].title()} + end + result + end + # + def window_repack_layers() + @window_registry = (@window_registry.sort_by {|uuid,entry| entry[:layer]}).to_h + @window_registry.values.each_with_index do |entry, indexer| + entry[:window].gxg_merge_style(:"z-index",(200 + indexer)) + entry[:layer] = indexer + end + true + end + # + def window_top_layer() + ((@window_registry.collect {|uuid,entry| entry[:layer]}).max || 0) + end + # + def window_back_layer() + back_layer = (((@window_registry.collect {|uuid,entry| entry[:layer]}).min || 0) - 1) + if back_layer < 100 + window_repack_layers + back_layer = (((@window_registry.collect {|uuid,entry| entry[:layer]}).min || 0) - 1) + end + back_layer + end + # + def window_next_layer() + next_layer = (((@window_registry.collect {|uuid,entry| entry[:layer]}).max || 0) + 1) + if next_layer > 99 + window_repack_layers + next_layer = (((@window_registry.collect {|uuid,entry| entry[:layer]}).max || 0) + 1) + end + next_layer + end + # + def window_register(the_reference=nil, the_instance=nil) + if the_reference.is_any?(::String, ::Symbol) + if the_instance + # {:window => nil, :layer => 0, :restore => nil} + front_layer = self.window_next_layer() + the_instance.gxg_merge_style(:"z-index",(200 + front_layer)) + @window_registry[(the_reference)] = {:window => the_instance, :layer => front_layer, :restore => nil} + true + else + false + end + else + false + end + end + # + def window_unregister(the_reference=nil) + if the_reference.is_any?(::String, ::Symbol) + @window_registry.delete(the_reference) + true + else + false + end + end + # + def window_open(build_source=nil, the_application=nil) + if build_source.is_any?(::Hash, GxG::Database::PersistedHash) + if build_source.is_a?(GxG::Database::PersistedHash) + the_uuid = build_source.uuid + the_options = build_source[:options].unpersist + if the_options[:title] + the_title = the_options.delete(:title) + else + the_title = build_source.title + end + the_options[:uuid] = the_uuid + else + the_uuid = GxG::uuid_generate.to_sym + the_options = build_source[:options].clone + the_title = the_options.delete(:title) + the_options[:uuid] = the_uuid + end + the_style = the_options.delete(:style) + the_states = (the_options.delete(:states) || {}) + new_window = GxG::DISPLAY_DETAILS[:object].add_child(the_uuid, GxG::Gui::Window, the_options) + if new_window + new_window.set_title(the_title) + # Set states: + unless the_states.keys.include?(:hidden) + the_states[:hidden] = false + end + new_window.gxg_set_states(the_states) + # process style info: + if the_style.is_a?(::Hash) + new_window.gxg_set_style(the_style) + end + if the_application.is_a?(::GxG::Application) + new_window.set_application(the_application) + unless the_application.get_window(the_uuid) + the_application.link_window(new_window) + end + if new_window.menu_reference() + the_menu_source = the_application.search_content(new_window.menu_reference()) + if the_menu_source.is_any?(::Hash, ::GxG::Database::PersistedHash) + new_window.set_menu(the_menu_source) + else + log_warn("Invalid Menu Resource referenced: #{new_window.menu_reference().inspect}") + end + end + end + # Set Script if any is provided + if build_source[:script].size > 0 + new_window.set_script(build_source[:script].to_s) + end + # + self.register_object(the_title, new_window) + self.window_register(the_uuid,new_window) + self.build_components([{:parent => GxG::DISPLAY_DETAILS[:object], :record => build_source, :element => new_window}],the_application) + new_window.before_open + new_window.open + unless the_states[:hidden] == true + new_window.show + end + new_window.after_open + true + else + false + end + else + false + end + end + # + def window_close(the_reference=nil, options={}) + if the_reference.is_any?(::String, ::Symbol) + registration = self.get_window_registration(the_reference) + if registration + registration[:window].before_close + registration[:window].close + registration[:window].hide + registration[:window].after_close + self.window_unregister(the_reference) + self.unregister_object(the_reference) + registration[:window].destroy + true + else + false + end + else + false + end + end + # + def window_is_in_back?(the_reference=nil) + the_record = self.get_window_registration(the_reference) + if the_record + bottom_layer = ((@window_registry.collect {|uuid,entry| entry[:layer]}).min || 0) + if the_record[:layer] == bottom_layer + true + else + false + end + else + false + end + end + # + def window_is_in_front?(the_reference=nil) + the_record = self.get_window_registration(the_reference) + if the_record + top_layer = ((@window_registry.collect {|uuid,entry| entry[:layer]}).max || 0) + if the_record[:layer] == top_layer + true + else + false + end + else + false + end + end + # + def window_send_to_back(the_reference=nil) + the_record = self.get_window_registration(the_reference) + if the_record + unless self.window_is_in_back?(the_reference) + back_layer = self.window_back_layer() + the_record[:window].gxg_merge_style(:"z-index",(200 + back_layer)) + the_record[:layer] = back_layer + end + else + false + end + end + # + def window_bring_to_front(the_reference=nil, options={}) + the_record = self.get_window_registration(the_reference) + if the_record + unless the_record[:layer] == self.window_top_layer() + front_layer = self.window_next_layer() + the_record[:window].gxg_merge_style(:"z-index",(200 + front_layer)) + the_record[:layer] = front_layer + end + true + else + false + end + end + # Transition Effect Methods (override for custom effects) + def next_minimize_slot() + # slots are allocated from the top / left of the content area: + cell_width = 200 + cell_height = 24 + bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + columns = (0..(bounds[:right] - bounds[:left]).step(cell_width).size) + rows = (0..(bounds[:bottom] - bounds[:top]).step(cell_height).size) + found = nil + (0..(rows - 1)).each do |the_row| + (0..(columns - 1)).each do |the_column| + is_in = false + test_rect = {:top => (bounds[:top] + (the_row * cell_height)), :left => (bounds[:left] + (the_column * cell_width)), :right => (bounds[:left] + (the_column * cell_width) + cell_width), :bottom => (bounds[:top] + (the_row * cell_height) + cell_height)} + @window_registry.values.each do |the_record| + # + this_rect = {:top => 0, :left => 0, :right => 0, :bottom => 0} + this_rect[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + this_rect[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + this_rect[:right] = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + this_rect[:bottom] = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + if (this_rect[:top] == test_rect[:top] && this_rect[:left] == test_rect[:left] && this_rect[:right] == test_rect[:right] && this_rect[:bottom] == test_rect[:bottom]) + is_in = true + break + end + # + end + unless is_in + found = test_rect + break + end + end + if found + break + end + end + found + end + # + def minimize_effect(the_record=nil,&block) + switcher = self.get_window_switcher() + if switcher + origin = {:opacity => 1} + origin[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + origin[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + origin_right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + origin_bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + origin[:width] = (origin_right - origin[:left]) + origin[:height] = (origin_bottom - origin[:top]) + destination = {:opacity => 0} + destination[:top] = `#{switcher.element}.getBoundingClientRect().top`.to_i + destination[:left] = `#{switcher.element}.getBoundingClientRect().right`.to_i + destination_right = `#{switcher.element}.getBoundingClientRect().right`.to_i + destination_bottom = `#{switcher.element}.getBoundingClientRect().top`.to_i + destination[:width] = (destination_right - destination[:left]) + destination[:height] = (destination_bottom - destination[:top]) + conclude = Proc.new { + if block.respond_to?(:call) + block.call(true) + end + } + else + # find a tile spot to land on: + the_slot = self.next_minimize_slot() + origin = {:opacity => 1} + origin[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + origin[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + origin_right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + origin_bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + origin[:width] = (origin_right - origin[:left]) + origin[:height] = (origin_bottom - origin[:top]) + destination = {:opacity => 0.5} + destination[:top] = the_slot[:top] + destination[:left] = the_slot[:left] + destination_right = the_slot[:right] + destination_bottom = the_slot[:bottom] + destination[:width] = (destination_right - destination[:left]) + destination[:height] = (destination_bottom - destination[:top]) + conclude = Proc.new { + self.window_send_to_back(the_record[:window].uuid) + if block.respond_to?(:call) + block.call(true) + end + } + end + animate_content = Proc.new { + top = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + left = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + the_record[:window].set_frame({:top => top, :left => left, :right => right, :bottom => bottom}) + } + the_record[:window].animate({:origin => origin, :destination => destination, :options => {:duration => 500, :animationiteration => animate_content, :animationend => conclude}}) + # + settings = {:action => :minimize, :top => 0, :left => 0, :right => 0, :bottom => 0} + settings[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + settings[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + settings[:right] = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + settings[:bottom] = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + settings + end + # + def unminimize_effect(the_record=nil,&block) + switcher = self.get_window_switcher() + if switcher + origin = {:opacity => 0} + origin[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + origin[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + origin_right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + origin_bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + origin[:width] = (origin_right - origin[:left]) + origin[:height] = (origin_bottom - origin[:top]) + destination = {:opacity => 1} + destination[:top] = the_record[:restore][:settings][:top] + destination[:left] = the_record[:restore][:settings][:left] + destination_right = the_record[:restore][:settings][:right] + destination_bottom = the_record[:restore][:settings][:bottom] + destination[:width] = (destination_right - destination[:left]) + destination[:height] = (destination_bottom - destination[:top]) + else + # + origin = {:opacity => 0.5} + origin[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + origin[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + origin_right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + origin_bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + origin[:width] = (origin_right - origin[:left]) + origin[:height] = (origin_bottom - origin[:top]) + destination = {:opacity => 1} + destination[:top] = the_record[:restore][:settings][:top] + destination[:left] = the_record[:restore][:settings][:left] + destination_right = the_record[:restore][:settings][:right] + destination_bottom = the_record[:restore][:settings][:bottom] + destination[:width] = (destination_right - destination[:left]) + destination[:height] = (destination_bottom - destination[:top]) + end + animate_content = Proc.new { + top = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + left = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + the_record[:window].set_frame({:top => top, :left => left, :right => right, :bottom => bottom}) + } + conclude = Proc.new { + if block.respond_to?(:call) + block.call(true) + end + } + the_record[:window].animate({:origin => origin, :destination => destination, :options => {:duration => 500, :animationiteration => animate_content, :animationend => conclude}}) + # + true + end + # + def maximize_effect(the_record=nil,&block) + bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + origin = {} + origin[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + origin[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + origin_right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + origin_bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + origin[:width] = (origin_right - origin[:left]) + origin[:height] = (origin_bottom - origin[:top]) + destination = {} + destination[:top] = bounds[:top] + destination[:left] = bounds[:left] + destination[:width] = (bounds[:right] - destination[:left]) + destination[:height] = (bounds[:bottom] - destination[:top]) + # run animation (which alters the state) + # animationiteration set_frame + animate_content = Proc.new { + top = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + left = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + the_record[:window].set_frame({:top => top, :left => left, :right => right, :bottom => bottom}) + } + conclude = Proc.new { + if block.respond_to?(:call) + block.call(true) + end + } + the_record[:window].animate({:origin => origin, :destination => destination, :options => {:duration => 500, :animationiteration => animate_content, :animationend => conclude}}) + # + settings = {:action => :maximize, :top => 0, :left => 0, :right => 0, :bottom => 0} + settings[:top] = bounds[:top] + settings[:left] = bounds[:left] + settings[:right] = bounds[:right] + settings[:bottom] = bounds[:bottom] + settings + end + # + def unmaximize_effect(the_record=nil,&block) + origin = {} + origin[:top] = the_record[:window].top() + origin[:left] = the_record[:window].left() + origin_right = the_record[:window].right() + origin_bottom = the_record[:window].bottom() + origin[:width] = (origin_right - origin[:left]) + origin[:height] = (origin_bottom - origin[:top]) + destination = {} + destination[:top] = the_record[:restore][:settings][:top] + destination[:left] = the_record[:restore][:settings][:left] + destination_right = the_record[:restore][:settings][:right] + destination_bottom = the_record[:restore][:settings][:bottom] + destination[:width] = (destination_right - destination[:left]) + destination[:height] = (destination_bottom - destination[:top]) + # run animation (which alters the state) + animate_content = Proc.new { + top = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + left = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + the_record[:window].set_frame({:top => top, :left => left, :right => right, :bottom => bottom}) + } + conclude = Proc.new { + if block.respond_to?(:call) + block.call(true) + end + } + the_record[:window].animate({:origin => origin, :destination => destination, :options => {:duration => 500, :animationiteration => animate_content, :animationend => conclude}}) + true + end + # + def rollup_effect(the_record=nil,&block) + origin = {} + the_top = the_record[:window].top() + origin[:height] = the_record[:window].height() + destination = {} + destination[:height] = 24 + # run animation (which alters the state) + window_body = the_record[:window].find_child("window_body",true) + if window_body + window_body.hide({:origin => {:opacity => 1.0}, :destination => {:opacity => 0.0}}) do + the_record[:window].animate({:origin => origin, :destination => destination}) + if block.respond_to?(:call) + block.call(true) + end + end + end + # + settings = {:action => :rollup, :top => 0, :left => 0, :right => 0, :bottom => 0} + settings[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + settings[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + settings[:right] = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + settings[:bottom] = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + settings + end + # + def rolldown_effect(the_record=nil,&block) + origin = {} + origin[:height] = 24 + destination = {} + destination[:height] = (the_record[:restore][:settings][:bottom] - the_record[:restore][:settings][:top]) + # run animation (which alters the state) + conclude = Proc.new { + window_body = the_record[:window].find_child("window_body",true) + if window_body + window_body.show({:origin => {:opacity => 0.0}, :destination => {:opacity => 1.0}}) do + the_record[:window].commit_settings + if block.respond_to?(:call) + block.call(true) + end + end + end + } + the_record[:window].animate({:origin => origin, :destination => destination, :options => {:duration => 500, :animationend => conclude}}) + true + end + # set/restore window state: + def window_restoreable?(the_reference=nil) + the_record = self.get_window_registration(the_reference) + if the_record + if the_record[:restore].is_a?(::Hash) + true + else + false + end + else + false + end + end + # + def window_restore(the_reference=nil, options={},&block) + the_record = self.get_window_registration(the_reference) + if the_record + if the_record[:restore].is_a?(::Hash) + # restore from last :action + case the_record[:restore][:action] + when :minimize + self.unminimize_effect(the_record) do + the_record[:window].clear_special_state(the_record[:restore][:settings]) + # clear the restore record + the_record[:restore] = nil + if block.respond_to?(:call) + block.call(true) + end + end + when :maximize + self.unmaximize_effect(the_record) do + the_record[:window].clear_special_state(the_record[:restore][:settings]) + # clear the restore record + the_record[:restore] = nil + if block.respond_to?(:call) + block.call(true) + end + end + when :rollup + self.rolldown_effect(the_record) do + the_record[:window].clear_special_state(the_record[:restore][:settings]) + # clear the restore record + the_record[:restore] = nil + if block.respond_to?(:call) + block.call(true) + end + end + end + true + else + false + end + else + false + end + end + # + def window_minimize(the_reference=nil, settings={}) + the_record = self.get_window_registration(the_reference) + if the_record + if self.window_restoreable?(the_reference) + self.window_restore(the_reference) do + settings = {:top => the_record[:window].top, :left => the_record[:window].left, :right => the_record[:window].right, :bottom => the_record[:window].bottom} + the_record[:restore] = {:action => :minimize, :settings => settings} + the_record[:window].set_special_state(self.minimize_effect(the_record)) + end + else + the_record[:restore] = {:action => :minimize, :settings => settings} + the_record[:window].set_special_state(self.minimize_effect(the_record)) + end + true + else + false + end + end + # + def window_maximize(the_reference=nil, settings={}) + the_record = self.get_window_registration(the_reference) + if the_record + if self.window_restoreable?(the_reference) + self.window_restore(the_reference) do + settings = {:top => the_record[:window].top, :left => the_record[:window].left, :right => the_record[:window].right, :bottom => the_record[:window].bottom} + the_record[:restore] = {:action => :maximize, :settings => settings} + the_record[:window].set_special_state(self.maximize_effect(the_record)) + self.window_bring_to_front(the_reference) + end + else + the_record[:restore] = {:action => :maximize, :settings => settings} + the_record[:window].set_special_state(self.maximize_effect(the_record)) + self.window_bring_to_front(the_reference) + end + true + else + false + end + end + # + def window_rollup(the_reference=nil, settings={}) + the_record = self.get_window_registration(the_reference) + if the_record + if self.window_restoreable?(the_reference) + self.window_restore(the_reference) do + settings = {:top => the_record[:window].top, :left => the_record[:window].left, :right => the_record[:window].right, :bottom => the_record[:window].bottom} + the_record[:restore] = {:action => :rollup, :settings => settings} + the_record[:window].set_special_state(self.rollup_effect(the_record)) + end + else + the_record[:restore] = {:action => :rollup, :settings => settings} + the_record[:window].set_special_state(self.rollup_effect(the_record)) + end + true + else + false + end + end + # + # Loading and Building: + def get_library(the_reference=nil) + result = nil + if the_reference.is_any?(::Hash, ::GxG::Database::PersistedHash) + GxG::APPLICATIONS[:libraries].values.each do |the_library| + if the_library.title == the_reference[:library].to_s && the_library.type == (the_reference[:type] || the_library.type) + if the_reference[:maximum] + if ((the_reference[:minimum].to_f)..(the_reference[:maximum].to_f)).include?(the_library.version) + result = the_library + break + end + else + if the_library.version >= the_reference[:minimum].to_f + result = the_library + break + end + end + end + end + found + else + if GxG::APPLICATIONS[:libraries][(the_reference.to_s.to_sym)] + result = GxG::APPLICATIONS[:libraries][(the_reference.to_s.to_sym)] + else + GxG::APPLICATIONS[:libraries].values.each do |the_library| + if the_library.title == the_reference + result = the_library + break + end + end + end + end + result + end + # + def library_loaded?(the_reference=nil) + if the_reference.is_any?(::Hash, ::GxG::Database::PersistedHash) + found = false + GxG::APPLICATIONS[:libraries].values.each do |the_library| + if the_library.title == the_reference[:library].to_s && the_library.type == (the_reference[:type] || the_library.type).to_s + if the_reference[:maximum] + if ((the_reference[:minimum].to_f)..(the_reference[:maximum].to_f)).include?(the_library.version) + found = the_library.loaded?() + break + end + else + if the_library.version >= the_reference[:minimum].to_f + found = the_library.loaded?() + break + end + end + end + end + found + else + if GxG::APPLICATIONS[:libraries][(the_reference.to_s.to_sym)] + true + else + found = false + GxG::APPLICATIONS[:libraries].values.each do |the_library| + if the_library.title == the_reference + found = the_library.loaded?() + break + end + end + found + end + end + end + # + def load_library(criteria=nil, &block) + if criteria.is_any?(::Hash, ::GxG::Database::PersistedHash) + # {:library => "lib-name", :manifest => true/false, :dependencies => true/false, :minimum => 0.0, :maximum => 0.0, :loaded => [...]} + if criteria[:maximum] + maximum = criteria[:maximum].to_f + else + maximum = nil + end + GxG::CONNECTION.library_pull({:library => criteria[:library].to_s, :type => criteria[:type].to_s, :minimum => (criteria[:minimum] || 0.0).to_f, :maximum => maximum,:dependencies => true, :loaded => GxG::APPLICATIONS[:libraries].keys}) do |response| + # process response[:result] + if response.is_a?(::Hash) + if response[:result].is_a?(::Array) + response[:result].each do |the_record| + unless GxG::APPLICATIONS[:libraries].keys.include?(the_record[:uuid].to_s.to_sym) + definition = the_record[:data] + if definition.is_a?(::String) + if definition.base64? + definition = definition.decode64 + end + if definition.json? + definition = ::JSON::parse(definition,{:symbolize_names => true}) + end + if definition.is_a?(::Hash) + definition = ::GxG::Database::process_import(definition) + end + if definition.is_a?(::GxG::Database::PersistedHash) + GxG::APPLICATIONS[:libraries][(the_record[:uuid].to_s.to_sym)] = ::GxG::Library.new(definition) + if block.respond_to?(:call) + block.call(GxG::APPLICATIONS[:libraries][(the_record[:uuid].to_s.to_sym)]) + end + end + end + end + end + end + end + end + # + end + end + # + def load_libraries(load_queue=[], &block) + result = true + if load_queue.is_any?(::Array, ::GxG::Database::PersistedArray) + load_queue.each do |the_requirement| + GxG::DISPLAY_DETAILS[:object].load_library(the_requirement) do |the_library| + unless the_library.loaded? + log_warn("Unable to load library: #{the_library.title}") + result = false + break + end + end + end + # + end + if block.respond_to?(:call) + block.call(result) + end + result + end + # Application Supports: + def application_open(specifier={}, parameters={}) + result = false + if specifier.is_a?(::Hash) + error_hander = Proc.new { + self.set_busy(false) + } + self.set_busy(true) + GxG::CONNECTION.application_open(specifier,error_hander) do |app_stub| + if app_stub.is_a?(::Hash) + if app_stub[:result].is_a?(::Hash) + GxG::CONNECTION.pull_object(app_stub[:result][:location]) do |source_object| + GxG::APPLICATIONS[:processes][(app_stub[:result][:application].to_s.to_sym)] = GxG::Application.new(app_stub[:result].merge({:source => source_object})) + # + result = GxG::APPLICATIONS[:processes][(app_stub[:result][:application].to_s.to_sym)].run(parameters) + # + self.set_busy(false) + end + else + log_warn('Error opening Application: ' + app_stub.inspect) + end + else + log_warn("Malformed response: #{app_stub.inspect}") + end + end + else + log_warn("Invalid Argument: #{specifier.inspect}") + end + result + end + # Theme Supports: + def load_theme(the_theme="") + @theme = the_theme + @theme_prefix = (::GxG::CONNECTION.relative_url() + "/themes/#{the_theme}") + # + ::GxG::CONNECTION.pull_object("/content/themes/#{the_theme}") do |response| + GxG::Gui::Css.refresh_css_cache() + # Set CSS Rules: + if response.is_any?(::Hash, ::GxG::Database::PersistedHash) + if response.is_a?(::GxG::Database::PersistedHash) + response = response.unpersist() + end + if response[:resources].is_a?(::Array) + # Load Font and other Resources: response[:resources] (Array of Hashes) + # Font Resource Profile: { + # :resource_type => "font-face", + # :font_family => "Varela", + # :font_style => "normal", + # :font_weight => 400, + # :format => "truetype", + # :source => "http://fonts.gstatic.com/s/varela/v10/DPEtYwqExx0AWHX5Ax4B.ttf" + # } + response[:resources].each do |the_resource_profile| + if the_resource_profile.is_a?(::Hash) + if the_resource_profile[:resource_type] == "font-face" + ::GxG::Gui::Css::load_font(the_resource_profile) + end + # expand for other resource types + end + end + end + if response[:rules].is_a?(::Hash) + response[:rules].keys.each do |the_css_key| + GxG::Gui::Css.set_rule(the_css_key, response[:rules][(the_css_key)]) + end + end + end + # + end + end + # Component Building: + def build_components(build_queue=[], the_application=nil) + result = false + if build_queue.is_any?(::Array, ::GxG::Database::PersistedArray) + while build_queue.size > 0 do + entry = build_queue.shift + if entry.is_any?(::Hash, ::GxG::Database::PersistedHash) + # Build out object based upon the record + if entry[:element].is_a?(::GxG::Gui::Page) + if entry[:record][:page_title] + `document.title = #{entry[:record][:page_title].to_s}` + # TODO: set meta data + # Set classes for page: + if entry[:record][:class].to_s.size > 0 + `document.body.setAttribute("class",#{entry[:record][:class].to_s})` + end + # Set Script if any is provided + if entry[:record][:script].size > 0 + entry[:element].set_script(entry[:record][:script].to_s) + end + end + else + # All other components / elements + end + # Queue up sub-components for construction + if entry[:record][:content].size > 0 + entry[:record][:content].each do |sub_component| + # puts "Building: #{sub_component.inspect}" + case sub_component[:component].to_s.downcase.to_sym + when :animation + # store the animation in GxG::ANIMATION[:animations] hash (by uuid) + if sub_component.is_a?(::GxG::Database::PersistedHash) + the_uuid = sub_component.uuid.to_s.to_sym + the_options = sub_component[:options].unpersist() + unless the_options[:title].is_a?(::String) + the_options[:title] = sub_component.title.to_s + end + else + the_uuid = ::GxG::uuid_generate.to_s.to_sym + the_options = sub_component[:options].clone + unless the_options[:title].is_a?(::String) + the_options[:title] = "Untitled Animation #{the_uuid.to_s}" + end + end + GxG::ANIMATION[:animations][(the_uuid)] = GxG::Gui::Animation.new(the_options,the_uuid) + if sub_component[:script].size > 0 + GxG::ANIMATION[:animations][(the_uuid)].set_script(sub_component[:script].to_s) + end + # store the animation assets in GxG::ANIMATION[:assets] hash (by uuid) (later) + if sub_component[:content].size > 0 + sub_component[:content].each do |the_asset| + # Expansion ?? + end + end + else + # + element_class = GxG::Gui::component_class(sub_component[:component].to_s.to_sym) + if element_class + if sub_component.is_a?(::GxG::Database::PersistedHash) + the_options = sub_component[:options].unpersist() + the_uuid = sub_component.uuid.to_s.to_sym + if the_options[:title] + the_title = the_options.delete(:title) + else + the_title = sub_component.title.to_s + end + else + the_options = sub_component[:options].clone + the_uuid = ::GxG::uuid_generate.to_sym + if the_options[:title] + the_title = the_options.delete(:title) + else + the_title = "Untitled Component #{the_uuid.to_s}" + end + end + # + the_style = the_options.delete(:style) + the_states = (the_options.delete(:states) || {}) + app_location = the_options.delete(:location) + app_name = the_options.delete(:name) + if the_options[:track] + tracking = the_options.delete(:track) + else + tracking = nil + end + # + new_entry = {:parent => entry[:element], :record => sub_component, :element => entry[:element].add_child((the_uuid), element_class, the_options.merge!({:uuid => the_uuid}))} + new_entry[:element].set_title(the_title) + # + self.register_object(the_title, new_entry[:element]) + # Window component tracking: + if tracking + the_window = new_entry[:element].window() + if the_window + the_window.track_deltas(the_uuid,new_entry[:element],tracking) + end + end + # + # Set Layout info if any: + if the_options[:zone] + self.layout_add_item(the_uuid, the_options[:zone].to_s.to_sym, false) + end + # Set states: + unless the_states.keys.include?(:hidden) + the_states[:hidden] = false + end + new_entry[:element].gxg_set_states(the_states) + # process style info: + if the_style.is_a?(::Hash) + new_entry[:element].gxg_set_style(the_style) + end + # linking + if sub_component[:component].to_s.to_sym == :window + if the_application.is_a?(::GxG::Application) + new_entry[:element].set_application(the_application) + unless the_application.get_window(the_uuid) + the_application.link_window(new_entry[:element]) + end + if new_entry[:element].menu_reference() + the_menu_source = the_application.search_content(new_entry[:element].menu_reference()) + if the_menu_source.is_any?(::Hash, ::GxG::Database::PersistedHash) + new_entry[:element].set_menu(the_menu_source) + else + log_warn("Invalid Menu Resource referenced: #{new_entry[:element].menu_reference().inspect}") + end + end + end + self.window_register(the_uuid, new_entry[:element]) + end + if sub_component[:component].to_s.to_sym == :dialog_box + if the_application.is_a?(::GxG::Application) + new_entry[:element].set_application(the_application) + unless the_application.get_window(the_uuid) + the_application.link_window(new_entry[:element]) + end + end + # DialogBoxes are NOT registered with the windowing system! + end + if sub_component[:component].to_s.to_sym == :application_viewport + # + if the_application.is_a?(::GxG::Application) + new_entry[:element].set_application(the_application) + unless the_application.get_viewport(the_uuid) + the_application.link_viewport(new_entry[:element]) + end + else + if app_location.is_a?(::String) || app_name.is_a?(::String) + # + allocated = false + GxG::APPLICATIONS[:processes].keys.each do |the_app_key| + # FIXME: if an app is not :unique there is no garanteeing the load order will + # match the page's :application_viewport load order. So mismatched page viewports can result. + # That is: they may end up in reversed page/screen positions due to app assignment ordinal. + if GxG::APPLICATIONS[:processes][(the_app_key)].location == app_location || GxG::APPLICATIONS[:processes][(the_app_key)].title == app_name + # + unless GxG::APPLICATIONS[:processes][(the_app_key)].get_viewport(the_uuid) + # + new_entry[:element].set_application(GxG::APPLICATIONS[:processes][(the_app_key)]) + GxG::APPLICATIONS[:processes][(the_app_key)].link_viewport(new_entry[:element]) + allocated = true + break + end + end + end + unless allocated == true + if app_location + specifier = {:location => app_location, :restore => true} + end + if app_name + specifier = {:name => app_name, :restore => true} + end + # Look for existing / loaded app to link viewport to + # if exists, link, if not: app_open and link + link_app = nil + GxG::APPLICATIONS[:processes].values.each do |application| + if specifier[:location] + if specifier[:location] == application.location + link_app = application + break + end + else + if specifier[:name] + if specifier[:name] == File.basename(application.location) + link_app = application + break + end + end + end + end + if link_app + new_entry[:element].set_application(link_app) + link_app.link_viewport(new_entry[:element]) + else + GxG::CONNECTION.application_open(specifier) do |app_stub| + if app_stub[:result].is_a?(::Hash) + GxG::CONNECTION.pull_object(app_stub[:result][:location]) do |source_object| + GxG::APPLICATIONS[:processes][(app_stub[:result][:application].to_s.to_sym)] = GxG::Application.new(app_stub[:result].merge({:source => source_object, :viewport => new_entry[:element]})) + new_entry[:element].set_application(GxG::APPLICATIONS[:processes][(app_stub[:result][:application].to_s.to_sym)]) + GxG::APPLICATIONS[:processes][(app_stub[:result][:application].to_s.to_sym)].link_viewport(new_entry[:element]) + # + end + else + log_warn("Failure retrieving: #{specifier.inspect} --> #{app_stub.inspect}") + end + end + end + end + else + log_warn("Neither :location nor :name are specified!") + end + # + end + end + # Set Script if any is provided + if sub_component[:script].size > 0 + new_entry[:element].set_script(sub_component[:script].to_s) + end + # + build_queue << new_entry + end + # + end + end + end + # + end + end + result = true + end + result + end + # + def load(page_profile=nil) + # Build out page with page_profile + if page_profile.is_any?(::Hash, ::GxG::Database::PersistedHash) + @title_registry = {} + @title_registry[(page_profile.uuid().to_s.to_sym)] = page_profile.title() + # Load CSS Theme + if page_profile[:theme].to_s.size > 0 + self.load_theme(page_profile[:theme].to_s) + else + self.load_theme("default") + end + # Build a list of viewport application references + viewport_apps = [] + page_profile[:content].search do |value, selector, container| + if selector == :location + viewport_apps << container[(selector)].to_s + end + end + # Restore prior-session applications: state-only, run @ tail end. + GxG::APPLICATIONS[:prefetch] = true + app_prefetched = [] + skipped_apps = 0 + list_failure = Proc.new do |response| + log_warn("Application List Failure: #{response.inspect}") + GxG::APPLICATIONS[:prefetch] = false + end + GxG::CONNECTION.application_list(list_failure) do |list_response| + if list_response[:result].is_a?(::Array) + if list_response[:result].size > 0 + GxG::APPLICATIONS[:prefetch] = true + list_response[:result].each do |entry| + # entry: {:application => , :credentialed => false, :unique => true, :location => "/Path/to/app"} + GxG::CONNECTION.pull_object(entry[:location]) do |source_object| + if GxG::DISPLAY_DETAILS[:logged_in] == true + app_prefetched << entry[:application].to_s.to_sym + GxG::APPLICATIONS[:processes][(entry[:application].to_s.to_sym)] = GxG::Application.new(entry.merge({:source => source_object})) + else + if source_object[:credentialed] == true + skipped_apps += 1 + else + app_prefetched << entry[:application].to_s.to_sym + GxG::APPLICATIONS[:processes][(entry[:application].to_s.to_sym)] = GxG::Application.new(entry.merge({:source => source_object})) + end + end + if (GxG::APPLICATIONS[:processes].size + skipped_apps) == list_response[:result].size + self.build_components([{:parent => nil, :record => page_profile, :element => self}]) + else + GxG::APPLICATIONS[:prefetch] = true + end + end + end + else + # Load Page and Components + self.build_components([{:parent => nil, :record => page_profile, :element => self}]) + end + end + end + prefetch_timer = nil + prefetch_timer = GxG::DISPATCHER.add_periodic_timer({:interval => 0.5}) do + if GxG::APPLICATIONS[:prefetch] == true + loaded_apps = 0 + GxG::APPLICATIONS[:processes].values.each do |the_app| + if viewport_apps.include?(the_app.location.to_s) + loaded_apps += 1 + end + end + if loaded_apps == viewport_apps.size + GxG::APPLICATIONS[:prefetch] = false + end + else + self.set_busy true + # + GxG::APPLICATIONS[:processes].values.each do |the_app| + if app_prefetched.include?(the_app.process_uuid) + the_app.run({:restore => true}) + else + the_app.run() + end + end + GxG::DISPATCHER.cancel_timer(prefetch_timer) + self.set_busy false + end + end + # + end + end + # + def start(navigate_to="") + request_path = navigate_to[(::GxG::CONNECTION.relative_url().size)..-1] + if request_path == "/" + request_path = "/index" + end + ::GxG::DISPLAY_DETAILS[:host_path] = request_path + # Load Page Content + ::GxG::CONNECTION.pull_object("/content/pages#{request_path}") do |response| + self.load(response) + self.layout_refresh + end + end + # + def stop() + # + end + # + end + # + end +end +# diff --git a/gxg/web/gxg_foundation.rb b/gxg/web/gxg_foundation.rb new file mode 100644 index 0000000..f440357 --- /dev/null +++ b/gxg/web/gxg_foundation.rb @@ -0,0 +1,2130 @@ +module GxG + module Gui + # ### Page Component + class Page < GxG::Gui::Vdom::Page + end + # ### Core Component Classes & Registration System + class Abbreviation < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :abbr + @options[:title] = @title.clone.to_s + end + end + # + class Address < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :address + end + end + # + class Area < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :area + end + end + # + class Bold < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :b + end + end + # + class Bdi < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :bdi + end + end + # + class Bdo < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :bdo + end + end + # + class BlockQuote < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :blockquote + end + end + # + class Break < GxG::Gui::Vdom::BaseElement + # Review has no closing tag - account for this in Factory + def _before_create + super() + @domtype = :br + end + end + # + class Caption < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :caption + end + end + # + class Cite < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :cite + end + end + # + class Code < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :code + end + end + # + class Column < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :col + end + end + # + class ColumnGroup < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :colgroup + end + end + # + class Data < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :data + end + end + # + class DataList < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :datalist + end + end + # + class Description < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :dd + end + end + # + class Deleted < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :del + end + end + # + class Details < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :details + end + end + # + class Dfn < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :dfn + end + end + # + class Dialog < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :dialog + end + end + # + class Division < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + end + end + # + class DescriptionList < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :dl + end + end + # + class DescriptionTerm < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :dt + end + end + # + class Emphasis < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :em + end + end + # + class Figure < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :figure + end + end + # + class FigureCaption < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :figcaption + end + end + # + class ThemeDivider < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :hr + end + end + # + class Italic < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :i + end + end + # + class InlineFrame < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :iframe + end + end + # + class Insertion < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ins + end + end + # + class KeyboardKey < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :kbd + end + end + # + class Map < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :map + end + end + # + class Mark < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :mark + end + end + # + class Meter < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :meter + end + end + # + class NoScript < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :noscript + end + end + # + class Object < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :object + end + end + # + class OptionGroup < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :optgroup + end + end + # + class Parameter < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :param + end + end + # + class Picture < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :picture + end + end + # + class Preformatted < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :pre + end + end + # + class Progress < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :progress + end + end + # + class Quotation < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :q + end + end + # + class StrikeThrough < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :s + end + end + # + class Sample < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :samp + end + end + # + class Source < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :source + end + end + # + class Strong < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :strong + end + end + # + class Small < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :small + end + end + # + class Style < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :style + end + end + # + class SubScript < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :sub + end + end + # + class Summary < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :summary + end + end + # + class SuperScript < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :sup + end + end + # + class Svg < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :svg + end + end + # + class Template < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :template + end + end + # + class Time < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :time + end + end + # + class Track < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :track + end + end + # + class Misspelled < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :u + end + end + # + class Variable < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :var + end + end + # + class WordBreak < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :wbr + end + end + # + class Header < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :header + end + end + # + class Navigation < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :nav + end + end + # + class Main < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :main + end + end + # + class Section < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :section + end + end + # + class Article < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :article + end + end + # + class Aside < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :aside + end + end + # + class Footer < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :footer + end + end + # + class Form < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + end + # + def form_data() + result = {} + search_queue = self.children.values + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.domtype == :input + result[(entry.title.to_s.to_sym)] = entry.value() + else + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + end + # ### Buttons + class ButtonInput < ::GxG::Gui::Vdom::BaseClickableElement + def _before_create + super() + @options[:type] = :button + end + end + # + class SubmitButton < ::GxG::Gui::Vdom::BaseClickableElement + def _before_create + super() + @options[:type] = :submit + end + end + # + class Block < ::GxG::Gui::Vdom::BaseClickableElement + def _before_create + super() + @domtype = :div + @options.delete(:type) + true + end + end + # + class CheckBox < ::GxG::Gui::Vdom::BaseClickableElement + def _before_create + super() + @options[:type] = :checkbox + end + end + # + class Image < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :image + if @options[:source] + @options[:src] = @options.delete(:source) + end + @options[:alt] ||= @options[:src] + end + end + # + class Video < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :video + if @options[:source] + @options[:src] = @options.delete(:source) + end + end + end + # + class Audio < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :audio + if @options[:source] + @options[:src] = @options.delete(:source) + end + end + end + # + class Canvas < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :canvas + if @options[:source] + @options[:src] = @options.delete(:source) + end + end + end + # + class HiddenInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :hidden + end + end + # + class FileInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :file + end + end + # + class TextInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :text + end + end + # + class PasswordInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :password + end + end + # + class ResetInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :reset + end + end + # + class RadioButton < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :radio + end + end + # + class ColorPicker < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :color + end + end + # + class DatePicker < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :date + end + end + # + class DateTimeLocal < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :"datetime-local" + end + end + # + class EmailInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :email + end + end + # + class MonthPicker < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :month + end + end + # + class NumberInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :number + end + end + # + class RangeInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :range + end + end + # + class SearchInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :search + end + end + # + class PhoneInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :tel + end + end + # + class TimePicker < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :time + end + end + # + class UrlInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :url + end + end + # + class WeekPicker < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :week + end + end + # + class Label < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :label + end + end + # + class TextArea < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :textarea + end + end + # + class Output < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :output + end + end + # + class Paragraph < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :p + end + end + # + class Header1 < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :h1 + end + end + # + class Header2 < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :h2 + end + end + # + class Header3 < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :h3 + end + end + # + class Header4 < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :h4 + end + end + # + class Header5 < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :h5 + end + end + # + class Header6 < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :h6 + end + end + # + class List < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + end + end + # + class OrderedList < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ol + end + end + # + class ListItem < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :li + end + end + # + class Anchor < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :a + end + end + # + class ExternalLink < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + # When link is clicked will navigate to a location outside the + # application and in a new tab. + # Specify option :target to set the location. + @options[:target] ||= "_blank" + @domtype = :a + end + end + # + class Button < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :button + unless @options[:type] == "button" + @options[:type] = "button" + end + unless @states[:button] == true + @states[:button] = true + end + end + # + def _after_create + super() + # Review : consider a non-gxg listener for un-focusing the active element. thus: + #`{@element}.addEventListener("click",function(e){#{clicked};document.activeElement.blur()})` + on(:click) do |the_event| + `document.activeElement.blur()` + end + end + end + # + class Table < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :table + end + end + # + class TableHeader < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :th + end + end + # + class TableRow < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :tr + end + end + # + class TableCell < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :td + end + end + # + class BlockTable < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:table] == true + @states[:table] = true + end + end + end + # + class BlockTableHeader < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:th] == true + @states[:th] = true + end + end + end + # + class BlockTableRow < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:tr] == true + @states[:tr] = true + end + end + end + # + class BlockTableCell < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:td] == true + @states[:td] = true + end + end + end + # + class Span < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :span + end + end + # + class Select < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :select + end + end + # + class Option < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :option + end + end + # + class FieldSet < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :fieldset + unless @states[:fieldset] == true + @states[:fieldset] = true + end + end + end + # + class Legend < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :legend + end + end + # + # ### Component Registration + COMPONENT_CLASSES = { + :"org.gxg.gui.page" => ::GxG::Gui::Page, + :"org.gxg.gui.abbreviation" => ::GxG::Gui::Abbreviation, + :"org.gxg.gui.address" => ::GxG::Gui::Address, + :"org.gxg.gui.area" => ::GxG::Gui::Area, + :"org.gxg.gui.bold" => ::GxG::Gui::Bold, + :"org.gxg.gui.bdi" => ::GxG::Gui::Bdi, + :"org.gxg.gui.bdo" => ::GxG::Gui::Bdo, + :"org.gxg.gui.blockquote" => ::GxG::Gui::BlockQuote, + :"org.gxg.gui.break" => ::GxG::Gui::Break, + :"org.gxg.gui.caption" => ::GxG::Gui::Caption, + :"org.gxg.gui.cite" => ::GxG::Gui::Cite, + :"org.gxg.gui.code" => ::GxG::Gui::Code, + :"org.gxg.gui.column" => ::GxG::Gui::Column, + :"org.gxg.gui.column.group" => ::GxG::Gui::ColumnGroup, + :"org.gxg.gui.data" => ::GxG::Gui::Data, + :"org.gxg.gui.data.list" => ::GxG::Gui::DataList, + :"org.gxg.gui.description" => ::GxG::Gui::Description, + :"org.gxg.gui.deleted" => ::GxG::Gui::Deleted, + :"org.gxg.gui.details" => ::GxG::Gui::Details, + :"org.gxg.gui.dfn" => ::GxG::Gui::Dfn, + :"org.gxg.gui.dialog" => ::GxG::Gui::Dialog, + :"org.gxg.gui.division" => ::GxG::Gui::Division, + :"org.gxg.gui.description.list" => ::GxG::Gui::DescriptionList, + :"org.gxg.gui.description.item" => ::GxG::Gui::DescriptionTerm, + :"org.gxg.gui.emphasis" => ::GxG::Gui::Emphasis, + :"org.gxg.gui.figure" => ::GxG::Gui::Figure, + :"org.gxg.gui.figure.caption" => ::GxG::Gui::FigureCaption, + :"org.gxg.gui.theme.divider" => ::GxG::Gui::ThemeDivider, + :"org.gxg.gui.italic" => ::GxG::Gui::Italic, + :"org.gxg.gui.frame.inline" => ::GxG::Gui::InlineFrame, + :"org.gxg.gui.insertion" => ::GxG::Gui::Insertion, + :"org.gxg.gui.keyboard.key" => ::GxG::Gui::KeyboardKey, + :"org.gxg.gui.map" => ::GxG::Gui::Map, + :"org.gxg.gui.mark" => ::GxG::Gui::Mark, + :"org.gxg.gui.meter" => ::GxG::Gui::Meter, + :"org.gxg.gui.noscript" => ::GxG::Gui::NoScript, + :"org.gxg.gui.object" => ::GxG::Gui::Object, + :"org.gxg.gui.option.group" => ::GxG::Gui::OptionGroup, + :"org.gxg.gui.parameter" => ::GxG::Gui::Parameter, + :"org.gxg.gui.picture" => ::GxG::Gui::Picture, + :"org.gxg.gui.preformatted" => ::GxG::Gui::Preformatted, + :"org.gxg.gui.progress" => ::GxG::Gui::Progress, + :"org.gxg.gui.quotation" => ::GxG::Gui::Quotation, + :"org.gxg.gui.strike.through" => ::GxG::Gui::StrikeThrough, + :"org.gxg.gui.sample" => ::GxG::Gui::Sample, + :"org.gxg.gui.section" => ::GxG::Gui::Section, + :"org.gxg.gui.small" => ::GxG::Gui::Small, + :"org.gxg.gui.source" => ::GxG::Gui::Source, + :"org.gxg.gui.strong" => ::GxG::Gui::Strong, + :"org.gxg.gui.style" => ::GxG::Gui::Style, + :"org.gxg.gui.sub.script" => ::GxG::Gui::SubScript, + :"org.gxg.gui.summary" => ::GxG::Gui::Summary, + :"org.gxg.gui.super.script" => ::GxG::Gui::SuperScript, + :"org.gxg.gui.svg" => ::GxG::Gui::Svg, + :"org.gxg.gui.template" => ::GxG::Gui::Template, + :"org.gxg.gui.text.area" => ::GxG::Gui::TextArea, + :"org.gxg.gui.time" => ::GxG::Gui::Time, + :"org.gxg.gui.track" => ::GxG::Gui::Track, + :"org.gxg.gui.misspelled" => ::GxG::Gui::Misspelled, + :"org.gxg.gui.variable" => ::GxG::Gui::Variable, + :"org.gxg.gui.word.break" => ::GxG::Gui::WordBreak, + :"org.gxg.gui.header" => ::GxG::Gui::Header, + :"org.gxg.gui.navigator" => ::GxG::Gui::Navigation, + :"org.gxg.gui.main" => ::GxG::Gui::Main, + :"org.gxg.gui.article" => ::GxG::Gui::Article, + :"org.gxg.gui.aside" => ::GxG::Gui::Aside, + :"org.gxg.gui.footer" => ::GxG::Gui::Footer, + :"org.gxg.gui.form" => ::GxG::Gui::Form, + :"org.gxg.gui.input.button" => ::GxG::Gui::ButtonInput, + :"org.gxg.gui.input.button.submit" => ::GxG::Gui::SubmitButton, + :"org.gxg.gui.block" => ::GxG::Gui::Block, + :"org.gxg.gui.button.checkbox" => ::GxG::Gui::CheckBox, + :"org.gxg.gui.image" => ::GxG::Gui::Image, + :"org.gxg.gui.video" => ::GxG::Gui::Video, + :"org.gxg.gui.audio" => ::GxG::Gui::Audio, + :"org.gxg.gui.canvas" => ::GxG::Gui::Canvas, + :"org.gxg.gui.input.hidden" => ::GxG::Gui::HiddenInput, + :"org.gxg.gui.input.file" => ::GxG::Gui::FileInput, + :"org.gxg.gui.input.text" => ::GxG::Gui::TextInput, + :"org.gxg.gui.input.password" => ::GxG::Gui::PasswordInput, + :"org.gxg.gui.input.reset" => ::GxG::Gui::ResetInput, + :"org.gxg.gui.button.radio" => ::GxG::Gui::RadioButton, + :"org.gxg.gui.picker.color" => ::GxG::Gui::ColorPicker, + :"org.gxg.gui.picker.date" => ::GxG::Gui::DatePicker, + :"org.gxg.gui.input.datetime.local" => ::GxG::Gui::DateTimeLocal, + :"org.gxg.gui.input.email" => ::GxG::Gui::EmailInput, + :"org.gxg.gui.picker.month" => ::GxG::Gui::MonthPicker, + :"org.gxg.gui.picker.number" =>::GxG::Gui::NumberInput, + :"org.gxg.gui.input.range" => ::GxG::Gui::RangeInput, + :"org.gxg.gui.input.search" => ::GxG::Gui::SearchInput, + :"org.gxg.gui.input.phone" => ::GxG::Gui::PhoneInput, + :"org.gxg.gui.picker.time" => ::GxG::Gui::TimePicker, + :"org.gxg.gui.input.url" => ::GxG::Gui::UrlInput, + :"org.gxg.gui.picker.week" => ::GxG::Gui::WeekPicker, + :"org.gxg.gui.label" => ::GxG::Gui::Label, + :"org.gxg.gui.output" => ::GxG::Gui::Output, + :"org.gxg.gui.paragraph" => ::GxG::Gui::Paragraph, + :"org.gxg.gui.header.one" => ::GxG::Gui::Header1, + :"org.gxg.gui.header.two" => ::GxG::Gui::Header2, + :"org.gxg.gui.header.three" => ::GxG::Gui::Header3, + :"org.gxg.gui.header.four" => ::GxG::Gui::Header4, + :"org.gxg.gui.header.five" => ::GxG::Gui::Header5, + :"org.gxg.gui.header.six" => ::GxG::Gui::Header6, + :"org.gxg.gui.list" => ::GxG::Gui::List, + :"org.gxg.gui.list.ordered" => ::GxG::Gui::OrderedList, + :"org.gxg.gui.list.item" => ::GxG::Gui::ListItem, + :"org.gxg.gui.link.anchor" => ::GxG::Gui::Anchor, + :"org.gxg.gui.link.external" => ::GxG::Gui::ExternalLink, + :"org.gxg.gui.button" => ::GxG::Gui::Button, + :"org.gxg.gui.table" => ::GxG::Gui::Table, + :"org.gxg.gui.table.header" => ::GxG::Gui::TableHeader, + :"org.gxg.gui.table.row" => ::GxG::Gui::TableRow, + :"org.gxg.gui.table.cell" => ::GxG::Gui::TableCell, + :"org.gxg.gui.block.table" => ::GxG::Gui::BlockTable, + :"org.gxg.gui.block.table.header" => ::GxG::Gui::BlockTableHeader, + :"org.gxg.gui.block.table.row" => ::GxG::Gui::BlockTableRow, + :"org.gxg.gui.block.table.cell" => ::GxG::Gui::BlockTableCell, + :"org.gxg.gui.span" => ::GxG::Gui::Span, + :"org.gxg.gui.select" => ::GxG::Gui::Select, + :"org.gxg.gui.option" => ::GxG::Gui::Option, + :"org.gxg.gui.field.set" => ::GxG::Gui::FieldSet, + :"org.gxg.gui.legend" => ::GxG::Gui::Legend + } + # + def self.component_class(the_type_specifier=nil) + result = nil + if the_type_specifier + result = GxG::Gui::COMPONENT_CLASSES[(the_type_specifier.to_s.to_sym)] + end + result + end + # + def self.class_component(the_class_specifier=nil) + result = nil + if the_class_specifier + found_index = GxG::Gui::COMPONENT_CLASSES.values.index(the_class_specifier) + if found_index + result = GxG::Gui::COMPONENT_CLASSES.keys[(found_index)] + end + end + result + end + # + def self.component_registered?(the_specifier=nil) + if GxG::Gui::component_class(the_specifier) + true + else + false + end + end + # + def self.register_component_class(the_specifier=nil, the_constant=nil) + if the_specifier.is_any?(::String, ::Symbol) && the_constant.is_a?(::Class) + if GxG::Gui::component_registered?(the_specifier.to_s.to_sym) + if GxG::Gui::component_class(the_specifier.to_s.to_sym) != the_constant + GxG::Gui::COMPONENT_CLASSES[(the_specifier.to_s.to_sym)] = the_constant + true + else + # Review : should re-defining component constants be allowed ?? + log_warn("Component Class #{the_specifier.to_s.to_sym.inspect} => #{the_constant.inspect} already defined.") + false + end + else + GxG::Gui::COMPONENT_CLASSES[(the_specifier.to_s.to_sym)] = the_constant + true + end + else + false + end + end + # + end + # End Gui +end +# End GxG +# ### Foundation Integration +module GxG + module Gui + # ### Foundation Component Classes + # See : https://get.foundation/sites/docs/index.html + # See : https://get.foundation/sites/docs/kitchen-sink.html + class GridContainer < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"grid-container"] == true + @states[:"grid-container"] = true + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.grid.container", ::GxG::Gui::GridContainer) + # + class GridX < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"grid-x"] == true + @states[:"grid-x"] = true + end + end + # + def _after_create + super() + `$(document).foundation()` + end + # + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.grid.x", ::GxG::Gui::GridX) + # + class GridY < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"grid-y"] == true + @states[:"grid-y"] = true + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.grid.y", ::GxG::Gui::GridY) + # + class Accordion < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + unless @states[:"accordion"] == true + @states[:"accordion"] = true + end + unless @options.keys.include?(:"data-accordion") + @options[:"data-accordion"] = nil + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.accordion", ::GxG::Gui::Accordion) + # + class AccordionItem < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :li + unless @states[:"accordion-item"] == true + @states[:"accordion-item"] = true + end + unless @options.keys.include?(:"data-accordion-item") + @options[:"data-accordion-item"] = nil + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.accordion.item", ::GxG::Gui::AccordionItem) + # + class AccordionMenu < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + unless @states[:"vertical"] == true + @states[:"vertical"] = true + end + unless @states[:"menu"] == true + @states[:"menu"] = true + end + unless @options.keys.include?(:"data-accordion-menu") + @options[:"data-accordion-menu"] = nil + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.accordion.menu", ::GxG::Gui::AccordionMenu) + # + class AccordionSubMenu < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + unless @states[:"vertical"] == true + @states[:"vertical"] = true + end + unless @states[:"menu"] == true + @states[:"menu"] = true + end + unless @states[:"nested"] == true + @states[:"nested"] = true + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.accordion.submenu", ::GxG::Gui::AccordionSubMenu) + # + class AccordionMenuItem < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :li + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.accordion.menu.item", ::GxG::Gui::AccordionMenuItem) + # + class AnchorButton < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :a + unless @states[:"button"] == true + @states[:"button"] = true + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.anchor.button", ::GxG::Gui::AnchorButton) + # + class Badge < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :span + unless @states[:"badge"] == true + @states[:"badge"] = true + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.badge", ::GxG::Gui::Badge) + # + class Breadcrumb < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + unless @states[:"breadcrumb"] == true + @states[:"breadcrumb"] = true + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.breadcrumb", ::GxG::Gui::Breadcrumb) + # + class BreadcrumbItem < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :li + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.breadcrumb.item", ::GxG::Gui::BreadcrumbItem) + # + class ButtonGroup < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"button-group"] == true + @states[:"button-group"] = true + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.button.group", ::GxG::Gui::ButtonGroup) + # + class Callout < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"callout"] == true + @states[:"callout"] = true + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.callout", ::GxG::Gui::Callout) + # + class ColoredLabel < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :span + unless @states[:"label"] == true + @states[:"label"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.label.colored", ::GxG::Gui::ColoredLabel) + # + class DrilldownMenu < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + unless @states[:"vertical"] == true + @states[:"vertical"] = true + end + unless @states[:"menu"] == true + @states[:"menu"] = true + end + unless @options.keys.include?(:"data-drilldown") + @options[:"data-drilldown"] = nil + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.drilldown.menu", ::GxG::Gui::DrilldownMenu) + # + class DrilldownSubMenu < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + unless @states[:"vertical"] == true + @states[:"vertical"] = true + end + unless @states[:"menu"] == true + @states[:"menu"] = true + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.drilldown.submenu", ::GxG::Gui::DrilldownSubMenu) + # + class DrilldownMenuItem < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :li + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.drilldown.menu.item", ::GxG::Gui::DrilldownMenuItem) + # + class DropdownSubMenu < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + unless @states[:"menu"] == true + @states[:"menu"] = true + end + end + # + def _after_create + super() + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.dropdown.submenu", ::GxG::Gui::DropdownSubMenu) + # + class DropdownMenuItem < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :li + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.dropdown.menu.item", ::GxG::Gui::DropdownMenuItem) + # + class Equalizer < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"grid-x"] == true + @states[:"grid-x"] = true + end + unless @states[:"grid-margin-x"] == true + @states[:"grid-margin-x"] = true + end + unless @options.keys.include?(:"data-equalizer") + @options[:"data-equalizer"] = nil + end + unless @options.keys.include?(:"data-equalize-on") + @options[:"data-equalize-on"] = "medium" + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.equalizer", ::GxG::Gui::Equalizer) + # + class FlexGridRow < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"row"] == true + @states[:"row"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.flexgrid.row", ::GxG::Gui::FlexGridRow) + # + class FlexGridColumn < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"columns"] == true + @states[:"columns"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.flexgrid.column", ::GxG::Gui::FlexGridColumn) + # + class Embed < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"responsive-embed"] == true + @states[:"responsive-embed"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.embed", ::GxG::Gui::Embed) + # + class MediaObject < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"media-object"] == true + @states[:"media-object"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.media.object", ::GxG::Gui::MediaObject) + # + class MediaObjectSection < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"media-object-section"] == true + @states[:"media-object-section"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.media.object.section", ::GxG::Gui::MediaObjectSection) + # DropdownMenu, See: https://get.foundation/sites/docs/kitchen-sink.html#dropdown-menu + class DropdownMenu < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + unless @states[:"dropdown"] == true + @states[:"dropdown"] = true + end + unless @states[:"menu"] == true + @states[:"menu"] = true + end + unless @options.keys.include?(:"data-dropdown-menu") + @options[:"data-dropdown-menu"] = nil + end + end + # + def add_menu_item(label=nil, data=nil) + the_item = ::GxG::Database::DetachedHash.new + the_item[:component] = "org.gxg.gui.dropdown.menu.item" + the_item[:settings] = {:label => label, :data => data} + the_item[:options] = {} + the_item[:script] = "" + the_item[:content] = [] + page.build_components(self, [(the_item)]) + true + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.dropdown.menu", ::GxG::Gui::DropdownMenu) + # + class MenuBar < ::GxG::Gui::DropdownMenu + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.menu.bar", ::GxG::Gui::MenuBar) + # + class Menu < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + unless @states[:"menu"] == true + @states[:"menu"] = true + end + end + # + def add_menu_item(label=nil, data=nil) + the_item = ::GxG::Database::DetachedHash.new + the_item[:component] = "org.gxg.gui.menu.item" + the_item[:settings] = {:label => label, :data => data} + the_item[:options] = {} + the_item[:script] = "" + the_item[:content] = [] + page.build_components(self, [(the_item)]) + true + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.menu", ::GxG::Gui::Menu) + # + class MenuItem < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :li + end + # + def initialize(the_parent, options) + @label = nil + super(the_parent, options) + self + end + # + def cascade + # + label = ::GxG::Database::DetachedHash.new + label.title = "label #{@uuid.to_s}" + label[:component] = "anchor" + label[:settings] = {} + # label[:options] = {:href => "#0", :content => (@settings[:label] || "Untitled").to_s} + label[:options] = {:content => (@settings[:label] || "Untitled").to_s} + label[:script] = " + on(:mouseup) do |event| + menu_item = self.find_parent_type(::GxG::Gui::MenuItem) + if menu_item + unless menu_item.get_state(:disabled) == true + menu_item.select() + end + end + end + " + label[:content] = [] + # + page.build_components(self, [(label)]) + the_object = page.find_object_by("label #{@uuid.to_s}") + if the_object + @label = the_object + end + end + # + def set_label(the_text=nil) + if @label + @label.set_text(the_text.to_s) + @settings[:label] = the_text.to_s + end + end + # + def set_data(data=nil) + @settings[:data] = data + end + # + def select(data=nil) + # override in object script + end + # + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.menu.item", ::GxG::Gui::MenuItem) + # + class OffCanvasWrapper < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"off-canvas-wrapper"] == true + @states[:"off-canvas-wrapper"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.offcanvas.wrapper", ::GxG::Gui::OffCanvasWrapper) + # + class OffCanvasWrapperInner < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"off-canvas-wrapper-inner"] == true + @states[:"off-canvas-wrapper-inner"] = true + end + unless @options.keys.include?(:"data-off-canvas-wrapper") + @options[:"data-off-canvas-wrapper"] = nil + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.offcanvas.wrapper.inner", ::GxG::Gui::OffCanvasWrapperInner) + # + class OffCanvasLeft < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"off-canvas"] == true + @states[:"off-canvas"] = true + end + unless @states[:"position-left"] == true + @states[:"position-left"] = true + end + unless @options.keys.include?(:"data-off-canvas") + @options[:"data-off-canvas"] = nil + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.offcanvas.left", ::GxG::Gui::OffCanvasLeft) + # + class OffCanvasRight < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"off-canvas"] == true + @states[:"off-canvas"] = true + end + unless @states[:"position-right"] == true + @states[:"position-right"] = true + end + unless @options.keys.include?(:"data-off-canvas") + @options[:"data-off-canvas"] = nil + end + unless @options.keys.include?(:"data-position") + @options[:"data-position"] = "right" + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.offcanvas.right", ::GxG::Gui::OffCanvasRight) + # + class OffCanvasContent < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"off-canvas-content"] == true + @states[:"off-canvas-content"] = true + end + unless @options.keys.include?(:"data-off-canvas-content") + @options[:"data-off-canvas-content"] = nil + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.offcanvas.content", ::GxG::Gui::OffCanvasContent) + # + class Orbit < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"orbit"] == true + @states[:"orbit"] = true + end + unless @options.keys.include?(:"role") + @options[:"role"] = "region" + end + unless @options.keys.include?(:"data-orbit") + @options[:"data-orbit"] = nil + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.orbit", ::GxG::Gui::Orbit) + # + class OrbitContainer < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + unless @states[:"orbit-container"] == true + @states[:"orbit-container"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.orbit.container", ::GxG::Gui::OrbitContainer) + # + class OrbitPrevious < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :button + unless @states[:"orbit-previous"] == true + @states[:"orbit-previous"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.orbit.previous", ::GxG::Gui::OrbitPrevious) + # + class OrbitNext < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :button + unless @states[:"orbit-next"] == true + @states[:"orbit-next"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.orbit.next", ::GxG::Gui::OrbitNext) + # + class OrbitSlide < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :li + unless @states[:"orbit-slide"] == true + @states[:"orbit-slide"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.orbit.slide", ::GxG::Gui::OrbitSlide) + # + class OrbitNavigator < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :nav + unless @states[:"orbit-bullets"] == true + @states[:"orbit-bullets"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.orbit.navigator", ::GxG::Gui::OrbitNavigator) + # + class OrbitBullet < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :button + unless @options.keys.include?(:"data-slide") + @options[:"data-slide"] = "0" + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.orbit.bullet", ::GxG::Gui::OrbitBullet) + # + class Pagination < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + unless @options.keys.include?(:"role") + @options[:"role"] = "navigation" + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.pagination", ::GxG::Gui::Pagination) + # + class PaginationItem < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :li + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.pagination.item", ::GxG::Gui::PaginationItem) + # + class ProgressBar < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"progress"] == true + @states[:"progress"] = true + end + unless @options.keys.include?(:"role") + @options[:"role"] = "progressbar" + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.progress.bar", ::GxG::Gui::ProgressBar) + # + class ProgressMeter < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"progress-meter"] == true + @states[:"progress-meter"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.progress.meter", ::GxG::Gui::ProgressMeter) + # + class HorizontalSlider < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"slider"] == true + @states[:"slider"] = true + end + # Data Initial and End + if @options[:"data-initial-start"] + @data_start = @options.delete(:"data-initial-start") + else + @data_start = "50" + end + if @options[:"data-initial-end"] + @data_second = @options.delete(:"data-initial-end") + else + @data_second = nil + end + if @options[:"data-end"] + @data_end = @options.delete(:"data-end") + else + @data_end = "200" + end + end + # + def _after_create + super() + if @data_second + container_one = self.add_child({:component => "org.gxg.gui.block", :title => "slider_container", :settings => {}, :options => {:states => ["small-10 columns"]}, :script =>"", :content => []}) + slider = container_one.add_child({:component => "org.gxg.gui.block", :title => "slider", :settings => {}, :options => {:'data-slider' => nil, :'data-initial-start' => @data_start, :'data-initial-end' => @data_second, :'data-end' => @data_end, :states => ["slider"]}, :script =>"", :content => []}) + handle = slider.add_child({:component => "org.gxg.gui.span", :title => "slider_handle", :settings => {}, :options => {:"data-slider-handle" => nil, :role => "slider", :tabindex => "1", :states => ["slider-handle"]}, :script =>"", :content => []}) + slider.add_child({:component => "org.gxg.gui.span", :title => "slider_groove", :settings => {}, :options => {:"data-slider-fill" => nil, :states => ["slider-fill"]}, :script =>"", :content => []}) + handle_two = slider.add_child({:component => "org.gxg.gui.span", :title => "slider_handle_two", :settings => {}, :options => {:"data-slider-handle" => nil, :role => "slider", :tabindex => "1", :states => ["slider-handle"]}, :script =>"", :content => []}) + container_two = self.add_child({:component => "org.gxg.gui.block", :title => ("value_container" + @uuid.to_s), :settings => {}, :options => {:states => ["small-2 columns"]}, :script =>"", :content => []}) + value_containter = container_two.add_child({:component => "org.gxg.gui.picker.number", :title => "slider_value", :settings => {}, :options => {}, :script =>"", :content => []}) + container_three = self.add_child({:component => "org.gxg.gui.block", :title => ("value_container_two" + @uuid.to_s), :settings => {}, :options => {:states => ["small-2 columns"]}, :script =>"", :content => []}) + value_containter_two = container_three.add_child({:component => "org.gxg.gui.picker.number", :title => "slider_value_two", :settings => {}, :options => {}, :script =>"", :content => []}) + handle.set_attribute(:"aria-controls", value_containter.uuid.to_s) + handle_two.set_attribute(:"aria-controls", value_containter_two.uuid.to_s) + # + else + container_one = self.add_child({:component => "org.gxg.gui.block", :title => "slider_container", :settings => {}, :options => {:states => ["small-10 columns"]}, :script =>"", :content => []}) + slider = container_one.add_child({:component => "org.gxg.gui.block", :title => "slider", :settings => {}, :options => {:'data-slider' => nil, :'data-initial-start' => @data_start, :'data-end' => @data_end, :states => ["slider"]}, :script =>"", :content => []}) + handle = slider.add_child({:component => "org.gxg.gui.span", :title => "slider_handle", :settings => {}, :options => {:"data-slider-handle" => nil, :role => "slider", :tabindex => "1", :states => ["slider-handle"]}, :script =>"", :content => []}) + slider.add_child({:component => "org.gxg.gui.span", :title => "slider_groove", :settings => {}, :options => {:"data-slider-fill" => nil, :states => ["slider-fill"]}, :script =>"", :content => []}) + container_two = self.add_child({:component => "org.gxg.gui.block", :title => ("value_container" + @uuid.to_s), :settings => {}, :options => {:states => ["small-2 columns"]}, :script =>"", :content => []}) + value_containter = container_two.add_child({:component => "org.gxg.gui.picker.number", :title => "slider_value", :settings => {}, :options => {}, :script =>"", :content => []}) + handle.set_attribute(:"aria-controls", value_containter.uuid.to_s) + end + `$(document).foundation()` + end + # + def set_script(the_script_body="", alternate_target=nil) + if alternate_target + alternate_target.set_script(the_script_body) + else + if @data_second + handle = find_descendant("slider_handle") + unless handle + raise "Unable to find slider handle object." + end + super(the_script_body, handle) + handle_two = find_descendant("slider_handle_two") + unless handle_two + raise "Unable to find slider handle object." + end + super(the_script_body, handle_two) + else + handle = find_descendant("slider_handle") + unless handle + raise "Unable to find slider handle object." + end + super(the_script_body, handle) + end + end + end + # + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.slider.horizontal", ::GxG::Gui::HorizontalSlider) + # + class VerticalSlider < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"slider"] == true + @states[:"slider"] = true + end + unless @states[:"vertical"] == true + @states[:"vertical"] = true + end + # Data Initial and End + if @options[:"data-initial-start"] + @data_start = @options.delete(:"data-initial-start") + else + @data_start = "50" + end + if @options[:"data-initial-end"] + @data_second = @options.delete(:"data-initial-end") + else + @data_second = nil + end + if @options[:"data-end"] + @data_end = @options.delete(:"data-end") + else + @data_end = "200" + end + end + # + def _after_create + super() + if @data_second + container_one = self.add_child({:component => "org.gxg.gui.block", :title => "slider_container", :settings => {}, :options => {:states => ["small-10 columns"]}, :script =>"", :content => []}) + slider = container_one.add_child({:component => "org.gxg.gui.block", :title => "slider", :settings => {}, :options => {:'data-slider' => nil, :'data-initial-start' => @data_start, :'data-initial-end' => @data_second, :'data-end' => @data_end, :states => ["vertical","slider"]}, :script =>"", :content => []}) + handle = slider.add_child({:component => "org.gxg.gui.span", :title => "slider_handle", :settings => {}, :options => {:"data-slider-handle" => nil, :role => "slider", :tabindex => "1", :states => ["slider-handle"]}, :script =>"", :content => []}) + slider.add_child({:component => "org.gxg.gui.span", :title => "slider_groove", :settings => {}, :options => {:"data-slider-fill" => nil, :states => ["slider-fill"]}, :script =>"", :content => []}) + handle_two = slider.add_child({:component => "org.gxg.gui.span", :title => "slider_handle_two", :settings => {}, :options => {:"data-slider-handle" => nil, :role => "slider", :tabindex => "1", :states => ["slider-handle"]}, :script =>"", :content => []}) + container_two = self.add_child({:component => "org.gxg.gui.block", :title => ("value_container" + @uuid.to_s), :settings => {}, :options => {:states => ["small-2 columns"]}, :script =>"", :content => []}) + value_containter = container_two.add_child({:component => "org.gxg.gui.picker.number", :title => "slider_value", :settings => {}, :options => {}, :script =>"", :content => []}) + container_three = self.add_child({:component => "org.gxg.gui.block", :title => ("value_container_two" + @uuid.to_s), :settings => {}, :options => {:states => ["small-2 columns"]}, :script =>"", :content => []}) + value_containter_two = container_three.add_child({:component => "org.gxg.gui.picker.number", :title => "slider_value_two", :settings => {}, :options => {}, :script =>"", :content => []}) + handle.set_attribute(:"aria-controls", value_containter.uuid.to_s) + handle_two.set_attribute(:"aria-controls", value_containter_two.uuid.to_s) + # + else + container_one = self.add_child({:component => "org.gxg.gui.block", :title => "slider_container", :settings => {}, :options => {:states => ["small-10 columns"]}, :script =>"", :content => []}) + slider = container_one.add_child({:component => "org.gxg.gui.block", :title => "slider", :settings => {}, :options => {:'data-slider' => nil, :'data-initial-start' => @data_start, :'data-end' => @data_end, :states => ["vertical","slider"]}, :script =>"", :content => []}) + handle = slider.add_child({:component => "org.gxg.gui.span", :title => "slider_handle", :settings => {}, :options => {:"data-slider-handle" => nil, :role => "slider", :tabindex => "1", :states => ["slider-handle"]}, :script =>"", :content => []}) + slider.add_child({:component => "org.gxg.gui.span", :title => "slider_groove", :settings => {}, :options => {:"data-slider-fill" => nil, :states => ["slider-fill"]}, :script =>"", :content => []}) + container_two = self.add_child({:component => "org.gxg.gui.block", :title => ("value_container" + @uuid.to_s), :settings => {}, :options => {:states => ["small-2 columns"]}, :script =>"", :content => []}) + value_containter = container_two.add_child({:component => "org.gxg.gui.picker.number", :title => "slider_value", :settings => {}, :options => {}, :script =>"", :content => []}) + handle.set_attribute(:"aria-controls", value_containter.uuid.to_s) + end + `$(document).foundation()` + end + # + def set_script(the_script_body="", alternate_target=nil) + if alternate_target + alternate_target.set_script(the_script_body) + else + if @data_second + handle = find_descendant("slider_handle") + unless handle + raise "Unable to find slider handle object." + end + super(the_script_body, handle) + handle_two = find_descendant("slider_handle_two") + unless handle_two + raise "Unable to find slider handle object." + end + super(the_script_body, handle_two) + else + handle = find_descendant("slider_handle") + unless handle + raise "Unable to find slider handle object." + end + super(the_script_body, handle) + end + end + end + # + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.slider.vertical", ::GxG::Gui::VerticalSlider) + # + class StickyContainer < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @options.keys.include?(:"data-sticky-container") + @options[:"data-sticky-container"] = nil + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.sticky.container", ::GxG::Gui::StickyContainer) + # + class Sticky < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"sticky"] == true + @states[:"sticky"] = true + end + unless @options.keys.include?(:"data-sticky") + @options[:"data-sticky"] = nil + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.sticky", ::GxG::Gui::Sticky) + # + class Switch < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"switch"] == true + @states[:"switch"] = true + end + end + def _after_create + input_one = self.add_child({:component => "org.gxg.gui.button.checkbox", :title => ("switch_input" + @uuid.to_s), :settings => {}, :options => {:states => ["switch-input"]}, :script =>"", :content => []}) + label_one = self.add_child({:component => "org.gxg.gui.label", :title => ("switch_paddle" + @uuid.to_s), :settings => {}, :options => {:for => (input_one.uuid.to_s), :states => ["switch-paddle"]}, :script =>"", :content => []}) + span_one = label_one.add_child({:component => "org.gxg.gui.span", :title => ("switch_span" + @uuid.to_s), :settings => {}, :options => {:states => ["show-for-sr"]}, :script =>"", :content => []}) + `$(document).foundation()` + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.switch", ::GxG::Gui::Switch) + # + class TabContent < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"tabs-panel"] == true + @states[:"tabs-panel"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.tab.content", ::GxG::Gui::TabContent) + # + class TabSet < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + end + def _after_create + @tabs = self.add_child({:component => "org.gxg.gui.list", :title => ("tabs" + @uuid.to_s), :settings => {}, :options => {:"data-tabs" => nil, :states => ["tabs"]}, :script =>"", :content => []}) + @tab_content = self.add_child({:component => "org.gxg.gui.block", :title => ("tab_content" + @uuid.to_s), :settings => {}, :options => {:"data-tabs-content" => (@tabs.uuid.to_s), :states => ["tabs-content"]}, :script =>"", :content => []}) + `$(document).foundation()` + end + def add_child(options={}) + new_child = nil + if options[:component].to_s.downcase.to_sym == :tab_content + new_child = @tab_content.add_child(options) + item = @tabs.add_child({:component => "org.gxg.gui.list.item", :title => ("tab" + new_child.uuid.to_s), :settings => {}, :options => {:states => ["tabs-title"]}, :script =>"", :content => []}) + link = item.add_child({:component => "org.gxg.gui.link.anchor", :title => ("link" + new_child.uuid.to_s), :settings => {}, :options => {:"href" => ("#" + new_child.uuid.to_s), :states => []}, :script =>"", :content => []}) + link.value = new_child.title.to_s + else + raise "Unacceptable Component Type" + end + new_child + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.tab.set", ::GxG::Gui::TabSet) + # + class Thumbnail < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :img + unless @states[:"thumbnail"] == true + @states[:"thumbnail"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.thumbnail", ::GxG::Gui::Thumbnail) + # + class Titlebar < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"title-bar"] == true + @states[:"title-bar"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.titlebar", ::GxG::Gui::Titlebar) + # + class TitlebarLeft < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"title-bar-left"] == true + @states[:"title-bar-left"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.titlebar.left", ::GxG::Gui::TitlebarLeft) + # + class TitlebarRight < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"title-bar-right"] == true + @states[:"title-bar-right"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.titlebar.right", ::GxG::Gui::TitlebarRight) + # + class Topbar < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"top-bar"] == true + @states[:"top-bar"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.topbar", ::GxG::Gui::Topbar) + # + class TopbarLeft < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"top-bar-left"] == true + @states[:"top-bar-left"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.topbar.left", ::GxG::Gui::TopbarLeft) + # + class TopbarRight < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:"top-bar-right"] == true + @states[:"top-bar-right"] = true + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.topbar.right", ::GxG::Gui::TopbarRight) + # + end +end \ No newline at end of file diff --git a/gxg/web/gxg_gui.rb b/gxg/web/gxg_gui.rb new file mode 100644 index 0000000..be5d522 --- /dev/null +++ b/gxg/web/gxg_gui.rb @@ -0,0 +1,1281 @@ +module GxG + module Gui + class Tree < ::GxG::Gui::Vdom::BaseElement + # + alias :original_add_child :add_child + def add_child(object_record=nil) + if object_record.is_a?(::GxG::Database::DetachedHash) + if @content + @content.add_child(object_record) + else + original_add_child(object_record) + end + end + end + # + alias :original_children :children + def children() + if @content + @content.children + else + {} + end + end + # + def find_child(the_reference=nil, interior=false) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + if interior == true + search_queue = self.original_children.values + else + search_queue = self.children.values + end + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def initialize(the_parent,the_options) + @content = nil + @selection = nil + @processors = {} + super(the_parent,the_options) + self + end + # + def cascade() + # + interior_component = ::GxG::Database::DetachedHash.new + interior_component.title = "content #{@uuid.to_s}" + interior_component[:component] = "org.gxg.gui.list" + interior_component[:settings] = {} + interior_component[:options] = {:style => {:"white-space" => "nowrap", :width => "auto", :height => "auto", :"list-style" => "none", :margin => "0px", :padding => "0px"}} + interior_component[:script] = "" + interior_component[:content] = [] + # + @content = nil + page.build_components(self,[(interior_component)]) + # + the_object = page.find_object_by("content #{@uuid.to_s}") + if the_object + @content = the_object + end + # + end + # + def update_appearance() + # TODO ?? + end + # Selection / Processing: + def set_processor(the_type=nil, &block) + # :select, :expand, :collapse + if block.respond_to?(:call) + if [:select, :expand, :collapse].include?(the_type.to_s.to_sym) + @processors[(the_type.to_s.to_sym)] = block + else + log_warn("Unsupported processor type: #{the_type.to_s.to_sym.inspect}") + end + end + end + # + def node_at_path(the_path=nil) + result = nil + search_queue = self.children.values.clone + while search_queue.size > 0 + entry = search_queue.shift + if entry.is_a?(GxG::Gui::TreeNode) + if entry.node_path() == the_path + result = entry + break + else + entry.nodes.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def expand_path(the_path=nil,&block) + if the_path.is_a?(::String) + path_stack = [] + path_array = the_path.split("/") + path_array.each do |entry| + if entry.size > 0 + path_stack << entry + the_node = self.node_at_path(('/' + path_stack.join("/"))) + if the_node.is_a?(GxG::Gui::TreeNode) + if the_node.gxg_get_state(:expanded) == false + the_node.expand() + GxG::DISPATCHER.post_event(:display) do + self.expand_path(the_path,&block) + end + break + end + if the_node.node_path() == the_path + if block.respond_to?(:call) + block.call() + end + end + else + GxG::DISPATCHER.post_event(:display) do + self.expand_path(the_path,&block) + end + break + end + end + end + end + true + end + # + def selection() + @selection + end + # + def select(the_node=nil) + if the_node.is_a?(GxG::Gui::TreeNode) + if @selection != the_node + if @selection + @selection.unhighlight() + end + @selection = the_node + @selection.highlight() + # TODO: scroll to reveal highlighted node ?? + if @processors[:select].respond_to?(:call) + @processors[:select].call(@selection) + end + end + end + end + # + def expand(the_node=nil) + if the_node.is_a?(GxG::Gui::TreeNode) + if @processors[:expand].respond_to?(:call) + @processors[:expand].call(the_node) + end + end + end + # + def collapse(the_node=nil) + if the_node.is_a?(GxG::Gui::TreeNode) + if @processors[:collapse].respond_to?(:call) + @processors[:collapse].call(the_node) + end + end + end + end + ::GxG::Gui::register_component_class(:tree, ::GxG::Gui::Tree) + # + class TreeNode < ::GxG::Gui::ListItem + # + def _before_create + if @data.is_a?(::Hash) + if @data[:title] + @title = @data[:title] + end + end + super() + end + # + alias :original_add_child :add_child + def add_child(object_record=nil) + if object_record.is_a?(::GxG::Database::DetachedHash) + if @content + @content.add_child(object_record) + else + original_add_child(object_record) + end + end + end + # + def nodes() + if @content + @content.children + else + {} + end + end + # + def node_path() + the_path = "" + lineage = [(self)] + the_parent = self.parent + while the_parent != nil do + if the_parent.is_a?(GxG::Gui::TreeNode) + lineage.unshift(the_parent) + end + the_parent = the_parent.parent + if the_parent.is_a?(GxG::Gui::Tree) + the_parent = nil + end + end + lineage.each do |the_node| + the_path = the_path + "/#{the_node.title}" + end + the_path + end + # + def find_child(the_reference=nil) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + search_queue = self.children.values + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def update_appearance() + if @appearance.is_any?(::Hash, ::GxG::Database::DetachedHash) + # + # TODO: update icon and label if different ?? + # + # @icon, @label component objects available. + true + else + false + end + end + # + def data() + @data + end + # + def appearance() + @appearance + end + # + def set_appearance(the_record=nil) + if the_record.is_any?(::Hash, ::GxG::Database::DetachedHash) + @appearance = the_record + # Look for :label, :icon w/in the hash record w/in the array. + self.update_appearance() + end + end + # + def tree_node() + # find nearest :tree_node parent in object heirarchy. + self.find_parent_type(::GxG::Gui::TreeNode) + end + # + def tree() + # find nearest :tree parent in object heirarchy. + self.find_parent_type(::GxG::Gui::Tree) + end + # + def initialize(the_parent,the_options) + @content = nil + @appearance = {:icon => theme_icon("folder.svg"), :label => "Untitled"} + if the_options.is_any?(::Hash, ::GxG::Database::DetachedHash) + # Pass an Hash record containing at LEAST: :title, :type, and :content. + @data = (the_options[:data] || {:title => "Untitled", :type => "virtual_directory", :content => {}}) + # select proper icon for @data[:type] value ?? (do in application!) + @appearance[:icon] = (the_options[:icon] || theme_icon("folder.svg")).to_s + @appearance[:label] = (the_options[:label] || @data[:title]).to_s + end + super(the_parent,the_options) + self + end + # + def cascade() + # + node_frame = ::GxG::Database::DetachedHash.new + node_frame[:component] = "org.gxg.gui.block.table" + node_frame[:settings] = {} + node_frame[:options] = {:nodepath => (self.node_path()), :style => {}} + node_frame[:script] = "" + # + frame_row_one = ::GxG::Database::DetachedHash.new + frame_row_one[:component] = "org.gxg.gui.block.table" + frame_row_one[:settings] = {} + frame_row_one[:options] = {} + frame_row_one[:script] = "" + frame_row_one[:content] = [] + # + expander = ::GxG::Database::DetachedHash.new + expander.title = "expander #{@uuid.to_s}" + expander[:component] = "org.gxg.gui.image" + expander[:settings] = {} + expander[:options] = {:src=>(theme_widget("collapse.svg")), :width=>32, :height=>32, :style => {:clear => "both", :'vertical-align' => 'middle'}} + expander[:script] = " + on(:mouseup) do |event| + the_node = self.tree_node() + if the_node + unless the_node.get_state(:disabled) == true + if self.get_state(:expanded) == true + self.set_attribute(:src, theme_widget('collapse.svg')) + self.set_state(:expanded,false) + the_node.collapse() + else + self.set_attribute(:src, theme_widget('expand.svg')) + self.set_state(:expanded,true) + the_node.expand() + end + end + end + end + " + expander[:content] = [] + # + node_expander_cell = ::GxG::Database::DetachedHash.new + node_expander_cell[:component] = "org.gxg.gui.block.table.cell" + node_expander_cell[:settings] = {} + node_expander_cell[:options] = {:style => {:float => "left", :width => "32px", :'vertical-align' => 'middle'}} + node_expander_cell[:script] = "" + node_expander_cell[:content] = [(expander)] + # + icon = ::GxG::Database::DetachedHash.new + icon.title = "icon #{@uuid.to_s}" + icon[:component] = "org.gxg.gui.image" + icon[:settings] = {} + icon[:options] = {:src=>@appearance[:icon], :style => {:clear => "both", :width=>"32px", :height=>"32px", :'vertical-align' => 'middle'}} + icon[:script] = " + on(:mouseup) do |event| + the_node = self.tree_node() + if the_node + the_tree = the_node.tree() + if the_tree + the_tree.select(the_node) + end + end + end + " + icon[:content] = [] + # + node_icon_cell = ::GxG::Database::DetachedHash.new + node_icon_cell[:component] = "org.gxg.gui.block.table.cell" + node_icon_cell[:settings] = {} + node_icon_cell[:options] = {:style => {:float => "left", :width => "32px", :'vertical-align' => 'middle'}} + node_icon_cell[:script] = "" + node_icon_cell[:content] = [(icon)] + # + the_title = ::GxG::Database::DetachedHash.new + the_title.title = "title #{@uuid.to_s}" + the_title[:component] = "org.gxg.gui.label" + the_title[:settings] = {} + the_title[:options] = {:content => @appearance[:label], :style => {:float => "left", :'font-size' => '16px', :'vertical-align' => 'middle', :'text-align' => 'left', :margin => "2px", :padding => "2px"}} + the_title[:script] = " + on(:mouseup) do |event| + the_node = self.tree_node() + if the_node + the_tree = the_node.tree() + if the_tree + the_tree.select(the_node) + end + end + end + " + the_title[:content] = [] + # + node_label_cell = ::GxG::Database::DetachedHash.new + node_label_cell[:component] = "org.gxg.gui.block.table.cell" + node_label_cell[:settings] = {} + node_label_cell[:options] = {:style => {:float => "left", :'vertical-align' => 'middle'}} + node_label_cell[:script] = "" + node_label_cell[:content] = [(the_title)] + # + frame_row_one = ::GxG::Database::DetachedHash.new + frame_row_one[:component] = "org.gxg.gui.block.table.row" + frame_row_one[:settings] = {} + frame_row_one[:options] = {} + frame_row_one[:script] = " + on(:mouseenter) do |event| + the_node = self.tree_node() + if the_node + the_tree = the_node.highlight() + end + end + on(:mouseleave) do |event| + the_node = self.tree_node() + if the_node + the_tree = the_node.unhighlight() + end + end + " + frame_row_one[:content] = [(node_expander_cell), (node_icon_cell), (node_label_cell)] + # + sublist = ::GxG::Database::DetachedHash.new + sublist.title = "content #{@uuid.to_s}" + sublist[:component] = "list" + sublist[:settings] = {} + sublist[:options] = {:width => "100%", :height => "100%", :style => {:"list-style" => "none"}} + sublist[:script] = "" + sublist[:content] = [] + # + subnodes_frame_cell = ::GxG::Database::DetachedHash.new + subnodes_frame_cell[:component] = "org.gxg.gui.block.table.cell" + subnodes_frame_cell[:settings] = {} + subnodes_frame_cell[:options] = {} + subnodes_frame_cell[:script] = "" + subnodes_frame_cell[:content] = [(sublist)] + # + frame_row_two = ::GxG::Database::DetachedHash.new + frame_row_two[:component] = "org.gxg.gui.block.table.row" + frame_row_two[:settings] = {} + frame_row_two[:options] = {} + frame_row_two[:script] = "" + frame_row_two[:content] = [(subnodes_frame_cell)] + # + node_frame[:content] = [(frame_row_one),(frame_row_two)] + # + @content = nil + page.build_components(self, [(node_frame)]) + # + the_object = page.find_object_by("content #{@uuid.to_s}") + if the_object + @content = the_object + end + # + the_object = page.find_object_by("expander #{@uuid.to_s}") + if the_object + @expander = the_object + end + # + the_object = page.find_object_by("icon #{@uuid.to_s}") + if the_object + @icon = the_object + end + # + the_object = page.find_object_by("title #{@uuid.to_s}") + if the_object + @label = the_object + end + # + end + # TODO: Add animation / transition effects. + def highlight(transition_parameters=nil) + if @label + @label.merge_style({:'background-color' => '#87acd5'}) + end + end + # + def unhighlight(transition_parameters=nil) + if @label + @label.merge_style({:'background-color' => '#f2f2f2'}) + end + end + # + def expand(transition_parameters=nil) + @content.show(transition_parameters) + self.set_state(:expanded, true) + if @expander + @expander.set_attribute(:src,GxG::DISPLAY_DETAILS[:object].theme_widget("expand.svg")) + end + the_tree = self.tree() + if the_tree + the_tree.expand(self) + end + end + # + def collapse(transition_parameters=nil) + @content.hide(transition_parameters) + self.set_state(:expanded, false) + if @expander + @expander.set_attribute(:src,GxG::DISPLAY_DETAILS[:object].theme_widget("collapse.svg")) + end + the_tree = self.tree() + if the_tree + the_tree.collapse(self) + end + end + # + def enable(transition_parameters=nil) + if transition_parameters.is_any?(::Hash, ::GxG::Database::DetachedHash) + if transition_parameters.is_a?(::GxG::Database::DetachedHash) + transition_parameters = transition_parameters.unpersist + end + else + transition_parameters = {:origin => {:opacity => 0.5}, :destination => {:opacity => 1.0}} + end + self.children.each_pair do |name, object| + unless name == "content #{@uuid.to_s}".to_sym + object.set_state(:disabled, false) + object.animate(transition_parameters) + end + end + end + # + def disable(transition_parameters=nil) + if transition_parameters.is_any?(::Hash, ::GxG::Database::DetachedHash) + if transition_parameters.is_a?(::GxG::Database::DetachedHash) + transition_parameters = transition_parameters.unpersist + end + else + transition_parameters = {:origin => {:opacity => 1.0}, :destination => {:opacity => 0.5}} + end + self.children.each_pair do |name, object| + unless name == "content #{@uuid.to_s}".to_sym + object.animate(transition_parameters) + object.set_state(:disabled, true) + end + end + end + # + end + ::GxG::Gui::register_component_class(:tree_node, ::GxG::Gui::TreeNode) + # + class Panel < ::GxG::Gui::Vdom::BaseElement + # TODO: Add auto-hide/hide/roll up-left-down-right capabilites with animations. + end + ::GxG::Gui::register_component_class(:panel, ::GxG::Gui::Panel) + # Page extensions + module Vdom + class Page + # Choice Dialog + def choice_dialog(the_application=nil, details=nil, &block) + # details: {:type => :choose, :title => "Some Title", :banner => "Some Text Here"} + self.layout_refresh() + # Determine Dialog Details: + window_title = (details || {:title => "Choose"})[:title].to_s + banner_text = (details || {:banner => "(Please provide some text here)"})[:banner].to_s + one_text = (details || {:one => ""})[:one].to_s + two_text = (details || {:two => ""})[:two].to_s + three_text = (details || {:three => ""})[:three].to_s + default_text = (details || {:default => ""})[:default].to_s + dialog_type = ((details || {:type => :choose})[:type] || :choose) + # Define bounds: (in px) + total_width = (self.layout_content_area()[:page_width].to_f * 0.25).to_i + form_padding = 20 + form_margin = 5 + inner_width = (total_width - ((form_padding + form_margin) * 2)) + total_height = ((form_padding + form_margin) * 2) + # + banner_font_size = 16 + if banner_text.to_s.size > 40 + banner_height = ((banner_text.size.to_f / 40.0) * banner_font_size.to_f).round.to_i + else + banner_height = banner_font_size + end + input_font_size = 16 + # Define parts: + # + banner = {:component=>"org.gxg.gui.text", :options=> {:content => banner_text, :style => {:"font-size" => "#{banner_font_size}px", :width => "100%"}}, :content => [], :script => ""} + banner_cell = {:component => "org.gxg.gui.block.table.cell", :options => {:style => {:padding => "0px", :margin => "0px", :width => "100%", :height => (banner_height), :"vertical-align" => "middle"}}, :content => [(banner)], :script => ""} + # + text_input = {:component=>"org.gxg.gui.input.text", :options=>{:title => "data", :content => default_text, :style => {:"background-color" => "#f2f2f2", :"font-size" => "#{input_font_size}px", :width => "100%"}}, :content=>[], :script=>""} + text_input_cell = {:component => "org.gxg.gui.block.table.cell", :options => {:style => {:padding => "0px", :margin => "0px", :width => "100%", :"vertical-align" => "middle"}}, :content => [(text_input)], :script => ""} + # Buttons: + one_btn = {:component=>"org.gxg.gui.button", :options=>{:content => one_text, :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + one_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.respond({:action => :one}) + end + end + " + one_btn_cell = {:component => "org.gxg.gui.block.table.cell", :options => {:style => {:padding => "0px", :margin => "0px", :width => "40%", :"vertical-align" => "middle"}}, :content => [(one_btn)], :script => ""} + # + two_btn = {:component=>"org.gxg.gui.button", :options=>{:content => two_text, :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + two_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.respond({:action => :two}) + end + end + " + two_btn_cell = {:component => "org.gxg.gui.block.table.cell", :options => {:style => {:padding => "0px", :margin => "0px", :width => "40%", :"vertical-align" => "middle"}}, :content => [(two_btn)], :script => ""} + # + three_btn = {:component=>"org.gxg.gui.button", :options=>{:content => three_text, :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + three_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.respond({:action => :three}) + end + end + " + three_btn_cell = {:component => "org.gxg.gui.block.table.cell", :options => {:style => {:padding => "0px", :margin => "0px", :width => "40%", :"vertical-align" => "middle"}}, :content => [(three_btn)], :script => ""} + # + cancel_btn = {:component=>"org.gxg.gui.button", :options=>{:content => "Cancel", :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + cancel_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.respond({:action => :cancel}) + end + end + " + cancel_cell = {:component => "org.gxg.gui.block.table.cell", :options => {:style => {:padding => "0px", :margin => "0px", :"vertical-align" => "middle"}}, :content => [(cancel_btn)], :script => ""} + # + # Define Containers: + # Rows: + row_one = {:component=>"org.gxg.gui.block.table.row", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "100%"}}, :content=>[(banner_cell)], :script=>""} + row_two = {:component=>"org.gxg.gui.block.table.row", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "100%"}}, :content=>[(text_input_cell)], :script=>""} + row_three = {:component=>"org.gxg.gui.block.table.row", :options=>{:style => {:padding => "0px", :margin => "0px", :width => "100%"}}, :content=>[], :script=>""} + # Tables: + table_one = {:component=>"org.gxg.gui.block.table", :options=>{:style => {:padding => "5px", :margin => "0px", :width => "100%"}}, :content=>[(row_one)], :script=>""} + table_two = {:component=>"org.gxg.gui.block.table", :options=>{:style => {:padding => "5px", :margin => "0px", :width => "100%"}}, :content=>[(row_two)], :script=>""} + table_three = {:component=>"org.gxg.gui.block.table", :options=>{:style => {:padding => "5px", :margin => "0px", :width => "100%"}}, :content=>[(row_three)], :script=>""} + # + # Form: + form = {:component=>"org.gxg.gui.form", :options=>{:style => {:"background-color" => "#f2f2f2", :padding => "#{form_padding}px", :margin => "#{form_margin}px"}}, :content=>[], :script=>""} + # + if banner_text.size > 0 + form[:content] << table_one + end + # Build form: + case dialog_type + when :choose + # no :cancel; :one, :two, and :three buttons + if one_text.size > 0 + row_three[:content] << one_btn_cell + end + if two_text.size > 0 + row_three[:content] << two_btn_cell + end + if three_text.size > 0 + row_three[:content] << three_btn_cell + end + when :alert + # no cancel button + action_btn = {:component=>"org.gxg.gui.button", :options=>{:content => "OK", :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + action_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.respond({:action => :ok}) + end + end + " + action_btn_cell = {:component => "org.gxg.gui.block.table.cell", :options => {:style => {:padding => "0px", :margin => "0px"}}, :content => [(action_btn)], :script => ""} + # + row_three[:content] << action_btn_cell + when :input + # text input, cancel and submit buttons + row_three[:content] << cancel_cell + action_btn = {:component=>"button", :options=>{:content => "OK", :style => {:padding => "2px", :width => "80px", :height => "32px"}}, :content=>[], :script=>""} + action_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + form = self.find_parent_type(:form) + if the_window && form + the_window.respond({:action => :ok, :form => form.form_data()}) + end + end + " + action_btn_cell = {:component => "org.gxg.gui.block.table.cell", :options => {:style => {:padding => "0px", :margin => "0px"}}, :content => [(action_btn)], :script => ""} + # + row_three[:content] << action_btn_cell + # + form[:content] << table_two + end + form[:content] << table_three + # Build Dialog + ### Calculate dialog width & height from content: + # + form[:options][:style][:width] = "#{total_width}px" + ### Set banner height and other settings: ???? + if banner_text.to_s.size > 0 + form[:content][0][:options][:style][:width] = "#{inner_width}px" + form[:content][0][:options][:style][:height] = "#{banner_height}px" + form[:content][0][:options][:style][:padding] = "10px 0px 10px 0px" + total_height += (banner_height + 20) + end + if dialog_type == :input + form[:content][1][:options][:style][:width] = "#{inner_width}px" + form[:content][1][:options][:style][:height] = "#{input_font_size}px" + form[:content][1][:options][:style][:padding] = "10px 0px 10px 0px" + total_height += (input_font_size + 20) + form[:content][2][:options][:style][:width] = "#{inner_width}px" + form[:content][2][:options][:style][:height] = "32px" + form[:content][2][:options][:style][:padding] = "10px 0px 10px 0px" + total_height += 52 + else + # just buttons + form[:content][1][:options][:style][:width] = "#{inner_width}px" + form[:content][1][:options][:style][:height] = "32px" + form[:content][1][:options][:style][:padding] = "10px 0px 10px 0px" + total_height += 52 + end + # Review : cuts buttons off @ bottom on Firefox (shim-fix) + total_height += 32 + # + form[:options][:style][:height] = "#{(total_height - ((form_padding + form_margin) * 2))}px" + ### Calculate button horizontal spacing: inner_width + case row_three[:content].size + when 1 + row_three[:content][0][:options][:style][:padding] = "5px 0px 5px #{((inner_width / 2) - 25)}px" + when 2 + row_three[:content][0][:options][:style][:padding] = "5px 0px 5px #{((inner_width / 4) - 12)}px" + row_three[:content][1][:options][:style][:padding] = "5px 0px 5px #{((inner_width / 4) - 12)}px" + when 3 + row_three[:content][0][:options][:style][:padding] = "5px 0px 5px #{((inner_width / 6) - 6)}px" + row_three[:content][1][:options][:style][:padding] = "5px 0px 5px #{((inner_width / 6) - 6)}px" + row_three[:content][2][:options][:style][:padding] = "5px 0px 5px #{((inner_width / 6) - 6)}px" + end + # + dialog_source = {:component => "org.gxg.gui.window.dialog", :options => {:window_title => window_title, :states => {:hidden => true}, :width => total_width, :height => total_height}, :script => "", :content => [(form)]} + dialog_source[:script] = " + on(:keyup) do |event| + if event[:which] == 27 + self.respond({:action => :cancel}) + end + end + " + # + begin + # log_info("Opening dialog ...") + self.dialog_open(dialog_source, the_application, details, &block) + true + rescue Exception => the_error + log_error({:error => the_error, :parameters => {}}) + false + end + end + # Mini Path Finder: + def vfs_dialog(the_application=nil, details=nil, &block) + # details: {:type => :folder, :path => "/path/to/somewhere"} + # Determine Dialog Type: + window_title = "Unsupported Dialog Type" + action_title = "Unsupported" + dialog_type = (details || {:type => :folder})[:type] + case dialog_type + when :save, "save" + window_title = "Save As ..." + action_title = "Save" + when :folder, "folder" + window_title = "Choose Folder" + action_title = "Choose" + when :object, "object" + window_title = "Select Object" + action_title = "Select" + end + # + menu_item_script = " + def select(data=nil) + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.settings[:data] = @settings[:data] + menu_bar_header = page.find_object_by('gxg_path_menu_header') + if menu_bar_header + menu_bar_header.set_label(File.basename(@settings[:data])) + menu_bar_header.set_data(@settings[:data]) + end + menu = page.find_object_by('gxg_path_menu') + if menu + menu.update_menu(@settings[:data]) + end + sublist = page.find_object_by('gxg_path_subitems') + if sublist + sublist.update_appearance() + end + end + end + " + # + menu_bar = ::GxG::Database::DetachedHash.new + menu_bar.title = "gxg_path_menu_bar" + menu_bar[:component] = "org.gxg.gui.menu.bar" + menu_bar[:settings] = {:path => (details[:path] || "/")} + menu_bar[:options] = {} + menu_bar[:script] = "" + menu_bar[:content] = [] + # + menu_header = ::GxG::Database::DetachedHash.new + menu_header.title = "gxg_path_menu_header" + menu_header[:component] = "org.gxg.gui.menu.item" + menu_header[:settings] = {:label => File.basename(menu_bar[:settings][:path]), :data => nil} + menu_header[:options] = {} + menu_header[:script] = "" + menu_header[:content] = [] + # + menu = ::GxG::Database::DetachedHash.new + menu.title = "gxg_path_menu" + menu[:component] = "org.gxg.gui.menu" + menu[:settings] = {:item_script => menu_item_script} + menu[:options] = {} + menu[:script] = " + def update_menu(the_path=nil) + if the_path.is_a?(::String) + build_list = [] + menu[:content] = [] + # + menu_manifest = [] + (0..(the_path.split('/').size - 1)).each do |the_index| + menu_manifest << (the_path.split('/')[0..(the_index)].join('/')) + end + menu_manifest.reverse.[1..-1].each do |the_subpath| + the_item = ::GxG::Database::DetachedHash.new + the_item[:component] = 'menu_item' + the_item[:settings] = {:label => File.basename(the_subpath), :data => the_subpath} + the_item[:options] = {} + the_item[:script] = @settings[:item_script].to_s + the_item[:content] = [] + build_list << the_item + end + children.values.reverse.each do |the_child| + the_child.destroy + end + if build_list.size > 0 + page.build_components(self, build_list) + end + # + end + end + " + menu[:content] = [] + # + menu_manifest = [] + (0..(menu_bar[:settings][:path].split("/").size - 1)).each do |the_index| + menu_manifest << (menu_bar[:settings][:path].split("/")[0..(the_index)].join("/")) + end + menu_manifest.reverse[1..-1].each do |the_path| + the_item = ::GxG::Database::DetachedHash.new + the_item[:component] = "org.gxg.gui.menu.item" + the_item[:settings] = {:label => File.basename(the_path), :data => the_path} + the_item[:options] = {} + the_item[:script] = menu_item_script + the_item[:content] = [] + menu[:content] << the_item + end + # + menu_header[:content] << menu + menu_bar[:content] << menu_header + # + menu_bar_cell = ::GxG::Database::DetachedHash.new + menu_bar_cell[:component] = "org.gxg.gui.block.table.cell" + menu_bar_cell[:settings] = {} + menu_bar_cell[:options] = {:style => {:border => "1px solid #c2c2c2", :padding => "0px", :margin => "0px", :width => "100%"}} + menu_bar_cell[:script] = "" + menu_bar_cell[:content] = [(menu_bar)] + # SubItem List: + list_item_script = " + def highlight() + self.merge_style({:'background-color' => '#c2c2c2'}) + end + def unhighlight() + self.merge_style({:'background-color' => '#ffffff'}) + end + def select() + # unhighlight all others + parent.children.values.each do |the_child| + the_child.unhighlight() + end + # set selection + self.highlight() + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.settings[:selection] = self + end + end + on(:mouseup) do |the_event| + self.select() + end + " + subitems = ::GxG::Database::DetachedHash.new + subitems.title = "gxg_path_subitems" + subitems[:component] = "org.gxg.gui.list" + subitems[:settings] = {:list_item_script => list_item_script} + subitems[:options] = {:style => {:clear => "both", :"list-style" => "none", :padding => "0px", :margin => "0px"}} + subitems[:script] = " + def add_list_item(the_icon=nil, the_path=nil, the_profile=nil) + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + # + the_item = ::GxG::Database::DetachedHash.new + the_item[:component] = 'list_tiem' + the_item[:settings] = {:label => File.basename(the_path), :data => the_path} + the_item[:options] = {} + the_item[:script] = @settings[:list_item_script].to_s + the_item[:content] = [] + # + the_window.settings[:manifest][(the_item.uuid.to_s.to_sym)] = {:path => the_path, :profile => the_profile} + # + icon_item = ::GxG::Database::DetachedHash.new + icon_item[:component] = 'image' + icon_item[:settings] = {} + icon_item[:options] = {src => the_icon.to_s, :style => {:width => '16px', :height => '16px'}} + icon_item[:script] = ' + on(:mouseup) do |the_event| + parent.select() + end + ' + icon_item[:content] = [] + # + label_item = ::GxG::Database::DetachedHash.new + label_item[:component] = 'label' + label_item[:settings] = {} + label_item[:options] = {:content => File.basename(the_path)} + label_item[:script] = ' + on(:mouseup) do |the_event| + parent.select() + end + ' + label_item[:content] = [] + # + the_item[:content] << icon_item + the_item[:content] << label_item + page.build_components(self, [(the_item)]) + true + else + false + end + end + # + def update_appearance() + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + dialog_type = (the_window.settings[:type] || :folder) + the_path = the_window.settings[:data] + if the_path.is_a?(::String) + # + the_window.settings[:selection] = nil + the_window.settings[:manifest] = {} + # Clear List + self.children.values.each do |the_child| + the_child.destroy + end + # Rebuild List + GxG::CONNECTION.entries({:location => (the_path)}) do |response| + if response.is_a?(::Hash) + response[:result].each do |the_profile| + # add list item + item_path = (the_path + '/' + the_profile[:title].to_s) + if dialog_type == :folder + if [:virtual_directory, :directory, :persisted_array].include?(the_profile[:type].to_s.to_sym) + self.add_list_item(self.theme_icon('folder.svg'), item_path, the_profile) + end + else + case (the_profile[:type].to_s.to_sym) + when :virtual_directory, :directory, :persisted_array + the_icon_path = self.theme_icon('folder.svg') + when :persisted_hash + the_icon_path = self.theme_icon('object.svg') + else + # Review: further qualify file type icons ?? + the_icon_path = self.theme_icon('file.svg') + end + self.add_list_item(the_icon_path, item_path, the_profile) + end + # + end + # + end + end + # + end + end + end + " + subitems[:content] = [] + # xxx + case dialog_type + when :save + subitems[:script] = (subitems[:script] + "\n" + " + self.set_select_responder do |the_uuid| + if @manifest[(the_uuid.to_s.to_sym)] + # nothing + end + end + self.set_open_responder do |the_uuid| + if @manifest[(the_uuid.to_s.to_sym)] + if [:virtual_directory, :directory, :persisted_array].include?(@manifest[(the_uuid.to_s.to_sym)][:profile][:type].to_s.to_sym) + # open folder + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + menu_bar = the_window.find_child('path_menu_bar') + if menu_bar + GxG::DISPATCHER.post_event(:root) do + menu_bar.path = @manifest[(the_uuid.to_s.to_sym)][:path] + end + end + end + end + end + end + ") + when :folder + subitems[:script] = (subitems[:script] + "\n" + " + self.set_select_responder do |the_uuid| + if @manifest[(the_uuid.to_s.to_sym)] + # nothing + end + end + self.set_open_responder do |the_uuid| + if @manifest[(the_uuid.to_s.to_sym)] + if [:virtual_directory, :directory, :persisted_array].include?(@manifest[(the_uuid.to_s.to_sym)][:profile][:type].to_s.to_sym) + # open folder + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + menu_bar = the_window.find_child('path_menu_bar') + if menu_bar + GxG::DISPATCHER.post_event(:root) do + menu_bar.path = @manifest[(the_uuid.to_s.to_sym)][:path] + end + end + end + end + end + end + ") + when :object + subitems[:script] = (subitems[:script] + "\n" + " + self.set_select_responder do |the_uuid| + if @manifest[(the_uuid.to_s.to_sym)] + # if object selected enable Select button else disable it. + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + action_button = the_window.find_child('action_button') + if action_button + if [:file, :persisted_hash].include?(@manifest[(the_uuid.to_s.to_sym)][:profile][:type].to_s.to_sym) + action_button.enable() + else + action_button.disable() + end + end + end + end + end + self.set_open_responder do |the_uuid| + if @manifest[(the_uuid.to_s.to_sym)] + if [:virtual_directory, :directory, :persisted_array].include?(@manifest[(the_uuid.to_s.to_sym)][:profile][:type].to_s.to_sym) + # open folder + # first, disable Select button + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + menu_bar = the_window.find_child('path_menu_bar') + if menu_bar + GxG::DISPATCHER.post_event(:root) do + menu_bar.path = @manifest[(the_uuid.to_s.to_sym)][:profile][:path] + end + end + end + end + end + end + ") + end + # + subitems_container = ::GxG::Database::DetachedHash.new + subitems_container[:component] = "org.gxg.gui.block" + subitems_container[:settings] = {} + subitems_container[:options] = {:style => {:width => "100%", :height => "192px", :"overflow-y" => "scroll"}} + subitems_container[:script] = "" + subitems_container[:content] = [(subitems)] + # + subitems_cell = ::GxG::Database::DetachedHash.new + subitems_cell[:component] = "org.gxg.gui.block.table.cell" + subitems_cell[:settings] = {} + subitems_cell[:options] = {:style => {:border => "1px solid #c2c2c2", :padding => "0px", :margin => "0px", :width => "100%"}} + subitems_cell[:script] = "" + subitems_cell[:content] = [(subitems_container)] + # Save As ... Field + save_name = ::GxG::Database::DetachedHash.new + save_name.title = "gxg_save_name" + save_name[:component] = "org.gxg.gui.input.text" + save_name[:settings] = {} + save_name[:options] = {:style => {:"background-color" => "#f2f2f2", :"font-size" => "16px", :width => "100%"}} + save_name[:script] = "" + save_name[:content] = [] + # + save_name_cell = ::GxG::Database::DetachedHash.new + save_name_cell[:component] = "org.gxg.gui.block.table.cell" + save_name_cell[:settings] = {} + save_name_cell[:options] = {:style => {:padding => "0px", :margin => "0px", :width => "100%"}} + save_name_cell[:script] = "" + save_name_cell[:content] = [(save_name)] + # Buttons: + action_btn = ::GxG::Database::DetachedHash.new + action_btn.title = "gxg_action_button" + action_btn[:component] = "org.gxg.gui.input.button.submit" + action_btn[:settings] = {} + action_btn[:options] = {:content => action_title, :style => {:padding => "0px", :width => "80px", :height => "32px"}} + action_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + form = self.find_parent_type(::GxG::Gui::Form) + if the_window && form + the_action = the_window.settings[:type].to_s.to_sym + the_path = the_window.settings[:data].to_s + if the_action == :save + the_path = (the_path + '/' + (form.form_data()[:gxg_save_name] || 'Untitled.data').to_s) + end + the_window.respond({:action => the_action, :path => the_path}) + end + end + " + action_btn[:content] = [] + # + action_cell = ::GxG::Database::DetachedHash.new + action_cell[:component] = "org.gxg.gui.block.table.cell" + action_cell[:settings] = {} + action_cell[:options] = {:style => {:padding => "10px 0px 0px 35px", :margin => "0px", :width => "50%"}} + action_cell[:script] = "" + action_cell[:content] = [(action_btn)] + # + cancel_btn = ::GxG::Database::DetachedHash.new + cancel_btn.title = "gxg_cancel_button" + cancel_btn[:component] = "org.gxg.gui.input.button.submit" + cancel_btn[:settings] = {} + cancel_btn[:options] = {:content => "Cancel", :style => {:padding => "2px", :width => "80px", :height => "32px"}} + cancel_btn[:script] = " + on(:mouseup) do |event| + the_window = self.find_parent_type(::GxG::Gui::DialogBox) + if the_window + the_window.respond({:action => :cancel}) + end + end + " + cancel_btn[:content] = [] + # + cancel_cell = ::GxG::Database::DetachedHash.new + cancel_cell[:component] = "org.gxg.gui.block.table.cell" + cancel_cell[:settings] = {} + cancel_cell[:options] = {:style => {:padding => "10px 0px 0px 35px", :margin => "0px", :width => "50%"}} + cancel_cell[:script] = "" + cancel_cell[:content] = [(cancel_btn)] + # + # Rows: + row_one = ::GxG::Database::DetachedHash.new + row_one[:component] = "org.gxg.gui.block.table.row" + row_one[:settings] = {} + row_one[:options] = {:style => {:padding => "0px", :margin => "0px", :width => "100%"}} + row_one[:script] = "" + row_one[:content] = [(menu_bar_cell)] + # + row_two = ::GxG::Database::DetachedHash.new + row_two[:component] = "org.gxg.gui.block.table.row" + row_two[:settings] = {} + row_two[:options] = {:style => {:padding => "0px", :margin => "0px", :width => "100%"}} + row_two[:script] = "" + row_two[:content] = [(subitems_cell)] + # + row_three = ::GxG::Database::DetachedHash.new + row_three[:component] = "org.gxg.gui.block.table.row" + row_three[:settings] = {} + row_three[:options] = {:style => {:padding => "0px", :margin => "0px", :width => "100%"}} + row_three[:script] = "" + row_three[:content] = [(save_name_cell)] + # + row_four = ::GxG::Database::DetachedHash.new + row_four[:component] = "org.gxg.gui.block.table.row" + row_four[:settings] = {} + row_four[:options] = {:style => {:padding => "0px", :margin => "0px", :width => "100%"}} + row_four[:script] = "" + row_four[:content] = [(cancel_cell),(action_cell)] + # + # Tables: + table_one = ::GxG::Database::DetachedHash.new + table_one[:component] = "org.gxg.gui.block.table" + table_one[:settings] = {} + table_one[:options] = {:style => {:padding => "0px", :margin => "0px", :width => "300px", :height => "32px"}} + table_one[:script] = "" + table_one[:content] = [(row_one)] + # + table_two = ::GxG::Database::DetachedHash.new + table_two[:component] = "org.gxg.gui.block.table" + table_two[:settings] = {} + table_two[:options] = {:style => {:padding => "0px", :margin => "0px", :width => "300px", :height => "192px"}} + table_two[:script] = "" + table_two[:content] = [(row_two)] + # + table_three = ::GxG::Database::DetachedHash.new + table_three[:component] = "org.gxg.gui.block.table" + table_three[:settings] = {} + table_three[:options] = {:style => {:padding => "0px", :margin => "0px", :width => "300px", :height => "32px"}} + table_three[:script] = "" + table_three[:content] = [(row_three)] + # + table_four = ::GxG::Database::DetachedHash.new + table_four[:component] = "org.gxg.gui.block.table" + table_four[:settings] = {} + table_four[:options] = {:style => {:padding => "0px", :margin => "0px", :width => "300px", :height => "32px"}} + table_four[:script] = "" + table_four[:content] = [(row_four)] + # + # Main Build: + form = ::GxG::Database::DetachedHash.new + form[:component] = "org.gxg.gui.form" + form[:settings] = {} + form[:options] = {:style => {:"background-color" => "#f2f2f2", :padding => "20px", :margin => "5px", :width => "100%"}} + form[:script] = "" + form[:content] = [] + # + total_height = 330 + form[:content] << table_one + form[:content] << table_two + if dialog_type == :save || dialog_type == "save" + form[:content] << table_three + total_height += 32 + end + form[:content] << table_four + # + viewport = ::GxG::Database::DetachedHash.new + viewport.title = "gxg_selection_viewport" + viewport[:component] = "org.gxg.gui.application.viewport" + viewport[:settings] = {} + viewport[:options] = {:style => {:overflow => "hidden", :width => "100%", :height => "100%"}} + viewport[:script] = "" + viewport[:content] = [(form)] + # + dialog_source = ::GxG::Database::DetachedHash.new + dialog_source.title = window_title + dialog_source[:component] = "org.gxg.gui.window.dialog" + dialog_source[:settings] = {} + dialog_source[:options] = {:window_title => window_title, :states => {:hidden => true}, :width => 350, :height => total_height} + dialog_source[:script] = "" + dialog_source[:content] = [(viewport)] + # + begin + # log_info("Opening dialog ...") + self.dialog_open(dialog_source, the_application, details, &block) + true + rescue Exception => the_error + log_error({:error => the_error, :parameters => {}}) + false + end + end + # + end + end + end +end \ No newline at end of file diff --git a/gxg/web/gxg_gui_old.rb b/gxg/web/gxg_gui_old.rb new file mode 100644 index 0000000..e1a2bac --- /dev/null +++ b/gxg/web/gxg_gui_old.rb @@ -0,0 +1,2349 @@ +# page format: {:uuid=>"9f7b8942-82ee-4695-8a61-d95f15f5dba3", :type=>:structure, :ufs=>:"org.gxg.gui.page", :title=>"Page", :version=>0.0, :mime_types=>"", :content=>{:component_type=>"page", :publish_path=>"", :background_uuid=>"", :menubar_uuid=>"", :attributes=>{}, :classes=>"", :style=>{}, :browsing=>{:draggable=>{}, :droppable=>{}, :resizable=>{}, :selectable=>{}, :sortable=>{}, :tool_tip=>{}, :alt_text=>""}, :authoring=>{:draggable=>{}, :droppable=>{}, :resizable=>{}, :selectable=>{}, :sortable=>{}, :tool_tip=>{}, :alt_text=>""}, :options=>{}, :value=>nil, :script=>"", :sub_components=>[]}} +# +# component format: {:uuid=>:"c44d8b14-1a97-4c6f-b07a-6999ca310241", :type=>:structure, :ufs=>:"org.gxg.gui.component", :title=>"Gui Component", :version=>0.0, :mime_types=>[], :content=>{:component_type=>"unspecified", :attributes=>{}, :classes=>"", :style=>{}, :browsing=>{:draggable=>{}, :droppable=>{}, :resizable=>{}, :selectable=>{}, :sortable=>{}, :tool_tip=>{}, :alt_text=>""}, :authoring=>{:draggable=>{}, :droppable=>{}, :resizable=>{}, :selectable=>{}, :sortable=>{}, :tool_tip=>{}, :alt_text=>""}, :options=>{}, :value=>nil, :script=>"", :sub_components=>[]}} +#{ +# :component_type => "page", +# :publish_path => "", +# :background_uuid => "", +# :menubar_uuid => "", +# :attributes => {}, +# :classes => "", +# :style => {}, +# :browsing => {:draggable => {}, :droppable => {}, :resizable => {}, :selectable => {}, :sortable => {}, :tool_tip => {}, :alt_text => ""}, +# :authoring => {:draggable => {}, :droppable => {}, :resizable => {}, :selectable => {}, :sortable => {}, :tool_tip => {}, :alt_text => ""}, +# :options => {}, +# :value => nil, +# :script => "", +# :sub_components => [] +#} +module GxG + DISPLAY_DETAILS = {:host_path => "", :use_ssl => false, :connection => nil, :object => nil, :mode => :browsing} + module Css + @@CssCache = [] + @@CssRuleProperties = {} + # + class CSSRule + # Provide EASY interface to native CSS Rule. + def initialize(the_native_rule) + @native_rule = the_native_rule + @name = (`#{@native_rule}.selectorText` || "").to_s.to_sym + end + # + def native_object() + @native_rule + end + # + def inspect() + "" + end + # + def name() + @name + end + # + def keys() + GxG::Css::valid_properties() + end + # + def [](the_key) + result = nil + csskey = @@CssRuleProperties[(the_key)] + if csskey + result = `#{@native_rule}.style[#{csskey}]` + end + result + end + # + def []=(the_key, the_value) + # + csskey = @@CssRuleProperties[(the_key)] + if csskey + `#{@native_rule}.style[#{csskey}] = #{(the_value || "")}` + else + # ignore invalid keys + end + the_value + end + # + def merge(properties={}) + properties.keys.each do |the_key| + self[(the_key)] = properties[(the_key)] + end + self + end + # + end + # + def self.refresh_css_cache() + sheets = `document.styleSheets.length`.to_i + if sheets > 0 + new_cache = [] + (0..(sheets - 1)).each do |sheet_index| + begin + ruleset = `document.styleSheets[#{sheet_index}].cssRules` + rescue Exception => the_error + ruleset = `document.styleSheets[#{sheet_index}].rules` + end + if ruleset + rules = (`#{ruleset}.length`.to_i) + if rules > 0 + (0..(rules - 1)).each do |rule_index| + begin + native_rule = `document.styleSheets[#{sheet_index}].cssRules[#{rule_index}]` + rescue Exception => the_error + native_rule = `document.styleSheets[#{sheet_index}].rules[#{rule_index}]` + end + if native_rule + new_cache << GxG::Css::CSSRule.new(native_rule) + if @@CssRuleProperties.size == 0 + `Object.keys(#{native_rule}.style)`.each do |the_property_label| + if the_property_label + if ["0", "1", "3", "4", "5", "6", "7", "8", "9"].include?(the_property_label[0]) + the_key = the_property_label.to_sym + else + key_text = (the_property_label[0].capitalize + the_property_label[1..-1]).split_camelcase + if key_text.is_a?(String) + key_text = [(key_text)] + end + the_key = key_text.join("-").downcase.to_sym + end + unless @@CssRuleProperties.keys.include?(the_key) + @@CssRuleProperties[(the_key)] = the_property_label.to_s + end + end + end + end + # + end + end + end + end + end + @@CssCache = new_cache + end + true + end + # + def self.valid_properties() + @@CssRuleProperties.keys + end + # + def self.get_property_name(the_key=nil) + result = nil + if the_key.is_a?(::Symbol) + result = @@CssRuleProperties[(the_key)] + end + result + end + # + def self.rules() + result = [] + @@CssCache.each do |entry| + result << entry.name + end + result + end + # + def self.find_rule(the_rule_selector=nil) + result = [] + @@CssCache.each do |entry| + if entry.name == (the_rule_selector.to_s.to_sym) + result << entry + end + end + result + end + # + def self.set_rule(the_rule_selector=nil, the_properties={}, options={:merge => true}) + # find existing + # overwrite or merge (options) + # create if non-existent + found = GxG::Css::find_rule(the_rule_selector) + if found.size > 0 + found.each do |the_rule_entry| + the_rule = the_rule_entry.values[0] + if options[:overwrite] == true + @@CssRuleProperties.keys.each do |clear_key| + unless the_properties.keys.include?(clear_key) + `#{the_rule.native_object()}.style[#{@@CssRuleProperties[(clear_key)]}] = ""` + end + end + end + the_properties.keys.each do |the_key| + csskey = @@CssRuleProperties[(the_key)] + if csskey + `#{the_rule.native_object()}.style[#{csskey}] = #{the_properties[(the_key)]}` + else + # ignore invalid keys + end + end + end + else + # create it + rule_body = {} + the_properties.keys.each do |property_key| + csskey = @@CssRuleProperties[(property_key)] + if csskey + rule_body[(csskey)] = the_properties[(property_key)] + end + end + sheet_index = (`document.styleSheets.length`.to_i - 1) + rules = `document.styleSheets[#{sheet_index}].cssRules` + until rules do + sheet_index -= 1 + if sheet_index < 0 + rules = nil + break + end + rules = `document.styleSheets[#{sheet_index}].cssRules` + end + if rules + # + rule_index = (`#{rules}.length`.to_i - 1) + response_code = `document.styleSheets[#{sheet_index}].addRule(#{the_rule_selector},"",#{rule_index})`.to_i + if response_code == -1 + begin + native_rule = `document.styleSheets[#{sheet_index}].cssRules[#{rule_index}]` + rescue Exception => the_error + native_rule = `document.styleSheets[#{sheet_index}].rules[#{rule_index}]` + end + the_properties.keys.each do |property_key| + csskey = @@CssRuleProperties[(property_key)] + if csskey + `#{native_rule}.style[#{csskey}] = #{the_properties[(property_key)]}` + end + end + @@CssCache << GxG::Css::CSSRule.new(native_rule) + end + end + end + true + end + # + def self.remove_rule(the_rule_selector=nil) + found_indexes = [] + @@CssCache.each_with_index do |entry, index| + if entry.name == the_rule_selector.to_s.to_sym + found_indexes << index + end + end + found = [] + found_indexes.reverse.each do |the_index| + found << @@CssCache.delete_at(the_index) + end + if found.size > 0 + found.each do |the_rule| + native_rule = the_rule.native_object() + the_sheet = `#{native_rule}.parentStyleSheet` + if the_sheet + rule_count = `#{the_sheet}.cssRules.length`.to_i + if rule_count > 0 + (0..(rule_count - 1)).to_a.reverse.each do |rule_index| + other_native_rule = `#{the_sheet}.cssRules[#{rule_index}]` + if other_native_rule + if `#{native_rule}.selectorText == #{other_native_rule}.selectorText` + `#{the_sheet}.deleteRule(#{rule_index})` + end + end + end + end + end + end + true + else + false + end + end + # + end + # + module Graphics + class WebGL + # + end + # + class Svg + # + end + # + class Canvas + # + end + end + # + module Gui + # + class Display + # There is typically only one of these instantiated. + # url('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'); + def self.valid_properties() + GxG::Css::valid_properties() + end + # + def initialize(settings={}) + if GxG::DISPLAY_DETAILS[:object].is_a?(GxG::Gui::Display) + raise Exception, "Display has already been constructed." + else + @uuid = (settings[:uuid] || GxG::uuid_generate) + @remote_uuid = nil + @channel = nil + @update_queue = [] + @document = ::Document + @dom_object = ::Element.find('body') + # + @attributes = {} + @classes = "" + @style = {} + @browsing = {:draggable => {}, :droppable => {}, :resizable => {}, :selectable => {}, :sortable => {}, :tool_tip => {}, :alt_text => ""} + @authoring = {:draggable => {}, :droppable => {}, :resizable => {}, :selectable => {}, :sortable => {}, :tool_tip => {}, :alt_text => ""} + @options = {} + @value = nil + # + @update_timer = post_event_periodic(0.333) do + self.transmit_update() + end + GxG::DISPLAY_DETAILS[:object] = self + GxG::register(self) + GxG::Css::refresh_css_cache() + end + self + end + # + def uuid() + @uuid + end + # + def set_remote_uuid(the_uuid=nil) + if the_uuid + @remote_uuid = the_uuid + end + end + # + def dom_object() + @dom_object + end + # + def establish_dom_link() + # No-op : already establish + end + # + def channel() + @channel + end + # + def set_channel(the_channel=nil) + @channel = the_channel + end + # + def export_display(local_export=false) + # Export entire key-frame of display state data. + # build up export db pairs: + export_db = [] + link_db = [(self)] + while link_db.size > 0 do + entry = link_db.shift + parent = GxG::parent_of(entry) + if parent + new_record = {:uuid => entry.uuid.to_s, :parent_uuid => parent.uuid.to_s, :type => nil, :attributes => {}, :classes => "", :style => {}, :browsing => {}, :authoring => {}, :options => {}, :value => nil, :content => []} + new_record[:type] = GxG::Gui::GuiComponent::class_to_key(entry.class).to_s + else + if local_export == true + new_record = {:uuid => @uuid.to_s, :parent_uuid => nil, :type => "display", :attributes => {}, :classes => "", :style => {}, :browsing => {}, :authoring => {}, :options => {}, :value => nil, :content => []} + else + new_record = {:uuid => @remote_uuid.to_s, :parent_uuid => nil, :type => "display", :attributes => {}, :classes => "", :style => {}, :browsing => {}, :authoring => {}, :options => {}, :value => nil, :content => []} + end + end + new_record[:attributes] = entry.get_attributes() + new_record[:classes] = entry.get_classes() + new_record[:style] = entry.get_style() + new_record[:browsing] = entry.get_features() + new_record[:authoring] = entry.get_authoring_features() + new_record[:options] = entry.get_options() + new_record[:value] = entry.get_value() + export_db << {:object => entry, :record => new_record} + GxG::children_of(entry).each do |the_child| + link_db << the_child + end + end + object_record = Proc.new do |the_object| + found = nil + export_db.each do |export_record| + if the_object.object_id == export_record[:object].object_id + found = export_record + break + end + end + found + end + # Assemble export contents of records + link_db = [(self)] + while link_db.size > 0 do + # {:uuid => "", :parent_uuid => "", :type => "", :classes => "", :style => {}, :features => {}, :value => nil, :content => []} + entry = link_db.shift + the_record = object_record.call(entry) + if the_record + GxG::children_of(entry).each do |the_child| + child_record = object_record.call(the_child) + the_record[:record][:content] << child_record[:record] + link_db << the_child + end + end + # + end + (export_db[0] || {})[:record] + end + # + def import_display(import_records={}) + result = false + if import_records.size > 0 + old_list = GxG::register_uuid_list() + retain_list = [(@uuid.to_sym)] + import_db = [] + link_db = [(import_records)] + while link_db.size > 0 do + entry = link_db.shift + # do a format check on the record? + if (entry[:uuid] && entry[:type] && entry[:attributes] && entry[:classes] && entry[:style] && entry[:browsing] && entry[:authoring] && entry[:options] && entry[:content]) + entry[:object] = GxG::object_by_uuid(entry[:uuid].to_sym) + unless entry[:object] + # create object + parent = nil + if entry[:parent_uuid] + parent = GxG::object_by_uuid(entry[:parent_uuid].to_sym) + end + the_class = GxG::Gui::GuiComponent::key_to_class(entry[:type].to_sym) + unless the_class == GxG::Gui::Display + entry[:object] = the_class.new({:uuid => entry[:uuid].to_sym, :parent => (parent || self)}) + end + end + if entry[:object] + retain_list << entry[:object].uuid.to_sym + end + import_db << entry + entry[:content].each do |the_record| + link_db << the_record + end + else + raise ArgumentError, "Malformed import record." + end + end + # Set object internal data. + while import_db.size > 0 do + entry = import_db.shift + if entry + entry[:object].set_attributes(entry[:attributes]) + entry[:object].set_classes(entry[:classes]) + entry[:object].set_style(entry[:style]) + entry[:object].set_features(entry[:browsing]) + entry[:object].set_authoring_features(entry[:authoring]) + entry[:object].set_options(entry[:options]) + entry[:object].set_value(entry[:value]) + end + end + # Render new state information to the DOM. + post_event(:display) do + self.render_dom + end + # Eliminate objects NOT included in this key-frame importation. + old_list.each do |the_key| + unless retain_list.include?(the_key) + # unregister the_key object_by_uuid + old_object = GxG::object_by_uuid(the_key) + unless old_object.is_a?(::GxG::Gui::Display) + if GxG::unregister(the_key) && old_object + # de-activate object + old_object.deactivate() + end + end + end + end + result = true + end + result + end + # Server synchronization methods: + def transmit_keyframe() + keyframe = self.export_display() + if keyframe.is_a?(::Hash) + gxg_message = new_message + gxg_message[:sender] = @uuid + gxg_message[:to] = @remote_uuid + gxg_message[:body] = {:method => "import_display", :parameters => keyframe} + @channel.remote_call(gxg_message) + end + end + # + def transmit_update() + if @update_queue.size > 0 + gxg_message = new_message + gxg_message[:sender] = @uuid + gxg_message[:to] = @remote_uuid + gxg_message[:body] = {:method => "receive_update", :parameters => {:operation => "update", :records => @update_queue}} + @channel.remote_call(gxg_message) + @update_queue = [] + end + end + # + def transmit_event(the_event_frame=nil) + if the_event_frame.is_any?(::Hash, ::Array) + gxg_message = new_message + gxg_message[:sender] = @uuid + gxg_message[:to] = @remote_uuid + gxg_message[:body] = {:method => "receive_update", :parameters => {:operation => "pass_event", :events => the_event_frame}} + @channel.remote_call(gxg_message) + end + end + # + def receive_update(operation_frame={}) + if operation_frame.is_a?(::Hash) + case operation_frame[:operation] + when "update" + if operation_frame[:records].is_a?(::Array) + operation_frame[:records].each do |the_delta| + dom_sync = true + the_object = GxG::object_by_uuid(the_delta[:uuid].to_sym) + if the_object + if the_delta[:attributes].is_a?(::Hash) + the_object.set_attributes(the_delta[:attributes]) + end + if the_delta[:classes] + the_object.set_classes(the_delta[:classes]) + end + if the_delta[:style].is_a?(::Hash) + the_object.set_style(the_delta[:style]) + end + if the_delta[:browsing].is_a?(::Hash) + the_object.set_features(the_delta[:browsing]) + end + if the_delta[:authoring].is_a?(::Hash) + the_object.set_authoring_features(the_delta[:authoring]) + end + if the_delta[:options].is_a?(::Hash) + # Options only come FROM the server (One-Way) + if the_delta[:options][:visibility] + dom_sync = false + case the_delta[:options][:visibility] + when "show" + the_object.show() + when "hide" + the_object.hide() + when "toggle" + the_object.toggle_visibility() + end + else + the_object.set_options(the_delta[:options]) + end + end + if the_delta[:value] + the_object.set_value(the_delta[:value]) + end + if dom_sync == true + the_object.sync_to_dom() + end + else + puts "No Object for #{the_delta[:uuid]}" + end + end + end + when "remove" + if operation_frame[:operation][:manifest].is_a?(::Array) + operation_frame[:operation][:manifest].each do |the_uuid| + the_object = GxG::object_by_uuid(the_uuid.to_sym) + if the_object + the_object.deactivate() + GxG::unregister(the_uuid.to_sym) + end + end + end + end + end + end + # + def post_update(the_delta=nil) + if the_delta.is_a?(::Hash) + @update_queue << the_delta + end + end + # + def process_event(the_frame=nil) + if the_frame.is_a?(::Hash) + record = {:type => the_frame[:event].type.to_s, :details => {}} + # ??? + # puts "#{the_frame[:event].public_methods.sort.inspect}" + # common details + record[:details][:page_x] = the_frame[:event].page_x + record[:details][:page_y] = the_frame[:event].page_y + record[:details][:which] = the_frame[:event].which + record[:details][:meta_key] = the_frame[:event].meta_key + record[:details][:alt_key] = the_frame[:event].alt_key + record[:details][:shift_key] = the_frame[:event].shift_key + record[:details][:control_key] = the_frame[:event].ctrl_key + if the_frame[:data] + record[:details][:data] = the_frame[:data] + else + record[:details][:data] = nil + end + # type-specific details + the_frame[:event] = record + # + self.transmit_event(the_frame) + end + end + # State setting methods: + def get_attributes() + @attributes + end + # + def set_attributes(data={}) + @attributes = data + end + # + def get_style() + @style + end + # + def set_style(style_data={}) + @style = style_data + end + # + def get_value() + @value + end + # + def set_value(data=nil) + @value = data + end + # + def get_classes() + @classes + end + # + def set_classes(data="") + @classes = data + end + # + def get_features() + @browsing + end + # + def set_features(data={}) + @browsing = data + end + # + def get_authoring_features() + @authoring + end + # + def set_authoring_features(data={}) + @authoring = data + end + # + def get_options() + @options + end + # + def set_options(data={}) + @options = data + end + # + def generate_html() + accumulator = [] + contents = GxG::children_of(self) + contents.each do |the_object| + accumulator << the_object.generate_html() + end + accumulator.join("\n") + end + # + def render_dom(settings={}) + # FIX: Make non-recursive, iterative. + # Generate a complete replacement for the current DOM content. + # Assume NO dom_objects are internally available to the child objects. + # Call generate_html on each child + new_dom_html = self.generate_html() + ::Element.find('body').append(::Element::parse(new_dom_html)) + # call establish_dom_link on each object after DOM commit. + link_db = [(self)] + while link_db.size > 0 do + entry = link_db.shift + GxG::children_of(entry).each do |the_object| + the_object.activate() + link_db << the_object + end + end + # + end + # + def refresh_dom() + link_db = [(self)] + while link_db.size > 0 do + entry = link_db.shift + GxG::children_of(entry).each do |the_object| + the_object.sync_to_dom() + link_db << the_object + end + end + # + end + # + def switch_to_mode(the_mode=:browsing) + if the_mode.is_a?(String) + the_mode = the_mode.to_sym + end + if GxG::DISPLAY_DETAILS[:mode] == :authoring + if the_mode == :browsing + # switch back to browsing mode + GxG::DISPLAY_DETAILS[:mode] = :browsing + link_db = [(self)] + while link_db.size > 0 do + entry = link_db.shift + GxG::children_of(entry).each do |the_object| + the_object.sync_from_dom() + the_object.activate() + link_db << the_object + end + end + # TODO: update server gui-mode as well + end + else + if the_mode == :authoring + # switch to authoring mode + GxG::DISPLAY_DETAILS[:mode] = :authoring + link_db = [(self)] + while link_db.size > 0 do + entry = link_db.shift + GxG::children_of(entry).each do |the_object| + the_object.sync_from_dom() + the_object.activate() + link_db << the_object + end + end + # TODO: update server gui-mode as well + end + end + end + # + def activate() + # No-op for display object + end + # + def deactivate(settings={}) + # No-op for display object + end + end + # + class GuiComponent + # Abstract Class - only instantiate sub-classes. + def self.class_mapping() + { + :display => GxG::Gui::Display, + :background => GxG::Gui::Background, + :page => GxG::Gui::Page, + :window => GxG::Gui::Window, + :menubar => GxG::Gui::MenuBar, + :accordion => GxG::Gui::Accordion, + :accordion_section => GxG::Gui::AccordionSection, + :accordion_section_label => GxG::Gui::AccordionSectionLabel, + :accordion_section_content => GxG::Gui::AccordionSectionContent, + :anchor => GxG::Gui::Anchor, + :button => GxG::Gui::Button, + :division => GxG::Gui::Division, + :input_label => GxG::Gui::InputLabel, + :paragraph => GxG::Gui::Paragraph, + :text_lable => GxG::Gui::TextLabel, + :legend => GxG::Gui::Legend, + :list_item => GxG::Gui::ListItem, + :ordered_list => GxG::Gui::OrderedList, + :unordered_list => GxG::Gui::UnorderedList, + :control_group => GxG::Gui::ControlGroup, + :field_set => GxG::Gui::FieldSet, + :text_input => GxG::Gui::TextInput, + :date_picker => GxG::Gui::DatePicker, + :menu => GxG::Gui::Menu, + :select_menu => GxG::Gui::SelectMenu, + :select_menu_option => GxG::Gui::SelectMenuOption, + :slider => GxG::Gui::Slider, + :spinner => GxG::Gui::Spinner, + :progress_bar => GxG::Gui::ProgressBar, + :tabs => GxG::Gui::Tabs, + :tab_section => GxG::Gui::TabSection, + :table => GxG::Gui::Table, + :table_row => GxG::Gui::TableRow, + :table_header => GxG::Gui::TableHeader, + :table_data => GxG::Gui::TableData, + :image => GxG::Gui::Image + } + end + # + def self.key_to_class(the_key) + GxG::Gui::GuiComponent::class_mapping()[(the_key)] + end + # + def self.class_to_key(the_class) + result = nil + mapping = GxG::Gui::GuiComponent::class_mapping() + mapping.keys.each do |the_key| + if mapping[(the_key)] == the_class + result = the_key + break + end + end + result + end + # + def initialize(settings={}) + @uuid = (settings[:uuid] || GxG::uuid_generate) + @parent = settings[:parent] + # Class, Style, and Features are for cached for browse-mode only. + @base_tag = "div" + @attributes = {} + @classes = "" + @style = {} + @browsing = {:draggable => {}, :droppable => {}, :resizable => {}, :selectable => {}, :sortable => {}, :tool_tip => {}, :alt_text => ""} + @authoring = {:draggable => {}, :droppable => {}, :resizable => {}, :selectable => {}, :sortable => {}, :tool_tip => {}, :alt_text => ""} + @options = {} + @value = nil + @dom_object = nil + @visible = true + GxG::register(self,@parent) + end + # + def uuid() + @uuid + end + # + def dom_object() + @dom_object + end + # + def visible?() + @visible + end + # + def hide() + if @dom_object + @dom_object.hide() + @visible = false + end + end + # + def show() + if @dom_object + @dom_object.show() + @visible = true + end + end + # + def toggle_visibility() + if @dom_object + if @visible == true + self.hide() + else + self.show() + end + end + end + # + def establish_dom_link() + # + @dom_object = Element.find("##{@uuid.to_s}") + if @dom_object + # Set event traps on @dom_object + # ??? + @dom_object.on :dblclick do |event| + GxG::DISPLAY_DETAILS[:object].process_event({:uuid => @uuid, :event => event}) + end + @dom_object.on :mouseup do |event| + GxG::DISPLAY_DETAILS[:object].process_event({:uuid => @uuid, :event => event}) + end + @dom_object.on :mousedown do |event| + GxG::DISPLAY_DETAILS[:object].process_event({:uuid => @uuid, :event => event}) + end + # @dom_object.on :mouseenter do |event| + # GxG::DISPLAY_DETAILS[:object].process_event({:uuid => @uuid, :event => event}) + # end + # @dom_object.on :mouseleave do |event| + # GxG::DISPLAY_DETAILS[:object].process_event({:uuid => @uuid, :event => event}) + # end + end + true + end + # + def get_dom_features() + valid_draggable_options = {:add_classes => "addClasses", :append_to => "appendTo", :axis => "axis", :cancel => "cancel", :classes => "classes", :connect_to_sortable => "connectToSortable", :containment => "containment", :cursor => "cursor", :cursor_at => "cursorAt", :delay => "delay", :disabled => "disabled", :distance => "distance", :grid => "grid", :handle => "handle", :helper => "helper", :iframe_fix => "iframeFix", :opacity => "opacity", :refresh_positions => "refreshPositions", :revert => "revert", :revert_duration => "revertDuration", :scope => "scope", :scroll => "scroll", :scroll_sensitivity => "scrollSensitivity", :scroll_speed => "scrollSpeed", :snap => "snap", :snap_mode => "snapMode", :snap_tolerance => "snapTolerance", :stack => "stack", :z_index => "z-index"} + valid_droppable_options = {:accept => "accept", :active_class => "activeClass", :add_classes => "addClasses", :classes => "classes", :disabled => "disabled", :greedy => "greedy", :hover_class => "hoverClass", :scope => "scope", :tolerance => "tolerance"} + valid_resizable_options = {:also_resize => "alsoResize", :animate => "animate", :animate_duration => "animateDuration", :animate_easing => "animateEasing", :aspect_ration => "aspectRatio", :auto_hide => "autoHide", :cancel => "cancel", :classes => "classes", :containment => "containment", :delay => "delay", :disabled => "disabled", :distance => "distance", :ghost => "ghost", :grid => "grid", :handles => "handles", :helper => "helper", :max_height => "maxHeight", :max_width => "maxWidth"} + valid_selectable_options = {:append_to => "appendTo", :auto_refresh => "autoRefresh", :cancel => "cancel", :classes => "classes", :delay => "delay", :disabled => "disabled", :distance => "distance", :filter => "filter", :tolerance => "tolerance"} + valid_sortable_options = {:append_to => "appendTo", :axis => "axis", :cancel => "cancel", :classes => "classes", :connect_with => "connectWith", :cursor => "cursor", :cursor_at => "cursorAt", :delay => "delay", :disabled => "disabled", :distance => "distance", :drop_on_empty => "dropOnEmpty", :grid => "grid", :handle => "handle", :helper => "helper", :items => "items", :opacity => "opacity", :placeholder => "placeholder", :revert => "revert", :scroll => "scroll", :scroll_sensitivity => "scrollSensitivity", :scroll_speed => "scrollSpeed", :tolerance => "tolerance", :z_index => "z-index"} + # TODO: include tool_tip in process. + the_features = {:draggable => {}, :droppable => {}, :resizable => {}, :selectable => {}, :sortable => {}, :tool_tip => {}, :alt_text => ""} + if @dom_object + begin + valid_draggable_options.keys.each do |the_option_key| + unless [].include?(the_option_key) + the_features[:draggable][(the_option_key)] = @dom_object.draggable("option",(valid_draggable_options[(the_option_key)])) + end + end + rescue Exception => the_error + end + # + begin + valid_droppable_options.keys.each do |the_option_key| + unless [].include?(the_option_key) + the_features[:droppable][(the_option_key)] = @dom_object.droppable("option",(valid_droppable_options[(the_option_key)])) + end + end + rescue Exception => the_error + end + # + begin + valid_resizable_options.keys.each do |the_option_key| + unless [].include?(the_option_key) + the_features[:resizable][(the_option_key)] = @dom_object.resizable("option",(valid_resizable_options[(the_option_key)])) + end + end + rescue Exception => the_error + end + # + begin + valid_selectable_options.keys.each do |the_option_key| + unless [].include?(the_option_key) + the_features[:selectable][(the_option_key)] = @dom_object.selectable("option",(valid_selectable_options[(the_option_key)])) + end + end + rescue Exception => the_error + end + # + begin + valid_sortable_options.keys.each do |the_option_key| + unless [].include?(the_option_key) + the_features[:sortable][(the_option_key)] = @dom_object.sortable("option",(valid_sortable_options[(the_option_key)])) + end + end + rescue Exception => the_error + end + # + end + # + the_features + end + # + def set_dom_features(the_features=nil) + valid_draggable_options = {:add_classes => "addClasses", :append_to => "appendTo", :axis => "axis", :cancel => "cancel", :classes => "classes", :connect_to_sortable => "connectToSortable", :containment => "containment", :cursor => "cursor", :cursor_at => "cursorAt", :delay => "delay", :disabled => "disabled", :distance => "distance", :grid => "grid", :handle => "handle", :helper => "helper", :iframe_fix => "iframeFix", :opacity => "opacity", :refresh_positions => "refreshPositions", :revert => "revert", :revert_duration => "revertDuration", :scope => "scope", :scroll => "scroll", :scroll_sensitivity => "scrollSensitivity", :scroll_speed => "scrollSpeed", :snap => "snap", :snap_mode => "snapMode", :snap_tolerance => "snapTolerance", :stack => "stack", :z_index => "z-index"} + valid_droppable_options = {:accept => "accept", :active_class => "activeClass", :add_classes => "addClasses", :classes => "classes", :disabled => "disabled", :greedy => "greedy", :hover_class => "hoverClass", :scope => "scope", :tolerance => "tolerance"} + valid_resizable_options = {:also_resize => "alsoResize", :animate => "animate", :animate_duration => "animateDuration", :animate_easing => "animateEasing", :aspect_ration => "aspectRatio", :auto_hide => "autoHide", :cancel => "cancel", :classes => "classes", :containment => "containment", :delay => "delay", :disabled => "disabled", :distance => "distance", :ghost => "ghost", :grid => "grid", :handles => "handles", :helper => "helper", :max_height => "maxHeight", :max_width => "maxWidth"} + valid_selectable_options = {:append_to => "appendTo", :auto_refresh => "autoRefresh", :cancel => "cancel", :classes => "classes", :delay => "delay", :disabled => "disabled", :distance => "distance", :filter => "filter", :tolerance => "tolerance"} + valid_sortable_options = {:append_to => "appendTo", :axis => "axis", :cancel => "cancel", :classes => "classes", :connect_with => "connectWith", :cursor => "cursor", :cursor_at => "cursorAt", :delay => "delay", :disabled => "disabled", :distance => "distance", :drop_on_empty => "dropOnEmpty", :grid => "grid", :handle => "handle", :helper => "helper", :items => "items", :opacity => "opacity", :placeholder => "placeholder", :revert => "revert", :scroll => "scroll", :scroll_sensitivity => "scrollSensitivity", :scroll_speed => "scrollSpeed", :tolerance => "tolerance", :z_index => "z-index"} + # + unless the_features.is_a?(::Hash) + the_features = @browsing + end + if @dom_object + if the_features[:draggable][:enable] == true + @dom_object.draggable() + the_features[:draggable].keys.each do |the_option_key| + if valid_draggable_options.keys.include?(the_option_key) && the_option_key != :enable + @dom_object.draggable("option",(valid_draggable_options[(the_option_key)]),(the_features[:draggable][(the_option_key)])) + end + end + else + # @dom_object.draggable('disable') + end + if the_features[:droppable][:enable] == true + @dom_object.droppable() + the_features[:droppable].keys.each do |the_option_key| + if valid_droppable_options.keys.include?(the_option_key) && the_option_key != :enable + @dom_object.droppable("option",(valid_droppable_options[(the_option_key)]),(the_features[:droppable][(the_option_key)])) + end + end + else + # @dom_object.droppable('disable') + end + if the_features[:resizable][:enable] == true + @dom_object.resizable() + the_features[:resizable].keys.each do |the_option_key| + if valid_resizable_options.keys.include?(the_option_key) && the_option_key != :enable + @dom_object.resizable("option",(valid_resizable_options[(the_option_key)]),(the_features[:resizable][(the_option_key)])) + end + end + else + # @dom_object.resizable('disable') + end + if the_features[:selectable][:enable] == true + @dom_object.selectable() + the_features[:selectable].keys.each do |the_option_key| + if valid_selectable_options.keys.include?(the_option_key) && the_option_key != :enable + @dom_object.selectable("option",(valid_selectable_options[(the_option_key)]),(the_features[:selectable][(the_option_key)])) + end + end + else + # @dom_object.selectable('disable') + end + if the_features[:sortable][:enable] == true + @dom_object.sortable() + the_features[:sortable].keys.each do |the_option_key| + if valid_sortable_options.keys.include?(the_option_key) && the_option_key != :enable + @dom_object.sortable("option",(valid_sortable_options[(the_option_key)]),(the_features[:sortable][(the_option_key)])) + end + end + else + # @dom_object.sortable('disable') + end + end + # + true + end + # + def activate() + unless @dom_object + self.establish_dom_link + end + if @dom_object + # + unless self.is_any?(GxG::Gui::Background, GxG::Gui::Page, GxG::Gui::Window) + # unless in :authoring mode: + if GxG::DISPLAY_DETAILS[:mode] == :authoring + self.set_dom_features(@authoring) + else + self.set_dom_features(@browsing) + end + # TODO: figure out how to incorporate @browsing[:tool_tip] + end + # + end + # + end + # + def deactivate(settings={}) + if @dom_object + begin + @dom_object.remove + rescue Exception + end + @dom_object = nil + end + end + # + def components() + GxG::children_of(self) + end + # + def add_component(the_type=nil, settings={}) + result = nil + if the_type.is_any?(::String, ::Symbol) + # [:display, :background, :page, :window, :menubar, :accordion, :accordion_section, :accordion_section_label, :accordion_section_content, :anchor, :button, :division, :input_label, :paragraph, :text_lable, :legend, :list_item, :ordered_list, :unordered_list, :control_group, :field_set, :text_input, :date_picker, :menu, :select_menu, :select_menu_option, :slider, :spinner, :progress_bar, :tabs, :tab_section] + viable = [] + case GxG::Gui::GuiComponent::class_to_key(self.class) + when :background + viable = [:accordion, :anchor, :button, :division, :input_label, :paragraph, :text_lable, :legend, :ordered_list, :unordered_list, :control_group, :field_set, :text_input, :date_picker, :menu, :select_menu, :slider, :spinner, :progress_bar, :tabs, :table, :image] + when :page + viable = [:accordion, :anchor, :button, :division, :input_label, :paragraph, :text_lable, :legend, :ordered_list, :unordered_list, :control_group, :field_set, :text_input, :date_picker, :menu, :select_menu, :slider, :spinner, :progress_bar, :tabs, :table, :image] + when :menubar + viable = [:accordion, :anchor, :button, :division, :input_label, :paragraph, :text_lable, :legend, :ordered_list, :unordered_list, :control_group, :field_set, :text_input, :date_picker, :menu, :select_menu, :slider, :spinner, :progress_bar, :tabs, :table, :image] + when :window + viable = [:accordion, :anchor, :button, :division, :input_label, :paragraph, :text_lable, :legend, :ordered_list, :unordered_list, :control_group, :field_set, :text_input, :date_picker, :menu, :select_menu, :slider, :spinner, :progress_bar, :tabs, :table, :image] + when :accordion + viable = [:accordion_section] + when :accordion_section + viable = [:accordion_section_label, :accordion_section_content] + when :accordion_section_label + viable = [] + when :accordion_section_content + viable = [:accordion, :anchor, :button, :division, :input_label, :paragraph, :text_lable, :legend, :ordered_list, :unordered_list, :control_group, :field_set, :text_input, :date_picker, :menu, :select_menu, :slider, :spinner, :progress_bar, :tabs, :table, :image] + when :anchor + viable = [] + when :button + viable = [] + when :division + viable = [:accordion, :anchor, :button, :division, :input_label, :paragraph, :text_lable, :legend, :ordered_list, :unordered_list, :control_group, :field_set, :text_input, :date_picker, :menu, :select_menu, :slider, :spinner, :progress_bar, :tabs, :table, :image] + when :input_label + viable = [] + when :paragraph + viable = [] + when :text_label + viable = [] + when :legend + viable = [] + when :list_item + viable = [:accordion, :anchor, :button, :division, :input_label, :paragraph, :text_lable, :legend, :ordered_list, :unordered_list, :control_group, :field_set, :text_input, :date_picker, :menu, :select_menu, :slider, :spinner, :progress_bar, :tabs, :table, :image] + when :ordered_list + viable = [:list_item] + when :unordered_list + viable = [:list_item] + when :control_group + viable = [:anchor, :button, :input_label, :text_lable, :text_input, :date_picker, :menu, :select_menu, :slider, :spinner, :progress_bar] + when :field_set + viable = [:anchor, :button, :input_label, :text_lable, :text_input, :legend, :date_picker, :menu, :select_menu, :slider, :spinner, :progress_bar] + when :text_input + viable = [] + when :date_picker + viable = [] + when :menu + viable = [] + when :select_menu + viable = [:select_menu_option] + when :select_menu_option + viable = [] + when :slider + viable = [] + when :spinner + viable = [] + when :progress_bar + viable = [] + when :tabs + viable = [:tab_section] + when :tab_section + viable = [:accordion, :anchor, :button, :division, :input_label, :paragraph, :text_lable, :legend, :ordered_list, :unordered_list, :control_group, :field_set, :text_input, :date_picker, :menu, :select_menu, :slider, :spinner, :progress_bar, :tabs, :table, :image] + when :table + viable = [:table_row] + when :table_row + viable = [:table_header, :table_data] + when :image + viable = [] + end + # + if viable.include?(the_type.to_sym) + the_class = GxG::Gui::GuiComponent::key_to_class(the_type.to_sym) + if the_class + result = the_class.new(settings.merge({:parent => self})) + else + # err + end + else + # err + end + # + end + result + end + # + def receive_an_update(the_delta=nil) + if the_delta.is_a?(::Hash) + # TODO: fine-grained updates + if the_delta[:attributes].is_a?(::Hash) + self.set_attributes(the_delta[:attributes]) + end + if the_delta[:classes] + self.set_classes(the_delta[:classes]) + end + if the_delta[:style].is_a?(::Hash) + self.set_style(the_delta[:style]) + end + if the_delta[:browsing].is_a?(::Hash) + self.set_features(the_delta[:browsing]) + end + if the_delta[:authoring].is_a?(::Hash) + self.set_authoring_features(the_delta[:authoring]) + end + if the_delta[:options].is_a?(::Hash) + self.set_options(the_delta[:options]) + end + if the_delta[:value] + self.set_value(the_delta[:value]) + end + self.sync_to_dom() + # ??? + end + end + # + def get_attributes() + @attributes + end + # + def set_attributes(data={}) + @attributes = data + end + # + def get_style() + @style + end + # + def set_style(style_data={}) + @style = style_data + end + # + def get_value() + @value + end + # + def set_value(data=nil) + @value = data + end + # + def get_classes() + @classes + end + # + def set_classes(data=nil) + @classes = data.to_s + end + # + def get_features() + @browsing + end + # + def set_features(data={}) + @browsing = data + end + # + def get_authoring_features() + @authoring + end + # + def set_authoring_features(data={}) + @authoring = data + end + # + def get_options() + @options + end + # + def set_options(data={}) + # ??? + if data.is_a?(::Hash) + data.keys.each do |op_key| + if op_key == :visibility + case op_key + when "hide" + self.hide() + when "show" + self.show() + when "toggle" + self.toggle_visibility() + end + else + @options[(op_key.to_sym)] = data[(op_key)] + end + end + end + # + end + # Rendering to DOM + def generate_html() + # TODO: figure out where to incorporate @browsing[:alt_text] (string) into html tag output. (508 compliance) + if @classes.size > 0 + result = "<#{@base_tag.to_s} id='#{@uuid.to_s}' class='#{@classes.to_s}'" + else + result = "<#{@base_tag.to_s} id='#{@uuid.to_s}'" + end + # + if @attributes.size > 0 + @attributes.keys.each do |the_key| + result = result + " #{the_key.to_s}='#{@attributes[(the_key)].to_s}'" + end + end + # + if @style.keys.size > 0 + style_string = " style='" + else + style_string = "" + end + @style.keys.each do |the_key| + unless @style[(the_key)] == "" + # ??? + # the_property = GxG::Css::get_property_name(the_key) + the_property = (the_key.to_s.gsub("_","-")) + if the_property + style_string = style_string + " #{the_property.to_s}: #{@style[(the_key)].to_s};" + end + end + end + if style_string.size > 0 + result = (result + style_string + "'") + end + if ["button", "input", "li", "option", "progress", "param"].include?(@base_tag) + # value is in the main tag def. + if @attributes[:value] + result = result + ">" + else + if @value + result = (result + " value='" + @value.to_s + "'>") + else + result = result + ">" + end + end + else + # close the main tag def. + result = result + ">" + end + accumulator = [] + contents = GxG::children_of(self) + contents.each do |the_object| + accumulator << the_object.generate_html() + end + if (@attributes[:value] || self.respond_to?(:set_dom_value)) + result = (result + accumulator.join("\n") + "") + else + if ["button", "input", "li", "option", "progress", "param"].include?(@base_tag) + result = (result + accumulator.join("\n") + "") + else + # value is in-line text + if @value + result = (result + @value.to_s + accumulator.join("\n") + "") + else + result = (result + accumulator.join("\n") + "") + end + end + end + result + end + # + def sync_to_dom() + if @dom_object + @dom_object.attr("class",@classes) + @style.keys.each do |css_key| + the_name = ::GxG::Css::get_property_name(css_key) + if the_name + @dom_object.css(the_name.to_s,@style[(css_key)]) + end + end + @attributes.keys.each do |attr_key| + @dom_object.attr(attr_key.to_s,@attributes[(attr_key)]) + end + unless @attributes[:value] + unless @base_tag == "div" + # @dom_object.innerHTML = @value.to_s + @dom_object.html(@value.to_s).fadeIn(500) + end + end + # + if GxG::DISPLAY_DETAILS[:mode] == :authoring + self.set_dom_features(self.get_authoring_features()) + else + self.set_dom_features(self.get_features()) + end + # + end + true + end + # + def sync_from_dom() + # Produces a state delta record to send to the server, as well as sets state to match dom. + delta = nil + if @dom_object + delta = {} + the_class = @dom_object.attr("class") + if the_class != @classes + @classes = the_class + delta[:classes] = the_class + end + new_style = {} + ::GxG::Css::valid_properties.each do |prop_key| + the_name = ::GxG::Css::get_property_name(prop_key) + if the_name && @style[(prop_key)] + new_style[(prop_key)] = @dom_object.css(the_name.to_s) + end + end + if new_style.size > 0 + if new_style != @style + @style = new_style + delta[:style] = new_style + end + end + if @attributes.size > 0 + delta[:attributes] = {} + end + @attributes.keys.each do |attr_key| + unless attr_key.to_s == "class" || attr_key.to_s == "style" + the_attribute = @dom_object.attr(attr_key.to_s) + if @attributes[(attr_key)] != the_attribute + @attributes[(attr_key)] = the_attribute + delta[:attributes][(attr_key)] = the_attribute + end + end + end + unless @attributes[:value] + unless @base_tag == "div" + the_value = @dom_object.innerHTML + if @value != the_value + @value = the_value + delta[:value] = the_value + end + end + end + # + the_features = self.get_dom_features() + if GxG::DISPLAY_DETAILS[:mode] == :authoring + if @authoring != the_features + @authoring = the_features + delta[:authoring] = the_features + end + else + if @browsing != the_features + @browsing = the_features + delta[:browsing] = the_features + end + end + # + if delta.size > 0 + delta[:uuid] = @uuid + else + delta = nil + end + end + delta + end + # + end + # + class Background < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "div" + # z-index of 0 or 1 + self + end + # + end + # + class Page < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "div" + # z-index of 50 + self + end + # + end + # + class Window < GuiComponent + # + #Window Options: + # Extended Options: + #closable + # + #Type: Boolean + # + #Usage: enable/disable close button + # + #Default: true + #maximizable + # + #Type: Boolean + # + #Usage: enable/disable maximize button + # + #Default: false + #minimizable + # + #Type: Boolean + # + #Usage: enable/disable minimize button + # + #Default: false + #collapsable + # + #Type: Boolean + # + #Usage: enable/disable collapse button + # + #Default: false + #minimizeLocation + # + #Type: String + # + #Usage: sets alignment of minimized dialogues + # + #Default: 'left' + # + #Valid: 'left', 'right' + #dblclick + # + #Type: Boolean, String + # + #Usage: set action on double click + # + #Default: false + # + #Valid: false, 'maximize', 'minimize', 'collapse' + #titlebar + # + #Type: Boolean, String + # + #Default: false + # + #Valid: false, 'none', 'transparent' + #icons + # + #Type: Object + # + #Default: + # + #{ + # "close" : "ui-icon-circle-closethick", // new in v1.0.1 + # "maximize" : "ui-icon-extlink", + # "minimize" : "ui-icon-minus", + # "restore" : "ui-icon-newwin" + #} + # + #Valid: + # Basic Example: + # $("
This is content
") + # .dialog({ "title" : "My Dialog" }) + # .dialogExtend({ + # "maximizable" : true, + # "dblclick" : "maximize", + # "icons" : { "maximize" : "ui-icon-arrow-4-diag" } + # }); + # + # So: @dom_object.dialog(standard_options).dialogExtend(extended_options) + # + def initialize(settings) + super(settings) + @base_tag = "div" + # minimum z-index of 100, managed by jQuery + # `#{@dom_object}.dialog(#{JSON.generate(standard_options)}).dialogExtend(#{JSON.generate(extended_options)})` + self + end + # + def activate() + super() + if @dom_object + valid_standard_options = {:append_to => "appendTo", :auto_open => "autoOpen", :buttons => "buttons", :classes => "classes", + :close_on_escape => "closeOnEscape", :close_text => "closeText", :dialog_class => "dialogClass", :draggable => "draggable", + :height => "height", :hide => "hide", :max_height => "maxHeight", :max_width => "maxWidth", :min_height => "minHeight", + :min_width => "minWidth", :modal => "modal", :position => "position", :resizable => "resizable", :show => "show", :title => "title", + :width => "width"} + valid_extended_options = {:closable => "closable", :maximizable => "maximizable", :minimizable => "minimizable", + :collapsable => "collapsable", :minimize_location => "minimizeLocation", :double_click => "dblclick", :titlebar => "titlebar", + :icons => "icons"} + standard_options = {} + extended_options = {} + @options.keys.each do |the_option| + if valid_standard_options.keys.include?(the_option) + standard_options[(valid_standard_options[(the_option)])] = @options[(the_option)] + end + if valid_extended_options.keys.include?(the_option) + extended_options[(valid_extended_options[(the_option)])] = @options[(the_option)] + end + end + #`#{@dom_object}.dialog(#{JSON.generate(standard_options)}).dialogExtend(#{JSON.generate(extended_options)})` + if extended_options.size > 0 + @dom_object.dialog(standard_options).dialogExtend(extended_options) + else + @dom_object.dialog(standard_options) + end + end + end + # + def hide() + if @dom_object + @dom_object.dialog("hide") + @visible = false + end + end + # + def show() + if @dom_object + @dom_object.dialog("show") + @visible = true + end + end + # + end + # + class Accordion < GuiComponent + # + def initialize(settings) + super(settings) + # `#{@dom_object}.accordion();` + # has: Title, and Content (sections) + self + end + # + def activate() + unless @dom_object + self.establish_dom_link + end + if @dom_object + @dom_object.accordion(); + end + # super() + true + end + # + end + # + class AccordionSection < GuiComponent + # + def initialize(settings) + super(settings) + # has: AccordionSectionLabel, and AccordionSectionContent: (any) + # some compound-obj slight of hand here. + # + @banner = nil + @content = nil + self + end + # + def establish_dom_link() + unless @banner && @content + contents = GxG::children_of(self) + contents.each do |the_object| + if the_object.is_a?(GxG::Gui::AccordionSectionLabel) + @banner = the_object + end + if the_object.is_a?(GxG::Gui::AccordionSectionContent) + @content = the_object + end + if @banner && @content + break + end + end + end + true + end + # + def activate() + unless @banner && @content + self.establish_dom_link() + end + true + end + # + def deactivate() + true + end + # + def generate_html() + accumulator = [] + contents = GxG::children_of(self) + contents.each do |the_object| + accumulator << the_object.generate_html() + end + accumulator.join("\n") + end + # + def sync_to_dom() + # this is called on the banner and content elsewhere + true + end + # + def sync_from_dom() + # this is called on the banner and content elsewhere ? + nil + end + # + def hide() + # No-op + end + # + def show() + # No-op + end + # + end + # + class AccordionSectionLabel < GuiComponent + def initialize(settings) + super(settings) + @base_tag = "H3" + # text held in @value ? + self + end + # + def set_options(options={}) + @options = options + if options[:font_size].is_a?(Numeric) + # Font size in EM units only + case options[:font_size].to_f + when 2.0 + @base_tag = "H1" + when 1.5 + @base_tag = "H2" + when 1.17 + @base_tag = "H3" + when 1.0 + @base_tag = "H4" + when 0.83 + @base_tag = "H5" + when 0.67 + @base_tag = "H6" + else + @base_tag = "H3" + @style[:'font-size'] = "#{options[:font_size].to_f}em" + end + end + end + # + end + # + class AccordionSectionContent < GuiComponent + def initialize(settings) + super(settings) + self + end + # + end + # + class Anchor < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "a" + # a (anchor) tag + # has: href, and text value. + self + end + # + def generate_html() + super({:href => "#"}) + end + end + # + class Button < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "button" + # button tag + self + end + # + end + # + class RadioButton < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "input" + @attributes[:type] = "radio" + self + end + # + end + # + class CheckBoxButton < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "input" + @attributes[:type] = "checkbox" + self + end + # + end + # + class Division < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "div" + self + end + # + end + # + class InputLabel < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "label" + # label tag + # for=(a control uuid) + self + end + # + def set_options(options={}) + @options = options + if options[:font_size].is_a?(Numeric) + # Font size in EM units only + @style[:'font-size'] = "#{options[:font_size].to_f}em" + end + end + # + end + # + class Paragraph < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "p" + # p tag + self + end + # + end + # + class TextLabel < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "h1" + # h1 tag + self + end + # + def set_options(options={}) + @options = options + if options[:font_size].is_a?(Numeric) + # Font size in EM units only + @style[:'font-size'] = "#{options[:font_size].to_f}em" + end + end + # + end + # + class Legend < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "legend" + # legend tag + self + end + # + end + # + class ListItem < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "li" + # li tag + self + end + # + end + # + class OrderedList < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "ol" + # ol tag + self + end + # + end + # + class UnorderedList < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "ul" + # ul tag + self + end + # + end + # + class ControlGroup < GuiComponent + # See: http://jqueryui.com/controlgroup/ + def initialize(settings) + super(settings) + self + end + # + end + # + class FieldSet < GuiComponent + def initialize(settings) + super(settings) + @base_tag = "fieldset" + # fieldset tag + self + end + # + end + # + class TextInput < GuiComponent + # See: http://jqueryui.com/controlgroup/ + def initialize(settings) + super(settings) + @base_tag = "input" + @attributes[:type] = "text" + # input tag, type="text" + # For auto-complete: `#{@dom_object}.autocomplete({source: });` + self + end + # + def generate_html() + super({:type => "text"}) + end + # + end + # + class DatePicker < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "input" + # `#{@dom_object}.datepicker();` + # input tag, type="text" + self + end + # + def generate_html() + super({:type => "text"}) + end + # + def establish_dom_link() + super() + if @dom_object + @dom_object.datepicker() + end + true + end + end + # + class MenuBar < GuiComponent + # + def initialize(settings) + super(settings) + # sections have: title + # @record[:style][:z_index] = 99 + self + end + # + def establish_dom_link() + # No event linking for menubar - just a place holder object. + @dom_object = Element.find("##{@uuid.to_s}") + end + end + # + class Menu < GuiComponent + private + # + def menu_body_html(menu_hash=nil) + result = "" + # FIX: watch your stack depth ! + unless menu_hash + menu_hash = @options[:menu_definition] + end + if menu_hash.is_a?(::Hash) + menu_hash.keys.each do |choice| + if menu_hash[(choice)].is_a?(::Array) + result = (result + "
  • " + "#{choice.to_s}" + choice.to_s + "
  • ") + # + end + if menu_hash[(choice)].is_a?(::Hash) + result = (result + "
  • " + choice.to_s + "
      " + menu_body_html(menu_hash[(choice)]) + "
  • ") + # + end + if menu_hash[(choice)].is_any?(::String, ::Symbol) + result = (result + "
  • " + menu_hash[(choice)] + "
  • ") + end + # + end + end + # + result + end + # + public + # + def decode_choice(selection=nil,menu_hash=nil) + result = nil + # FIX: watch your stack depth ! + unless menu_hash + menu_hash = @options[:menu_definition] + end + if menu_hash.is_a?(::Hash) + menu_hash.keys.each do |choice| + if menu_hash[(choice)].is_a?(::Array) + if choice.to_s == selection + result = menu_hash[(choice)][1] + break + end + # + end + if menu_hash[(choice)].is_a?(::Hash) + result = decode_choice(selection,menu_hash[(choice)]) + if result + break + end + end + if menu_hash[(choice)].is_any?(::String, ::Symbol) + if choice.to_s == selection + result = menu_hash[(choice)] + break + end + end + end + end + result + end + # + def initialize(settings) + super(settings) + @base_tag = "ul" + # Needs custom generateHTML + # Contains Ordered and Unordered Lists: class='ui-state-disabled' to disable menu any menu item/sub-item. + # Idea: + # @options[:menu_definition] contains the menu definition. + # Structure: key is text label of choice, value is a) code associated, b) Array with image path to icon then code associated (later: 3rd item key-code). + # If Hash = sub-menu following same pattern + # trap menuselect to match string with @options[:menu_definition] hash/sub-hash + # ??? + self + end + # + def establish_dom_link() + @dom_object = Element.find("##{@uuid.to_s}") + true + end + # + def activate() + unless @dom_object + self.establish_dom_link + end + if @dom_object + # Set event traps on @dom_object + @dom_object.menu() + self.hide() + @dom_object.on :menuselect do |event,ui| + self.hide() + data = self.decode_choice(`#{ui}.item.text()`) + if data + GxG::DISPLAY_DETAILS[:object].process_event({:uuid => @uuid, :event => event, :data => data}) + end + end + end + true + end + # + def set_options(the_options={}) + if the_options.is_a?(::Hash) + the_options.keys.each do |op_key| + if op_key == :visibility + case op_key + when "hide" + self.hide() + when "show" + self.show() + when "toggle" + self.toggle_visibility() + end + else + # + if op_key == :menu_definition + if @options[:menu_definition].hash != the_options[:menu_definition].hash + @options[:menu_definition] = the_options[:menu_definition] + if @dom_object + @dom_object.html(menu_body_html()) + end + end + else + @options[(op_key.to_sym)] = the_options[(op_key)] + end + # + end + end + end + # + end + # + def generate_html() + if @classes.size > 0 + result = "<#{@base_tag.to_s} id='#{@uuid.to_s}' class='#{@classes.to_s}'" + else + result = "<#{@base_tag.to_s} id='#{@uuid.to_s}'" + end + # + if @attributes.size > 0 + @attributes.keys.each do |the_key| + result = result + " #{the_key.to_s}='#{@attributes[(the_key)].to_s}'" + end + end + # + if @style.keys.size > 0 + style_string = " style='" + else + style_string = "" + end + @style.keys.each do |the_key| + unless @style[(the_key)] == "" + #the_property = GxG::Css::get_property_name(the_key) + the_property = (the_key.to_s.gsub("_","-")) + if the_property + style_string = style_string + " #{the_property.to_s}: #{@style[(the_key)].to_s};" + end + end + end + if style_string.size > 0 + result = (result + style_string + "'") + end + result = (result + ">" + menu_body_html() + "") + result + end + # + end + # + class SelectMenu < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "select" + # select tag + # Contains: SelectMenuOption(s). + self + end + # + end + # + class SelectMenuOption < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "option" + # option tag + # to select the SelectMenuOption. + self + end + # + end + # + class Slider < GuiComponent + # + def initialize(settings) + super(settings) + # div tag + # `#{@dom_object}.slider();` + # Read Value: `#{@dom_object}.slider('value');` + # Write Value: `#{@dom_object}.slider('value', the_value);` + self + end + # + def activate() + unless @dom_object + self.establish_dom_link() + end + if @dom_object + @dom_object.slider() + end + end + # + end + # + class Spinner < GuiComponent + # + def initialize(settings) + super(settings) + @base_tag = "input" + # input tag + # `#{@dom_object}.spinner();` + # Read Value: `#{@dom_object}.spinner('value');` + # Write Value: `#{@dom_object}.spinner('value', the_value);` + self + end + # + def activate() + if @dom_object + @dom_object.spinner() + end + end + end + # + class ProgressBar < GuiComponent + # + def initialize(settings) + super(settings) + # uses div tag + # `#{@dom_object}.progressbar({value: 37});` + # Read Progress Percentage Value: `#{@dom_object}.progressbar('value');` + # Write Progress Percentage Value: `#{@dom_object}.progressbar('value', the_percentage);` + # Inner div tag for progress-label:
    Loading...
    + self + end + # + def activate() + unless @dom_object + self.establish_dom_link() + end + if @dom_object + @dom_object.progressbar() + end + end + # + end + # + class Tabs < GuiComponent + # + def initialize(settings) + super(settings) + # div tag + # `#{@dom_object}.tabs();` + #
    + # + #
    + #

    Proin elit arcu, rutrum commodo, vehicula tempus, commodo a, risus. Curabitur nec arcu. Donec sollicitudin mi sit amet mauris. Nam elementum quam ullamcorper ante. Etiam aliquet massa et lorem. Mauris dapibus lacus auctor risus. Aenean tempor ullamcorper leo. Vivamus sed magna quis ligula eleifend adipiscing. Duis orci. Aliquam sodales tortor vitae ipsum. Aliquam nulla. Duis aliquam molestie erat. Ut et mauris vel pede varius sollicitudin. Sed ut dolor nec orci tincidunt interdum. Phasellus ipsum. Nunc tristique tempus lectus.

    + #
    + #
    + #

    Morbi tincidunt, dui sit amet facilisis feugiat, odio metus gravida ante, ut pharetra massa metus id nunc. Duis scelerisque molestie turpis. Sed fringilla, massa eget luctus malesuada, metus eros molestie lectus, ut tempus eros massa ut dolor. Aenean aliquet fringilla sem. Suspendisse sed ligula in ligula suscipit aliquam. Praesent in eros vestibulum mi adipiscing adipiscing. Morbi facilisis. Curabitur ornare consequat nunc. Aenean vel metus. Ut posuere viverra nulla. Aliquam erat volutpat. Pellentesque convallis. Maecenas feugiat, tellus pellentesque pretium posuere, felis lorem euismod felis, eu ornare leo nisi vel felis. Mauris consectetur tortor et purus.

    + #
    + #
    + #

    Mauris eleifend est et turpis. Duis id erat. Suspendisse potenti. Aliquam vulputate, pede vel vehicula accumsan, mi neque rutrum erat, eu congue orci lorem eget lorem. Vestibulum non ante. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Fusce sodales. Quisque eu urna vel enim commodo pellentesque. Praesent eu risus hendrerit ligula tempus pretium. Curabitur lorem enim, pretium nec, feugiat nec, luctus a, lacus.

    + #

    Duis cursus. Maecenas ligula eros, blandit nec, pharetra at, semper at, magna. Nullam ac lacus. Nulla facilisi. Praesent viverra justo vitae neque. Praesent blandit adipiscing velit. Suspendisse potenti. Donec mattis, pede vel pharetra blandit, magna ligula faucibus eros, id euismod lacus dolor eget odio. Nam scelerisque. Donec non libero sed nulla mattis commodo. Ut sagittis. Donec nisi lectus, feugiat porttitor, tempor ac, tempor vitae, pede. Aenean vehicula velit eu tellus interdum rutrum. Maecenas commodo. Pellentesque nec elit. Fusce in lacus. Vivamus a libero vitae lectus hendrerit hendrerit.

    + #
    + #
    + self + end + # + def activate() + unless @dom_object + self.establish_dom_link() + end + if @dom_object + @dom_object.tabs() + end + end + # + def generate_html() + if @classes.size > 0 + result = "<#{@base_tag.to_s} id='#{@uuid.to_s}' class='#{@classes.to_s}'" + else + result = "<#{@base_tag.to_s} id='#{@uuid.to_s}'" + end + # + if @attributes.size > 0 + @attributes.keys.each do |the_key| + result = result + " #{the_key.to_s}='#{@attributes[(the_key)].to_s}'" + end + end + # + if @style.keys.size > 0 + style_string = " style='" + else + style_string = "" + end + @style.keys.each do |the_key| + unless @style[(the_key)] == "" + the_property = GxG::Css::get_property_name(the_key) + if the_property + style_string = style_string + " #{the_property.to_s}: #{@style[(the_key)].to_s};" + end + end + end + if style_string.size > 0 + result = (result + style_string + "'") + end + result = result + ">" + section_headers = [] + accumulator = [] + contents = GxG::children_of(self) + contents.each do |the_object| + section_headers << {:uuid => the_object.uuid.to_s, :title => the_object.get_options()[:title].to_s} + accumulator << the_object.generate_html() + end + result = result + "" + result = (result + accumulator.join("\n") + "") + # + result + end + # + end + # + class TabSection < GuiComponent + # has label (bound to content uuid) and content (any): options = {:title => "Tab Label Text"} + def hide() + # No-op + end + def show() + # No-op + end + end + # + class Table < GuiComponent + # ??? + def initialize(settings) + super(settings) + @base_tag = "table" + self + end + end + # + class TableRow < GuiComponent + def initialize(settings) + super(settings) + @base_tag = "tr" + self + end + end + # + class TableHeader < GuiComponent + def initialize(settings) + super(settings) + @base_tag = "th" + self + end + end + # + class TableData < GuiComponent + def initialize(settings) + super(settings) + @base_tag = "td" + self + end + end + # + class Image < GuiComponent + def initialize(settings) + super(settings) + @base_tag = "img" + # REQUIRES attributes src=ImageURL and alt=TEXT to be set + # set width and height in attributes, NOT style + self + end + # + def generate_html() + if @classes.size > 0 + result = "<#{@base_tag.to_s} id='#{@uuid.to_s}' class='#{@classes.to_s}'" + else + result = "<#{@base_tag.to_s} id='#{@uuid.to_s}'" + end + # + if @attributes.size > 0 + @attributes.keys.each do |the_key| + result = result + " #{the_key.to_s}='#{@attributes[(the_key)].to_s}'" + end + end + # + if @style.keys.size > 0 + style_string = " style='" + else + style_string = "" + end + @style.keys.each do |the_key| + unless @style[(the_key)] == "" + the_property = GxG::Css::get_property_name(the_key) + if the_property + style_string = style_string + " #{the_property.to_s}: #{@style[(the_key)].to_s};" + end + end + end + if style_string.size > 0 + result = (result + style_string + "'") + end + result = result + ">" + # NO end tag on img tag + result + end + # + end + # + class ToolTip < GuiComponent + # + def initialize(settings) + super(settings) + # This should be a modifier method (feature)- not a component. + # uses *any* tag + # `#{any_dom_object}.tooltip();` + # Set the title attribute on the tag to allow display of the tooltip. + self + end + # + end + # + end +end diff --git a/gxg/web/gxg_iac.rb b/gxg/web/gxg_iac.rb new file mode 100644 index 0000000..3c78ddc --- /dev/null +++ b/gxg/web/gxg_iac.rb @@ -0,0 +1,483 @@ +# +module GxG + module Networking + class Channel + # + def initialize(the_connector=nil, settings={}) + unless the_connector.is_a?(GxG::Networking::Connector) + raise ArgumentError, "You MUST provide a GxG::Networking::Connector to associate with." + end + @connector = the_connector + @uuid = (settings[:uuid] || GxG::uuid_generate().to_sym) + @remote_uuid = settings[:remote_uuid] + @remote_interface = [] + @mounted_object = nil + @pending = {} + # messages from server + @buffer = [] + # messages from objects + @inbox = [] + # deferred messages to send (to object or server) ??? + @outbox = [] + # + @timer = post_event_periodic(0.333) do + self.process_messages + end + # + post_event(:communications) do + gxg_message = new_message + gxg_message[:sender] = @uuid + gxg_message[:to] = @remote_uuid + gxg_message[:body] = {:operation => "fetch_interface"} + @connector.transmit(gxg_message) + end + # + self + end + # + def closed?() + if @timer.is_a?(Hash) + false + else + true + end + end + # + def close() + result = false + if @timer.is_a?(Hash) + cancel_periodic(@timer) + @timer = nil + end + # send out close_channel operation to partner on server, set result = true + result + end + # + def uuid() + @uuid + end + # + def remote_uuid() + @remote_uuid + end + # + def mounted() + @mounted_object + end + # + def mount(something=nil) + @mounted_object = something + end + # + def process_messages() + # process @buffer items, add to @outbox + server_message = @buffer.shift + if server_message[:body].is_a?(Hash) + # puts "Channel Processing: #{server_message.inspect}" + # puts "Got channel message: #{server_message.inspect}" + if server_message[:body][:operation] + case server_message[:body][:operation] + when "define_interface" + if server_message[:body][:interface].is_a?(Array) + @remote_interface = [] + server_message[:body][:interface].each do |entry| + @remote_interface << entry.to_s.to_sym + end + end + if @mounted_object + @mounted_object.set_remote_uuid(server_message[:body][:remote_uuid].to_s.to_sym) + end + when "fetch_interface" + if @mounted_object + method_list = [] + @mounted_object.public_methods.each do |entry| + method_list << entry.to_s + end + gxg_message = new_message + gxg_message[:sender] = @uuid + gxg_message[:to] = server_message[:sender] + gxg_message[:body] = {:operation => "define_interface", :remote_uuid => @mounted_object.uuid.to_s, :interface => method_list} + @connector.transmit(gxg_message) + end + when "mount_object" + # a) by registry uuid, b) create with params, c) ? + # GxG::OBJECT_SPACE[:registry][(the_uuid)] + # GxG::object_by_uuid(the_uuid) + # puts "Attempting to mount object" + if server_message[:body][:uuid] + @mounted_object = GxG::object_by_uuid(server_message[:body][:uuid].to_s.to_sym) + if @mounted_object + if server_message[:body][:remote_uuid] + @mounted_object.set_remote_uuid(server_message[:body][:remote_uuid].to_s.to_sym) + end + end + end + # ??? + if server_message[:body][:type] + case server_message[:body][:type].to_s + when "GxG::Gui::Display" + @mounted_object = nil + # puts "Creating Display ..." + if server_message[:body][:parameters] + if server_message[:body][:parameters].is_a?(Array) + @mounted_object = ::GxG::Gui::Display.new(*server_message[:body][:parameters]) + else + @mounted_object = ::GxG::Gui::Display.new(server_message[:body][:parameters]) + end + else + @mounted_object = ::GxG::Gui::Display.new() + end + # BUG - FIX - Never gets to this line of code: + # puts "Mounted: #{@mounted_object.inspect} with Parameters: #{server_message[:body][:parameters].inspect}" + else + log_error({:error => (Exception.new("Unrecognized object type")), :parameters => {:message => server_message}}) + end + # + end + # + if @mounted_object != nil + # + # puts "Object MOUNTED." + @mounted_object.set_channel(self) + method_list = [] + @mounted_object.public_methods.each do |entry| + method_list << entry.to_s + end + gxg_message = new_message + gxg_message[:sender] = @uuid + gxg_message[:to] = server_message[:sender] + gxg_message[:body] = {:operation => "define_interface", :remote_uuid => @mounted_object.uuid.to_s, :interface => method_list} + @connector.transmit(gxg_message) + else + log_error({:error => (Exception.new("Failed to mount object")), :parameters => {:mesaage => server_message}}) + end + # + end + else + # RPC message + # Layer 3 + rpc_message = new_message + server_message[:body].keys.each do |rpc_key| + rpc_message[(rpc_key)] = server_message[:body][(rpc_key)] + end + if rpc_message[:body][:response] + case rpc_message[:body][:response] + when "success" + responder = @pending.delete(rpc_message.id().to_s.to_sym) + if responder + if responder[:success].respond_to?(:call) + responder[:success].call(rpc_message) + end + end + when "fail" + responder = @pending.delete(rpc_message.id().to_s.to_sym) + if responder + if responder[:fail].respond_to?(:call) + responder[:fail].call(rpc_message) + end + end + end + else + if @mounted_object + # {:method => :nil, :parameters => []/{}/etc} + if rpc_message[:body][:method] + if @mounted_object.public_methods.include?(rpc_message[:body][:method].to_s.to_sym) + begin + if rpc_message[:body][:parameters] + if rpc_message[:body][:parameters].is_a?(Array) + result = @mounted_object.send(rpc_message[:body][:method].to_s.to_sym, *rpc_message[:body][:parameters]) + else + result = @mounted_object.send(rpc_message[:body][:method].to_s.to_sym, rpc_message[:body][:parameters]) + end + else + result = @mounted_object.send(rpc_message[:body][:method].to_s.to_sym) + end + # result success back. + gxg_message = new_message + gxg_message[:sender] = @uuid + gxg_message[:to] = server_message[:sender] + gxg_message[:body] = {:sender => rpc_message[:to], :to => rpc_message[:sender], :id => rpc_message[:id], :subject => rpc_message[:subject], :body => {:response => "success", :result => result}} + @connector.transmit(gxg_message) + # + rescue Exception => the_error + gxg_message = new_message + gxg_message[:sender] = @uuid + gxg_message[:to] = server_message[:sender] + gxg_message[:body] = {:sender => rpc_message[:to], :to => rpc_message[:sender], :id => rpc_message[:id], :subject => rpc_message[:subject], :body => {:response => "fail", :error => the_error}} + @connector.transmit(gxg_message) + # + end + else + gxg_message = new_message + gxg_message[:sender] = @uuid + gxg_message[:to] = server_message[:sender] + gxg_message[:body] = {:sender => rpc_message[:to], :to => rpc_message[:sender], :id => rpc_message[:id], :subject => rpc_message[:subject], :body => {:response => "fail", :error => "remote method missing"}} + @connector.transmit(gxg_message) + # + end + end + else + gxg_message = new_message + gxg_message[:sender] = @uuid + gxg_message[:to] = server_message[:sender] + gxg_message[:body] = {:sender => rpc_message[:to], :to => rpc_message[:sender], :id => rpc_message[:id], :subject => rpc_message[:subject], :body => {:response => "fail", :error => "no mount available"}} + @connector.transmit(gxg_message) + # + end + end + # + end + end + # process next message in @inbox + end + # + def in_buffer(the_message) + @buffer << the_message + end + # + def transmit(the_message) + if the_message.is_a?(GxG::Events::Message) + @connector.transmit(the_message) + end + # + end + # + def remote_call(the_message) + # Layer 3 + if @remote_interface.size > 0 + if (the_message[:success].respond_to?(:call)) || (the_message[:fail].respond_to?(:call)) + @pending[(the_message.id().to_s.to_sym)] = the_message + end + rpc_message = {} + the_message.keys.each do |the_key| + unless [:success, :fail].include?(the_key.to_sym) + rpc_message[(the_key)] = the_message[(the_key)] + end + end + gxg_message = new_message + gxg_message[:sender] = @uuid + gxg_message[:to] = @remote_uuid + gxg_message[:body] = rpc_message + @connector.transmit(gxg_message) + else + error_message = "No Remote Interface Loaded." + @pending.delete(the_message.id().to_s.to_sym) + if (the_message[:fail].respond_to?(:call)) + the_message[:fail].call({:response => "fail", :error => error_message}) + else + raise Exception, error_message + end + end + end + # + def remote_interface() + @remote_interface + end + # + end + # + class Connector + # + def transmit(the_message) + # Fully qualified GxG::Events::Message with all the data needed to send it out. + # Packet Frame Format: {:id => , :routing => , :index => , :total => , :subject => , :payload => } + if @active == true + begin + while @out_busy == true do + sleep 0.333 + end + @out_busy = true + if the_message.is_a?(Hash) + if the_message[:operation] + @socket << the_message.to_json.encode64 + end + else + unless the_message.is_a?(GxG::Events::Message) + raise ArgumentError, "You MUST provide a valid GxG::Events::Message message object." + end + if the_message[:body].is_a?(String) + if the_message[:body].base64? + payload = the_message[:body] + else + payload = the_message[:body].encode64 + end + else + if the_message[:body].is_any?(Hash,Array) + payload = the_message[:body].to_json.encode64 + else + raise ArgumentError, "You MUST supply a message body that is a Hash, an Array, or a Base64 String." + end + end + if the_message[:routing].is_a?(Array) + packet = {:id => the_message[:id], :routing => the_message[:routing], :index => 0, :total => 10000, :subject => the_message[:subject], :payload => ""} + else + packet = {:id => the_message[:id], :routing => [(the_message[:sender]), (the_message[:to])], :index => 0, :total => 10000, :subject => the_message[:subject], :payload => ""} + end + overhead = packet.to_json.size + ranges = GxG.apportioned_ranges(payload.size,(65536 - overhead)) + packet[:total] = ranges.size + ranges.each_with_index do |the_range,indexer| + packet[:index] = indexer + packet[:payload] = payload[(the_range)] + @socket << packet.to_json.encode64 + end + end + @out_busy = false + true + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:message => the_message}}) + @out_busy = false + false + end + end + end + # + def receive(data) + # Data will ALWAYS be: Base64 JSON + # Packet Frame Format: {:id => , :routing => , :index => , :total => , :subject => , :payload => } + # On Mulit-Packet transfers: :id, :routing, :total, :subject are the same on each packet - only :index and :payload differ. + # Routing: first=sender, last=recipient; Connectors are invisible and don't have an entry in the routing table. Channels operate as Pairs. + # :index (which packet of :total set)? 0-offset (Marshalling) + # recipient Channel is found and sent the completed data set when all packets are in. Further processing will happen in the Channel object. + begin + while @in_busy == true do + sleep 0.333 + end + @in_busy = true + packet = JSON.parse(data.decode64,{:symbolize_names => true}) + unless packet.is_a?(Hash) + raise Exception, "Malformed Packet received." + end + # puts "Message Received: #{packet.inspect}" + if packet[:operation] + # Connector Operation + case packet[:operation] + when "introduction" + @remote_uuid = packet[:uuid] + when "heartbeat_query" + self.transmit({:operation => "heartbeat_acknowledge"}) + when "create_channel" + unless @channels[(packet[:uuid])] + @channels[(packet[:uuid])] = GxG::Networking::Channel.new(self,{:uuid => packet[:uuid], :remote_uuid => packet[:remote_uuid]}) + end + end + else + # Data packet + unless (packet[:id] && packet[:routing] && packet[:index] && packet[:total] && packet[:payload]) + raise Exception, "Malformed Packet received." + end + # puts "Routing: #{packet[:routing].inspect}" + sender = packet[:routing].first.to_sym + recipient = packet[:routing].last.to_sym + if packet[:total] > 1 + # Multi-part Packet Set + unless @buffers[(recipient)].is_a?(Array) + @buffers[(recipient)] = [] + packet[:total].times do + @buffers[(recipient)] << nil + end + end + @buffers[(recipient)][(packet[:index])] = packet + complete = true + @buffers[(recipient)].each_index do |indexer| + unless @buffers[(recipient)][(indexer)] + complete = false + break + end + end + if complete == true + buffer = "" + @buffers[(recipient)].each_index do |indexer| + buffer = (buffer + @buffers[(recipient)][(indexer)][:payload]) + end + if buffer.base64? + begin + buffer = ::JSON.parse(buffer.decode64,{:symbolize_names => true}) + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:payload => buffer}}) + end + end + the_message = new_message + the_message[:sender] = sender + the_message[:to] = recipient + the_message[:id] = packet[:id] + the_message[:subject] = packet[:subject] + the_message[:body] = buffer + unless @channels[(recipient)] + @channels[(recipient)] = GxG::Networking::Channel.new(self,{:uuid => recipient, :remote_uuid => sender}) + end + @channels[(recipient)].in_buffer(the_message) + @buffers.delete(recipient) + end + else + # Single Packet + if packet[:payload].is_a?(String) + if packet[:payload].base64? + begin + packet[:payload] = ::JSON.parse(packet[:payload].decode64,{:symbolize_names => true}) + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:payload => packet[:payload]}}) + end + end + end + the_message = new_message + the_message[:sender] = sender + the_message[:to] = recipient + the_message[:id] = packet[:id] + the_message[:subject] = packet[:subject] + the_message[:body] = packet[:payload] + unless @channels[(recipient)] + @channels[(recipient)] = GxG::Networking::Channel.new(self,{:uuid => recipient, :remote_uuid => sender}) + end + @channels[(recipient)].in_buffer(the_message) + end + end + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:data => data}}) + end + @in_busy = false + true + end + # + def close() + self.transmit({:operation => "disconnect"}) + @socket.close(1000) + @active = false + end + # Channel supports: + def channels() + @channels + end + # + def initialize(the_url=nil) + unless the_url + raise ArgumentError, "You MUST provide a URL String to contact a host." + end + @uuid = GxG.uuid_generate().to_sym + @remote_uuid = nil + @active = false + @in_busy = false + @out_busy = false + @pending = {} + @channels = {} + @buffers = {} + @the_connector = self + @socket = Browser::Socket.new(the_url) do |the_socket| + the_socket.on :open do + @active = true + @the_connector.transmit({:operation => "introduction", :uuid => @uuid.to_s, :location => `window.location["href"]`}) + end + the_socket.on :close do + @active = false + end + the_socket.on :message do |the_event| + @the_connector.receive(the_event.data) + end + end + # Set trap for page closure / refresh / unload / etc + `jQuery(window).bind("beforeunload", function() { Opal.eval("GxG::CONNECTION.close"); });` + self + end + end + end +end diff --git a/gxg/web/gxg_layout.rb b/gxg/web/gxg_layout.rb new file mode 100644 index 0000000..4fcbc4b --- /dev/null +++ b/gxg/web/gxg_layout.rb @@ -0,0 +1,140 @@ +module GxG + module Gui + # + # Layout System Supports: + def self.layout_refresh() + # get rectangle of item -> convert to page % offsets + # `window.innerHeight`.to_i + # `window.innerWidth`.to_i + # .getClientRects()[0].left + page_width = `window.innerWidth`.to_i + # Calculating proper page height, See: https://stackoverflow.com/questions/1145850/how-to-get-height-of-entire-document-with-javascript + body = `document.body` + html = `document.documentElement` + page_bottom = 0 + %x{ + page_bottom = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); + } + page_height = [(`window.innerHeight`.to_i),(page_bottom.to_i)].max + # bounding areas: + top_bound = 0.0 + left_bound = 0.0 + right_bound = 1.0 + bottom_bound = 1.0 + # Update object percentages + [:"top-left", :top, :"top-right", :right, :"bottom-right", :bottom, :"bottom-left", :left].each do |the_position| + ::GxG::LAYOUT[(the_position)].each_pair do |the_uuid,the_record| + object = self.find_object(the_uuid) + if object + rectangle = {:top => 0, :left => 0, :right => 0, :bottom => 0} + rectangle[:top] = `#{object.element}.getBoundingClientRect().top`.to_i + rectangle[:left] = `#{object.element}.getBoundingClientRect().left`.to_i + rectangle[:right] = `#{object.element}.getBoundingClientRect().right`.to_i + rectangle[:bottom] = `#{object.element}.getBoundingClientRect().bottom`.to_i + percentages = {:top => 0.0, :left => 0.0, :right => 0.0, :bottom => 0.0} + percentages[:top] = (rectangle[:top].to_f / page_height.to_f) + percentages[:left] = (rectangle[:left].to_f / page_width.to_f) + percentages[:right] = (rectangle[:right].to_f / page_width.to_f) + percentages[:bottom] = (rectangle[:bottom].to_f / page_height.to_f) + ::GxG::LAYOUT[(the_position.to_s.to_sym)][(the_uuid.to_s.to_sym)] = percentages + end + end + end + # + [:"top-left", :top, :"top-right", :right, :"bottom-right", :bottom, :"bottom-left", :left].each do |the_position| + ::GxG::LAYOUT[(the_position)].values.each do |the_record| + case the_position + when :top + if the_record[:bottom] > top_bound + top_bound = the_record[:bottom] + end + when :left + if the_record[:right] > left_bound + left_bound = the_record[:right] + end + when :right + if the_record[:left] < right_bound + right_bound = the_record[:left] + end + when :bottom + if the_record[:top] < bottom_bound + bottom_bound = the_record[:top] + end + when :"top-left" + if the_record[:bottom] > top_bound + top_bound = the_record[:bottom] + end + if the_record[:right] > left_bound + left_bound = the_record[:right] + end + when :"top-right" + if the_record[:bottom] > top_bound + top_bound = the_record[:bottom] + end + if the_record[:left] < right_bound + right_bound = the_record[:left] + end + when :"bottom-right" + if the_record[:top] < bottom_bound + bottom_bound = the_record[:top] + end + if the_record[:left] < right_bound + right_bound = the_record[:left] + end + when :"bottom-left" + if the_record[:top] < bottom_bound + bottom_bound = the_record[:top] + end + if the_record[:right] > left_bound + left_bound = the_record[:right] + end + end + end + end + # + ::GxG::LAYOUT_LIMITS[:page_width] = page_width.to_i + ::GxG::LAYOUT_LIMITS[:page_height] = page_height.to_i + ::GxG::LAYOUT_LIMITS[:top] = (top_bound * page_height.to_f).to_i + ::GxG::LAYOUT_LIMITS[:top_percent] = top_bound * 100.0 + ::GxG::LAYOUT_LIMITS[:left] = (left_bound * page_width.to_f).to_i + ::GxG::LAYOUT_LIMITS[:left_percent] = left_bound * 100.0 + ::GxG::LAYOUT_LIMITS[:right] = (right_bound * page_width.to_f).to_i + ::GxG::LAYOUT_LIMITS[:right_percent] = right_bound * 100.0 + ::GxG::LAYOUT_LIMITS[:bottom] = (bottom_bound * page_height.to_f).to_i + ::GxG::LAYOUT_LIMITS[:bottom_percent] = bottom_bound * 100.0 + true + end + # + def self.layout_add_item(the_uuid=nil, the_position=nil, refresh=true) + # store by uuid in position's record + if [:"top-left", :top, :"top-right", :right, :"bottom-right", :bottom, :"bottom-left", :left].include?(the_position.to_s.to_sym) + # + ::GxG::LAYOUT[(the_position.to_s.to_sym)][(the_uuid.to_s.to_sym)] = {:top => 0.0, :left => 0.0, :right => 0.0, :bottom => 0.0} + if refresh + ::GxG::Gui::layout_refresh + end + # + true + else + false + end + end + # + def self.layout_remove_item(the_uuid=nil) + [:"top-left", :top, :"top-right", :right, :"bottom-right", :bottom, :"bottom-left", :left].each do |the_position| + if ::GxG::LAYOUT[(the_position.to_s.to_sym)][(the_uuid.to_s.to_sym)] + ::GxG::LAYOUT[(the_position.to_s.to_sym)].delete((the_uuid.to_s.to_sym)) + break + end + end + ::GxG::Gui::layout_refresh + # + true + end + # + def layout_content_area() + ::GxG::LAYOUT_LIMITS + end + # + end +end \ No newline at end of file diff --git a/gxg/web/gxg_libraries.rb b/gxg/web/gxg_libraries.rb new file mode 100644 index 0000000..00cdd0b --- /dev/null +++ b/gxg/web/gxg_libraries.rb @@ -0,0 +1,207 @@ + +# ---------------------------------------------------------------------------------------------------- +# Provided Module Space for libraries to declare their :global within. +module Libraries +end +# ---------------------------------------------------------------------------------------------------- +module GxG + class Library + # + def initialize(the_definition=nil) + unless the_definition.is_a?(GxG::Database::PersistedHash) + raise ArgumentError, "You MUST provide a persisted source object." + end + # + @uuid = the_definition.uuid + @title = the_definition.title + @version = the_definition.version + @type = (the_definition[:type] || "text/javascript").to_s + # + load_list = [] + if the_definition[:requirements].is_a?(::GxG::Database::PersistedArray) + the_definition[:requirements].each do |the_requirement| + unless GxG::DISPLAY_DETAILS[:object].library_loaded?(the_requirement) + load_list << the_requirement + end + end + end + GxG::DISPLAY_DETAILS[:object].load_libraries(load_list) do |all_loaded| + if all_loaded == true + # + @src = the_definition[:options][:src].to_s + unless @src.size > 0 + @src = nil + end + @charset = (the_definition[:options][:charset] || "utf-8").to_s + @local = the_definition[:options][:local] + unless @local + @local = false + end + @global = the_definition[:options][:global].to_s + unless @global.size > 0 + @global = nil + end + # Load native code portion if supplied. + # If the global is already defined, your wrapper code will still work. + unless self.loaded?() + unless @global.to_s.size > 0 + if @type = "text/javascript" + @global = "Opal" + else + @global = "GxG" + end + end + if @src.is_a?(::String) + the_uuid = @uuid + the_type = @type + the_charset = @charset + the_src = @src + %x{ + var the_element = document.createElement('script'); + the_element.id = the_uuid; + the_element.type = the_type; + the_element.charset = the_charset; + the_element.src = the_src; + document.body.appendChild(the_element); + } + else + the_script = the_definition[:options][:native].to_s + if the_script.is_a?(::String) + if the_script.size > 0 + if the_script.base64? + the_script = the_script.to_s.decode64 + end + the_uuid = @uuid + the_type = @type + the_charset = @charset + %x{ + var the_element = document.createElement('script'); + the_element.id = the_uuid; + the_element.type = the_type; + the_element.charset = the_charset; + the_element.innerHTML = the_script; + document.body.appendChild(the_element); + } + end + end + end + end + @source = the_definition + self.set_script(@source[:script].to_s) + @source = nil + else + raise Exception, "Library load error - skipping library intitialization: #{@title}" + end + end + # + self + end + # + def uuid + @uuid + end + # + def title + @title + end + # + def version() + @version + end + # + def type() + @type + end + # + def local() + @local + end + # + def global() + @global + end + # + def set_script(the_script_body="") + result = false + if the_script_body.size > 0 + # + begin + # + if the_script_body.base64? + the_script_body = the_script_body.decode64 + end + # + if @local == true + instance_eval(the_script_body) + else + eval(the_script_body) + end + result = true + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:script => the_script_body}}) + end + end + result + end + # + def require_resource(the_reference=nil) + # call self.require_resource("script-name") to load content.script-name + result = false + if @source + # content source record format: {:component => "script", :type => "text/ruby", :script => ""} + @source[:content].each do |the_script| + if (the_script.title == the_reference || the_script.uuid == the_refrence.to_s.to_sym) && the_script[:component].to_s == "script" && the_script[:type].to_s == "text/ruby" + self.set_script(the_script[:script].to_s) + result = true + break + end + end + end + result + end + # + def require_module_resource(the_reference=nil) + # call self.require_module_resource("script-name") to load content.script-name + result = false + if @source + # content source record format: {:component => "script", :type => "text/ruby", :script => ""} + @source[:content].each do |the_script| + if (the_script.title == the_reference || the_script.uuid == the_refrence.to_s.to_sym) && the_script[:component].to_s == "script" && the_script[:type].to_s == "text/ruby" + if the_script[:script].size > 0 + if the_script[:script].base64? + eval(the_script[:script].to_s.decode64) + else + eval(the_script[:script].to_s) + end + result = true + break + end + end + end + end + result + end + # + def loaded? + # Opal/JS Note: by saying `!!` it will coerse it into a ruby boolean. + if @type == "text/javascript" + if (`(window.hasOwnProperty(#{@global.to_s}))`) + true + else + false + end + else + if @global + if (eval("defined? #{@global.to_s}")) + true + else + false + end + else + false + end + end + end + end + # +end \ No newline at end of file diff --git a/gxg/web/gxg_pre_foundation.rb b/gxg/web/gxg_pre_foundation.rb new file mode 100644 index 0000000..652c9af --- /dev/null +++ b/gxg/web/gxg_pre_foundation.rb @@ -0,0 +1,1111 @@ +module GxG + module Gui + # ### Core Component Classes & Registration System + class Abbreviation < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :abbr + @options[:title] = @title.clone.to_s + end + end + # + class Address < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :address + end + end + # + class Area < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :area + end + end + # + class Bold < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :b + end + end + # + class Bdi < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :bdi + end + end + # + class Bdo < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :bdo + end + end + # + class BlockQuote < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :blockquote + end + end + # + class Break < GxG::Gui::Vdom::BaseElement + # Review has no closing tag - account for this in Factory + def _before_create + super() + @domtype = :br + end + end + # + class Caption < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :caption + end + end + # + class Cite < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :cite + end + end + # + class Code < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :code + end + end + # + class Column < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :col + end + end + # + class ColumnGroup < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :colgroup + end + end + # + class Data < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :data + end + end + # + class DataList < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :datalist + end + end + # + class Description < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :dd + end + end + # + class Deleted < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :del + end + end + # + class Details < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :details + end + end + # + class Dfn < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :dfn + end + end + # + class Dialog < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :dialog + end + end + # + class Division < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + end + end + # + class DescriptionList < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :dl + end + end + # + class DescriptionTerm < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :dt + end + end + # + class Emphasis < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :em + end + end + # + class Embed < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :embed + end + end + # + class Figure < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :figure + end + end + # + class FigureCaption < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :figcaption + end + end + # + class ThemeDivider < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :hr + end + end + # + class Italic < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :i + end + end + # + class InlineFrame < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :iframe + end + end + # + class Insertion < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ins + end + end + # + class KeyboardKey < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :kbd + end + end + # + class Map < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :map + end + end + # + class Mark < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :mark + end + end + # + class Meter < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :meter + end + end + # + class NoScript < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :noscript + end + end + # + class Object < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :object + end + end + # + class OptionGroup < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :optgroup + end + end + # + class Parameter < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :param + end + end + # + class Picture < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :picture + end + end + # + class Preformatted < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :pre + end + end + # + class Progress < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :progress + end + end + # + class Quotation < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :q + end + end + # + class StrikeThrough < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :s + end + end + # + class Sample < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :samp + end + end + # + class Source < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :source + end + end + # + class Strong < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :strong + end + end + # + class Small < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :small + end + end + # + class Style < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :style + end + end + # + class SubScript < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :sub + end + end + # + class Summary < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :summary + end + end + # + class SuperScript < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :sup + end + end + # + class Svg < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :svg + end + end + # + class Template < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :template + end + end + # + class Time < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :time + end + end + # + class Track < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :track + end + end + # + class Misspelled < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :u + end + end + # + class Variable < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :var + end + end + # + class WordBreak < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :wbr + end + end + # + class Header < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :header + end + end + # + class Navigation < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :nav + end + end + # + class Main < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :main + end + end + # + class Section < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :section + end + end + # + class Article < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :article + end + end + # + class Aside < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :aside + end + end + # + class Footer < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :footer + end + end + # + class Form < GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + end + # + def form_data() + result = {} + search_queue = self.children.values + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.domtype == :input + result[(entry.title.to_s.to_sym)] = entry.value() + else + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + end + # ### Buttons + class ButtonInput < ::GxG::Gui::Vdom::BaseClickableElement + def _before_create + super() + @options[:type] = :button + end + end + # + class SubmitButton < ::GxG::Gui::Vdom::BaseClickableElement + def _before_create + super() + @options[:type] = :submit + end + end + # + class Block < ::GxG::Gui::Vdom::BaseClickableElement + def _before_create + super() + @domtype = :div + @options.delete(:type) + true + end + end + # + class CheckBox < ::GxG::Gui::Vdom::BaseClickableElement + def _before_create + super() + @options[:type] = :checkbox + end + end + # + class Image < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :image + if @options[:source] + @options[:src] = @options.delete(:source) + end + @options[:alt] ||= @options[:src] + end + end + # + class Video < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :video + if @options[:source] + @options[:src] = @options.delete(:source) + end + end + end + # + class Audio < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :audio + if @options[:source] + @options[:src] = @options.delete(:source) + end + end + end + # + class Canvas < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :canvas + if @options[:source] + @options[:src] = @options.delete(:source) + end + end + end + # + class HiddenInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :hidden + end + end + # + class FileInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :file + end + end + # + class TextInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :text + end + end + # + class PasswordInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :password + end + end + # + class ResetInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :reset + end + end + # + class RadioButton < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :radio + end + end + # + class ColorPicker < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :color + end + end + # + class DatePicker < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :date + end + end + # + class DateTimeLocal < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :"datetime-local" + end + end + # + class EmailInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :email + end + end + # + class MonthPicker < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :month + end + end + # + class NumberInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :number + end + end + # + class RangeInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :range + end + end + # + class SearchInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :search + end + end + # + class PhoneInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :tel + end + end + # + class TimePicker < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :time + end + end + # + class UrlInput < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :url + end + end + # + class WeekPicker < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + @options[:type] = :week + end + end + # + class Label < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :label + end + end + # + class TextArea < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :textarea + end + end + # + class Output < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :output + end + end + # + class Paragraph < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :p + end + end + # + class Header1 < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :h1 + end + end + # + class Header2 < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :h2 + end + end + # + class Header3 < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :h3 + end + end + # + class Header4 < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :h4 + end + end + # + class Header5 < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :h5 + end + end + # + class Header6 < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :h6 + end + end + # + class List < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ul + end + end + # + class OrderedList < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :ol + end + end + # + class ListItem < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :li + end + end + # + class Anchor < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :a + end + end + # + class ExternalLink < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + # When link is clicked will navigate to a location outside the + # application and in a new tab. + # Specify option :target to set the location. + @options[:target] ||= "_blank" + @domtype = :a + end + end + # + class Button < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :button + unless @options[:type] == "button" + @options[:type] = "button" + end + unless @states[:button] == true + @states[:button] = true + end + end + # + def _after_create + super() + # Review : consider a non-gxg listener for un-focusing the active element. thus: + #`{@element}.addEventListener("click",function(e){#{clicked};document.activeElement.blur()})` + on(:click) do |the_event| + `document.activeElement.blur()` + end + end + end + # + class Table < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :table + end + end + # + class TableHeader < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :th + end + end + # + class TableRow < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :tr + end + end + # + class TableCell < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :td + end + end + # + class BlockTable < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:table] == true + @states[:table] = true + end + end + end + # + class BlockTableHeader < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:th] == true + @states[:th] = true + end + end + end + # + class BlockTableRow < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:tr] == true + @states[:tr] = true + end + end + end + # + class BlockTableCell < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :div + unless @states[:td] == true + @states[:td] = true + end + end + end + # + class Span < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :span + end + end + # + class Select < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :select + end + end + # + class Option < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :option + end + end + # + class FieldSet < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :fieldset + unless @states[:fieldset] == true + @states[:fieldset] = true + end + end + end + # + class Legend < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :legend + end + end + # + # ### Component Registration + COMPONENT_CLASSES = { + :abbreviation => ::GxG::Gui::Abbreviation, + :address => ::GxG::Gui::Address, + :area => ::GxG::Gui::Area, + :bold => ::GxG::Gui::Bold, + :bdi => ::GxG::Gui::Bdi, + :bdo => ::GxG::Gui::Bdo, + :blockquote => ::GxG::Gui::BlockQuote, + :break => ::GxG::Gui::Break, + :caption => ::GxG::Gui::Caption, + :cite => ::GxG::Gui::Cite, + :code => ::GxG::Gui::Code, + :column => ::GxG::Gui::Column, + :column_group => ::GxG::Gui::ColumnGroup, + :data => ::GxG::Gui::Data, + :datalist => ::GxG::Gui::DataList, + :description => ::GxG::Gui::Description, + :deleted => ::GxG::Gui::Deleted, + :details => ::GxG::Gui::Details, + :dfn => ::GxG::Gui::Dfn, + :dialog => ::GxG::Gui::Dialog, + :division => ::GxG::Gui::Division, + :description_list => ::GxG::Gui::DescriptionList, + :description_term => ::GxG::Gui::DescriptionTerm, + :emphasis => ::GxG::Gui::Emphasis, + :embed => ::GxG::Gui::Embed, + :figure => ::GxG::Gui::Figure, + :figure_caption => ::GxG::Gui::FigureCaption, + :theme_divider => ::GxG::Gui::ThemeDivider, + :italic => ::GxG::Gui::Italic, + :inline_frame => ::GxG::Gui::InlineFrame, + :Insertion => ::GxG::Gui::Insertion, + :keyboard_key => ::GxG::Gui::KeyboardKey, + :map => ::GxG::Gui::Map, + :mark => ::GxG::Gui::Mark, + :meter => ::GxG::Gui::Meter, + :noscript => ::GxG::Gui::NoScript, + :object => ::GxG::Gui::Object, + :option_group => ::GxG::Gui::OptionGroup, + :parameter => ::GxG::Gui::Parameter, + :picture => ::GxG::Gui::Picture, + :preformatted => ::GxG::Gui::Preformatted, + :progress => ::GxG::Gui::Progress, + :quotation => ::GxG::Gui::Quotation, + :strike_through => ::GxG::Gui::StrikeThrough, + :sample => ::GxG::Gui::Sample, + :section => ::GxG::Gui::Section, + :small => ::GxG::Gui::Small, + :source => ::GxG::Gui::Source, + :strong => ::GxG::Gui::Strong, + :style => ::GxG::Gui::Style, + :sub_script => ::GxG::Gui::SubScript, + :summary => ::GxG::Gui::Summary, + :super_script => ::GxG::Gui::SuperScript, + :svg => ::GxG::Gui::Svg, + :template => ::GxG::Gui::Template, + :text_area => ::GxG::Gui::TextArea, + :time => ::GxG::Gui::Time, + :track => ::GxG::Gui::Track, + :misspelled => ::GxG::Gui::Misspelled, + :variable => ::GxG::Gui::Variable, + :word_break => ::GxG::Gui::WordBreak, + :header => ::GxG::Gui::Header, + :navigation => ::GxG::Gui::Navigation, + :main => ::GxG::Gui::Main, + :article => ::GxG::Gui::Article, + :aside => ::GxG::Gui::Aside, + :footer => ::GxG::Gui::Footer, + :form => ::GxG::Gui::Form, + :button_input => ::GxG::Gui::ButtonInput, + :submit_button => ::GxG::Gui::SubmitButton, + :block => ::GxG::Gui::Block, + :checkbox => ::GxG::Gui::CheckBox, + :image => ::GxG::Gui::Image, + :video => ::GxG::Gui::Video, + :audio => ::GxG::Gui::Audio, + :canvas => ::GxG::Gui::Canvas, + :hidden => ::GxG::Gui::HiddenInput, + :file_input => ::GxG::Gui::FileInput, + :text_input => ::GxG::Gui::TextInput, + :password_input => ::GxG::Gui::PasswordInput, + :reset_input => ::GxG::Gui::ResetInput, + :radio_button => ::GxG::Gui::RadioButton, + :color_picker => ::GxG::Gui::ColorPicker, + :date_picker => ::GxG::Gui::DatePicker, + :datetime_local => ::GxG::Gui::DateTimeLocal, + :email_input => ::GxG::Gui::EmailInput, + :month_picker => ::GxG::Gui::MonthPicker, + :number_input =>::GxG::Gui::NumberInput, + :range_input => ::GxG::Gui::RangeInput, + :search_input => ::GxG::Gui::SearchInput, + :phone_input => ::GxG::Gui::PhoneInput, + :time_picker => ::GxG::Gui::TimePicker, + :url_input => ::GxG::Gui::UrlInput, + :week_picker => ::GxG::Gui::WeekPicker, + :label => ::GxG::Gui::Label, + :output => ::GxG::Gui::Output, + :paragraph => ::GxG::Gui::Paragraph, + :header1 => ::GxG::Gui::Header1, + :header2 => ::GxG::Gui::Header2, + :header3 => ::GxG::Gui::Header3, + :header4 => ::GxG::Gui::Header4, + :header5 => ::GxG::Gui::Header5, + :header6 => ::GxG::Gui::Header6, + :list => ::GxG::Gui::List, + :ordered_list => ::GxG::Gui::OrderedList, + :list_item => ::GxG::Gui::ListItem, + :anchor => ::GxG::Gui::Anchor, + :external_link => ::GxG::Gui::ExternalLink, + :button => ::GxG::Gui::Button, + :table => ::GxG::Gui::Table, + :table_header => ::GxG::Gui::TableHeader, + :table_row => ::GxG::Gui::TableRow, + :table_cell => ::GxG::Gui::TableCell, + :block_table => ::GxG::Gui::BlockTable, + :block_table_header => ::GxG::Gui::BlockTableHeader, + :block_table_row => ::GxG::Gui::BlockTableRow, + :block_table_cell => ::GxG::Gui::BlockTableCell, + :span => ::GxG::Gui::Span, + :select => ::GxG::Gui::Select, + :option => ::GxG::Gui::Option, + :field_set => ::GxG::Gui::FieldSet, + :legend => ::GxG::Gui::Legend + } + # + def self.component_class(the_type_specifier=nil) + result = nil + if the_type_specifier + result = GxG::Gui::COMPONENT_CLASSES[(the_type_specifier.to_s.to_sym)] + end + result + end + # + def self.class_component(the_class_specifier=nil) + result = nil + if the_class_specifier + found_index = GxG::Gui::COMPONENT_CLASSES.values.index(the_class_specifier) + if found_index + result = GxG::Gui::COMPONENT_CLASSES.keys[(found_index)] + end + end + result + end + # + def self.component_registered?(the_specifier=nil) + if GxG::Gui::component_class(the_specifier) + true + else + false + end + end + # + def self.register_component_class(the_specifier=nil, the_constant=nil) + if the_specifier.is_any?(::String, ::Symbol) && the_constant.is_a?(::Class) + if GxG::Gui::component_registered?(the_specifier.to_s.to_sym) + if GxG::Gui::component_class(the_specifier.to_s.to_sym) != the_constant + GxG::Gui::COMPONENT_CLASSES[(the_specifier.to_s.to_sym)] = the_constant + true + else + # Review : should re-defining component constants be allowed ?? + log_warn("Component Class #{the_specifier.to_s.to_sym.inspect} => #{the_constant.inspect} already defined.") + false + end + else + GxG::Gui::COMPONENT_CLASSES[(the_specifier.to_s.to_sym)] = the_constant + true + end + else + false + end + end + # + end + # End Gui +end +# End GxG +# ### Foundation Integration +module GxG + module Gui + # ### Foundation Component Classes + # See : https://get.foundation/sites/docs/index.html + # + end +end \ No newline at end of file diff --git a/gxg/web/gxg_uri.rb b/gxg/web/gxg_uri.rb new file mode 100644 index 0000000..91675d9 --- /dev/null +++ b/gxg/web/gxg_uri.rb @@ -0,0 +1,3786 @@ +# frozen_string_literal: false +#-- +# = uri/common.rb +# +# Author:: Akira Yamada +# Revision:: $Id$ +# License:: +# You can redistribute it and/or modify it under the same term as Ruby. +# +# See URI for general documentation +# + +module URI + # + # Includes URI::REGEXP::PATTERN + # + module RFC2396_REGEXP + # + # Patterns used to parse URI's + # + module PATTERN + # :stopdoc: + + # RFC 2396 (URI Generic Syntax) + # RFC 2732 (IPv6 Literal Addresses in URL's) + # RFC 2373 (IPv6 Addressing Architecture) + + # alpha = lowalpha | upalpha + ALPHA = "a-zA-Z" + # alphanum = alpha | digit + ALNUM = "#{ALPHA}\\d" + + # hex = digit | "A" | "B" | "C" | "D" | "E" | "F" | + # "a" | "b" | "c" | "d" | "e" | "f" + HEX = "a-fA-F\\d" + # escaped = "%" hex hex + ESCAPED = "%[#{HEX}]{2}" + # mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | + # "(" | ")" + # unreserved = alphanum | mark + UNRESERVED = "\\-_.!~*'()#{ALNUM}" + # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + # "$" | "," + # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + # "$" | "," | "[" | "]" (RFC 2732) + RESERVED = ";/?:@&=+$,\\[\\]" + + # domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum + DOMLABEL = "(?:[#{ALNUM}](?:[-#{ALNUM}]*[#{ALNUM}])?)" + # toplabel = alpha | alpha *( alphanum | "-" ) alphanum + TOPLABEL = "(?:[#{ALPHA}](?:[-#{ALNUM}]*[#{ALNUM}])?)" + # hostname = *( domainlabel "." ) toplabel [ "." ] + HOSTNAME = "(?:#{DOMLABEL}\\.)*#{TOPLABEL}\\.?" + + # :startdoc: + end # PATTERN + + # :startdoc: + end # REGEXP + + # class that Parses String's into URI's + # + # It contains a Hash set of patterns and Regexp's that match and validate. + # + class RFC2396_Parser + include RFC2396_REGEXP + + # + # == Synopsis + # + # URI::Parser.new([opts]) + # + # == Args + # + # The constructor accepts a hash as options for parser. + # Keys of options are pattern names of URI components + # and values of options are pattern strings. + # The constructor generates set of regexps for parsing URIs. + # + # You can use the following keys: + # + # * :ESCAPED (URI::PATTERN::ESCAPED in default) + # * :UNRESERVED (URI::PATTERN::UNRESERVED in default) + # * :DOMLABEL (URI::PATTERN::DOMLABEL in default) + # * :TOPLABEL (URI::PATTERN::TOPLABEL in default) + # * :HOSTNAME (URI::PATTERN::HOSTNAME in default) + # + # == Examples + # + # p = URI::Parser.new(:ESCAPED => "(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})") + # u = p.parse("http://example.jp/%uABCD") #=> # + # URI.parse(u.to_s) #=> raises URI::InvalidURIError + # + # s = "http://example.com/ABCD" + # u1 = p.parse(s) #=> # + # u2 = URI.parse(s) #=> # + # u1 == u2 #=> true + # u1.eql?(u2) #=> false + # + def initialize(opts = {}) + @pattern = initialize_pattern(opts) + @pattern.each_value(&:freeze) + @pattern.freeze + + @regexp = initialize_regexp(@pattern) + @regexp.each_value(&:freeze) + @regexp.freeze + end + + # The Hash of patterns. + # + # see also URI::Parser.initialize_pattern + attr_reader :pattern + + # The Hash of Regexp + # + # see also URI::Parser.initialize_regexp + attr_reader :regexp + + # Returns a split URI against regexp[:ABS_URI] + def split(uri) + case uri + when '' + # null uri + + when @regexp[:ABS_URI] + scheme, opaque, userinfo, host, port, + registry, path, query, fragment = $~[1..-1] + + # URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + # hier_part = ( net_path | abs_path ) [ "?" query ] + # opaque_part = uric_no_slash *uric + + # abs_path = "/" path_segments + # net_path = "//" authority [ abs_path ] + + # authority = server | reg_name + # server = [ [ userinfo "@" ] hostport ] + + if !scheme + raise InvalidURIError, + "bad URI(absolute but no scheme): #{uri}" + end + if !opaque && (!path && (!host && !registry)) + raise InvalidURIError, + "bad URI(absolute but no path): #{uri}" + end + + when @regexp[:REL_URI] + scheme = nil + opaque = nil + + userinfo, host, port, registry, + rel_segment, abs_path, query, fragment = $~[1..-1] + if rel_segment && abs_path + path = rel_segment + abs_path + elsif rel_segment + path = rel_segment + elsif abs_path + path = abs_path + end + + # URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + + # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + + # net_path = "//" authority [ abs_path ] + # abs_path = "/" path_segments + # rel_path = rel_segment [ abs_path ] + + # authority = server | reg_name + # server = [ [ userinfo "@" ] hostport ] + + else + raise InvalidURIError, "bad URI(is not URI?): #{uri}" + end + + path = '' if !path && !opaque # (see RFC2396 Section 5.2) + ret = [ + scheme, + userinfo, host, port, # X + registry, # X + path, # Y + opaque, # Y + query, + fragment + ] + return ret + end + + # + # == Args + # + # +uri+:: + # String + # + # == Description + # + # parses +uri+ and constructs either matching URI scheme object + # (FTP, HTTP, HTTPS, LDAP, LDAPS, or MailTo) or URI::Generic + # + # == Usage + # + # p = URI::Parser.new + # p.parse("ldap://ldap.example.com/dc=example?user=john") + # #=> # + # + def parse(uri) + scheme, userinfo, host, port, + registry, path, opaque, query, fragment = self.split(uri) + + if scheme && URI.scheme_list.include?(scheme.upcase) + URI.scheme_list[scheme.upcase].new(scheme, userinfo, host, port, + registry, path, opaque, query, + fragment, self) + else + Generic.new(scheme, userinfo, host, port, + registry, path, opaque, query, + fragment, self) + end + end + + + # + # == Args + # + # +uris+:: + # an Array of Strings + # + # == Description + # + # Attempts to parse and merge a set of URIs + # + def join(*uris) + uris[0] = convert_to_uri(uris[0]) + uris.inject :merge + end + + # + # :call-seq: + # extract( str ) + # extract( str, schemes ) + # extract( str, schemes ) {|item| block } + # + # == Args + # + # +str+:: + # String to search + # +schemes+:: + # Patterns to apply to +str+ + # + # == Description + # + # Attempts to parse and merge a set of URIs + # If no +block+ given , then returns the result, + # else it calls +block+ for each element in result. + # + # see also URI::Parser.make_regexp + # + def extract(str, schemes = nil) + if block_given? + str.scan(make_regexp(schemes)) { yield $& } + nil + else + result = [] + str.scan(make_regexp(schemes)) { result.push $& } + result + end + end + + # returns Regexp that is default self.regexp[:ABS_URI_REF], + # unless +schemes+ is provided. Then it is a Regexp.union with self.pattern[:X_ABS_URI] + def make_regexp(schemes = nil) + unless schemes + @regexp[:ABS_URI_REF] + else + /(?=#{Regexp.union(*schemes)}:)#{@pattern[:X_ABS_URI]}/x + end + end + + # + # :call-seq: + # escape( str ) + # escape( str, unsafe ) + # + # == Args + # + # +str+:: + # String to make safe + # +unsafe+:: + # Regexp to apply. Defaults to self.regexp[:UNSAFE] + # + # == Description + # + # constructs a safe String from +str+, removing unsafe characters, + # replacing them with codes. + # + def escape(str, unsafe = @regexp[:UNSAFE]) + unless unsafe.kind_of?(Regexp) + # perhaps unsafe is String object + unsafe = Regexp.new("[#{Regexp.quote(unsafe)}]", false) + end + str.gsub(unsafe) do + us = $& + tmp = '' + us.each_byte do |uc| + tmp << sprintf('%%%02X', uc) + end + tmp + end.force_encoding(Encoding::US_ASCII) + end + + # + # :call-seq: + # unescape( str ) + # unescape( str, unsafe ) + # + # == Args + # + # +str+:: + # String to remove escapes from + # +unsafe+:: + # Regexp to apply. Defaults to self.regexp[:ESCAPED] + # + # == Description + # + # Removes escapes from +str+ + # + def unescape(str, escaped = @regexp[:ESCAPED]) + str.gsub(escaped) { [$&[1, 2].hex].pack('C') }.force_encoding(str.encoding) + end + + @@to_s = Kernel.instance_method(:to_s) + def inspect + @@to_s.bind(self).call + end + + private + + # Constructs the default Hash of patterns + def initialize_pattern(opts = {}) + ret = {} + ret[:ESCAPED] = escaped = (opts.delete(:ESCAPED) || PATTERN::ESCAPED) + ret[:UNRESERVED] = unreserved = opts.delete(:UNRESERVED) || PATTERN::UNRESERVED + ret[:RESERVED] = reserved = opts.delete(:RESERVED) || PATTERN::RESERVED + ret[:DOMLABEL] = opts.delete(:DOMLABEL) || PATTERN::DOMLABEL + ret[:TOPLABEL] = opts.delete(:TOPLABEL) || PATTERN::TOPLABEL + ret[:HOSTNAME] = hostname = opts.delete(:HOSTNAME) + + # RFC 2396 (URI Generic Syntax) + # RFC 2732 (IPv6 Literal Addresses in URL's) + # RFC 2373 (IPv6 Addressing Architecture) + + # uric = reserved | unreserved | escaped + ret[:URIC] = uric = "(?:[#{unreserved}#{reserved}]|#{escaped})" + # uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" | + # "&" | "=" | "+" | "$" | "," + ret[:URIC_NO_SLASH] = uric_no_slash = "(?:[#{unreserved};?:@&=+$,]|#{escaped})" + # query = *uric + ret[:QUERY] = query = "#{uric}*" + # fragment = *uric + ret[:FRAGMENT] = fragment = "#{uric}*" + + # hostname = *( domainlabel "." ) toplabel [ "." ] + # reg-name = *( unreserved / pct-encoded / sub-delims ) # RFC3986 + unless hostname + ret[:HOSTNAME] = hostname = "(?:[a-zA-Z0-9\\-.]|%\\h\\h)+" + end + + # RFC 2373, APPENDIX B: + # IPv6address = hexpart [ ":" IPv4address ] + # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT + # hexpart = hexseq | hexseq "::" [ hexseq ] | "::" [ hexseq ] + # hexseq = hex4 *( ":" hex4) + # hex4 = 1*4HEXDIG + # + # XXX: This definition has a flaw. "::" + IPv4address must be + # allowed too. Here is a replacement. + # + # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT + ret[:IPV4ADDR] = ipv4addr = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}" + # hex4 = 1*4HEXDIG + hex4 = "[#{PATTERN::HEX}]{1,4}" + # lastpart = hex4 | IPv4address + lastpart = "(?:#{hex4}|#{ipv4addr})" + # hexseq1 = *( hex4 ":" ) hex4 + hexseq1 = "(?:#{hex4}:)*#{hex4}" + # hexseq2 = *( hex4 ":" ) lastpart + hexseq2 = "(?:#{hex4}:)*#{lastpart}" + # IPv6address = hexseq2 | [ hexseq1 ] "::" [ hexseq2 ] + ret[:IPV6ADDR] = ipv6addr = "(?:#{hexseq2}|(?:#{hexseq1})?::(?:#{hexseq2})?)" + + # IPv6prefix = ( hexseq1 | [ hexseq1 ] "::" [ hexseq1 ] ) "/" 1*2DIGIT + # unused + + # ipv6reference = "[" IPv6address "]" (RFC 2732) + ret[:IPV6REF] = ipv6ref = "\\[#{ipv6addr}\\]" + + # host = hostname | IPv4address + # host = hostname | IPv4address | IPv6reference (RFC 2732) + ret[:HOST] = host = "(?:#{hostname}|#{ipv4addr}|#{ipv6ref})" + # port = *digit + ret[:PORT] = port = '\d*' + # hostport = host [ ":" port ] + ret[:HOSTPORT] = hostport = "#{host}(?::#{port})?" + + # userinfo = *( unreserved | escaped | + # ";" | ":" | "&" | "=" | "+" | "$" | "," ) + ret[:USERINFO] = userinfo = "(?:[#{unreserved};:&=+$,]|#{escaped})*" + + # pchar = unreserved | escaped | + # ":" | "@" | "&" | "=" | "+" | "$" | "," + pchar = "(?:[#{unreserved}:@&=+$,]|#{escaped})" + # param = *pchar + param = "#{pchar}*" + # segment = *pchar *( ";" param ) + segment = "#{pchar}*(?:;#{param})*" + # path_segments = segment *( "/" segment ) + ret[:PATH_SEGMENTS] = path_segments = "#{segment}(?:/#{segment})*" + + # server = [ [ userinfo "@" ] hostport ] + server = "(?:#{userinfo}@)?#{hostport}" + # reg_name = 1*( unreserved | escaped | "$" | "," | + # ";" | ":" | "@" | "&" | "=" | "+" ) + ret[:REG_NAME] = reg_name = "(?:[#{unreserved}$,;:@&=+]|#{escaped})+" + # authority = server | reg_name + authority = "(?:#{server}|#{reg_name})" + + # rel_segment = 1*( unreserved | escaped | + # ";" | "@" | "&" | "=" | "+" | "$" | "," ) + ret[:REL_SEGMENT] = rel_segment = "(?:[#{unreserved};@&=+$,]|#{escaped})+" + + # scheme = alpha *( alpha | digit | "+" | "-" | "." ) + ret[:SCHEME] = scheme = "[#{PATTERN::ALPHA}][\\-+.#{PATTERN::ALPHA}\\d]*" + + # abs_path = "/" path_segments + ret[:ABS_PATH] = abs_path = "/#{path_segments}" + # rel_path = rel_segment [ abs_path ] + ret[:REL_PATH] = rel_path = "#{rel_segment}(?:#{abs_path})?" + # net_path = "//" authority [ abs_path ] + ret[:NET_PATH] = net_path = "//#{authority}(?:#{abs_path})?" + + # hier_part = ( net_path | abs_path ) [ "?" query ] + ret[:HIER_PART] = hier_part = "(?:#{net_path}|#{abs_path})(?:\\?(?:#{query}))?" + # opaque_part = uric_no_slash *uric + ret[:OPAQUE_PART] = opaque_part = "#{uric_no_slash}#{uric}*" + + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + ret[:ABS_URI] = abs_uri = "#{scheme}:(?:#{hier_part}|#{opaque_part})" + # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + ret[:REL_URI] = rel_uri = "(?:#{net_path}|#{abs_path}|#{rel_path})(?:\\?#{query})?" + + # URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + ret[:URI_REF] = "(?:#{abs_uri}|#{rel_uri})?(?:##{fragment})?" + + ret[:X_ABS_URI] = " + (#{scheme}): (?# 1: scheme) + (?: + (#{opaque_part}) (?# 2: opaque) + | + (?:(?: + //(?: + (?:(?:(#{userinfo})@)? (?# 3: userinfo) + (?:(#{host})(?::(\\d*))?))? (?# 4: host, 5: port) + | + (#{reg_name}) (?# 6: registry) + ) + | + (?!//)) (?# XXX: '//' is the mark for hostport) + (#{abs_path})? (?# 7: path) + )(?:\\?(#{query}))? (?# 8: query) + ) + (?:\\#(#{fragment}))? (?# 9: fragment) + " + + ret[:X_REL_URI] = " + (?: + (?: + // + (?: + (?:(#{userinfo})@)? (?# 1: userinfo) + (#{host})?(?::(\\d*))? (?# 2: host, 3: port) + | + (#{reg_name}) (?# 4: registry) + ) + ) + | + (#{rel_segment}) (?# 5: rel_segment) + )? + (#{abs_path})? (?# 6: abs_path) + (?:\\?(#{query}))? (?# 7: query) + (?:\\#(#{fragment}))? (?# 8: fragment) + " + + ret + end + + # Constructs the default Hash of Regexp's + def initialize_regexp(pattern) + ret = {} + + # for URI::split + ret[:ABS_URI] = Regexp.new('\A\s*' + pattern[:X_ABS_URI] + '\s*\z', Regexp::EXTENDED) + ret[:REL_URI] = Regexp.new('\A\s*' + pattern[:X_REL_URI] + '\s*\z', Regexp::EXTENDED) + + # for URI::extract + ret[:URI_REF] = Regexp.new(pattern[:URI_REF]) + ret[:ABS_URI_REF] = Regexp.new(pattern[:X_ABS_URI], Regexp::EXTENDED) + ret[:REL_URI_REF] = Regexp.new(pattern[:X_REL_URI], Regexp::EXTENDED) + + # for URI::escape/unescape + ret[:ESCAPED] = Regexp.new(pattern[:ESCAPED]) + ret[:UNSAFE] = Regexp.new("[^#{pattern[:UNRESERVED]}#{pattern[:RESERVED]}]") + + # for Generic#initialize + ret[:SCHEME] = Regexp.new("\\A#{pattern[:SCHEME]}\\z") + ret[:USERINFO] = Regexp.new("\\A#{pattern[:USERINFO]}\\z") + ret[:HOST] = Regexp.new("\\A#{pattern[:HOST]}\\z") + ret[:PORT] = Regexp.new("\\A#{pattern[:PORT]}\\z") + ret[:OPAQUE] = Regexp.new("\\A#{pattern[:OPAQUE_PART]}\\z") + ret[:REGISTRY] = Regexp.new("\\A#{pattern[:REG_NAME]}\\z") + ret[:ABS_PATH] = Regexp.new("\\A#{pattern[:ABS_PATH]}\\z") + ret[:REL_PATH] = Regexp.new("\\A#{pattern[:REL_PATH]}\\z") + ret[:QUERY] = Regexp.new("\\A#{pattern[:QUERY]}\\z") + ret[:FRAGMENT] = Regexp.new("\\A#{pattern[:FRAGMENT]}\\z") + + ret + end + + def convert_to_uri(uri) + if uri.is_a?(URI::Generic) + uri + elsif uri = String.try_convert(uri) + parse(uri) + else + raise ArgumentError, + "bad argument (expected URI object or URI string)" + end + end + + end # class Parser +end # module URI +# frozen_string_literal: false +module URI + class RFC3986_Parser # :nodoc: + # URI defined in RFC3986 + # this regexp is modified not to host is not empty string + RFC3986_URI = /\A(?(?[A-Za-z][+\-.0-9A-Za-z]*):(?\/\/(?(?:(?(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?(?\[(?:(?(?:\h{1,4}:){6}(?\h{1,4}:\h{1,4}|(?(?[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g\.\g\.\g))|::(?:\h{1,4}:){5}\g|\h{1,4}?::(?:\h{1,4}:){4}\g|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g|(?(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])+))?(?::(?\d*))?)(?(?:\/(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?\/(?:(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g)*)?)|(?\g(?:\/\g)*)|(?))(?:\?(?[^#]*))?(?:\#(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/ + RFC3986_relative_ref = /\A(?(?\/\/(?(?:(?(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?(?\[(?(?:\h{1,4}:){6}(?\h{1,4}:\h{1,4}|(?(?[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g\.\g\.\g))|::(?:\h{1,4}:){5}\g|\h{1,4}?::(?:\h{1,4}:){4}\g|(?:(?:\h{1,4}:){,1}\h{1,4})?::(?:\h{1,4}:){3}\g|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?v\h+\.[!$&-.0-;=A-Z_a-z~]+)\])|\g|(?(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])+))?(?::(?\d*))?)(?(?:\/(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?\/(?:(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g)*)?)|(?(?(?:%\h\h|[!$&-.0-9;=@-Z_a-z~])+)(?:\/\g)*)|(?))(?:\?(?[^#]*))?(?:\#(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/ + attr_reader :regexp + + def initialize + @regexp = default_regexp.each_value(&:freeze).freeze + end + + def split(uri) #:nodoc: + begin + uri = uri.to_str + rescue NoMethodError + raise InvalidURIError, "bad URI(is not URI?): #{uri}" + end + uri.ascii_only? or + raise InvalidURIError, "URI must be ascii only #{uri.dump}" + if m = RFC3986_URI.match(uri) + query = m["query".freeze] + scheme = m["scheme".freeze] + opaque = m["path-rootless".freeze] + if opaque + opaque << "?#{query}" if query + [ scheme, + nil, # userinfo + nil, # host + nil, # port + nil, # registry + nil, # path + opaque, + nil, # query + m["fragment".freeze] + ] + else # normal + [ scheme, + m["userinfo".freeze], + m["host".freeze], + m["port".freeze], + nil, # registry + (m["path-abempty".freeze] || + m["path-absolute".freeze] || + m["path-empty".freeze]), + nil, # opaque + query, + m["fragment".freeze] + ] + end + elsif m = RFC3986_relative_ref.match(uri) + [ nil, # scheme + m["userinfo".freeze], + m["host".freeze], + m["port".freeze], + nil, # registry, + (m["path-abempty".freeze] || + m["path-absolute".freeze] || + m["path-noscheme".freeze] || + m["path-empty".freeze]), + nil, # opaque + m["query".freeze], + m["fragment".freeze] + ] + else + raise InvalidURIError, "bad URI(is not URI?): #{uri}" + end + end + + def parse(uri) # :nodoc: + scheme, userinfo, host, port, + registry, path, opaque, query, fragment = self.split(uri) + scheme_list = URI.scheme_list + if scheme && scheme_list.include?(uc = scheme.upcase) + scheme_list[uc].new(scheme, userinfo, host, port, + registry, path, opaque, query, + fragment, self) + else + Generic.new(scheme, userinfo, host, port, + registry, path, opaque, query, + fragment, self) + end + end + + + def join(*uris) # :nodoc: + uris[0] = convert_to_uri(uris[0]) + uris.inject :merge + end + + @@to_s = Kernel.instance_method(:to_s) + def inspect + @@to_s.bind(self).call + end + + private + + def default_regexp # :nodoc: + { + SCHEME: /\A[A-Za-z][A-Za-z0-9+\-.]*\z/, + USERINFO: /\A(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*\z/, + HOST: /\A(?:(?\[(?:(?(?:\h{1,4}:){6}(?\h{1,4}:\h{1,4}|(?(?[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g\.\g\.\g))|::(?:\h{1,4}:){5}\g|\h{,4}::(?:\h{1,4}:){4}\g|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g|(?(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])*))\z/, + ABS_PATH: /\A\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*(?:\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*)*\z/, + REL_PATH: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+(?:\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*)*\z/, + QUERY: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*\z/, + FRAGMENT: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*\z/, + OPAQUE: /\A(?:[^\/].*)?\z/, + PORT: /\A[\x09\x0a\x0c\x0d ]*\d*[\x09\x0a\x0c\x0d ]*\z/, + } + end + + def convert_to_uri(uri) + if uri.is_a?(URI::Generic) + uri + elsif uri = String.try_convert(uri) + parse(uri) + else + raise ArgumentError, + "bad argument (expected URI object or URI string)" + end + end + + end # class Parser +end # module URI +# frozen_string_literal: false +#-- +# = uri/common.rb +# +# Author:: Akira Yamada +# Revision:: $Id: common.rb 61155 2017-12-12 11:56:25Z shyouhei $ +# License:: +# You can redistribute it and/or modify it under the same term as Ruby. +# +# See URI for general documentation +# + +module URI + REGEXP = RFC2396_REGEXP + Parser = RFC2396_Parser + RFC3986_PARSER = RFC3986_Parser.new + + # URI::Parser.new + DEFAULT_PARSER = Parser.new + DEFAULT_PARSER.pattern.each_pair do |sym, str| + unless REGEXP::PATTERN.const_defined?(sym) + REGEXP::PATTERN.const_set(sym, str) + end + end + DEFAULT_PARSER.regexp.each_pair do |sym, str| + const_set(sym, str) + end + + module Util # :nodoc: + def make_components_hash(klass, array_hash) + tmp = {} + if array_hash.kind_of?(Array) && + array_hash.size == klass.component.size - 1 + klass.component[1..-1].each_index do |i| + begin + tmp[klass.component[i + 1]] = array_hash[i].clone + rescue TypeError + tmp[klass.component[i + 1]] = array_hash[i] + end + end + + elsif array_hash.kind_of?(Hash) + array_hash.each do |key, value| + begin + tmp[key] = value.clone + rescue TypeError + tmp[key] = value + end + end + else + raise ArgumentError, + "expected Array of or Hash of components of #{klass} (#{klass.component[1..-1].join(', ')})" + end + tmp[:scheme] = klass.to_s.sub(/\A.*::/, '').downcase + + return tmp + end + module_function :make_components_hash + end + + # module for escaping unsafe characters with codes. + module Escape + # + # == Synopsis + # + # URI.escape(str [, unsafe]) + # + # == Args + # + # +str+:: + # String to replaces in. + # +unsafe+:: + # Regexp that matches all symbols that must be replaced with codes. + # By default uses UNSAFE. + # When this argument is a String, it represents a character set. + # + # == Description + # + # Escapes the string, replacing all unsafe characters with codes. + # + # This method is obsolete and should not be used. Instead, use + # CGI.escape, URI.encode_www_form or URI.encode_www_form_component + # depending on your specific use case. + # + # == Usage + # + # require 'uri' + # + # enc_uri = URI.escape("http://example.com/?a=\11\15") + # p enc_uri + # # => "http://example.com/?a=%09%0D" + # + # p URI.unescape(enc_uri) + # # => "http://example.com/?a=\t\r" + # + # p URI.escape("@?@!", "!?") + # # => "@%3F@%21" + # + def escape(*arg) + warn "URI.escape is obsolete", uplevel: 1 if $VERBOSE + DEFAULT_PARSER.escape(*arg) + end + alias encode escape + # + # == Synopsis + # + # URI.unescape(str) + # + # == Args + # + # +str+:: + # Unescapes the string. + # + # == Description + # + # This method is obsolete and should not be used. Instead, use + # CGI.unescape, URI.decode_www_form or URI.decode_www_form_component + # depending on your specific use case. + # + # == Usage + # + # require 'uri' + # + # enc_uri = URI.escape("http://example.com/?a=\11\15") + # p enc_uri + # # => "http://example.com/?a=%09%0D" + # + # p URI.unescape(enc_uri) + # # => "http://example.com/?a=\t\r" + # + def unescape(*arg) + warn "URI.unescape is obsolete", uplevel: 1 if $VERBOSE + DEFAULT_PARSER.unescape(*arg) + end + alias decode unescape + end # module Escape + + extend Escape + include REGEXP + + @@schemes = {} + # Returns a Hash of the defined schemes + def self.scheme_list + @@schemes + end + + # + # Base class for all URI exceptions. + # + class Error < StandardError; end + # + # Not a URI. + # + class InvalidURIError < Error; end + # + # Not a URI component. + # + class InvalidComponentError < Error; end + # + # URI is valid, bad usage is not. + # + class BadURIError < Error; end + + # + # == Synopsis + # + # URI::split(uri) + # + # == Args + # + # +uri+:: + # String with URI. + # + # == Description + # + # Splits the string on following parts and returns array with result: + # + # * Scheme + # * Userinfo + # * Host + # * Port + # * Registry + # * Path + # * Opaque + # * Query + # * Fragment + # + # == Usage + # + # require 'uri' + # + # p URI.split("http://www.ruby-lang.org/") + # # => ["http", nil, "www.ruby-lang.org", nil, nil, "/", nil, nil, nil] + # + def self.split(uri) + RFC3986_PARSER.split(uri) + end + + # + # == Synopsis + # + # URI::parse(uri_str) + # + # == Args + # + # +uri_str+:: + # String with URI. + # + # == Description + # + # Creates one of the URI's subclasses instance from the string. + # + # == Raises + # + # URI::InvalidURIError + # Raised if URI given is not a correct one. + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://www.ruby-lang.org/") + # p uri + # # => # + # p uri.scheme + # # => "http" + # p uri.host + # # => "www.ruby-lang.org" + # + # It's recommended to first ::escape the provided +uri_str+ if there are any + # invalid URI characters. + # + def self.parse(uri) + RFC3986_PARSER.parse(uri) + end + + # + # == Synopsis + # + # URI::join(str[, str, ...]) + # + # == Args + # + # +str+:: + # String(s) to work with, will be converted to RFC3986 URIs before merging. + # + # == Description + # + # Joins URIs. + # + # == Usage + # + # require 'uri' + # + # p URI.join("http://example.com/","main.rbx") + # # => # + # + # p URI.join('http://example.com', 'foo') + # # => # + # + # p URI.join('http://example.com', '/foo', '/bar') + # # => # + # + # p URI.join('http://example.com', '/foo', 'bar') + # # => # + # + # p URI.join('http://example.com', '/foo/', 'bar') + # # => # + # + # + def self.join(*str) + RFC3986_PARSER.join(*str) + end + + # + # == Synopsis + # + # URI::extract(str[, schemes][,&blk]) + # + # == Args + # + # +str+:: + # String to extract URIs from. + # +schemes+:: + # Limit URI matching to a specific schemes. + # + # == Description + # + # Extracts URIs from a string. If block given, iterates through all matched URIs. + # Returns nil if block given or array with matches. + # + # == Usage + # + # require "uri" + # + # URI.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.") + # # => ["http://foo.example.com/bla", "mailto:test@example.com"] + # + def self.extract(str, schemes = nil, &block) + warn "URI.extract is obsolete", uplevel: 1 if $VERBOSE + DEFAULT_PARSER.extract(str, schemes, &block) + end + + # + # == Synopsis + # + # URI::regexp([match_schemes]) + # + # == Args + # + # +match_schemes+:: + # Array of schemes. If given, resulting regexp matches to URIs + # whose scheme is one of the match_schemes. + # + # == Description + # Returns a Regexp object which matches to URI-like strings. + # The Regexp object returned by this method includes arbitrary + # number of capture group (parentheses). Never rely on it's number. + # + # == Usage + # + # require 'uri' + # + # # extract first URI from html_string + # html_string.slice(URI.regexp) + # + # # remove ftp URIs + # html_string.sub(URI.regexp(['ftp']) + # + # # You should not rely on the number of parentheses + # html_string.scan(URI.regexp) do |*matches| + # p $& + # end + # + def self.regexp(schemes = nil) + warn "URI.regexp is obsolete", uplevel: 1 if $VERBOSE + DEFAULT_PARSER.make_regexp(schemes) + end + + TBLENCWWWCOMP_ = {} # :nodoc: + 256.times do |i| + TBLENCWWWCOMP_[i.chr] = '%%%02X' % i + end + TBLENCWWWCOMP_[' '] = '+' + TBLENCWWWCOMP_.freeze + TBLDECWWWCOMP_ = {} # :nodoc: + 256.times do |i| + h, l = i>>4, i&15 + TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr + end + TBLDECWWWCOMP_['+'] = ' ' + TBLDECWWWCOMP_.freeze + + HTML5ASCIIINCOMPAT = defined? Encoding::UTF_7 ? [Encoding::UTF_7, Encoding::UTF_16BE, Encoding::UTF_16LE, + Encoding::UTF_32BE, Encoding::UTF_32LE] : [] # :nodoc: + + # Encode given +str+ to URL-encoded form data. + # + # This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP + # (ASCII space) to + and converts others to %XX. + # + # If +enc+ is given, convert +str+ to the encoding before percent encoding. + # + # This is an implementation of + # http://www.w3.org/TR/2013/CR-html5-20130806/forms.html#url-encoded-form-data + # + # See URI.decode_www_form_component, URI.encode_www_form + def self.encode_www_form_component(str, enc=nil) + str = str.to_s.dup + if str.encoding != Encoding::ASCII_8BIT + if enc && enc != Encoding::ASCII_8BIT + str.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace) + str.encode!(enc, fallback: ->(x){"&#{x.ord};"}) + end + str.force_encoding(Encoding::ASCII_8BIT) + end + str.gsub!(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP_) + str.force_encoding(Encoding::US_ASCII) + end + + # Decode given +str+ of URL-encoded form data. + # + # This decodes + to SP. + # + # See URI.encode_www_form_component, URI.decode_www_form + def self.decode_www_form_component(str, enc=Encoding::UTF_8) + raise ArgumentError, "invalid %-encoding (#{str})" if /%(?!\h\h)/ =~ str + str.b.gsub(/\+|%\h\h/, TBLDECWWWCOMP_).force_encoding(enc) + end + + # Generate URL-encoded form data from given +enum+. + # + # This generates application/x-www-form-urlencoded data defined in HTML5 + # from given an Enumerable object. + # + # This internally uses URI.encode_www_form_component(str). + # + # This method doesn't convert the encoding of given items, so convert them + # before call this method if you want to send data as other than original + # encoding or mixed encoding data. (Strings which are encoded in an HTML5 + # ASCII incompatible encoding are converted to UTF-8.) + # + # This method doesn't handle files. When you send a file, use + # multipart/form-data. + # + # This refers http://url.spec.whatwg.org/#concept-urlencoded-serializer + # + # URI.encode_www_form([["q", "ruby"], ["lang", "en"]]) + # #=> "q=ruby&lang=en" + # URI.encode_www_form("q" => "ruby", "lang" => "en") + # #=> "q=ruby&lang=en" + # URI.encode_www_form("q" => ["ruby", "perl"], "lang" => "en") + # #=> "q=ruby&q=perl&lang=en" + # URI.encode_www_form([["q", "ruby"], ["q", "perl"], ["lang", "en"]]) + # #=> "q=ruby&q=perl&lang=en" + # + # See URI.encode_www_form_component, URI.decode_www_form + def self.encode_www_form(enum, enc=nil) + enum.map do |k,v| + if v.nil? + encode_www_form_component(k, enc) + elsif v.respond_to?(:to_ary) + v.to_ary.map do |w| + str = encode_www_form_component(k, enc) + unless w.nil? + str << '=' + str << encode_www_form_component(w, enc) + end + end.join('&') + else + str = encode_www_form_component(k, enc) + str << '=' + str << encode_www_form_component(v, enc) + end + end.join('&') + end + + # Decode URL-encoded form data from given +str+. + # + # This decodes application/x-www-form-urlencoded data + # and returns array of key-value array. + # + # This refers http://url.spec.whatwg.org/#concept-urlencoded-parser , + # so this supports only &-separator, don't support ;-separator. + # + # ary = URI.decode_www_form("a=1&a=2&b=3") + # p ary #=> [['a', '1'], ['a', '2'], ['b', '3']] + # p ary.assoc('a').last #=> '1' + # p ary.assoc('b').last #=> '3' + # p ary.rassoc('a').last #=> '2' + # p Hash[ary] # => {"a"=>"2", "b"=>"3"} + # + # See URI.decode_www_form_component, URI.encode_www_form + def self.decode_www_form(str, enc=Encoding::UTF_8, separator: '&', use__charset_: false, isindex: false) + raise ArgumentError, "the input of #{self.name}.#{__method__} must be ASCII only string" unless str.ascii_only? + ary = [] + return ary if str.empty? + enc = Encoding.find(enc) + str.b.each_line(separator) do |string| + string.chomp!(separator) + key, sep, val = string.partition('=') + if isindex + if sep.empty? + val = key + key = '' + end + isindex = false + end + + if use__charset_ and key == '_charset_' and e = get_encoding(val) + enc = e + use__charset_ = false + end + + key.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_) + if val + val.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_) + else + val = '' + end + + ary << [key, val] + end + ary.each do |k, v| + k.force_encoding(enc) + k.scrub! + v.force_encoding(enc) + v.scrub! + end + ary + end + + private +=begin command for WEB_ENCODINGS_ + curl https://encoding.spec.whatwg.org/encodings.json| + ruby -rjson -e 'H={} + h={ + "shift_jis"=>"Windows-31J", + "euc-jp"=>"cp51932", + "iso-2022-jp"=>"cp50221", + "x-mac-cyrillic"=>"macCyrillic", + } + JSON($<.read).map{|x|x["encodings"]}.flatten.each{|x| + Encoding.find(n=h.fetch(n=x["name"].downcase,n))rescue next + x["labels"].each{|y|H[y]=n} + } + puts "{" + H.each{|k,v|puts %[ #{k.dump}=>#{v.dump},]} + puts "}" +' +=end + WEB_ENCODINGS_ = { + "unicode-1-1-utf-8"=>"utf-8", + "utf-8"=>"utf-8", + "utf8"=>"utf-8", + "866"=>"ibm866", + "cp866"=>"ibm866", + "csibm866"=>"ibm866", + "ibm866"=>"ibm866", + "csisolatin2"=>"iso-8859-2", + "iso-8859-2"=>"iso-8859-2", + "iso-ir-101"=>"iso-8859-2", + "iso8859-2"=>"iso-8859-2", + "iso88592"=>"iso-8859-2", + "iso_8859-2"=>"iso-8859-2", + "iso_8859-2:1987"=>"iso-8859-2", + "l2"=>"iso-8859-2", + "latin2"=>"iso-8859-2", + "csisolatin3"=>"iso-8859-3", + "iso-8859-3"=>"iso-8859-3", + "iso-ir-109"=>"iso-8859-3", + "iso8859-3"=>"iso-8859-3", + "iso88593"=>"iso-8859-3", + "iso_8859-3"=>"iso-8859-3", + "iso_8859-3:1988"=>"iso-8859-3", + "l3"=>"iso-8859-3", + "latin3"=>"iso-8859-3", + "csisolatin4"=>"iso-8859-4", + "iso-8859-4"=>"iso-8859-4", + "iso-ir-110"=>"iso-8859-4", + "iso8859-4"=>"iso-8859-4", + "iso88594"=>"iso-8859-4", + "iso_8859-4"=>"iso-8859-4", + "iso_8859-4:1988"=>"iso-8859-4", + "l4"=>"iso-8859-4", + "latin4"=>"iso-8859-4", + "csisolatincyrillic"=>"iso-8859-5", + "cyrillic"=>"iso-8859-5", + "iso-8859-5"=>"iso-8859-5", + "iso-ir-144"=>"iso-8859-5", + "iso8859-5"=>"iso-8859-5", + "iso88595"=>"iso-8859-5", + "iso_8859-5"=>"iso-8859-5", + "iso_8859-5:1988"=>"iso-8859-5", + "arabic"=>"iso-8859-6", + "asmo-708"=>"iso-8859-6", + "csiso88596e"=>"iso-8859-6", + "csiso88596i"=>"iso-8859-6", + "csisolatinarabic"=>"iso-8859-6", + "ecma-114"=>"iso-8859-6", + "iso-8859-6"=>"iso-8859-6", + "iso-8859-6-e"=>"iso-8859-6", + "iso-8859-6-i"=>"iso-8859-6", + "iso-ir-127"=>"iso-8859-6", + "iso8859-6"=>"iso-8859-6", + "iso88596"=>"iso-8859-6", + "iso_8859-6"=>"iso-8859-6", + "iso_8859-6:1987"=>"iso-8859-6", + "csisolatingreek"=>"iso-8859-7", + "ecma-118"=>"iso-8859-7", + "elot_928"=>"iso-8859-7", + "greek"=>"iso-8859-7", + "greek8"=>"iso-8859-7", + "iso-8859-7"=>"iso-8859-7", + "iso-ir-126"=>"iso-8859-7", + "iso8859-7"=>"iso-8859-7", + "iso88597"=>"iso-8859-7", + "iso_8859-7"=>"iso-8859-7", + "iso_8859-7:1987"=>"iso-8859-7", + "sun_eu_greek"=>"iso-8859-7", + "csiso88598e"=>"iso-8859-8", + "csisolatinhebrew"=>"iso-8859-8", + "hebrew"=>"iso-8859-8", + "iso-8859-8"=>"iso-8859-8", + "iso-8859-8-e"=>"iso-8859-8", + "iso-ir-138"=>"iso-8859-8", + "iso8859-8"=>"iso-8859-8", + "iso88598"=>"iso-8859-8", + "iso_8859-8"=>"iso-8859-8", + "iso_8859-8:1988"=>"iso-8859-8", + "visual"=>"iso-8859-8", + "csisolatin6"=>"iso-8859-10", + "iso-8859-10"=>"iso-8859-10", + "iso-ir-157"=>"iso-8859-10", + "iso8859-10"=>"iso-8859-10", + "iso885910"=>"iso-8859-10", + "l6"=>"iso-8859-10", + "latin6"=>"iso-8859-10", + "iso-8859-13"=>"iso-8859-13", + "iso8859-13"=>"iso-8859-13", + "iso885913"=>"iso-8859-13", + "iso-8859-14"=>"iso-8859-14", + "iso8859-14"=>"iso-8859-14", + "iso885914"=>"iso-8859-14", + "csisolatin9"=>"iso-8859-15", + "iso-8859-15"=>"iso-8859-15", + "iso8859-15"=>"iso-8859-15", + "iso885915"=>"iso-8859-15", + "iso_8859-15"=>"iso-8859-15", + "l9"=>"iso-8859-15", + "iso-8859-16"=>"iso-8859-16", + "cskoi8r"=>"koi8-r", + "koi"=>"koi8-r", + "koi8"=>"koi8-r", + "koi8-r"=>"koi8-r", + "koi8_r"=>"koi8-r", + "koi8-ru"=>"koi8-u", + "koi8-u"=>"koi8-u", + "dos-874"=>"windows-874", + "iso-8859-11"=>"windows-874", + "iso8859-11"=>"windows-874", + "iso885911"=>"windows-874", + "tis-620"=>"windows-874", + "windows-874"=>"windows-874", + "cp1250"=>"windows-1250", + "windows-1250"=>"windows-1250", + "x-cp1250"=>"windows-1250", + "cp1251"=>"windows-1251", + "windows-1251"=>"windows-1251", + "x-cp1251"=>"windows-1251", + "ansi_x3.4-1968"=>"windows-1252", + "ascii"=>"windows-1252", + "cp1252"=>"windows-1252", + "cp819"=>"windows-1252", + "csisolatin1"=>"windows-1252", + "ibm819"=>"windows-1252", + "iso-8859-1"=>"windows-1252", + "iso-ir-100"=>"windows-1252", + "iso8859-1"=>"windows-1252", + "iso88591"=>"windows-1252", + "iso_8859-1"=>"windows-1252", + "iso_8859-1:1987"=>"windows-1252", + "l1"=>"windows-1252", + "latin1"=>"windows-1252", + "us-ascii"=>"windows-1252", + "windows-1252"=>"windows-1252", + "x-cp1252"=>"windows-1252", + "cp1253"=>"windows-1253", + "windows-1253"=>"windows-1253", + "x-cp1253"=>"windows-1253", + "cp1254"=>"windows-1254", + "csisolatin5"=>"windows-1254", + "iso-8859-9"=>"windows-1254", + "iso-ir-148"=>"windows-1254", + "iso8859-9"=>"windows-1254", + "iso88599"=>"windows-1254", + "iso_8859-9"=>"windows-1254", + "iso_8859-9:1989"=>"windows-1254", + "l5"=>"windows-1254", + "latin5"=>"windows-1254", + "windows-1254"=>"windows-1254", + "x-cp1254"=>"windows-1254", + "cp1255"=>"windows-1255", + "windows-1255"=>"windows-1255", + "x-cp1255"=>"windows-1255", + "cp1256"=>"windows-1256", + "windows-1256"=>"windows-1256", + "x-cp1256"=>"windows-1256", + "cp1257"=>"windows-1257", + "windows-1257"=>"windows-1257", + "x-cp1257"=>"windows-1257", + "cp1258"=>"windows-1258", + "windows-1258"=>"windows-1258", + "x-cp1258"=>"windows-1258", + "x-mac-cyrillic"=>"macCyrillic", + "x-mac-ukrainian"=>"macCyrillic", + "chinese"=>"gbk", + "csgb2312"=>"gbk", + "csiso58gb231280"=>"gbk", + "gb2312"=>"gbk", + "gb_2312"=>"gbk", + "gb_2312-80"=>"gbk", + "gbk"=>"gbk", + "iso-ir-58"=>"gbk", + "x-gbk"=>"gbk", + "gb18030"=>"gb18030", + "big5"=>"big5", + "big5-hkscs"=>"big5", + "cn-big5"=>"big5", + "csbig5"=>"big5", + "x-x-big5"=>"big5", + "cseucpkdfmtjapanese"=>"cp51932", + "euc-jp"=>"cp51932", + "x-euc-jp"=>"cp51932", + "csiso2022jp"=>"cp50221", + "iso-2022-jp"=>"cp50221", + "csshiftjis"=>"Windows-31J", + "ms932"=>"Windows-31J", + "ms_kanji"=>"Windows-31J", + "shift-jis"=>"Windows-31J", + "shift_jis"=>"Windows-31J", + "sjis"=>"Windows-31J", + "windows-31j"=>"Windows-31J", + "x-sjis"=>"Windows-31J", + "cseuckr"=>"euc-kr", + "csksc56011987"=>"euc-kr", + "euc-kr"=>"euc-kr", + "iso-ir-149"=>"euc-kr", + "korean"=>"euc-kr", + "ks_c_5601-1987"=>"euc-kr", + "ks_c_5601-1989"=>"euc-kr", + "ksc5601"=>"euc-kr", + "ksc_5601"=>"euc-kr", + "windows-949"=>"euc-kr", + "utf-16be"=>"utf-16be", + "utf-16"=>"utf-16le", + "utf-16le"=>"utf-16le", + } # :nodoc: + + # :nodoc: + # return encoding or nil + # http://encoding.spec.whatwg.org/#concept-encoding-get + def self.get_encoding(label) + Encoding.find(WEB_ENCODINGS_[label.to_str.strip.downcase]) rescue nil + end +end # module URI + +module Kernel + + # + # Returns +uri+ converted to a URI object. + # + def URI(uri) + if uri.is_a?(URI::Generic) + uri + elsif uri = String.try_convert(uri) + URI.parse(uri) + else + raise ArgumentError, + "bad argument (expected URI object or URI string)" + end + end + module_function :URI +end +# frozen_string_literal: true + +# = uri/generic.rb +# +# Author:: Akira Yamada +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# Revision:: $Id: generic.rb 61225 2017-12-14 06:30:22Z naruse $ +# +# See URI for general documentation +# + +autoload :IPSocket, 'socket' +autoload :IPAddr, 'ipaddr' + +module URI + + # + # Base class for all URI classes. + # Implements generic URI syntax as per RFC 2396. + # + class Generic + include URI + + # + # A Default port of nil for URI::Generic + # + DEFAULT_PORT = nil + + # + # Returns default port + # + def self.default_port + self::DEFAULT_PORT + end + + # + # Returns default port + # + def default_port + self.class.default_port + end + + # + # An Array of the available components for URI::Generic + # + COMPONENT = [ + :scheme, + :userinfo, :host, :port, :registry, + :path, :opaque, + :query, + :fragment + ].freeze + + # + # Components of the URI in the order. + # + def self.component + self::COMPONENT + end + + USE_REGISTRY = false # :nodoc: + + def self.use_registry # :nodoc: + self::USE_REGISTRY + end + + # + # == Synopsis + # + # See #new + # + # == Description + # + # At first, tries to create a new URI::Generic instance using + # URI::Generic::build. But, if exception URI::InvalidComponentError is raised, + # then it URI::Escape.escape all URI components and tries again. + # + # + def self.build2(args) + begin + return self.build(args) + rescue InvalidComponentError + if args.kind_of?(Array) + return self.build(args.collect{|x| + if x.is_a?(String) + DEFAULT_PARSER.escape(x) + else + x + end + }) + elsif args.kind_of?(Hash) + tmp = {} + args.each do |key, value| + tmp[key] = if value + DEFAULT_PARSER.escape(value) + else + value + end + end + return self.build(tmp) + end + end + end + + # + # == Synopsis + # + # See #new + # + # == Description + # + # Creates a new URI::Generic instance from components of URI::Generic + # with check. Components are: scheme, userinfo, host, port, registry, path, + # opaque, query and fragment. You can provide arguments either by an Array or a Hash. + # See #new for hash keys to use or for order of array items. + # + def self.build(args) + if args.kind_of?(Array) && + args.size == ::URI::Generic::COMPONENT.size + tmp = args.dup + elsif args.kind_of?(Hash) + tmp = ::URI::Generic::COMPONENT.collect do |c| + if args.include?(c) + args[c] + else + nil + end + end + else + component = self.class.component rescue ::URI::Generic::COMPONENT + raise ArgumentError, + "expected Array of or Hash of components of #{self.class} (#{component.join(', ')})" + end + + tmp << nil + tmp << true + return self.new(*tmp) + end + # + # == Args + # + # +scheme+:: + # Protocol scheme, i.e. 'http','ftp','mailto' and so on. + # +userinfo+:: + # User name and password, i.e. 'sdmitry:bla' + # +host+:: + # Server host name + # +port+:: + # Server port + # +registry+:: + # Registry of naming authorities. + # +path+:: + # Path on server + # +opaque+:: + # Opaque part + # +query+:: + # Query data + # +fragment+:: + # A part of URI after '#' sign + # +parser+:: + # Parser for internal use [URI::DEFAULT_PARSER by default] + # +arg_check+:: + # Check arguments [false by default] + # + # == Description + # + # Creates a new URI::Generic instance from ``generic'' components without check. + # + def initialize(scheme, + userinfo, host, port, registry, + path, opaque, + query, + fragment, + parser = DEFAULT_PARSER, + arg_check = false) + @scheme = nil + @user = nil + @password = nil + @host = nil + @port = nil + @path = nil + @query = nil + @opaque = nil + @fragment = nil + @parser = parser == DEFAULT_PARSER ? nil : parser + + if arg_check + self.scheme = scheme + self.userinfo = userinfo + self.hostname = host + self.port = port + self.path = path + self.query = query + self.opaque = opaque + self.fragment = fragment + else + self.set_scheme(scheme) + self.set_userinfo(userinfo) + self.set_host(host) + self.set_port(port) + self.set_path(path) + self.query = query + self.set_opaque(opaque) + self.fragment=(fragment) + end + if registry + raise InvalidURIError, + "the scheme #{@scheme} does not accept registry part: #{registry} (or bad hostname?)" + end + + @scheme + self.set_path('') if !@path && !@opaque # (see RFC2396 Section 5.2) + self.set_port(self.default_port) if self.default_port && !@port + end + + # + # returns the scheme component of the URI. + # + # URI("http://foo/bar/baz").scheme #=> "http" + # + attr_reader :scheme + + # returns the host component of the URI. + # + # URI("http://foo/bar/baz").host #=> "foo" + # + # It returns nil if no host component. + # + # URI("mailto:foo@example.org").host #=> nil + # + # The component doesn't contains the port number. + # + # URI("http://foo:8080/bar/baz").host #=> "foo" + # + # Since IPv6 addresses are wrapped by brackets in URIs, + # this method returns IPv6 addresses wrapped by brackets. + # This form is not appropriate to pass socket methods such as TCPSocket.open. + # If unwrapped host names are required, use "hostname" method. + # + # URI("http://[::1]/bar/baz").host #=> "[::1]" + # URI("http://[::1]/bar/baz").hostname #=> "::1" + # + attr_reader :host + + # returns the port component of the URI. + # + # URI("http://foo/bar/baz").port #=> "80" + # + # URI("http://foo:8080/bar/baz").port #=> "8080" + # + attr_reader :port + + def registry # :nodoc: + nil + end + + # returns the path component of the URI. + # + # URI("http://foo/bar/baz").path #=> "/bar/baz" + # + attr_reader :path + + # returns the query component of the URI. + # + # URI("http://foo/bar/baz?search=FooBar").query #=> "search=FooBar" + # + attr_reader :query + + # returns the opaque part of the URI. + # + # URI("mailto:foo@example.org").opaque #=> "foo@example.org" + # + # Portion of the path that does make use of the slash '/'. + # The path typically refers to the absolute path and the opaque part. + # (See RFC2396 Section 3 and 5.2.) + # + attr_reader :opaque + + # returns the fragment component of the URI. + # + # URI("http://foo/bar/baz?search=FooBar#ponies").fragment #=> "ponies" + # + attr_reader :fragment + + # returns the parser to be used. + # + # Unless a URI::Parser is defined, then DEFAULT_PARSER is used. + # + def parser + if !defined?(@parser) || !@parser + DEFAULT_PARSER + else + @parser || DEFAULT_PARSER + end + end + + # replace self by other URI object + def replace!(oth) + if self.class != oth.class + raise ArgumentError, "expected #{self.class} object" + end + + component.each do |c| + self.__send__("#{c}=", oth.__send__(c)) + end + end + private :replace! + + # + # Components of the URI in the order. + # + def component + self.class.component + end + + # + # check the scheme +v+ component against the URI::Parser Regexp for :SCHEME + # + def check_scheme(v) + if v && parser.regexp[:SCHEME] !~ v + raise InvalidComponentError, + "bad component(expected scheme component): #{v}" + end + + return true + end + private :check_scheme + + # protected setter for the scheme component +v+ + # + # see also URI::Generic.scheme= + # + def set_scheme(v) + @scheme = v.downcase + end + protected :set_scheme + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the scheme component +v+. + # (with validation) + # + # see also URI::Generic.check_scheme + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com") + # uri.scheme = "https" + # # => "https" + # uri + # #=> # + # + def scheme=(v) + check_scheme(v) + set_scheme(v) + v + end + + # + # check the +user+ and +password+. + # + # If +password+ is not provided, then +user+ is + # split, using URI::Generic.split_userinfo, to + # pull +user+ and +password. + # + # see also URI::Generic.check_user, URI::Generic.check_password + # + def check_userinfo(user, password = nil) + if !password + user, password = split_userinfo(user) + end + check_user(user) + check_password(password, user) + + return true + end + private :check_userinfo + + # + # check the user +v+ component for RFC2396 compliance + # and against the URI::Parser Regexp for :USERINFO + # + # Can not have a registry or opaque component defined, + # with a user component defined. + # + def check_user(v) + if @opaque + raise InvalidURIError, + "can not set user with opaque" + end + + return v unless v + + if parser.regexp[:USERINFO] !~ v + raise InvalidComponentError, + "bad component(expected userinfo component or user component): #{v}" + end + + return true + end + private :check_user + + # + # check the password +v+ component for RFC2396 compliance + # and against the URI::Parser Regexp for :USERINFO + # + # Can not have a registry or opaque component defined, + # with a user component defined. + # + def check_password(v, user = @user) + if @opaque + raise InvalidURIError, + "can not set password with opaque" + end + return v unless v + + if !user + raise InvalidURIError, + "password component depends user component" + end + + if parser.regexp[:USERINFO] !~ v + raise InvalidComponentError, + "bad password component" + end + + return true + end + private :check_password + + # + # Sets userinfo, argument is string like 'name:pass' + # + def userinfo=(userinfo) + if userinfo.nil? + return nil + end + check_userinfo(*userinfo) + set_userinfo(*userinfo) + # returns userinfo + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the +user+ component. + # (with validation) + # + # see also URI::Generic.check_user + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://john:S3nsit1ve@my.example.com") + # uri.user = "sam" + # # => "sam" + # uri + # #=> # + # + def user=(user) + check_user(user) + set_user(user) + # returns user + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the +password+ component. + # (with validation) + # + # see also URI::Generic.check_password + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://john:S3nsit1ve@my.example.com") + # uri.password = "V3ry_S3nsit1ve" + # # => "V3ry_S3nsit1ve" + # uri + # #=> # + # + def password=(password) + check_password(password) + set_password(password) + # returns password + end + + # protect setter for the +user+ component, and +password+ if available. + # (with validation) + # + # see also URI::Generic.userinfo= + # + def set_userinfo(user, password = nil) + unless password + user, password = split_userinfo(user) + end + @user = user + @password = password if password + + [@user, @password] + end + protected :set_userinfo + + # protected setter for the user component +v+ + # + # see also URI::Generic.user= + # + def set_user(v) + set_userinfo(v, @password) + v + end + protected :set_user + + # protected setter for the password component +v+ + # + # see also URI::Generic.password= + # + def set_password(v) + @password = v + # returns v + end + protected :set_password + + # returns the userinfo +ui+ as user, password + # if properly formatted as 'user:password' + def split_userinfo(ui) + return nil, nil unless ui + user, password = ui.split(':', 2) + + return user, password + end + private :split_userinfo + + # escapes 'user:password' +v+ based on RFC 1738 section 3.1 + def escape_userpass(v) + parser.escape(v, /[@:\/]/o) # RFC 1738 section 3.1 #/ + end + private :escape_userpass + + # returns the userinfo, either as 'user' or 'user:password' + def userinfo + if @user.nil? + nil + elsif @password.nil? + @user + else + @user + ':' + @password + end + end + + # returns the user component + def user + @user + end + + # returns the password component + def password + @password + end + + # + # check the host +v+ component for RFC2396 compliance + # and against the URI::Parser Regexp for :HOST + # + # Can not have a registry or opaque component defined, + # with a host component defined. + # + def check_host(v) + return v unless v + + if @opaque + raise InvalidURIError, + "can not set host with registry or opaque" + elsif parser.regexp[:HOST] !~ v + raise InvalidComponentError, + "bad component(expected host component): #{v}" + end + + return true + end + private :check_host + + # protected setter for the host component +v+ + # + # see also URI::Generic.host= + # + def set_host(v) + @host = v + end + protected :set_host + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the host component +v+. + # (with validation) + # + # see also URI::Generic.check_host + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com") + # uri.host = "foo.com" + # # => "foo.com" + # uri + # #=> # + # + def host=(v) + check_host(v) + set_host(v) + v + end + + # extract the host part of the URI and unwrap brackets for IPv6 addresses. + # + # This method is same as URI::Generic#host except + # brackets for IPv6 (and future IP) addresses are removed. + # + # u = URI("http://[::1]/bar") + # p u.hostname #=> "::1" + # p u.host #=> "[::1]" + # + def hostname + v = self.host + /\A\[(.*)\]\z/ =~ v ? $1 : v + end + + # set the host part of the URI as the argument with brackets for IPv6 addresses. + # + # This method is same as URI::Generic#host= except + # the argument can be bare IPv6 address. + # + # u = URI("http://foo/bar") + # p u.to_s #=> "http://foo/bar" + # u.hostname = "::1" + # p u.to_s #=> "http://[::1]/bar" + # + # If the argument seems IPv6 address, + # it is wrapped by brackets. + # + def hostname=(v) + v = "[#{v}]" if /\A\[.*\]\z/ !~ v && /:/ =~ v + self.host = v + end + + # + # check the port +v+ component for RFC2396 compliance + # and against the URI::Parser Regexp for :PORT + # + # Can not have a registry or opaque component defined, + # with a port component defined. + # + def check_port(v) + return v unless v + + if @opaque + raise InvalidURIError, + "can not set port with registry or opaque" + elsif !v.kind_of?(Integer) && parser.regexp[:PORT] !~ v + raise InvalidComponentError, + "bad component(expected port component): #{v.inspect}" + end + + return true + end + private :check_port + + # protected setter for the port component +v+ + # + # see also URI::Generic.port= + # + def set_port(v) + v = v.empty? ? nil : v.to_i unless !v || v.kind_of?(Integer) + @port = v + end + protected :set_port + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the port component +v+. + # (with validation) + # + # see also URI::Generic.check_port + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com") + # uri.port = 8080 + # # => 8080 + # uri + # #=> # + # + def port=(v) + check_port(v) + set_port(v) + port + end + + def check_registry(v) # :nodoc: + raise InvalidURIError, "can not set registry" + end + private :check_registry + + def set_registry(v) #:nodoc: + raise InvalidURIError, "can not set registry" + end + protected :set_registry + + def registry=(v) + raise InvalidURIError, "can not set registry" + end + + # + # check the path +v+ component for RFC2396 compliance + # and against the URI::Parser Regexp + # for :ABS_PATH and :REL_PATH + # + # Can not have a opaque component defined, + # with a path component defined. + # + def check_path(v) + # raise if both hier and opaque are not nil, because: + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + # hier_part = ( net_path | abs_path ) [ "?" query ] + if v && @opaque + raise InvalidURIError, + "path conflicts with opaque" + end + + # If scheme is ftp, path may be relative. + # See RFC 1738 section 3.2.2, and RFC 2396. + if @scheme && @scheme != "ftp" + if v && v != '' && parser.regexp[:ABS_PATH] !~ v + raise InvalidComponentError, + "bad component(expected absolute path component): #{v}" + end + else + if v && v != '' && parser.regexp[:ABS_PATH] !~ v && + parser.regexp[:REL_PATH] !~ v + raise InvalidComponentError, + "bad component(expected relative path component): #{v}" + end + end + + return true + end + private :check_path + + # protected setter for the path component +v+ + # + # see also URI::Generic.path= + # + def set_path(v) + @path = v + end + protected :set_path + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the path component +v+. + # (with validation) + # + # see also URI::Generic.check_path + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com/pub/files") + # uri.path = "/faq/" + # # => "/faq/" + # uri + # #=> # + # + def path=(v) + check_path(v) + set_path(v) + v + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the query component +v+. + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com/?id=25") + # uri.query = "id=1" + # # => "id=1" + # uri + # #=> # + # + def query=(v) + return @query = nil unless v + raise InvalidURIError, "query conflicts with opaque" if @opaque + + x = v.to_str + v = x.dup if x.equal? v + v.encode!(Encoding::UTF_8) rescue nil + v.delete!("\t\r\n") + v.force_encoding(Encoding::ASCII_8BIT) + v.gsub!(/(?!%\h\h|[!$-&(-;=?-_a-~])./n.freeze){'%%%02X' % $&.ord} + v.force_encoding(Encoding::US_ASCII) + @query = v + end + + # + # check the opaque +v+ component for RFC2396 compliance and + # against the URI::Parser Regexp for :OPAQUE + # + # Can not have a host, port, user or path component defined, + # with an opaque component defined. + # + def check_opaque(v) + return v unless v + + # raise if both hier and opaque are not nil, because: + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + # hier_part = ( net_path | abs_path ) [ "?" query ] + if @host || @port || @user || @path # userinfo = @user + ':' + @password + raise InvalidURIError, + "can not set opaque with host, port, userinfo or path" + elsif v && parser.regexp[:OPAQUE] !~ v + raise InvalidComponentError, + "bad component(expected opaque component): #{v}" + end + + return true + end + private :check_opaque + + # protected setter for the opaque component +v+ + # + # see also URI::Generic.opaque= + # + def set_opaque(v) + @opaque = v + end + protected :set_opaque + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the opaque component +v+. + # (with validation) + # + # see also URI::Generic.check_opaque + # + def opaque=(v) + check_opaque(v) + set_opaque(v) + v + end + + # + # check the fragment +v+ component against the URI::Parser Regexp for :FRAGMENT + # + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the fragment component +v+. + # (with validation) + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com/?id=25#time=1305212049") + # uri.fragment = "time=1305212086" + # # => "time=1305212086" + # uri + # #=> # + # + def fragment=(v) + return @fragment = nil unless v + + x = v.to_str + v = x.dup if x.equal? v + v.encode!(Encoding::UTF_8) rescue nil + v.delete!("\t\r\n") + v.force_encoding(Encoding::ASCII_8BIT) + v.gsub!(/(?!%\h\h|[!-~])./n){'%%%02X' % $&.ord} + v.force_encoding(Encoding::US_ASCII) + @fragment = v + end + + # + # Checks if URI has a path + # + def hierarchical? + if @path + true + else + false + end + end + + # + # Checks if URI is an absolute one + # + def absolute? + if @scheme + true + else + false + end + end + alias absolute absolute? + + # + # Checks if URI is relative + # + def relative? + !absolute? + end + + # + # returns an Array of the path split on '/' + # + def split_path(path) + path.split("/", -1) + end + private :split_path + + # + # Merges a base path +base+, with relative path +rel+, + # returns a modified base path. + # + def merge_path(base, rel) + + # RFC2396, Section 5.2, 5) + # RFC2396, Section 5.2, 6) + base_path = split_path(base) + rel_path = split_path(rel) + + # RFC2396, Section 5.2, 6), a) + base_path << '' if base_path.last == '..' + while i = base_path.index('..') + base_path.slice!(i - 1, 2) + end + + if (first = rel_path.first) and first.empty? + base_path.clear + rel_path.shift + end + + # RFC2396, Section 5.2, 6), c) + # RFC2396, Section 5.2, 6), d) + rel_path.push('') if rel_path.last == '.' || rel_path.last == '..' + rel_path.delete('.') + + # RFC2396, Section 5.2, 6), e) + tmp = [] + rel_path.each do |x| + if x == '..' && + !(tmp.empty? || tmp.last == '..') + tmp.pop + else + tmp << x + end + end + + add_trailer_slash = !tmp.empty? + if base_path.empty? + base_path = [''] # keep '/' for root directory + elsif add_trailer_slash + base_path.pop + end + while x = tmp.shift + if x == '..' + # RFC2396, Section 4 + # a .. or . in an absolute path has no special meaning + base_path.pop if base_path.size > 1 + else + # if x == '..' + # valid absolute (but abnormal) path "/../..." + # else + # valid absolute path + # end + base_path << x + tmp.each {|t| base_path << t} + add_trailer_slash = false + break + end + end + base_path.push('') if add_trailer_slash + + return base_path.join('/') + end + private :merge_path + + # + # == Args + # + # +oth+:: + # URI or String + # + # == Description + # + # Destructive form of #merge + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com") + # uri.merge!("/main.rbx?page=1") + # p uri + # # => # + # + def merge!(oth) + t = merge(oth) + if self == t + nil + else + replace!(t) + self + end + end + + # + # == Args + # + # +oth+:: + # URI or String + # + # == Description + # + # Merges two URI's. + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com") + # p uri.merge("/main.rbx?page=1") + # # => # + # + def merge(oth) + rel = parser.send(:convert_to_uri, oth) + + if rel.absolute? + #raise BadURIError, "both URI are absolute" if absolute? + # hmm... should return oth for usability? + return rel + end + + unless self.absolute? + raise BadURIError, "both URI are relative" + end + + base = self.dup + + authority = rel.userinfo || rel.host || rel.port + + # RFC2396, Section 5.2, 2) + if (rel.path.nil? || rel.path.empty?) && !authority && !rel.query + base.fragment=(rel.fragment) if rel.fragment + return base + end + + base.query = nil + base.fragment=(nil) + + # RFC2396, Section 5.2, 4) + if !authority + base.set_path(merge_path(base.path, rel.path)) if base.path && rel.path + else + # RFC2396, Section 5.2, 4) + base.set_path(rel.path) if rel.path + end + + # RFC2396, Section 5.2, 7) + base.set_userinfo(rel.userinfo) if rel.userinfo + base.set_host(rel.host) if rel.host + base.set_port(rel.port) if rel.port + base.query = rel.query if rel.query + base.fragment=(rel.fragment) if rel.fragment + + return base + end # merge + alias + merge + + # :stopdoc: + def route_from_path(src, dst) + case dst + when src + # RFC2396, Section 4.2 + return '' + when %r{(?:\A|/)\.\.?(?:/|\z)} + # dst has abnormal absolute path, + # like "/./", "/../", "/x/../", ... + return dst.dup + end + + src_path = src.scan(%r{[^/]*/}) + dst_path = dst.scan(%r{[^/]*/?}) + + # discard same parts + while !dst_path.empty? && dst_path.first == src_path.first + src_path.shift + dst_path.shift + end + + tmp = dst_path.join + + # calculate + if src_path.empty? + if tmp.empty? + return './' + elsif dst_path.first.include?(':') # (see RFC2396 Section 5) + return './' + tmp + else + return tmp + end + end + + return '../' * src_path.size + tmp + end + private :route_from_path + # :startdoc: + + # :stopdoc: + def route_from0(oth) + oth = parser.send(:convert_to_uri, oth) + if self.relative? + raise BadURIError, + "relative URI: #{self}" + end + if oth.relative? + raise BadURIError, + "relative URI: #{oth}" + end + + if self.scheme != oth.scheme + return self, self.dup + end + rel = URI::Generic.new(nil, # it is relative URI + self.userinfo, self.host, self.port, + nil, self.path, self.opaque, + self.query, self.fragment, parser) + + if rel.userinfo != oth.userinfo || + rel.host.to_s.downcase != oth.host.to_s.downcase || + rel.port != oth.port + + if self.userinfo.nil? && self.host.nil? + return self, self.dup + end + + rel.set_port(nil) if rel.port == oth.default_port + return rel, rel + end + rel.set_userinfo(nil) + rel.set_host(nil) + rel.set_port(nil) + + if rel.path && rel.path == oth.path + rel.set_path('') + rel.query = nil if rel.query == oth.query + return rel, rel + elsif rel.opaque && rel.opaque == oth.opaque + rel.set_opaque('') + rel.query = nil if rel.query == oth.query + return rel, rel + end + + # you can modify `rel', but can not `oth'. + return oth, rel + end + private :route_from0 + # :startdoc: + + # + # == Args + # + # +oth+:: + # URI or String + # + # == Description + # + # Calculates relative path from oth to self + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse('http://my.example.com/main.rbx?page=1') + # p uri.route_from('http://my.example.com') + # #=> # + # + def route_from(oth) + # you can modify `rel', but can not `oth'. + begin + oth, rel = route_from0(oth) + rescue + raise $!.class, $!.message + end + if oth == rel + return rel + end + + rel.set_path(route_from_path(oth.path, self.path)) + if rel.path == './' && self.query + # "./?foo" -> "?foo" + rel.set_path('') + end + + return rel + end + + alias - route_from + + # + # == Args + # + # +oth+:: + # URI or String + # + # == Description + # + # Calculates relative path to oth from self + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse('http://my.example.com') + # p uri.route_to('http://my.example.com/main.rbx?page=1') + # #=> # + # + def route_to(oth) + parser.send(:convert_to_uri, oth).route_from(self) + end + + # + # Returns normalized URI. + # + # require 'uri' + # + # URI("HTTP://my.EXAMPLE.com").normalize + # #=> # + # + # Normalization here means: + # + # * scheme and host are converted to lowercase, + # * an empty path component is set to "/". + # + def normalize + uri = dup + uri.normalize! + uri + end + + # + # Destructive version of #normalize + # + def normalize! + if path.empty? + set_path('/') + end + if scheme && scheme != scheme.downcase + set_scheme(self.scheme.downcase) + end + if host && host != host.downcase + set_host(self.host.downcase) + end + end + + # + # Constructs String from URI + # + def to_s + str = ''.dup + if @scheme + str << @scheme + str << ':' + end + + if @opaque + str << @opaque + else + if @host || %w[file postgres].include?(@scheme) + str << '//' + end + if self.userinfo + str << self.userinfo + str << '@' + end + if @host + str << @host + end + if @port && @port != self.default_port + str << ':' + str << @port.to_s + end + str << @path + if @query + str << '?' + str << @query + end + end + if @fragment + str << '#' + str << @fragment + end + str + end + + # + # Compares two URIs + # + def ==(oth) + if self.class == oth.class + self.normalize.component_ary == oth.normalize.component_ary + else + false + end + end + + def hash + self.component_ary.hash + end + + def eql?(oth) + self.class == oth.class && + parser == oth.parser && + self.component_ary.eql?(oth.component_ary) + end + +=begin + +--- URI::Generic#===(oth) + +=end +# def ===(oth) +# raise NotImplementedError +# end + +=begin +=end + + + # returns an Array of the components defined from the COMPONENT Array + def component_ary + component.collect do |x| + self.send(x) + end + end + protected :component_ary + + # == Args + # + # +components+:: + # Multiple Symbol arguments defined in URI::HTTP + # + # == Description + # + # Selects specified components from URI + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse('http://myuser:mypass@my.example.com/test.rbx') + # p uri.select(:userinfo, :host, :path) + # # => ["myuser:mypass", "my.example.com", "/test.rbx"] + # + def select(*components) + components.collect do |c| + if component.include?(c) + self.send(c) + else + raise ArgumentError, + "expected of components of #{self.class} (#{self.class.component.join(', ')})" + end + end + end + + def inspect + "#<#{self.class} #{self}>" + end + + # + # == Args + # + # +v+:: + # URI or String + # + # == Description + # + # attempts to parse other URI +oth+, + # returns [parsed_oth, self] + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("http://my.example.com") + # uri.coerce("http://foo.com") + # #=> [#, #] + # + def coerce(oth) + case oth + when String + oth = parser.parse(oth) + else + super + end + + return oth, self + end + + # returns a proxy URI. + # The proxy URI is obtained from environment variables such as http_proxy, + # ftp_proxy, no_proxy, etc. + # If there is no proper proxy, nil is returned. + # + # If the optional parameter, +env+, is specified, it is used instead of ENV. + # + # Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.) + # are examined too. + # + # But http_proxy and HTTP_PROXY is treated specially under CGI environment. + # It's because HTTP_PROXY may be set by Proxy: header. + # So HTTP_PROXY is not used. + # http_proxy is not used too if the variable is case insensitive. + # CGI_HTTP_PROXY can be used instead. + def find_proxy(env=ENV) + raise BadURIError, "relative URI: #{self}" if self.relative? + name = self.scheme.downcase + '_proxy' + proxy_uri = nil + if name == 'http_proxy' && env.include?('REQUEST_METHOD') # CGI? + # HTTP_PROXY conflicts with *_proxy for proxy settings and + # HTTP_* for header information in CGI. + # So it should be careful to use it. + pairs = env.reject {|k, v| /\Ahttp_proxy\z/i !~ k } + case pairs.length + when 0 # no proxy setting anyway. + proxy_uri = nil + when 1 + k, _ = pairs.shift + if k == 'http_proxy' && env[k.upcase] == nil + # http_proxy is safe to use because ENV is case sensitive. + proxy_uri = env[name] + else + proxy_uri = nil + end + else # http_proxy is safe to use because ENV is case sensitive. + proxy_uri = env.to_hash[name] + end + if !proxy_uri + # Use CGI_HTTP_PROXY. cf. libwww-perl. + proxy_uri = env["CGI_#{name.upcase}"] + end + elsif name == 'http_proxy' + unless proxy_uri = env[name] + if proxy_uri = env[name.upcase] + warn 'The environment variable HTTP_PROXY is discouraged. Use http_proxy.', uplevel: 1 + end + end + else + proxy_uri = env[name] || env[name.upcase] + end + + if proxy_uri.nil? || proxy_uri.empty? + return nil + end + + if self.hostname + begin + addr = IPSocket.getaddress(self.hostname) + return nil if /\A127\.|\A::1\z/ =~ addr + rescue SocketError + end + end + + name = 'no_proxy' + if no_proxy = env[name] || env[name.upcase] + return nil unless URI::Generic.use_proxy?(self.hostname, addr, self.port, no_proxy) + end + URI.parse(proxy_uri) + end + + def self.use_proxy?(hostname, addr, port, no_proxy) # :nodoc: + no_proxy.scan(/(?!\.)([^:,\s]+)(?::(\d+))?/) {|p_host, p_port| + if !p_port || port == p_port.to_i + if /(\A|\.)#{Regexp.quote p_host}\z/i =~ hostname + return false + elsif addr + begin + return false if IPAddr.new(p_host).include?(addr) + rescue IPAddr::InvalidAddressError + next + end + end + end + } + true + end + end +end +module URI + + # The default port for HTTPS URIs is 443, and the scheme is 'https:' rather + # than 'http:'. Other than that, HTTPS URIs are identical to HTTP URIs; + # see URI::HTTP. + class HTTPS < HTTP + # A Default port of 443 for URI::HTTPS + DEFAULT_PORT = 443 + end + @@schemes['HTTPS'] = HTTPS +end + +module URI + + # + # FTP URI syntax is defined by RFC1738 section 3.2. + # + # This class will be redesigned because of difference of implementations; + # the structure of its path. draft-hoffman-ftp-uri-04 is a draft but it + # is a good summary about the de facto spec. + # http://tools.ietf.org/html/draft-hoffman-ftp-uri-04 + # + class FTP < Generic + # A Default port of 21 for URI::FTP + DEFAULT_PORT = 21 + + # + # An Array of the available components for URI::FTP + # + COMPONENT = [ + :scheme, + :userinfo, :host, :port, + :path, :typecode + ].freeze + + # + # Typecode is "a", "i" or "d". + # + # * "a" indicates a text file (the FTP command was ASCII) + # * "i" indicates a binary file (FTP command IMAGE) + # * "d" indicates the contents of a directory should be displayed + # + TYPECODE = ['a', 'i', 'd'].freeze + + # Typecode prefix + # ';type=' + TYPECODE_PREFIX = ';type='.freeze + + def self.new2(user, password, host, port, path, + typecode = nil, arg_check = true) # :nodoc: + # Do not use this method! Not tested. [Bug #7301] + # This methods remains just for compatibility, + # Keep it undocumented until the active maintainer is assigned. + typecode = nil if typecode.size == 0 + if typecode && !TYPECODE.include?(typecode) + raise ArgumentError, + "bad typecode is specified: #{typecode}" + end + + # do escape + + self.new('ftp', + [user, password], + host, port, nil, + typecode ? path + TYPECODE_PREFIX + typecode : path, + nil, nil, nil, arg_check) + end + + # + # == Description + # + # Creates a new URI::FTP object from components, with syntax checking. + # + # The components accepted are +userinfo+, +host+, +port+, +path+ and + # +typecode+. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the order + # [userinfo, host, port, path, typecode] + # + # If the path supplied is absolute, it will be escaped in order to + # make it absolute in the URI. Examples: + # + # require 'uri' + # + # uri = URI::FTP.build(['user:password', 'ftp.example.com', nil, + # '/path/file.zip', 'i']) + # puts uri.to_s -> ftp://user:password@ftp.example.com/%2Fpath/file.zip;type=i + # + # uri2 = URI::FTP.build({:host => 'ftp.example.com', + # :path => 'ruby/src'}) + # puts uri2.to_s -> ftp://ftp.example.com/ruby/src + # + def self.build(args) + + # Fix the incoming path to be generic URL syntax + # FTP path -> URL path + # foo/bar /foo/bar + # /foo/bar /%2Ffoo/bar + # + if args.kind_of?(Array) + args[3] = '/' + args[3].sub(/^\//, '%2F') + else + args[:path] = '/' + args[:path].sub(/^\//, '%2F') + end + + tmp = Util::make_components_hash(self, args) + + if tmp[:typecode] + if tmp[:typecode].size == 1 + tmp[:typecode] = TYPECODE_PREFIX + tmp[:typecode] + end + tmp[:path] << tmp[:typecode] + end + + return super(tmp) + end + + # + # == Description + # + # Creates a new URI::FTP object from generic URL components with no + # syntax checking. + # + # Unlike build(), this method does not escape the path component as + # required by RFC1738; instead it is treated as per RFC2396. + # + # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+, + # +opaque+, +query+ and +fragment+, in that order. + # + def initialize(scheme, + userinfo, host, port, registry, + path, opaque, + query, + fragment, + parser = nil, + arg_check = false) + raise InvalidURIError unless path + path = path.sub(/^\//,'') + path.sub!(/^%2F/,'/') + super(scheme, userinfo, host, port, registry, path, opaque, + query, fragment, parser, arg_check) + @typecode = nil + if tmp = @path.index(TYPECODE_PREFIX) + typecode = @path[tmp + TYPECODE_PREFIX.size..-1] + @path = @path[0..tmp - 1] + + if arg_check + self.typecode = typecode + else + self.set_typecode(typecode) + end + end + end + + # typecode accessor + # + # see URI::FTP::COMPONENT + attr_reader :typecode + + # validates typecode +v+, + # returns a +true+ or +false+ boolean + # + def check_typecode(v) + if TYPECODE.include?(v) + return true + else + raise InvalidComponentError, + "bad typecode(expected #{TYPECODE.join(', ')}): #{v}" + end + end + private :check_typecode + + # Private setter for the typecode +v+ + # + # see also URI::FTP.typecode= + # + def set_typecode(v) + @typecode = v + end + protected :set_typecode + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # public setter for the typecode +v+. + # (with validation) + # + # see also URI::FTP.check_typecode + # + # == Usage + # + # require 'uri' + # + # uri = URI.parse("ftp://john@ftp.example.com/my_file.img") + # #=> # + # uri.typecode = "i" + # # => "i" + # uri + # #=> # + # + def typecode=(typecode) + check_typecode(typecode) + set_typecode(typecode) + typecode + end + + def merge(oth) # :nodoc: + tmp = super(oth) + if self != tmp + tmp.set_typecode(oth.typecode) + end + + return tmp + end + + # Returns the path from an FTP URI. + # + # RFC 1738 specifically states that the path for an FTP URI does not + # include the / which separates the URI path from the URI host. Example: + # + # +ftp://ftp.example.com/pub/ruby+ + # + # The above URI indicates that the client should connect to + # ftp.example.com then cd pub/ruby from the initial login directory. + # + # If you want to cd to an absolute directory, you must include an + # escaped / (%2F) in the path. Example: + # + # +ftp://ftp.example.com/%2Fpub/ruby+ + # + # This method will then return "/pub/ruby" + # + def path + return @path.sub(/^\//,'').sub(/^%2F/,'/') + end + + # Private setter for the path of the URI::FTP + def set_path(v) + super("/" + v.sub(/^\//, "%2F")) + end + protected :set_path + + # Returns a String representation of the URI::FTP + def to_s + save_path = nil + if @typecode + save_path = @path + @path = @path + TYPECODE_PREFIX + @typecode + end + str = super + if @typecode + @path = save_path + end + + return str + end + end + @@schemes['FTP'] = FTP +end + +module URI + + # + # LDAP URI SCHEMA (described in RFC2255) + # ldap:///[?[?[?[?]]]] + # + class LDAP < Generic + + # A Default port of 389 for URI::LDAP + DEFAULT_PORT = 389 + + # An Array of the available components for URI::LDAP + COMPONENT = [ + :scheme, + :host, :port, + :dn, + :attributes, + :scope, + :filter, + :extensions, + ].freeze + + # Scopes available for the starting point. + # + # * SCOPE_BASE - the Base DN + # * SCOPE_ONE - one level under the Base DN, not including the base DN and + # not including any entries under this. + # * SCOPE_SUB - subtress, all entries at all levels + # + SCOPE = [ + SCOPE_ONE = 'one', + SCOPE_SUB = 'sub', + SCOPE_BASE = 'base', + ].freeze + + # + # == Description + # + # Create a new URI::LDAP object from components, with syntax checking. + # + # The components accepted are host, port, dn, attributes, + # scope, filter, and extensions. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the order + # [host, port, dn, attributes, scope, filter, extensions]. + # + # Example: + # + # newuri = URI::LDAP.build({:host => 'ldap.example.com', + # :dn> => '/dc=example'}) + # + # newuri = URI::LDAP.build(["ldap.example.com", nil, + # "/dc=example;dc=com", "query", nil, nil, nil]) + # + def self.build(args) + tmp = Util::make_components_hash(self, args) + + if tmp[:dn] + tmp[:path] = tmp[:dn] + end + + query = [] + [:extensions, :filter, :scope, :attributes].collect do |x| + next if !tmp[x] && query.size == 0 + query.unshift(tmp[x]) + end + + tmp[:query] = query.join('?') + + return super(tmp) + end + + # + # == Description + # + # Create a new URI::LDAP object from generic URI components as per + # RFC 2396. No LDAP-specific syntax checking is performed. + # + # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+, + # +opaque+, +query+ and +fragment+, in that order. + # + # Example: + # + # uri = URI::LDAP.new("ldap", nil, "ldap.example.com", nil, + # "/dc=example;dc=com", "query", nil, nil, nil, nil) + # + # + # See also URI::Generic.new + # + def initialize(*arg) + super(*arg) + + if @fragment + raise InvalidURIError, 'bad LDAP URL' + end + + parse_dn + parse_query + end + + # private method to cleanup +dn+ from using the +path+ component attribute + def parse_dn + @dn = @path[1..-1] + end + private :parse_dn + + # private method to cleanup +attributes+, +scope+, +filter+ and +extensions+, + # from using the +query+ component attribute + def parse_query + @attributes = nil + @scope = nil + @filter = nil + @extensions = nil + + if @query + attrs, scope, filter, extensions = @query.split('?') + + @attributes = attrs if attrs && attrs.size > 0 + @scope = scope if scope && scope.size > 0 + @filter = filter if filter && filter.size > 0 + @extensions = extensions if extensions && extensions.size > 0 + end + end + private :parse_query + + # private method to assemble +query+ from +attributes+, +scope+, +filter+ and +extensions+. + def build_path_query + @path = '/' + @dn + + query = [] + [@extensions, @filter, @scope, @attributes].each do |x| + next if !x && query.size == 0 + query.unshift(x) + end + @query = query.join('?') + end + private :build_path_query + + # returns dn. + def dn + @dn + end + + # private setter for dn +val+ + def set_dn(val) + @dn = val + build_path_query + @dn + end + protected :set_dn + + # setter for dn +val+ + def dn=(val) + set_dn(val) + val + end + + # returns attributes. + def attributes + @attributes + end + + # private setter for attributes +val+ + def set_attributes(val) + @attributes = val + build_path_query + @attributes + end + protected :set_attributes + + # setter for attributes +val+ + def attributes=(val) + set_attributes(val) + val + end + + # returns scope. + def scope + @scope + end + + # private setter for scope +val+ + def set_scope(val) + @scope = val + build_path_query + @scope + end + protected :set_scope + + # setter for scope +val+ + def scope=(val) + set_scope(val) + val + end + + # returns filter. + def filter + @filter + end + + # private setter for filter +val+ + def set_filter(val) + @filter = val + build_path_query + @filter + end + protected :set_filter + + # setter for filter +val+ + def filter=(val) + set_filter(val) + val + end + + # returns extensions. + def extensions + @extensions + end + + # private setter for extensions +val+ + def set_extensions(val) + @extensions = val + build_path_query + @extensions + end + protected :set_extensions + + # setter for extensions +val+ + def extensions=(val) + set_extensions(val) + val + end + + # Checks if URI has a path + # For URI::LDAP this will return +false+ + def hierarchical? + false + end + end + + @@schemes['LDAP'] = LDAP +end + +module URI + + # The default port for LDAPS URIs is 636, and the scheme is 'ldaps:' rather + # than 'ldap:'. Other than that, LDAPS URIs are identical to LDAP URIs; + # see URI::LDAP. + class LDAPS < LDAP + # A Default port of 636 for URI::LDAPS + DEFAULT_PORT = 636 + end + @@schemes['LDAPS'] = LDAPS +end + +module URI + + # + # RFC6068, The mailto URL scheme + # + class MailTo < Generic + include REGEXP + + # A Default port of nil for URI::MailTo + DEFAULT_PORT = nil + + # An Array of the available components for URI::MailTo + COMPONENT = [ :scheme, :to, :headers ].freeze + + # :stopdoc: + # "hname" and "hvalue" are encodings of an RFC 822 header name and + # value, respectively. As with "to", all URL reserved characters must + # be encoded. + # + # "#mailbox" is as specified in RFC 822 [RFC822]. This means that it + # consists of zero or more comma-separated mail addresses, possibly + # including "phrase" and "comment" components. Note that all URL + # reserved characters in "to" must be encoded: in particular, + # parentheses, commas, and the percent sign ("%"), which commonly occur + # in the "mailbox" syntax. + # + # Within mailto URLs, the characters "?", "=", "&" are reserved. + + # ; RFC 6068 + # hfields = "?" hfield *( "&" hfield ) + # hfield = hfname "=" hfvalue + # hfname = *qchar + # hfvalue = *qchar + # qchar = unreserved / pct-encoded / some-delims + # some-delims = "!" / "$" / "'" / "(" / ")" / "*" + # / "+" / "," / ";" / ":" / "@" + # + # ; RFC3986 + # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + # pct-encoded = "%" HEXDIG HEXDIG + HEADER_REGEXP = /\A(?(?:%\h\h|[!$'-.0-;@-Z_a-z~])*=(?:%\h\h|[!$'-.0-;@-Z_a-z~])*)(?:&\g)*\z/ + # practical regexp for email address + # http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address + EMAIL_REGEXP = /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z/ + # :startdoc: + + # + # == Description + # + # Creates a new URI::MailTo object from components, with syntax checking. + # + # Components can be provided as an Array or Hash. If an Array is used, + # the components must be supplied as [to, headers]. + # + # If a Hash is used, the keys are the component names preceded by colons. + # + # The headers can be supplied as a pre-encoded string, such as + # "subject=subscribe&cc=address", or as an Array of Arrays like + # [['subject', 'subscribe'], ['cc', 'address']] + # + # Examples: + # + # require 'uri' + # + # m1 = URI::MailTo.build(['joe@example.com', 'subject=Ruby']) + # puts m1.to_s -> mailto:joe@example.com?subject=Ruby + # + # m2 = URI::MailTo.build(['john@example.com', [['Subject', 'Ruby'], ['Cc', 'jack@example.com']]]) + # puts m2.to_s -> mailto:john@example.com?Subject=Ruby&Cc=jack@example.com + # + # m3 = URI::MailTo.build({:to => 'listman@example.com', :headers => [['subject', 'subscribe']]}) + # puts m3.to_s -> mailto:listman@example.com?subject=subscribe + # + def self.build(args) + tmp = Util.make_components_hash(self, args) + + case tmp[:to] + when Array + tmp[:opaque] = tmp[:to].join(',') + when String + tmp[:opaque] = tmp[:to].dup + else + tmp[:opaque] = '' + end + + if tmp[:headers] + query = + case tmp[:headers] + when Array + tmp[:headers].collect { |x| + if x.kind_of?(Array) + x[0] + '=' + x[1..-1].join + else + x.to_s + end + }.join('&') + when Hash + tmp[:headers].collect { |h,v| + h + '=' + v + }.join('&') + else + tmp[:headers].to_s + end + unless query.empty? + tmp[:opaque] << '?' << query + end + end + + super(tmp) + end + + # + # == Description + # + # Creates a new URI::MailTo object from generic URL components with + # no syntax checking. + # + # This method is usually called from URI::parse, which checks + # the validity of each component. + # + def initialize(*arg) + super(*arg) + + @to = nil + @headers = [] + + # The RFC3986 parser does not normally populate opaque + @opaque = "?#{@query}" if @query && !@opaque + + unless @opaque + raise InvalidComponentError, + "missing opaque part for mailto URL" + end + to, header = @opaque.split('?', 2) + # allow semicolon as a addr-spec separator + # http://support.microsoft.com/kb/820868 + unless /\A(?:[^@,;]+@[^@,;]+(?:\z|[,;]))*\z/ =~ to + raise InvalidComponentError, + "unrecognised opaque part for mailtoURL: #{@opaque}" + end + + if arg[10] # arg_check + self.to = to + self.headers = header + else + set_to(to) + set_headers(header) + end + end + + # The primary e-mail address of the URL, as a String + attr_reader :to + + # E-mail headers set by the URL, as an Array of Arrays + attr_reader :headers + + # check the to +v+ component + def check_to(v) + return true unless v + return true if v.size == 0 + + v.split(/[,;]/).each do |addr| + # check url safety as path-rootless + if /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*\z/ !~ addr + raise InvalidComponentError, + "an address in 'to' is invalid as URI #{addr.dump}" + end + + # check addr-spec + # don't s/\+/ /g + addr.gsub!(/%\h\h/, URI::TBLDECWWWCOMP_) + if EMAIL_REGEXP !~ addr + raise InvalidComponentError, + "an address in 'to' is invalid as uri-escaped addr-spec #{addr.dump}" + end + end + + true + end + private :check_to + + # private setter for to +v+ + def set_to(v) + @to = v + end + protected :set_to + + # setter for to +v+ + def to=(v) + check_to(v) + set_to(v) + v + end + + # check the headers +v+ component against either + # * HEADER_REGEXP + def check_headers(v) + return true unless v + return true if v.size == 0 + if HEADER_REGEXP !~ v + raise InvalidComponentError, + "bad component(expected opaque component): #{v}" + end + + true + end + private :check_headers + + # private setter for headers +v+ + def set_headers(v) + @headers = [] + if v + v.split('&').each do |x| + @headers << x.split(/=/, 2) + end + end + end + protected :set_headers + + # setter for headers +v+ + def headers=(v) + check_headers(v) + set_headers(v) + v + end + + # Constructs String from URI + def to_s + @scheme + ':' + + if @to + @to + else + '' + end + + if @headers.size > 0 + '?' + @headers.collect{|x| x.join('=')}.join('&') + else + '' + end + + if @fragment + '#' + @fragment + else + '' + end + end + + # Returns the RFC822 e-mail text equivalent of the URL, as a String. + # + # Example: + # + # require 'uri' + # + # uri = URI.parse("mailto:ruby-list@ruby-lang.org?Subject=subscribe&cc=myaddr") + # uri.to_mailtext + # # => "To: ruby-list@ruby-lang.org\nSubject: subscribe\nCc: myaddr\n\n\n" + # + def to_mailtext + to = URI.decode_www_form_component(@to) + head = '' + body = '' + @headers.each do |x| + case x[0] + when 'body' + body = URI.decode_www_form_component(x[1]) + when 'to' + to << ', ' + URI.decode_www_form_component(x[1]) + else + head << URI.decode_www_form_component(x[0]).capitalize + ': ' + + URI.decode_www_form_component(x[1]) + "\n" + end + end + + "To: #{to} +#{head} +#{body} +" + end + alias to_rfc822text to_mailtext + end + + @@schemes['MAILTO'] = MailTo +end diff --git a/gxg/web/gxg_vdom.rb b/gxg/web/gxg_vdom.rb new file mode 100644 index 0000000..cdede3e --- /dev/null +++ b/gxg/web/gxg_vdom.rb @@ -0,0 +1,2673 @@ +module GxG + module Gui + module Vdom + @@CssCache = [] + @@CssRuleProperties = {} + # + def self.load_font(font_profile=nil) + if font_profile.is_any?(::Hash, ::GxG::Database::DetachedHash, ::GxG::Database::DetachedHash) + if font_profile.is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedHash) + font_profile = font_profile.unpersist() + end + if font_profile[:resource_type] == "font-face" + thefamily = (font_profile[:font_family] || "Unknown").to_s + thestyle = (font_profile[:font_style] || "normal").to_s + theweight = (font_profile[:font_weight] || 400).to_s + if font_profile[:format] + fontformat = font_profile[:format].to_s.downcase + else + case File.extname(font_profile[:source]).downcase + when ".ttf", ".otf" + fontformat = "truetype" + when ".eot" + fontformat = "embedded-opentype" + when ".svg" + fontformat = "svg" + when ".woff" + fontformat = "woff" + when ".woff2" + fontformat = "woff2" + else + fontformat = "unknown" + end + end + if ["sftp:","ftp:/","http:", "https"].include?(font_profile[:source][0..4].downcase) + # reach out over the internet for the resource + source_url = font_profile[:source] + else + # search local site at resource path + source_url = ::GxG::CONNECTION.relative_url().to_s + font_profile[:source] + # source_url = GxG::DISPLAY_DETAILS[:object].site_asset(font_profile[:source]) + end + # puts "Source: #{source_url.inspect}" + # + %x{ + var family = #{thefamily.to_s}; + var sourceurl = "url('" + #{source_url} + "')"; + var theformat = "format('" + #{fontformat} + "')"; + if (theformat == "format('unknown')") { + var sourcestring = sourceurl; + } else { + var sourcestring = (sourceurl + " " + theformat); + }; + var theweight = #{theweight}; + var thestyle = #{thestyle}; + var thefont = new FontFace(family, sourceurl, {weight: theweight, style: thestyle}); + thefont.load().then(function(loaded_face) { + document.fonts.add(loaded_face); + }).catch(function(error) { + alert(error); + }); + } + true + else + log_warn("You attempted to load a non-font resource as a font: #{font_profile.inspect}") + false + end + else + log_warn("You attempted to load a font with the wrong value: #{font_profile.inspect}") + false + end + end + # + class CSSRule + # Provide EASY interface to native CSS Rule. + def initialize(the_native_rule) + @native_rule = the_native_rule + @name = (`#{@native_rule}.selectorText` || "").to_s.to_sym + end + # + def native_object() + @native_rule + end + # + def inspect() + "" + end + # + def name() + @name + end + # + def keys() + GxG::Gui::Vdom::valid_properties() + end + # + def [](the_key) + `#{@native_rule}.style[#{the_key}]` + end + # + def []=(the_key, the_value) + # + `#{@native_rule}.style[#{the_key}] = #{(the_value || "")}` + the_value + end + # + def merge(properties={}) + properties.keys.each do |the_key| + self[(the_key)] = properties[(the_key)] + end + self + end + # + end + # + def self.style_sheet() + result = nil + sheets = `document.styleSheets.length`.to_i + (0..(sheets - 1)).each do |sheet_index| + the_sheet = `document.styleSheets[#{sheet_index}]` + if the_sheet + if `#{the_sheet}.href`.to_s.include?("/themes/page.css") + result = the_sheet + break + end + end + end + result + end + # + def self.refresh_css_cache() + the_style_sheet = GxG::Gui::Vdom::style_sheet() + if the_style_sheet + new_cache = [] + begin + ruleset = `#{the_style_sheet}.cssRules` + rescue Exception => the_error + ruleset = `#{the_style_sheet}.rules` + end + if ruleset + rules_count = (`#{ruleset}.length`.to_i) + if rules_count > 0 + end_index = ((`#{ruleset}.length`.to_i) - 1) + else + end_index = 0 + end + (0..(end_index)).each do |indexer| + begin + native_rule = `#{the_style_sheet}.cssRules[#{indexer}]` + rescue Exception => the_error + native_rule = `#{the_style_sheet}.rules[#{indexer}]` + end + if native_rule + new_cache << GxG::Gui::Vdom::CSSRule.new(native_rule) + end + end + end + @@CssCache = new_cache + end + true + end + # + def self.valid_properties() + @@CssRuleProperties.keys + end + # + def self.get_property_name(the_key=nil) + result = nil + if the_key.is_a?(::Symbol) + result = @@CssRuleProperties[(the_key)] + end + result + end + # + def self.rules() + result = [] + @@CssCache.each do |entry| + result << entry.name + end + result + end + # + def self.rule_names() + result = [] + @@CssCache.each do |entry| + result << entry.name + end + result + end + # + def self.find_rule(the_rule_selector=nil) + result = [] + @@CssCache.each do |entry| + if entry.name == (the_rule_selector.to_s.to_sym) + result << entry + end + end + result + end + # + def self.clear_rules() + list = ::GxG::Gui::Vdom::rule_names() + list.each do |the_rule_name| + unless the_rule_name == :".hidden" + ::GxG::Gui::Vdom::remove_rule(the_rule_name) + end + end + true + end + # + def self.set_rule(the_rule_selector=nil, the_properties={}, options={:merge => true}) + # find existing + # remove existing + # create or re-create + the_style_sheet = GxG::Gui::Vdom::style_sheet() + if the_style_sheet + found = GxG::Gui::Vdom::find_rule(the_rule_selector) + if found.size > 0 + GxG::Gui::Vdom::remove_rule(the_rule_selector) + end + # create it + the_site_prefix = ::GxG::CONNECTION.relative_url() + rule_string = "{ " + the_properties.keys.each do |property_key| + the_value = the_properties[(property_key)].to_s + if the_value.include?("url(") + end_point = the_value.index(")") + the_path = the_value[4..(end_point - 1)] + if the_path[0] == "'" || the_path[0] == '"' + the_path = the_path[1..-1] + end + if the_path[-1] == "'" || the_path[-1] == '"' + the_path = the_path[0..-2] + end + unless the_path.include?("data:") || the_path.include?("http:") || the_path.include?("https:") + unless (the_path[(0..(the_site_prefix.size - 1))] == the_site_prefix) + the_value = ("url('" + File.expand_path(the_site_prefix + "/" + the_path) + "')").gsub("//","/") + end + end + end + rule_string = rule_string + ("#{property_key.to_s}: #{the_value.to_s}; ") + end + rule_string = rule_string + " }" + if `#{the_style_sheet}.insertRule(#{the_rule_selector.to_s + rule_string.to_s},(#{the_style_sheet}.cssRules.length))`.to_i == `(#{the_style_sheet}.cssRules.length - 1)`.to_i + begin + native_rule = `#{the_style_sheet}.cssRules[(#{the_style_sheet}.cssRules.length - 1)]` + rescue Exception => the_error + native_rule = `#{the_style_sheet}.rules[(#{the_style_sheet}.cssRules.length - 1)]` + end + @@CssCache << GxG::Gui::Vdom::CSSRule.new(native_rule) + true + else + log_warn("GxG Style Sheet Rule creation failed..") + false + end + else + log_warn("GxG Style Sheet not found.") + false + end + # + end + # + def self.remove_rule(the_rule_selector=nil) + found_indexes = [] + @@CssCache.each_with_index do |entry, index| + if entry.name == the_rule_selector.to_s.to_sym || entry.name == the_rule_selector.to_s + found_indexes << index + end + end + found = [] + found_indexes.reverse.each do |the_index| + found << @@CssCache.delete_at(the_index) + end + if found.size > 0 + found.each do |the_rule| + native_rule = the_rule.native_object() + the_sheet = GxG::Gui::Vdom::style_sheet() + if the_sheet + rule_count = `#{the_sheet}.cssRules.length`.to_i + if rule_count > 0 + (0..(rule_count - 1)).to_a.reverse.each do |rule_index| + other_native_rule = `#{the_sheet}.cssRules[#{rule_index}]` + if other_native_rule + if `#{native_rule}.selectorText == #{other_native_rule}.selectorText` + `#{the_sheet}.deleteRule(#{rule_index})` + end + end + end + end + end + end + true + else + false + end + end + # + # Derivitive of Ferro Virtual Dom gem (greatly simplified) + # -------------------------------------------------------------------------------------------------------------------------------------- + module ElementMethods + # Internal Methods: + def _before_create;end + def before_create;end + # ## Calls the factory to create the DOM element. + def create + if @domtype + @element = factory.create_element(self, @domtype, @parent, @options) + end + end + # + def _after_create + self.update_classes + _stylize + end + # + def after_create;end + # Override this method to return a Hash of styles. + # Hash-key is the CSS style name, hash-value is the CSS style value. + def style + @style ||= {} + end + def _stylize + styles = style + + if styles.class == Hash + set_attribute( + 'style', + styles.map { |k, v| "#{k}:#{v};" }.join + ) + end + end + # Override this method to continue the MOM creation process. + def cascade;end + # ## Creation + def creation + _before_create + before_create + create + _after_create + after_create + _stylize + cascade + end + # Add a child element. + # + # @param [String] name A unique name for the element that is not + # in RESERVED_NAMES + # @param [String] element_class Ruby class name for the new element + # @param [Hash] options Options to pass to the element. Any option key + # that is not recognized is set as an attribute on the DOM element. + # Recognized keys are: + # prepend Prepend the new element before this DOM element + # content Add the value of content as a textnode to the DOM element + def add_child(options={}) + new_child = nil + if options[:component].to_s.downcase.to_sym == :unknown + raise "Unknown Component Type" + else + element_class = GxG::Gui::component_class(options[:component].to_s.downcase.to_sym) + if element_class.is_a?(::Class) + new_child = element_class.new(self,options) + @children[(new_child.title.to_s.to_sym)] = new_child + # Register Object with Page + ::GxG::DISPLAY_DETAILS[:object].register_object(new_child.title, new_child) + else + raise "Unknown Component Type" + end + end + new_child + end + # + def each_child(&block) + @children.values.each do |the_child| + block.call(the_child) + end + end + # Recursively iterate all child elements + def all_descendants(&block) + @process_queue = @children.values.clone + while @process_queue.size > 0 do + entry = @process_queue.shift + if entry + block.call(entry) + if entry.children.values.size > 0 + entry.each_child do |the_child| + @process_queue << the_child + end + end + end + end + end + # Remove all child entries. + def forget_children + clearing_queue = @children.values + while clearning_queue.size > 0 + entry = clearing_queue.shift + if entry + entry.children.values.each do |the_child| + clearing_queue << the_child + end + page.unregister_object(entry.title.to_s) + end + end + @children = {} + end + # + # Remove a specific child entry. + def forget_child(the_title) + page.unregister_object(the_title) + @children.delete(the_title.to_s.to_sym) + end + # Remove a DOM element. + def destroy + self.every_child do |the_child| + the_child.remove_listeners + page.unregister_object(the_child.title.to_s) + end + self.remove_listeners + parent.forget_child(@title.to_s) + `#{parent.element}.removeChild(#{element})` + true + end + + # Getter for children. + def method_missing(method_name, *args, &block) + if @children.has_key?(method_name) + @children[method_name] + else + super + end + end + # ### Element identity + # Get the id of the elements corresponding DOM element + # + # @return [String] The id + def dom_id + # "_#{self.object_id}" + self.uuid.to_s + end + # + def title() + @title + end + # + # def title=(the_title=nil) + # if the_title.is_any?(::String, ::Symbol) + # @title = the_title + # end + # end + # + # ### Universal Element Style, Class, and Attribute Methods: + def update_classes() + new_classes = "" + class_list = `#{@element}.className`.split(" ") + class_list.each_with_index do |the_classname, indexer| + unless @states.keys.include?(the_classname.to_s.to_sym) + new_classes = (new_classes + the_classname) + unless the_classname == class_list.last + new_classes = (new_classes + " ") + end + end + end + if @states.keys.size > 0 + new_classes = (new_classes + " ") + end + @states.each_pair do |the_state, the_value| + if the_value == true + new_classes = (new_classes + the_state.to_s) + unless @states.keys.last == the_state + new_classes = (new_classes + " ") + end + else + # `#{@element}.classList.remove(#{the_state.to_s})` + end + end + `#{@element}.className = #{new_classes.to_s}` + true + end + # Delete a key from the elements options hash. Will be renamed + # to option_delete. + # + # @param [key] key Key of the option hash to be removed + # @param [value] default Optional value to use if option value is nil + # @return [value] Return the current option value or value of + # default parameter + def option_replace(key, default = nil) + value = @options[key] || default + @options.delete(key) if @options.has_key?(key) + value + end + # + def set_state(the_state, the_value=false) + if the_value == true + @states[(the_state.to_s.to_sym)] = true + else + @states[(the_state.to_s.to_sym)] = false + end + self.update_classes + end + # + def set_states(the_state_list={}) + the_state_list.each_pair do |the_state, the_value| + if the_value == true + @states[(the_state.to_s.to_sym)] = true + else + @states[(the_state.to_s.to_sym)] = false + end + end + self.update_classes + end + # + def get_state(the_state) + (@states[(the_state.to_s.to_sym)] || false) + end + # + def get_states() + @states + end + # + def toggle_state(the_state) + self.set_state(the_state,(! self.get_state(the_state))) + end + # Determine if the state is active + # + # @param [String] state The state name + # @return [Boolean] The state value + def state_active?(the_state) + @states.keys.include?(the_state.to_s.to_sym) + end + # @param [Array] states An array of state names to add to the + # element. All disabled (false) state initially. + def add_states(states) + states.each do |state| + add_state(state) + end + end + # Add a state to the element. A state toggles a CSS class with the + # same (dasherized) name as the state. + # If the state is thruthy (not nil or true) the CSS class is added + # to the element. Otherwise the CSS class is removed. + # + # @param [String] state The state name to add to the element + # @param [value] value The initial enabled/disabled state value + def add_state(state, value = false) + self.set_state(state,value) + end + # Get the current html value of the element + # + # @return [String] The html value + def value + `#{@element}.innerHTML` + end + + # Get the current text content of the element + # + # @return [String] The text value + def get_text + `#{@element}.textContent` + end + + # Set the current html value of the element + # Useful for input elements + # + # @param [String] The new value + def value=(value) + `#{@element}.value = #{value}` + end + + # Set the current text content of the element + # + # @param [String] value The new text value + def set_text(value) + # https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent + `#{@element}.textContent = #{value}` + end + # Set the current html content of the element. Use with caution if + # the html content is not trusted, it may be invalid or contain scripts. + # + # @param [String] raw_html The new html value + def html(raw_html) + `#{@element}.innerHTML = #{raw_html}` + end + # + # + def set_style(the_style={}) + new_style = "" + if the_style.is_a?(::Hash) + the_style.each_pair do |property,value| + new_style = (new_style + "#{property.to_s}: " + "#{value.to_s};") + end + `#{@element}.setAttribute("style", #{new_style}); ` + true + else + false + end + end + # + def merge_style(the_style={}) + if the_style.is_a?(::Hash) + the_style.each_pair do |property,value| + `#{element}.style[#{property}]=#{value.to_s}` + end + true + else + false + end + end + # + def set_bg_color(the_color=0) + self.merge_style({:"background-color" => "##{the_color.to_s}"}) + end + # + def get_attribute(the_attribute=nil) + result = "" + if the_attribute + the_element = self.element + the_value = nil + # Boolean Attributes: if attribute exists, can be a boolean. + # if present returns "true" if null (since it exists), else other value + %x{ + if (the_element.getAttributeNames().includes(the_attribute)) { + if (the_element.getAttribute(the_attribute) != null) { + the_value = the_element.getAttribute(the_attribute); + } else { + the_value = true; + }; + }; + } + result = the_value.to_s + end + result + end + # + def set_attribute(the_attribute=nil, the_value=nil) + result = false + if the_attribute + if the_value + result = `#{self.element}.setAttribute(#{the_attribute},#{the_value})` + else + # Boolean attribute + result = `#{self.element}.setAttribute(#{the_attribute},null)` + end + end + result + end + # + def clear_attribute(the_attribute=nil) + result = false + if the_attribute + `#{self.element}.removeAttribute(#{the_attribute})` + result = true + end + result + end + def each_child(&block) + if block.respond_to?(:call) + @children.values.each do |the_child| + block.call(the_child) + end + else + @children.values.each + end + end + # + def every_child(&block) + child_manifest = [] + build_queue = [(self)] + while build_queue.size > 0 do + entry = build_queue.shift + if entry + if entry.children.keys.size > 0 + entry.children.values.each do |the_child| + child_manifest << the_child + if the_child.children.keys.size > 0 + build_queue << the_child + end + end + end + end + end + if block.respond_to?(:call) + child_manifest.each do |the_child| + block.call(the_child) + end + else + child_manifest.values.each + end + end + # ### Finding Objects + def find_child(the_reference=nil) + result = nil + search_queue = self.children.values + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.uuid == the_reference || entry.title == the_reference || entry.component == the_reference || entry.class == the_reference || entry.domtype == the_reference + result = entry + break + end + # + end + end + result + end + # + def find_descendant(the_reference=nil) + result = nil + search_queue = self.children.values + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.uuid == the_reference || entry.title == the_reference || entry.component == the_reference || entry.class == the_reference || entry.domtype == the_reference + result = entry + break + end + # + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def find_descendants(the_reference=nil) + result = [] + search_queue = self.children.values + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.uuid == the_reference || entry.title == the_reference || entry.component == the_reference || entry.class == the_reference || entry.domtype == the_reference + result << entry + end + # + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + result + end + # + def find_ancestor(the_reference=nil) + result = nil + search_queue = [(self.parent)] + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.uuid == the_reference || entry.title == the_reference || entry.component == the_reference || entry.class == the_reference || entry.domtype == the_reference + result = entry + break + else + unless entry.component == :page + search_queue << entry.parent + end + end + end + end + result + end + # + def find_ancestors(the_reference=nil) + result = [] + search_queue = [(self.parent)] + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.uuid == the_reference || entry.title == the_reference || entry.component == the_reference || entry.class == the_reference || entry.domtype == the_reference + result << entry + end + unless entry.component == :page + search_queue << entry.parent + end + end + end + result + end + # + end + # -------------------------------------------------------------------------------------------------------------------------------------- + module EventHandlers + # Constants: + EVENT_TYPES = [ + "abort", + "afterprint", + "animatedend", + "animationiteration", + "animationstart", + "beforeprint", + "beforeunload", + "blur", + "canplay", + "canplaythrough", + "change", + "click", + "contextmenu", + "copy", + "cut", + "dblclick", + "drag", + "dragend", + "dragcenter", + "dragleave", + "dragover", + "dragstart", + "drop", + "durationchange", + "ended", + "error", + "focus", + "focusin", + "focusout", + "fullscreenchange", + "fullscreenerror", + "hashchange", + "input", + "invalid", + "keydown", + "keypress", + "keyup", + "load", + "loadeddata", + "loadedmetadata", + "loadstart", + "message", + "mousedown", + "mouseenter", + "mouseleave", + "mousemove", + "mouseover", + "mouseup", + "offline", + "online", + "open", + "pagehide", + "pageshow", + "paste", + "play", + "playing", + "popstate", + "progress", + "ratechange", + "resize", + "reset", + "scroll", + "search", + "seeked", + "select", + "show", + "stalled", + "storage", + "submit", + "suspend", + "timeupdate", + "toggle", + "touchcancel", + "touchend", + "touchmove", + "touchstart", + "transitionend", + "unload", + "volumechange", + "waiting", + "wheel" + ] + # + EVENT_GENRES = { + :click => :mouse, + :contextmenu => :mouse, + :dblclick => :mouse, + :mouseenter => :mouse, + :mouseleave => :mouse, + :mouseover => :mouse, + :mouseup => :mouse, + :mousedown => :mouse, + :mousemove => :mouse, + :mouseout => :mouse, + :keydown => :keyboard, + :keypress => :keyboard, + :keyup => :keyboard, + :touchcancel => :touch, + :touchmove => :touch, + :touchstart => :touch, + :touchend => :touch, + :animationstart => :animation, + :animationend => :animation, + :animationiteration => :animation, + :animationpause => :animation, + :animationresume => :animation, + :animationstop => :animation, + :input => :input, + :drag => :drag, + :dragend => :drag, + :dragenter => :drag, + :dragleave => :drag, + :dragover => :drag, + :dragstart => :drag, + :drop => :drag, + :wheel => :wheel, + :abort => :ui, + :beforeunload => :ui, + :error => :ui, + :load => :ui, + :resize => :ui, + :scroll => :ui, + :select => :ui, + :unload => :ui, + :blur => :focus, + :focus => :focus, + :focusin => :focus, + :focusout => :focus, + :afterprint => :event, + :beforeprint => :event, + :canplay => :event, + :canplaythrough => :event, + :change => :event, + :fullscreenchange => :event, + :fullscreenerror => :event, + :invalid => :event, + :loadeddata => :event, + :loadedmetadata => :event, + :message => :event, + :offline => :event, + :online => :event, + :open => :event, + :close => :event, + :pause => :event, + :play => :event, + :playing => :event, + :progress => :event, + :changerate => :event, + :reset => :event, + :search => :event, + :seek => :event, + :seeking => :event, + :show => :event, + :hide => :event, + :stalled => :event, + :submit => :event, + :suspend => :event, + :timeupdate => :event, + :toggle => :event, + :waiting => :event, + :copy => :clipboard, + :cut => :clipboard, + :paste => :clipboard, + :pagehide => :transition, + :pageshow => :transition, + :popstate => :history, + :loadstart => :progress, + :storage => :storage, + :credentialchanged => :ui + } + #TODO: look into fully supporting touch screen gleaning functions. + EVENT_FIELDS = { + :mouse => [:altKey, :button, :buttons, :clientX, :clientY, :ctrlKey, :metaKey, :movementX, :movementY, :offsetX, :offsetY, :pageX, :pageY, :screenX, :screenY, :shiftKey, :which, :detail, :view, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :keyboard => [:altKey, :charCode, :code, :ctrlKey, :metaKey, :isComposing, :key, :keyCode, :location, :shift, :repeat, :shiftKey, :which, :detail, :view, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :touch => [:altKey, :ctrlKey, :metaKey, :shiftKey, :detail, :view, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :animation => [:animationName, :elapsedTime, :pseudoElement, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :input => [:data, :inputType, :isComposing, :detail, :view, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :drag => [:dataTranser, :altKey, :button, :buttons, :clientX, :clientY, :ctrlKey, :metaKey, :movementX, :movementY, :offsetX, :offsetY, :pageX, :pageY, :screenX, :screenY, :shiftKey, :which, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :wheel => [:deltaX, :deltaY, :deltaZ, :deltaMode, :altKey, :button, :buttons, :clientX, :clientY, :ctrlKey, :metaKey, :movementX, :movementY, :offsetX, :offsetY, :pageX, :pageY, :screenX, :screenY, :shiftKey, :which, :detail, :view, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :ui => [:detail, :view,:bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :focus => [:relatedTarget, :detail, :view, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :event => [:bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :transition => [:persisted, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :history => [:state, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :progress => [:lengthComputable, :loaded, :total, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :storage => [:key, :newValue, :oldValue, :storageArea, :url, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp], + :clipboard => [:clipboardData, :bubbles, :cancelable, :currentTarget, :defaultPrevented, :eventPhase, :isTrusted, :target, :timeStamp] + } + # Internal method on + def on(the_event_type=nil, captured=false, &block) + result = false + unless @event_handlers.is_a?(::Hash) + @event_handlers = {} + @event_listeners = {} + end + if the_event_type.is_any?(::String, ::Symbol) + if block.respond_to?(:call) + @event_handlers[(the_event_type.to_s.downcase.to_sym)] = block + @event_listeners[(the_event_type.to_s.downcase.to_sym)] = `#{self.method(:event_handler).to_proc}` + `#{self.element}.addEventListener(#{the_event_type.to_s.downcase}, #{@event_listeners[(the_event_type.to_s.downcase.to_sym)]}, #{captured})` + result = true + end + end + result + end + # + def remove_listener(the_event_type=nil) + if @event_handlers[(the_event_type.to_s.downcase.to_sym)] && @event_listeners[(the_event_type.to_s.downcase.to_sym)] + `#{self.element}.removeEventListener(#{the_event_type.to_s.downcase},#{@event_listeners[(the_event_type.to_s.downcase.to_sym)]});` + @event_handlers.delete(the_event_type.to_s.downcase.to_sym) + @event_listeners.delete(the_event_type.to_s.downcase.to_sym) + end + true + end + # + def remove_listeners() + @event_handlers.keys.clone.each do |the_event_type| + self.remove_listener(the_event_type) + end + true + end + # + def event_handler(the_event) + # format the event record for travel in the system. + # See: https://www.w3schools.com/jsref/dom_obj_event.asp + # Setup event type groupings to glean appropriate details from the event. + the_type = `#{the_event}.type`.to_s.downcase.to_sym + event_record = {} + if @event_handlers[(the_type)] + genre = (GxG::Gui::Vdom::EventHandlers::EVENT_GENRES[(the_type)] || :event) + if genre + fields = GxG::Gui::Vdom::EventHandlers::EVENT_FIELDS[(genre)] + if fields + # + %x{ + serializer = function thisOne (e) { + if (e) { + var o = { + eventName: e.toString(), + altKey: e.altKey, + bubbles: e.bubbles, + button: e.button, + buttons: e.buttons, + cancelBubble: e.cancelBubble, + cancelable: e.cancelable, + clientX: e.clientX, + clientY: e.clientY, + composed: e.composed, + ctrlKey: e.ctrlKey, + currentTarget: e.currentTarget ? e.currentTarget.outerHTML : null, + defaultPrevented: e.defaultPrevented, + detail: e.detail, + eventPhase: e.eventPhase, + fromElement: e.fromElement ? e.fromElement.outerHTML : null, + isTrusted: e.isTrusted, + layerX: e.layerX, + layerY: e.layerY, + metaKey: e.metaKey, + movementX: e.movementX, + movementY: e.movementY, + offsetX: e.offsetX, + offsetY: e.offsetY, + pageX: e.pageX, + pageY: e.pageY, + relatedTarget: e.relatedTarget ? e.relatedTarget.outerHTML : null, + returnValue: e.returnValue, + screenX: e.screenX, + screenY: e.screenY, + shiftKey: e.shiftKey, + sourceCapabilities: e.sourceCapabilities ? e.sourceCapabilities.toString() : null, + target: e.target ? e.target.outerHTML : null, + timeStamp: e.timeStamp, + toElement: e.toElement ? e.toElement.outerHTML : null, + type: e.type, + view: e.view ? e.view.toString() : null, + which: e.which, + x: e.x, + y: e.y + }; + return o; + }; + }; + event_record = JSON.stringify(serializer(#{the_event})); + } + event_record = ::JSON::parse(event_record.to_s, {:symbolize_names => true}) + # puts "Got: #{event_record.inspect}" + end + end + # + @event_handlers[(the_type)].call(event_record) + # + end + end + # + def set_script(the_script_body="", alternate_target=nil) + result = false + if the_script_body.size > 0 + # + begin + # + if the_script_body.base64? + the_script_body = the_script_body.decode64 + end + # + if alternate_target + alternate_target.remove_listeners() + alternate_target.instance_eval(the_script_body) + else + self.remove_listeners() + instance_eval(the_script_body) + end + result = true + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:script => the_script_body}}) + end + end + result + end + # + end + # -------------------------------------------------------------------------------------------------------------------------------------- + class Animation + include GxG::Gui::Vdom::EventHandlers + # + def initialize(details={}, the_uuid=nil) + @event_handlers = {} + @event_listeners = {} + if details.is_any?(::Hash, ::GxG::Database::DetachedHash) + if details[:origin] + if details[:origin].is_a?(::GxG::Database::DetachedHash) + @origin = details[:origin].unpersist + else + @origin = details[:origin] + end + else + @origin = nil + end + # + unless details[:destination] + raise ArgumentError, "You MUST provide a :destination state for the object." + end + # + if details[:destination].is_a?(::GxG::Database::DetachedHash) + @destination = details[:destination].unpersist + else + @destination = details[:destination] + end + # + if details[:options] + @options = {} + if details[:options].is_a?(::GxG::Database::DetachedHash) + the_options = details[:options].unpersist + else + the_options = details[:options] + end + callback_map = {:animationstart => :start, :animationend => :complete, :animationiteration => :update, :animationstop => :stop, :animationpause => :pause, :animationresume => :resume} + the_options.each_pair do |the_key, the_value| + if callback_map.keys.include?(the_key) + if the_value.respond_to?(:call) + # Expansion: compile formatted hashes of script records ?? + @event_handlers[(the_key)] = the_value + @options[(the_key)] = the_value + else + log_warn("Attempted to pass an invalid object as an event handler. #{the_key.inspect} --> #{the_value.inspect}") + end + else + @options[(the_key)] = the_value + end + end + else + @options = nil + end + # + unless details[:targets] + raise ArgumentError, "You MUST provide a one or more :targets." + end + # + if details[:targets].is_a?(::GxG::Database::DetachedHash) + @target_list = details[:targets].unpersist + else + @target_list = details[:targets] + end + end + @uuid = (the_uuid || ::GxG::uuid_generate).to_s.to_sym + @targets = nil + @tweens = [] + # + self + end + # + def update_targets(data=nil) + result = false + if data.is_any?(::Hash, ::GxG::Database::DetachedHash) + if data.is_a?(::GxG::Database::DetachedHash) + @target_list = data.unpersist + else + @target_list = data + end + result = true + end + result + end + # + def update_origin(data=nil) + result = false + if data.is_any?(::Hash, ::GxG::Database::DetachedHash) + if data.is_a?(::GxG::Database::DetachedHash) + @origin = data.unpersist + else + @origin = data + end + result = true + end + result + end + # + def update_destination(data=nil) + result = false + if data.is_any?(::Hash, ::GxG::Database::DetachedHash) + if data.is_a?(::GxG::Database::DetachedHash) + @destination = data.unpersist + else + @destination = data + end + result = true + end + result + end + # + def update_options(data=nil) + result = false + if data.is_any?(::Hash, ::GxG::Database::DetachedHash) + if data.is_a?(::GxG::Database::DetachedHash) + the_options = data.unpersist + else + the_options = data + end + @options = {} + callback_map = {:animationstart => :start, :animationend => :complete, :animationiteration => :update, :animationstop => :stop, :animationpause => :pause, :animationresume => :resume} + the_options.each_pair do |the_key, the_value| + if callback_map.keys.include?(the_key) + if the_value.respond_to?(:call) + # Expansion: compile formatted hashes of script records ?? + @event_handlers[(the_key)] = the_value + @options[(the_key)] = the_value + else + log_warn("Attempted to pass an invalid object as an event handler. #{the_key.inspect} --> #{the_value.inspect}") + end + else + @options[(the_key)] = the_value + end + end + result = true + end + result + end + # + def uuid() + @uuid + end + # + # + def compile() + result = false + begin + @targets = [] + native_targets = `[]` + @target_list.keys.each do |the_method| + if the_method == :class + unless @target_list[:class].is_a?(::String) + raise Exception, "You MUST provide :class target as a String, not: #{@target_list[:class].class.inspect}" + end + @target_list[:class].split(" ").each do |the_classname| + if the_classname.size > 0 + found_list = `document.getElementsByClassName(#{the_classname})` + found_length = `#{found_list}.length`.to_i + if found_length > 0 + (0..(found_length - 1)).each do |the_index| + the_element = `#{found_list}.item(#{the_index})` + unless @targets.include?(the_element) + @targets << the_element + end + end + end + end + end + end + # + if the_method == :objects + # provide a list of UUIDs of components to include as targets + unless @target_list[:objects].is_a?(::Array) + raise Exception, "You MUST provide :object target list as an Array, not: #{@target_list[:objects].class.inspect}" + end + @target_list[:objects].each do |the_reference| + found_object = GxG::DISPLAY_DETAILS[:object].find_object(the_reference.to_s.to_sym) + if found_object + element_id = `#{found_object.element}.attributes.getNamedItem('id') || 0` + unless `#{element_id} == 0` + found_element = `document.getElementById(#{element_id})` + if found_element + unless @targets.include?(found_element) + @targets << found_element + end + end + end + end + end + end + if the_method == :elements + # provide a list of native elements to include as targets + unless @target_list[:elements].is_a?(::Array) + raise Exception, "You MUST provide :elements target list as an Array, not: #{@target_list[:elements].class.inspect}" + end + @target_list[:elements].each do |the_element| + element_id = `#{the_element}.attributes.getNamedItem('id') || 0` + unless `#{element_id} == 0` + element_id = `#{element_id}.value` + found_element = `document.getElementById(#{element_id})` + if found_element + unless @targets.include?(found_element) + @targets << found_element + end + end + end + end + end + if the_method == :assets + # Future expansion + end + end + # DEBUG: + unless @targets.size > 0 + puts "No valid targets resulted." + end + @targets.each do |the_element| + `#{native_targets}.push(#{the_element})` + end + # + if @origin.is_a?(::Hash) + if @origin.size > 0 + from_state = {} + @origin.each_pair do |the_key, the_value| + # key translations: + if the_key == :attribue || the_key == "attribute" + new_key = :attr + else + new_key = the_key + end + # + from_state[(new_key)] = the_value + end + else + from_state = nil + end + else + from_state = nil + end + # + if @destination.size > 0 + to_state = {} + @destination.each_pair do |the_key, the_value| + # key translations: + if the_key == :attribue || the_key == "attribute" + new_key = :attr + else + new_key = the_key + end + # + to_state[(new_key)] = the_value + end + else + raise Exception, "You MUST define a valid :destination state. not: #{@destination.inspect}" + end + # + if @options.is_a?(::Hash) + if @options.size > 0 + options = {} + callback_map = {:animationstart => :start, :animationend => :complete, :animationiteration => :update, :animationstop => :stop, :animationpause => :pause, :animationresume => :resume} + @options.each_pair do |the_key, the_value| + # Key translations: + if callback_map.keys.include?(the_key) + # include handlers + new_key = callback_map[(the_key)] + # + if @event_handlers[(the_key)].respond_to?(:call) + # FIXME + # puts "Setting callback for: #{new_key.inspect}" + options[(new_key)] = @event_handlers[(the_key)] + end + else + unless [:start_time].include?(the_key) + # TODO: research ALL options and only allow valid options to be set. + options[(the_key)] = the_value + end + end + end + else + options = nil + end + else + options = nil + end + # + @tweens = [] + # FIXME: + # puts "Compiling Options: #{options.inspect}" + @targets.each do |the_target| + if from_state + if options + @tweens << `KUTE.fromTo(#{the_target}, #{from_state.to_n}, #{to_state.to_n}, #{options.to_n})` + else + @tweens << `KUTE.fromTo(#{the_target}, #{from_state.to_n}, #{to_state.to_n})` + end + else + if options + @tweens << `KUTE.to(#{the_target},#{to_state.to_n}, #{options.to_n})` + else + @tweens << `KUTE.to(#{the_target},#{to_state.to_n})` + end + end + end + # + result = true + rescue Exception => the_error + log_error({:error => the_error}) + @targets = nil + @tweens = [] + end + result + end + # + def start(the_time=nil) + unless @tweens.size > 0 + self.compile + end + unless the_time.is_a?(::Numeric) + the_time = `window.performance.now()` + end + if @tweens.size > 0 + @tweens.each do |the_tween| + `#{the_tween}.start(#{the_time})` + end + end + end + # + def stop() + if @tweens.size > 0 + @tweens.each do |the_tween| + `#{the_tween}.stop()` + end + end + end + # + def pause() + if @tweens.size > 0 + @tweens.each do |the_tween| + `#{the_tween}.pause()` + end + end + end + # + def resume() + if @tweens.size > 0 + @tweens.each do |the_tween| + `#{the_tween}.resume()` + end + end + end + # + def included() + # return a list of all page components included in this animation + result = [] + if @targets + @targets.each do |the_element| + element_id = `#{the_element}.attributes.getNamedItem('id') || 0` + unless `#{element_id} == 0` + element_id = `#{element_id}.value` + found_object = GxG::DISPLAY_DETAILS[:object].find_child(element_id.to_s) + if found_object + result << found_object + end + end + end + end + result + end + # + def each_included(&block) + if block.respond_to?(:call) + self.included.each do |the_object| + block.call(the_object) + end + else + self.included.each + end + end + # send events to all target components (not their elements) + def broadcast(the_method=nil, the_parameters=nil) + self.each_included do |the_object| + the_object.send(the_method, the_parameters) + end + end + # + end + # -------------------------------------------------------------------------------------------------------------------------------------- + module AnimationSupport + # + def animate(animation_parameters=nil) + if animation_parameters[:destination].is_any?(::Hash, ::GxG::Database::DetachedHash) + the_animation = ::GxG::Gui::Vdom::Animation.new({:targets => {:elements => [(self.element)]}, :origin => animation_parameters[:origin], :destination => animation_parameters[:destination], :options => animation_parameters[:options]}) + start_time = `window.performance.now()` + if animation_parameters[:options].is_any?(::Hash, ::GxG::Database::DetachedHash) + start_time = animation_parameters[:options][:start_time] + end + # FIXME: + # GxG::ANIMATION[:animations][(the_animation.uuid)] = the_animation + # + the_animation.compile + the_animation.start(start_time) + true + else + log_warn("Ignoring invalid animation: NO valid :destination state specified (required). not: #{animation_parameters[:destination].inspect}") + false + end + end + # ------------------------------------------------------------------------------ + def show(transition_parameters=nil,&block) + if transition_parameters.is_any?(::Hash, ::GxG::Database::DetachedHash) + if transition_parameters.is_a?(::GxG::Database::DetachedHash) + transition_parameters = transition_parameters.unpersist + end + # + if transition_parameters[:destination].is_a?(::Hash) + if transition_parameters[:destination][:opacity].is_a?(::Numeric) + if transition_parameters[:origin].is_a?(::Hash) + if transition_parameters[:origin][:opacity].is_a?(::Numeric) + self.merge_style({:opacity => transition_parameters[:origin][:opacity]}) + else + self.merge_style({:opacity => 0}) + end + else + self.merge_style({:opacity => 0}) + end + end + end + self.set_state(:hidden, false) + if block.respond_to?(:call) + unless transition_parameters[:options] + transition_parameters[:options] = {} + end + transition_parameters[:options][:animationend] = Proc.new do + block.call() + end + end + # + self.animate(transition_parameters) + else + self.set_state(:hidden, false) + if block.respond_to?(:call) + block.call() + end + end + # GxG::DISPLAY_DETAILS[:object].layout_refresh + ::GxG::Gui::layout_refresh + true + end + # + def hide(transition_parameters=nil,&block) + if transition_parameters.is_any?(::Hash, ::GxG::Database::DetachedHash) + if transition_parameters.is_a?(::GxG::Database::DetachedHash) + transition_parameters = transition_parameters.unpersist + end + if block.respond_to?(:call) + unless transition_parameters[:options] + transition_parameters[:options] = {} + end + transition_parameters[:options][:animationend] = Proc.new do + self.set_state(:hidden, true) + block.call() + end + else + set_state = nil + if transition_parameters[:options] + if transition_parameters[:options][:animationend].respond_to?(:call) + old_complete = transition_parameters[:options][:animationend] + set_state = Proc.new do + old_complete.call() + self.set_state(:hidden, true) + end + else + set_state = Proc.new do + self.set_state(:hidden, true) + end + end + else + transition_parameters[:options] = {} + set_state = Proc.new do + self.set_state(:hidden, true) + end + end + if set_state + unless transition_parameters[:options].is_a?(::Hash) + transition_parameters[:options] = {} + end + transition_parameters[:options][:animationend] = set_state + end + end + # + self.animate(transition_parameters) + else + self.set_state(:hidden, true) + if block.respond_to?(:call) + block.call() + end + end + # GxG::DISPLAY_DETAILS[:object].layout_refresh + ::GxG::Gui::layout_refresh + true + end + # + end + # -------------------------------------------------------------------------------------------------------------------------------------- + module ApplicationSupport + # + def find_parent_type(the_type=nil) + # return the first parent of type: the_type (Class, String, Symbol) + result = nil + if the_type.is_any?(::Class, ::String, ::Symbol) + if the_type.is_any?(::String, ::Symbol) + the_type = GxG::Gui::component_class(the_type.to_s.to_sym) + end + if the_type.is_a?(::Class) + search_queue = [(self)] + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.is_a?(the_type) + result = entry + break + end + search_queue << entry.parent + end + end + end + end + result + end + # + def viewport() + # find nearest viewport in object heirarchy. + self.find_parent_type(::GxG::Gui::ApplicationViewport) + end + # + def tree_node() + # find nearest :tree_node parent in object heirarchy. + self.find_parent_type(::GxG::Gui::TreeNode) + end + # + def tree() + # find nearest :tree parent in object heirarchy. + self.find_parent_type(::GxG::Gui::Tree) + end + # + def window() + # find nearest window in object heirarchy. + self.find_parent_type(::GxG::Gui::Window) + end + # + def find_parent(the_reference=nil) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + search_queue = [(self.parent)] + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + search_queue << entry.parent + end + end + end + result + end + # + def find_child_type(the_type=nil) + # return the first parent of type: the_type (Class, String, Symbol) + result = nil + if the_type.is_any?(::Class, ::String, ::Symbol) + if the_type.is_any?(::String, ::Symbol) + the_type = GxG::Gui::component_class(the_type.to_s.to_sym) + end + if the_type.is_a?(::Class) + search_queue = self.children.values + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.is_a?(the_type) + result = entry + break + end + end + end + end + end + result + end + # + def find_child(the_reference=nil) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + search_queue = self.children.values + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def find_peer(the_reference=nil) + self.parent.find_child(the_reference) + end + # + end + # -------------------------------------------------------------------------------------------------------------------------------------- + module ThemeSupport + def theme_icon(image_name=nil) + GxG::DISPLAY_DETAILS[:object].theme_icon(image_name) + end + # + def theme_background(image_name=nil) + GxG::DISPLAY_DETAILS[:object].theme_background(image_name) + end + # + def theme_widget(image_name=nil) + GxG::DISPLAY_DETAILS[:object].theme_widget(image_name) + end + # + def theme_font(font_name=nil) + GxG::DISPLAY_DETAILS[:object].theme_font(font_name) + end + # Asset retrieval support: + def site_asset(asset_path=nil) + GxG::DISPLAY_DETAILS[:object].site_asset(asset_path) + end + end + # -------------------------------------------------------------------------------------------------------------------------------------- + # + # Create DOM elements. + class Factory + + attr_reader :body + + # Creates the factory. Do not create a factory directly, instead + # call the 'factory' method that is available in all Ferro classes + # + # @param [Object] target The Ruby class instance + # @param [Compositor] compositor A style-compositor object or nil + def initialize(target) + @body = `document.body` + %x{ + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + }; + } + # composite_classes(target, @body, false) + end + + # Create a DOM element. + # + # @param [Object] target The Ruby class instance + # @param [String] type Type op DOM element to create + # @param [String] parent The Ruby parent element + # @param [Hash] options Options to pass to the element. + # See FerroElementary::add_child + # @return [String] the DOM element + def create_element(target, type, parent, options = {}) + # Create element + if type == :body + element = `document.body` + else + element = `document.createElement(#{type})` + end + + # Add element to DOM + unless type == :body + if options[:prepend] == true + `#{parent.element}.insertBefore(#{element}, #{options[:prepend].element})` + else + `#{parent.element}.appendChild(#{element})` + end + end + + # if !@compositor + # # Add ruby class to the node + `#{element}.classList.add(#{dasherize(target.class.name)})` + + # # Add ruby superclass to the node to allow for more generic styling + # if target.class.superclass != BaseElement + # `#{element}.classList.add(#{dasherize(target.class.superclass.name)})` + # end + # else + # # Add classes defined by compositor + # composite_classes(target, element, target.class.superclass != BaseElement) + # end + + # Set ruby object_id as default element id + if !options.has_key?(:id) + `#{element}.id = #{target.uuid.to_s}` + end + + # Set attributes + # Review : alter to support set_attribute toolbox and Foundation + options.each do |name, value| + case name + when :prepend + nil + when :content + `#{element}.appendChild(document.createTextNode(#{value}))` + else + `#{element}.setAttribute(#{name}, #{value})` + end + end + + element + end + + # Convert a Ruby classname to a dasherized name for use with CSS. + # + # @param [String] class_name The Ruby class name + # @return [String] CSS class name + def dasherize(class_name) + the_name = class_name.gsub("::","-").downcase.gsub("_","-") + if the_name[0] == "-" + the_name = the_name[(1..-1)] + end + the_name + end + # Convert a CSS classname to a camelized Ruby class name. + # + # @param [String] class_name CSS class name + # @return [String] A Ruby class name + def camelize(class_name) + return class_name if class_name !~ /-/ + class_name.gsub(/(?:-|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.strip + end + # Convert a state-name to a list of CSS class names. + # + # @param [String] class_name Ruby class name + # @param [String] state State name + # @return [String] A list of CSS class names + # def composite_state(class_name, state) + # if @compositor + # list = @compositor.css_classes_for("#{class_name}::#{state}") + # return list if !list.empty? + # end + + # [ dasherize(state) ] + # end + + # Internal method + # Composite CSS classes from Ruby class name + # def composite_classes(target, element, add_superclass) + # if @compositor + # composite_for(target.class.name, element) + + # if add_superclass + # composite_for(target.class.superclass.name, element) + # end + # end + # end + + # Internal method + # def composite_for(classname, element) + # @compositor.css_classes_for(classname).each do |name| + # `#{element}.classList.add(#{name})` + # end + # end + end + # End Factory + class BaseElement + + include GxG::Gui::Vdom::ElementMethods + include GxG::Gui::Vdom::EventHandlers + include GxG::Gui::Vdom::AnimationSupport + include GxG::Gui::Vdom::ApplicationSupport + include GxG::Gui::Vdom::ThemeSupport + + attr_reader :uuid, :title, :component, :parent, :children, :element, :domtype, :settings + def set_settings(the_settings=nil) + @settings = the_settings + end + alias :settings= :set_settings + # Create the element and continue the creation + # process (casading). + # Ferro elements should not be instanciated directly. Instead the + # parent element should call the add_child method. + # + # @param [String] parent The parent Ruby element + # @param [Hash] options Any options for the creation process + def initialize(parent, options = {}) + if options.is_a?(GxG::Database::DetachedHash) + @uuid = options.uuid.to_s.to_sym + @title = options.title.to_s + else + @uuid = (options[:uuid] || ::GxG::uuid_generate()).to_s.to_sym + @title = (options[:title] || "Untitled Component #{@uuid.to_s}").to_s + end + @component = (options[:component] || :unknown).to_s.downcase.to_sym + @parent = parent + @settings = options[:settings] + @children = {} + @element = nil + @domtype = :div + # ### CSS states + @states = {} + @states[(@component.to_s.gsub(".","-").to_sym)] = true + if options[:options].is_a?(::Hash, GxG::Database::DetachedHash) + if options[:options][:states].is_a?(::Array, GxG::Database::DetachedArray) + options[:options][:states].each do |the_state| + @states[(the_state.to_s.to_sym)] = true + end + end + end + # ### Style + @style = {} + if options[:options].is_a?(::Hash, GxG::Database::DetachedHash) + if options[:options][:style].is_a?(::Hash, GxG::Database::DetachedHash) + @style = options[:options][:style] + end + end + # ### Other Attributes + @options = {} + if options[:options].is_a?(::Hash, GxG::Database::DetachedHash) + options[:options].each_pair do |the_option, the_value| + unless [:"background-color", :icon, :label, :data, :content, :uuid, :title, :opacity, :window_title, :menu, :top, :right, :bottom, :left, :height, :width, :close, :minimize, :maximize, :resize, :scroll, :modal, :track, :states, :style].include?(the_option.to_s.to_sym) + @options[(the_option.to_s.to_sym)] = the_value.to_s + end + end + end + # Event Stuff + @event_handlers = {} + @event_listeners = {} + # + creation + # + if options[:options].is_a?(::Hash, GxG::Database::DetachedHash) + if options[:content].is_a?(::String) + self.set_text(options[:content]) + end + end + # Review : return self ?? + self + end + # + def parent() + @parent + end + # Searches the element hierarchy upwards until the root element is found. + def root + result = nil + search_queue = [(@parent)] + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if entry.component == :page + result = entry + break + else + search_queue << entry.parent() + end + end + end + result + end + # Searches the element hierarchy upwards until the factory is found + def factory + self.root.factory + end + # + def build(content_manifest=nil, options={}) + page.build_components(self, content_manifest, options) + end + # + end + # End BaseElement + class BaseInputElement < ::GxG::Gui::Vdom::BaseElement + def _before_create + super() + @domtype = :input + @options[:type] ||= :text + if @options[:content] + @options[:value] = @options.delete(:content) + end + @disabled = @options[:disabled] || false + end + # Getter method for input value. + def value + `#{@element}.value` + end + # Setter method for input value. + def value=(value) + `#{@element}.value = #{value}` + end + # + def _after_create + super() + if @disabled + disable + end + end + # Disable this input. + def disable + set_attribute(:disabled, "disabled") + end + # Enable this input. + def enable + clear_attribute(:disabled) + end + end + # End BaseInputElement + class BaseClickableElement < ::GxG::Gui::Vdom::BaseInputElement + def _before_create + super() + # on(:click) do |event| + # `document.activeElement.blur()` + # end + end + # Review : alter default click behavior ?? Requires setting on(:click);`document.activeElement.blur()`;end sequence. + end + # End BaseClickableElement + # + # This is the entry point for any Ferro application. + # It represents the top level object of the + # Master Object Model (MOM). + # There should be only one class that inhertits + # from FerroDocument in an application. + # This class attaches itself to the DOM + # `document.body` object. + # Any existing child nodes of `document.body` are removed. + class Page + include GxG::Gui::Vdom::ElementMethods + include GxG::Gui::Vdom::EventHandlers + include GxG::Gui::Vdom::AnimationSupport + include GxG::Gui::Vdom::ApplicationSupport + include GxG::Gui::Vdom::ThemeSupport + # Universal attribute readers: + attr_reader :uuid, :title, :component, :parent, :children, :domtype, :settings + def set_settings(the_settings=nil) + @settings = the_settings + end + alias :settings= :set_settings + # Page-specific attribute readers (if not covered by a method): + # ### Loading Libraries + def get_library(the_reference=nil) + result = nil + if the_reference.is_any?(::Hash, ::GxG::Database::DetachedHash) + GxG::APPLICATIONS[:libraries].values.each do |the_library| + if the_library.title == the_reference[:library].to_s && the_library.type == (the_reference[:type] || the_library.type) + if the_reference[:maximum] + if ((the_reference[:minimum].to_f)..(the_reference[:maximum].to_f)).include?(the_library.version) + result = the_library + break + end + else + if the_library.version >= the_reference[:minimum].to_f + result = the_library + break + end + end + end + end + else + if GxG::APPLICATIONS[:libraries][(the_reference.to_s.to_sym)] + result = GxG::APPLICATIONS[:libraries][(the_reference.to_s.to_sym)] + else + GxG::APPLICATIONS[:libraries].values.each do |the_library| + if the_library.title == the_reference + result = the_library + break + end + end + end + end + result + end + # + def library_loaded?(the_reference=nil) + if the_reference.is_any?(::Hash, ::GxG::Database::DetachedHash) + found = false + GxG::APPLICATIONS[:libraries].values.each do |the_library| + if the_library.title == the_reference[:library].to_s && the_library.type == (the_reference[:type] || the_library.type).to_s + if the_reference[:maximum] + if ((the_reference[:minimum].to_f)..(the_reference[:maximum].to_f)).include?(the_library.version) + found = the_library.loaded?() + break + end + else + if the_library.version >= the_reference[:minimum].to_f + found = the_library.loaded?() + break + end + end + end + end + found + else + if GxG::APPLICATIONS[:libraries][(the_reference.to_s.to_sym)] + true + else + found = false + GxG::APPLICATIONS[:libraries].values.each do |the_library| + if the_library.title == the_reference + found = the_library.loaded?() + break + end + end + found + end + end + end + # + def load_library(criteria=nil, &block) + if criteria.is_any?(::Hash, ::GxG::Database::DetachedHash) + # {:library => "lib-name", :manifest => true/false, :dependencies => true/false, :minimum => 0.0, :maximum => 0.0, :loaded => [...]} + if criteria[:maximum] + maximum = criteria[:maximum].to_f + else + maximum = nil + end + GxG::CONNECTION.library_pull({:library => criteria[:library].to_s, :type => (criteria[:type] || "text/ruby").to_s, :minimum => (criteria[:minimum] || 0.0).to_f, :maximum => maximum,:dependencies => true, :loaded => GxG::APPLICATIONS[:libraries].keys}) do |response| + # process response[:result] + if response.is_a?(::Hash) + if response[:result].is_a?(::Array) + response[:result].each do |the_record| + unless GxG::APPLICATIONS[:libraries].keys.include?(the_record[:uuid].to_s.to_sym) + definition = the_record[:data] + if definition.is_a?(::String) + if definition.base64? + definition = definition.decode64 + end + if definition.json? + definition = ::JSON::parse(definition,{:symbolize_names => true}) + end + if definition.is_a?(::Hash) + definition = ::GxG::Database::process_import(definition) + end + if definition.is_a?(::GxG::Database::DetachedHash) + GxG::APPLICATIONS[:libraries][(the_record[:uuid].to_s.to_sym)] = ::GxG::Library.new(definition) + if block.respond_to?(:call) + block.call(GxG::APPLICATIONS[:libraries][(the_record[:uuid].to_s.to_sym)]) + end + end + end + end + end + end + end + end + # + end + end + # + def load_libraries(load_queue=[], &block) + result = true + if load_queue.is_any?(::Array, ::GxG::Database::DetachedArray) + load_queue.each do |the_requirement| + GxG::DISPLAY_DETAILS[:object].load_library(the_requirement) do |the_library| + unless the_library.loaded? + log_warn("Unable to load library: #{the_library.title}") + result = false + break + end + end + end + # + end + if block.respond_to?(:call) + block.call(result) + end + result + end + # ### Application Supports: + def application_open(specifier={}, parameters={}) + result = false + if specifier.is_a?(::Hash) + error_hander = Proc.new { + # self.set_busy(false) + } + self.set_busy(true) + GxG::CONNECTION.application_open(specifier,error_hander) do |app_stub| + if app_stub.is_a?(::Hash) + if app_stub[:result].is_a?(::Hash) + GxG::CONNECTION.pull_object(app_stub[:result][:location]) do |source_object| + GxG::APPLICATIONS[:processes][(app_stub[:result][:application].to_s.to_sym)] = GxG::Application.new(app_stub[:result].merge({:source => source_object})) + # + result = GxG::APPLICATIONS[:processes][(app_stub[:result][:application].to_s.to_sym)].run(parameters) + # + self.set_busy(false) + end + else + log_warn('Error opening Application: ' + app_stub.inspect) + end + else + log_warn("Malformed response: #{app_stub.inspect}") + end + end + else + log_warn("Invalid Argument: #{specifier.inspect}") + end + result + end + # ### Theme Supports: + def site_asset(asset_path=nil) + if (`window.location['href']`).include?("https://") + host_prefix = ("https://" + `window.location['host']`) + else + host_prefix = ("http://" + `window.location['host']`) + end + if ::GxG::CONNECTION.relative_url().to_s[0] != "/" + host_prefix = (host_prefix + "/") + end + if asset_path + (host_prefix + File.expand_path((::GxG::CONNECTION.relative_url().to_s + "/" + asset_path.to_s))) + else + "" + end + end + # + def theme_icon(image_name=nil) + if image_name + (@theme_prefix + "/icons/" + image_name.to_s) + else + "" + end + end + # + def theme_background(image_name=nil) + if image_name + (@theme_prefix + "/backgrounds/" + image_name.to_s) + else + "" + end + end + # + def theme_widget(image_name=nil) + if image_name + (@theme_prefix + "/widgets/" + image_name.to_s) + else + "" + end + end + # + def theme_font(font_name=nil) + if font_name + (@theme_prefix + "/fonts/" + font_name.to_s) + else + "" + end + end + # + def theme_window_title_layout() + {:left => [:close, :minimize, :maximize], :center => [:title], :right => []} + end + # ### Component Building + def build_components(root_object=nil, content_manifest=nil, options={}) + result = false + # + unless root_object + root_object = ::GxG::DISPLAY_DETAILS[:object] + end + # + if root_object && content_manifest.is_any?(::Array, ::GxG::Database::DetachedArray) + build_queue = [{:parent => root_object, :content => content_manifest}] + while build_queue.size > 0 do + record = build_queue.shift + if record + record[:content].each do |the_entry| + new_object = record[:parent].add_child(the_entry) + if the_entry[:content].size > 0 + build_queue << {:parent => new_object, :content => the_entry[:content]} + end + # Embed new object settings + # new_object.set_settings(the_entry[:settings]) + # Link viewport to provided application + if the_entry[:component].to_s.downcase.to_sym == :application_viewport && options[:application] + new_object.set_application(options[:application]) + unless options[:application].get_viewport(new_object.uuid) + options[:application].link_viewport(new_object) + end + end + # Link window to provided application + if the_entry[:component].to_s.downcase.to_sym == :window && options[:application] + options[:application].link_window(new_object) + new_object.set_application(options[:application]) + end + # Link dialog_box to provided application + if the_entry[:component].to_s.downcase.to_sym == :dialog_box && options[:application] + new_object.set_application(options[:application]) + end + # Window Content Tracking + if record[:parent].is_a?(::GxG::Gui::Window) + if the_entry[:options][:track] + record[:parent].track_deltas(new_object.uuid, new_object, the_entry[:options][:track]) + end + end + end + end + end + result = true + end + # + result + end + # + def build(content_manifest=nil, options={}) + self.build_components(self, content_manifest, options) + end + # ### Page Building + def build_page(page_definition=nil) + # ### Example page_definition : + # page_format = { + # :component => "page", + # :requirements => [], + # :auto_start => [], + # :settings => {:page_title => "Untitled Page", :theme => "default", :accesskey => "", :contenteditable => false, :dir => "ltr", :draggable => false, :dropzone => "", :lang => "en", :spellcheck => false, :translate => "no"}, + # :options => {:style => {}, :states => ["page"], :tabindex => 0}, + # :script => "", + # :content => [] + # } + # + if page_definition.is_any?(::GxG::Database::DetachedHash, ::GxG::Database::DetachedHash) + # ### Universal component attributes: + @element = `document.body` + @settings = page_definition[:settings] + @theme_prefix = (::GxG::CONNECTION.relative_url() + "/themes/#{(@settings[:theme] || 'default')}") + @uuid = page_definition.uuid.to_s.to_sym + `#{@element}.id = #{@uuid.to_s}` + @title = @settings[:page_title].to_s + `document.title = #{@title.to_s}` + @component = :"org.gxg.gui.page" + @parent = self + @children = {} + @domtype = :body + # ### CSS states + @states = {:"org-gxg-gui-page" => true} + if page_definition[:options].is_any?(::Hash, ::GxG::Database::DetachedHash, ::GxG::Database::DetachedHash) + if page_definition[:options][:states].is_any?(::Array, ::GxG::Database::PersistedArray, ::GxG::Database::DetachedArray) + page_definition[:options][:states].each do |the_state| + @states[(the_state.to_s.to_sym)] = true + end + end + end + # ### Style + @style = {} + if page_definition[:options].is_any?(::Hash, ::GxG::Database::DetachedHash, ::GxG::Database::DetachedHash) + if page_definition[:options][:style].is_any?(::Hash, ::GxG::Database::DetachedHash, ::GxG::Database::DetachedHash) + @style = page_definition[:options][:style] + end + end + # ### Other Attributes + @options = {} + if page_definition[:options].is_any?(::Hash, ::GxG::Database::DetachedHash, ::GxG::Database::DetachedHash) + page_definition[:options].each_pair do |the_option, the_value| + unless [:"background-color", :data, :icon, :label, :content, :uuid, :title, :opacity, :window_title, :menu, :top, :right, :bottom, :left, :height, :width, :close, :minimize, :maximize, :resize, :scroll, :modal, :track, :states, :style].include?(the_option.to_s.to_sym) + @options[(the_option.to_s.to_sym)] = the_value.to_s + end + end + end + # ### Event Stuff + @event_handlers = {} + @event_listeners = {} + # ### Commit Base Page Build + creation + # ### Set Script if any + if page_definition[:script].is_a?(::String) + if page_definition[:script].size > 0 + self.set_script(page_definition[:script]) + end + end + # ### Commit Component Builds + self.build_components(self, page_definition[:content]) + # + end + end + # Create the document and start the creation process (casading). + def initialize(page_definition=nil) + ::GxG::DISPLAY_DETAILS[:object] = self + ::GxG::DISPLAY_DETAILS[:object].on :onbeforeunload do |event| + ::GxG::DISPLAY_DETAILS[:socket].close + # ::GxG::CONNECTION.close + end + ::GxG::DISPLAY_DETAILS[:object].on :beforeunload do |event| + ::GxG::DISPLAY_DETAILS[:socket].close + # ::GxG::CONNECTION.close + end + ::GxG::DISPLAY_DETAILS[:object].on :onunload do |event| + ::GxG::DISPLAY_DETAILS[:socket].close + # ::GxG::CONNECTION.close + end + @theme_prefix = (::GxG::CONNECTION.relative_url() + "/themes/#{::GxG::DISPLAY_DETAILS[:theme]}") + @settings = nil + @busy = false + @window_switcher = nil + @window_registry = {} + @dialog = nil + @dialog_queue = [] + @title_registry = {} + self.build_page(page_definition) + # Page-specific attributes: + # Review : go over object formats again and then lay out additional instance vars. + # + self + end + # ### Object Registration and Search + def find_object_by_dom_id(the_id=nil) + result = nil + if the_id.is_a?(::Numeric) + the_id = ("_" + the_id.to_i.to_s()) + else + the_id = the_id.to_s + end + search_queue = [(self)] + while search_queue.size > 0 do + entry = search_queue.shift + + attrib = `#{entry.element()}.attributes.getNamedItem('id') || 0` + unless `#{attrib} == 0` + attrib = `#{attrib}.value` + if `(#{attrib.to_s} == #{the_id.to_s})` + result = entry + break + end + end + if entry.respond_to?(:children) + entry.children.keys.each do |the_child_key| + attrib = `#{entry.children[(the_child_key)].element()}.attributes.getNamedItem('id') || 0` + unless `#{attrib} == 0` + attrib = `#{attrib}.value` + if `(#{attrib.to_s} == #{the_id.to_s})` + result = entry.children[(the_child_key)] + break + else + search_queue << entry.children[(the_child_key)] + end + end + end + end + end + result + end + # + def find_object(object_name=nil) + result = nil + search_queue = [(self)] + while search_queue.size > 0 do + entry = search_queue.shift + if entry.respond_to?(:children) + if entry.children[(object_name)] + result = entry.children[(object_name)] + break + else + entry.children.keys.each do |the_child| + search_queue << entry.children[(the_child)] + end + end + end + end + result + end + # + def find_object_by(the_title=nil) + result = nil + if GxG::valid_uuid?(the_title) + uuid_check = true + else + uuid_check = false + end + @title_registry.keys.each do |the_uuid| + if uuid_check == true + if @title_registry[(the_uuid)].to_s.include?(the_title.to_s) + result = self.find_object(the_uuid) + break + end + else + if the_title == @title_registry[(the_uuid)] + result = self.find_object(the_uuid) + break + end + end + end + result + end + # + def register_object(the_title=nil, the_object=nil) + if the_title && the_object + @title_registry[(the_object.uuid)] = the_title.to_s + true + else + false + end + end + # + def unregister_object(the_title=nil) + if the_title + the_object = self.find_object_by(the_title) + if the_object + if the_object.is_a?(::GxG::Gui::ApplicationViewport) + the_object.detach() + end + @title_registry.delete(the_object.uuid) + end + true + else + false + end + end + # Returns the one and only instance of the factory. + def factory + @factory ||= GxG::Gui::Vdom::Factory.new(self) + end + # Returns the DOM element. + def element() + factory.body + end + def parent() + self + end + # The document class is the root element. + def root() + self + end + # + # ### Display Helpers + def domclear() + %x{ + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + }; + } + true + end + # + def clear() + self.all_descendants do |the_child| + the_child.remove_listeners() + end + self.remove_listeners() + self.each_child do |the_child| + the_child.destroy() + end + the_attributes = `document.body.getAttributeNames()` + the_attributes.each do |the_attribute| + self.clear_attribute(the_attribute) + end + true + end + # + end + # End Page + def self.load_page(page_object=nil) + if ::GxG::DISPLAY_DETAILS[:object].is_any?(::GxG::Gui::Page, ::GxG::Gui::Vdom::Page) + ::GxG::DISPLAY_DETAILS[:object].clear() + ::GxG::Gui::Vdom::clear_rules() + ::GxG::Gui::Vdom::refresh_css_cache() + # + end + # Load current theme CSS rules + the_theme = (page_object[:settings][:theme] || "default") + ::GxG::CONNECTION.pull_object("/Public/www/content/themes/#{the_theme}") do |response| + if response.is_a?(::Array) + # puts "Response (Theme Load): #{response.inspect}" + # Resources + response[0][:resources].each do |resource_profile| + if resource_profile[:resource_type] == "font-face" + ::GxG::Gui::Vdom::load_font(resource_profile) + end + end + # Rules + # puts "set css rules" + response[0][:rules].each_pair do |rule_name, rule_properties| + ::GxG::Gui::Vdom::set_rule(rule_name, rule_properties) + end + # Build Page + # puts "build page" + if ::GxG::DISPLAY_DETAILS[:object].is_any?(::GxG::Gui::Page, ::GxG::Gui::Vdom::Page) + ::GxG::DISPLAY_DETAILS[:object].build_page(page_object) + else + ::GxG::DISPLAY_DETAILS[:object] = ::GxG::Gui::Page.new(page_object) + end + # self.layout_refresh + ::GxG::Gui::layout_refresh + else + log_error({:error => Exception.new("Invalid theme object as response.")}) + end + end + # Load Libraries + # puts "load libraries" + if page_object[:requirements].is_any?(::Array, ::GxG::Database::DetachedArray, ::GxG::Database::PersistedArray) + if page_object[:requirements].size > 0 + self.load_libraries(page_object[:requirements]) + end + end + # Load Applications + # puts "launch applications" + if page_object[:auto_start].is_any?(::Array, ::GxG::Database::DetachedArray, ::GxG::Database::PersistedArray) + if page_object[:auto_start].size > 0 + page_object[:auto_start].each do |the_requirement| + self.application_open({:name => the_requirement[:name], :restore => true}) + end + end + end + # + true + end + # + def self.navigate_to(the_location="") + # ### Fetch page content and set build options + # + if ::GxG::CONNECTION.relative_url().to_s.size > 0 + request_path = the_location[(::GxG::CONNECTION.relative_url().size)..-1] + else + request_path = the_location + end + if request_path == "/" || request_path == "" + request_path = "/index" + end + unless request_path[0] == "/" + request_path = ("/" + request_path) + end + ::GxG::DISPLAY_DETAILS[:host_path] = request_path + if GxG::PAGES[(request_path)] + GxG::Gui::Vdom::load_page(GxG::PAGES[(request_path)]) + else + error_handler = Proc.new { |error| + log_error error.inspect + } + # Load Page Content + ::GxG::CONNECTION.pull_object("/Public/www/content/pages#{request_path}",error_handler) do |response| + puts "Response (Page Load): #{response.inspect}" + if response.is_any?(::Array) + GxG::PAGES[(request_path)] = response[0] + GxG::Gui::Vdom::load_page(GxG::PAGES[(request_path)]) + else + log_error({:error => Exception.new("Invalid page object as response.")}) + end + # self.layout_refresh + ::GxG::Gui::layout_refresh + end + end + # + end + end + end +end \ No newline at end of file diff --git a/gxg/web/gxg_windows.rb b/gxg/web/gxg_windows.rb new file mode 100644 index 0000000..5bb5d75 --- /dev/null +++ b/gxg/web/gxg_windows.rb @@ -0,0 +1,2283 @@ +module GxG + module Gui + # Viewport + class ApplicationViewport < ::GxG::Gui::Vdom::BaseElement + def set_application(the_application=nil) + @application = the_application + end + alias :application= :set_application + # + def detach() + if @application + if @application.respond_to?(:unlink_viewport) + @application.unlink_viewport(@uuid) + @application = nil + end + end + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.application.viewport", ::GxG::Gui::ApplicationViewport) + # ### Window support + class Window < ::GxG::Gui::Vdom::BaseElement + # + # Methods to override in window's script + def before_close(data=nil) + end + # + def close(data=nil) + end + # + def after_close(data=nil) + end + # + def before_open(data=nil) + end + # + def open(data=nil) + end + # + def after_open(data=nil) + end + # + def set_application(the_application) + @application = the_application + end + alias :application= :set_application + # + def application() + @application + end + # + # Support methods: + def commit_settings() + # conform to layout restrictions: + bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + # convert to % ?? + top_percent = (@top.to_f / bounds[:page_height].to_f) * 100.0 + left_percent = (@left.to_f / bounds[:page_width].to_f) * 100.0 + height_percent = (self.height.to_f / bounds[:page_height].to_f) * 100.0 + width_percent = (self.width.to_f / bounds[:page_width].to_f) * 100.0 + # + setting = {} + # + setting[:position] = "absolute" + setting[:display] = "block" + # setting[:overflow] = "hidden" + setting[:tabindex] = -1 + setting[:top] = "#{top_percent}%" + setting[:left] = "#{left_percent}%" + setting[:height] = "#{height_percent}%" + setting[:width] = "#{width_percent}%" + # + self.gxg_merge_style(setting) + if @title_object + @title_object.parent.gxg_merge_style({:padding => "0px 0px 0px #{((self.width() / 2) - ((@window_title.size / 2) * 8)) - 60}px"}) + end + if @content + @content.gxg_merge_style({:height => "#{self.height - (@title_margin + @menu_margin + @resize_margin)}px", :width => "#{self.width - (@resize_margin * 2)}px"}) + # update tracking objects + @tracking.each_pair do |selector, record| + new_style = {} + # + record[:details].each_pair do |op,value| + case op.to_s.to_sym + when :width + if value.to_s.include?("%") + # convert this % to px value width in new_style + new_style[:width] = (((value.to_s.gsub("%","").to_f / 100.0) * (self.width - (@resize_margin * 2)).to_f).to_i.to_s + "px") + else + # assume numeric px value + new_style[:width] = "#{value.to_s}px" + end + when :height + if value.to_s.include?("%") + # convert this % to px value width in new_style + new_style[:height] = (((value.to_s.gsub("%","").to_f / 100.0) * (self.height - (@title_margin + @menu_margin + @resize_margin)).to_f).to_i.to_s + "px") + else + # assume numeric px value + new_style[:height] = "#{value.to_s}px" + end + end + end + # + record[:object].gxg_merge_style(new_style) + end + end + if @menu_bar + @menu_area.gxg_merge_style({:width => "#{self.width - (@resize_margin * 2)}px"}) + # @menu_bar.gxg_merge_style({:width => "100%"}) + if @menu_bar.respond_to?(:update_appearance) + @menu_bar.update_appearance() + end + end + end + # + def track_deltas(the_reference=nil, the_object=nil, details={}) + if the_reference && the_object && details.is_any?(::Hash, ::GxG::Database::DetachedHash) + @tracking[(the_reference)] = {:object => the_object, :details => details} + true + else + false + end + end + def untrack_deltas(the_reference=nil) + if the_reference + @tracking.delete(the_reference) + true + else + false + end + end + # + def content() + @content + end + # + def content_width() + if @content + (`#{@content.element}.getBoundingClientRect().right - #{@content.element}.getBoundingClientRect().left`).to_i + else + 0 + end + end + # + def content_height() + if @content + (`#{@content.element}.getBoundingClientRect().bottom - #{@content.element}.getBoundingClientRect().top`).to_i + else + 0 + end + end + # + def set_top(the_top=nil) + if the_top.is_a?(::Numeric) + if @bottom - the_top >= 100 + if the_top >= GxG::DISPLAY_DETAILS[:object].layout_content_area()[:top] + @top = the_top + self.fit_within_boundaries() + commit_settings + end + end + end + end + alias :top= :set_top + # + def top() + @top + end + # + def set_left(the_left=nil) + if the_left.is_a?(::Numeric) + if @right - the_left >= 200 + if the_left >= GxG::DISPLAY_DETAILS[:object].layout_content_area()[:left] + @left = the_left + self.fit_within_boundaries() + commit_settings + end + end + end + end + alias :left= :set_left + # + def left() + @left + end + # + def set_right(the_right=nil) + if the_right.is_a?(::Numeric) + if the_right - @left >= 200 + if the_right <= GxG::DISPLAY_DETAILS[:object].layout_content_area()[:right] + @right = the_right + self.fit_within_boundaries() + commit_settings + end + end + end + end + alias :right= :set_right + # + def right() + @right + end + # + def set_bottom(the_bottom=nil) + if the_bottom.is_a?(::Numeric) + if the_bottom - @top >= 100 + if the_bottom <= GxG::DISPLAY_DETAILS[:object].layout_content_area()[:bottom] + @bottom = the_bottom + self.fit_within_boundaries() + commit_settings + end + end + end + end + alias :bottom= :set_bottom + # + def bottom() + @bottom + end + # + def set_width(the_width=nil) + if the_width.is_a?(::Numeric) + if the_width >= 200 + if (@left + the_width) <= GxG::DISPLAY_DETAILS[:object].layout_content_area()[:right] + @right = (@left + the_width) + self.fit_within_boundaries() + commit_settings + end + end + end + end + alias :width= :set_width + # + def width() + (@right - @left) + end + # + def set_height(the_height=nil) + if the_height.is_a?(::Numeric) + if the_height >= 100 + if (@top + the_height) <= GxG::DISPLAY_DETAILS[:object].layout_content_area()[:bottom] + @bottom = (@top + the_height) + self.fit_within_boundaries() + commit_settings + end + end + end + end + alias :height= :set_height + # + def height() + (@bottom - @top) + end + # + def set_position(the_x=nil,the_y=nil) + the_width = self.width + the_height = self.height + bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + if ((bounds[:top])..(bounds[:bottom])).include?(the_y.to_i) && ((bounds[:top])..(bounds[:bottom])).include?((the_y.to_i + the_height)) + @top = (the_y.to_i) + end + if ((bounds[:left])..(bounds[:right])).include?(the_x.to_i) && ((bounds[:left])..(bounds[:right])).include?((the_x.to_i + the_width)) + @left = (the_x.to_i) + end + @bottom = (@top + the_height) + @right = (@left + the_width) + self.fit_within_boundaries() + # commit_settings + # convert to % ?? + # top_percent = (@top.to_f / bounds[:page_height].to_f) * 100.0 + # left_percent = (@left.to_f / bounds[:page_width].to_f) * 100.0 + # self.gxg_merge_style({:left => "#{left_percent}%", :top => "#{top_percent}%"}) + self.commit_settings + end + # + def set_frame(settings=nil) + if settings.is_a?(::Hash) + @top = (settings[:top] || @top) + @left = (settings[:left] || @left) + @right = (settings[:right] || @right) + @bottom = (settings[:bottom] || @bottom) + self.fit_within_boundaries() + self.commit_settings + end + end + # + def position() + {:x => @left, :y => @top} + end + # + def fit_within_boundaries() + the_width = self.width + the_height = self.height + bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + if @top < bounds[:top] + @top = bounds[:top] + end + if @left < bounds[:left] + @left = bounds[:left] + end + @right = @left + the_width + @bottom = @top + the_height + if @bottom > bounds[:bottom] + @bottom = bounds[:bottom] + end + if @right > bounds[:right] + @right = bounds[:right] + end + end + # + def set_window_title(the_title=nil) + if @title_object + if the_title.is_a?(::String) + @window_title = the_title + @title_object.set_text(@window_title) + @title_object.parent.gxg_merge_style({:padding => "0px 0px 0px #{((self.width() / 2) - ((@window_title.size / 2) * 8)) - 60}px"}) + end + end + end + alias :window_title= :set_window_title + # + def window_title() + if @title_object + @window_title + else + nil + end + end + # + def special_state() + @special_state + end + # + def set_special_state(settings=nil) + if settings.is_a?(::Hash) + @special_state = settings + @top = (settings[:top] || @top) + @left = (settings[:left] || @left) + @right = (settings[:right] || @right) + @bottom = (settings[:bottom] || @bottom) + self.fit_within_boundaries() + self.commit_settings + end + end + # + def clear_special_state(settings=nil) + @special_state = nil + if settings.is_a?(::Hash) + self.set_frame(settings) + end + end + # Initialization and Setup + def initialize(the_parent,the_options) + # FIXME: work around for strange bug in layout_refresh + ::GxG::DISPLAY_DETAILS[:object].layout_refresh + bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + # + @special_state = nil + @content = nil + # Process the_options + # So, at this point I've decided to use pixels internally, but percentages externally. (interface) + # You can pass Integer (as Pixels), Float, or String Percentage + if the_options[:top].is_a?(::String) || the_options[:top].to_s.include?(".") + if the_options[:top].is_a?(::String) + @top = (bounds[:page_height] * (the_options[:top].gsub("%","").to_f / 100.0)) + else + @top = (bounds[:page_height] * (the_options[:top] / 100.0)) + end + else + @top = (the_options[:top] || ((`window.innerHeight`.to_i / 2) - 50)) + end + if the_options[:left].is_a?(::String) || the_options[:left].to_s.include?(".") + if the_options[:left].is_a?(::String) + @left = (bounds[:page_width] * (the_options[:left].gsub("%","").to_f / 100.0)) + else + @left = (bounds[:page_width] * (the_options[:left] / 100.0)) + end + else + @left = (the_options[:left] || ((`window.innerWidth`.to_i / 2) - 100)) + end + if the_options[:right].is_a?(::String) || the_options[:right].to_s.include?(".") + if the_options[:right].is_a?(::String) + @right = (bounds[:page_width] * (the_options[:right].gsub("%","").to_f / 100.0)) + else + @right = (bounds[:page_width] * (the_options[:right] / 100.0)) + end + else + @right = (the_options[:right] || ((`window.innerWidth`.to_i / 2) + 100)) + end + if the_options[:bottom].is_a?(::String) || the_options[:bottom].to_s.include?(".") + if the_options[:bottom].is_a?(::String) + @bottom = (bounds[:page_height] * (the_options[:bottom].gsub("%","").to_f / 100.0)) + else + @bottom = (bounds[:page_height] * (the_options[:bottom] / 100.0)) + end + else + @bottom = (the_options[:bottom] || ((`window.innerHeight`.to_i / 2) + 50)) + end + # + @window_title = the_options[:window_title] || "Untitled" + @title_object = nil + # + if the_options[:width].is_a?(::String) || the_options[:width].to_s.include?(".") + if the_options[:width].is_a?(::String) + @right = @left + ((bounds[:right] - bounds[:left]) * (the_options[:width].gsub("%","").to_f / 100.0)) + else + @right = @left + ((bounds[:right] - bounds[:left]) * (the_options[:width] / 100.0)) + end + else + if the_options[:width].is_a?(::Numeric) + @right = (@left + the_options.delete(:width)) + end + end + if the_options[:height].is_a?(::String) || the_options[:height].to_s.include?(".") + if the_options[:height].is_a?(::String) + @bottom = @top + ((bounds[:bottom] - bounds[:top]) * (the_options[:height].gsub("%","").to_f / 100.0)) + else + @bottom = @top + ((bounds[:bottom] - bounds[:top]) * (the_options[:height] / 100.0)) + end + else + if the_options[:height].is_a?(::Numeric) + @bottom = (@top + the_options[:height]) + end + end + # Bounds check top, left, right, and bottom + self.fit_within_boundaries() + # + @modal = (the_options[:modal] || false) + @scrolling = the_options[:scroll] + @title_margin = 24 + if @modal == true + @resize = false + @resize_margin = 0 + else + @resize = (the_options[:resize] || true) + @resize_margin = 2 + @title_margin = 26 + end + if @modal == true + @closebox = false + @minimizebox = false + @maximizebox = false + else + if the_options[:close] == false + @closebox = false + else + @closebox = (the_options[:close] || :enabled).to_s.to_sym + end + if the_options[:minimize] == false + @minimizebox = false + else + @minimizebox = (the_options[:minimize] || :enabled).to_s.to_sym + end + if the_options[:maximize] == false + @maximizebox = false + else + @maximizebox = (the_options[:maximize] || :enabled).to_s.to_sym + end + end + # + @tracking = {} + # + @menu_reference = the_options[:menu] + @menu_area = nil + @menu_bar = nil + if @menu_reference + @menu_margin = 16 + else + @menu_margin = 0 + end + # + super(the_parent,the_options) + # + # self.gxg_set_attribute(:draggable,true) + # + self + end + # + def menu_reference() + @menu_reference + end + # + def menu() + @menu_bar + end + # + def set_menu(the_menu_source=nil) + if the_menu_source.is_any?(::Hash, ::GxG::Database::DetachedHash) + if @menu_area + page.build_components(@menu_area, [(the_menu_source)]) + # + @menu_margin = (`#{@menu_area.element}.getBoundingClientRect().bottom - #{@menu_area.element}.getBoundingClientRect().top`.to_i) + # + @menu_bar = self.find_child(@menu_reference,true) + # + if @menu_bar + self.commit_settings + true + else + log_warn("Unable to properly initialize window menu #{@menu_reference.inspect} .") + false + end + else + false + end + else + false + end + end + alias :menu= :set_menu + # + alias :original_add_child :add_child + def add_child(object_record=nil) + if @content + @content.add_child(object_record) + else + original_add_child(object_record) + end + end + # + alias :original_children :children + def children() + if @content + @content.children + else + {} + end + end + # + def find_child(the_reference=nil, interior=false) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + if interior == true + search_queue = self.original_children.values + else + search_queue = self.children.values + end + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + def cascade() + commit_settings + # + build_list = [] + # + # Define Title Bar + # FIXME: CSS override of image :src does NOT work -- research. + # title_object_title = "Object #{::GxG::uuid_generate.to_s}" + the_title = ::GxG::Database::DetachedHash.new + the_title.title = "title_object #{@uuid.to_s}" + the_title[:component] = "org.gxg.gui.label" + the_title[:settings] = {} + the_title[:options] = {:content => @window_title, :style => {:width => "100%", :"font-size" => "16px", :"text-align" => "center"}} + the_title[:script] = "" + the_title[:content] = [] + # + close_box = ::GxG::Database::DetachedHash.new + close_box[:component] = "org.gxg.gui.image" + close_box[:settings] = {} + close_box[:options] = {:src => theme_widget("close.png"), :width=>20, :height=>20, :style => {:padding => "2px"}} + close_box[:script] = " + on(:mouseup) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('close.png')) + the_window = self.window + if the_window + if the_window.application + the_window.application.window_close({:window => the_window.uuid}) + else + GxG::DISPLAY_DETAILS[:object].window_close(the_window.uuid) + end + else + log_warn('Window Not Found') + end + end + end + on(:mousedown) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('close_mousedown.png')) + end + end + on(:mouseenter) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('close_mouseenter.png')) + end + end + on(:mouseleave) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('close.png')) + end + end + " + close_box[:content] = [] + # + minimize_box = ::GxG::Database::DetachedHash.new + minimize_box[:component] = "org.gxg.gui.image" + minimize_box[:settings] = {} + minimize_box[:options] = {:src => theme_widget("minimize.png"), :width=>20, :height=>20, :style => {:padding => "2px"}} + minimize_box[:script] = " + on(:mouseup) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('minimize.png')) + the_window = self.window + if the_window + special = the_window.special_state() + if special.is_a?(::Hash) + if special[:action] == :minimize + GxG::DISPLAY_DETAILS[:object].window_restore(the_window.uuid) + else + GxG::DISPLAY_DETAILS[:object].window_minimize(the_window.uuid,{:top => the_window.top, :left => the_window.left, :right => the_window.right, :bottom => the_window.bottom}) + end + else + GxG::DISPLAY_DETAILS[:object].window_minimize(the_window.uuid,{:top => the_window.top, :left => the_window.left, :right => the_window.right, :bottom => the_window.bottom}) + end + end + end + end + on(:mousedown) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('minimize_mousedown.png')) + end + end + on(:mouseenter) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('minimize_mouseenter.png')) + end + end + on(:mouseleave) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('minimize.png')) + end + end + " + minimize_box[:content] = [] + # + maximize_box = ::GxG::Database::DetachedHash.new + maximize_box[:component] = "org.gxg.gui.image" + maximize_box[:settings] = {} + maximize_box[:options] = {:src => theme_widget("maximize.png"), :width=>20, :height=>20, :style => {:padding => "2px"}} + maximize_box[:script] = " + on(:mouseup) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('maximize.png')) + the_window = self.window + if the_window + special = the_window.special_state() + if special.is_a?(::Hash) + if special[:action] == :maximize + GxG::DISPLAY_DETAILS[:object].window_restore(the_window.uuid) + else + GxG::DISPLAY_DETAILS[:object].window_maximize(the_window.uuid,{:top => the_window.top, :left => the_window.left, :right => the_window.right, :bottom => the_window.bottom}) + end + else + GxG::DISPLAY_DETAILS[:object].window_maximize(the_window.uuid,{:top => the_window.top, :left => the_window.left, :right => the_window.right, :bottom => the_window.bottom}) + end + end + end + end + on(:mousedown) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('maximize_mousedown.png')) + end + end + on(:mouseenter) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('maximize_mouseenter.png')) + end + end + on(:mouseleave) do |event| + unless self.gxg_get_state(:disabled) == true + self.gxg_set_attribute(:src,theme_widget('maximize.png')) + end + end + " + maximize_box[:content] = [] + # + table = ::GxG::Database::DetachedHash.new + table[:component] = "org.gxg.gui.block.table" + table[:settings] = {} + table[:options] = {:style => {:clear => "both", :height=>"24px", :padding => "0px", :margin => "0px", :"border-radius" => "5px 5px 0px 0px", :"border-bottom" => "1px solid #c2c2c2"}, :states => {:"default-background-color" => true}} + table[:script] = "" + table[:content] = [] + # + row = ::GxG::Database::DetachedHash.new + row[:component] = "org.gxg.gui.block.table.row" + row[:settings] = {} + row[:options] = {:style => {:padding => "0px", :margin => "0px", :"vertical-align" => "middle", :"background-color" => "transparent"}} + row[:script] = "" + row[:content] = [] + # + cell_one = ::GxG::Database::DetachedHash.new + cell_one[:component] = "org.gxg.gui.block.table.cell" + cell_one[:settings] = {} + cell_one[:options] = {:style => {:padding => "0px", :margin => "0px", :"vertical-align" => "middle", :"background-color" => "transparent"}} + cell_one[:script] = "" + cell_one[:content] = [] + # + cell_two = ::GxG::Database::DetachedHash.new + cell_two[:component] = "org.gxg.gui.block.table.cell" + cell_two[:settings] = {} + cell_two[:options] = {:style => {:width => "100%",:padding => "0px 0px 0px #{((self.width() / 2) - ((@window_title.size / 2) * 8)) - 60}px", :"vertical-align" => "middle", :margin => "0px", :"background-color" => "transparent"}} + cell_two[:script] = "" + cell_two[:content] = [] + # + cell_three = ::GxG::Database::DetachedHash.new + cell_three[:component] = "org.gxg.gui.block.table.cell" + cell_three[:settings] = {} + cell_three[:options] = {:style => {:float => "right", :padding => "0px", :"vertical-align" => "middle", :margin => "0px", :"background-color" => "transparent"}} + cell_three[:script] = "" + cell_three[:content] = [] + # + # link pieces + if @closebox + if @closebox == :disabled + close_box[:options][:src] = theme_widget("close_disable.png") + close_box[:options][:states] = {:disabled => true} + end + cell_one[:content] << close_box + end + if @minimizebox + if @minimizebox == :disabled + minimize_box[:options][:src] = theme_widget("minimize_disable.png") + minimize_box[:options][:states] = {:disabled => true} + end + cell_one[:content] << minimize_box + end + if @maximizebox + if @maximizebox == :disabled + maximize_box[:options][:src] = theme_widget("maximize_disable.png") + maximize_box[:options][:states] = {:disabled => true} + end + cell_one[:content] << maximize_box + end + cell_two[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + # + if @window_handle + unless @last_mouse_x + @last_mouse_x = event[:pageX] + end + unless @last_mouse_y + @last_mouse_y = event[:pageY] + end + offset_x = (event[:pageX] - @window_handle.left) + if event[:pageY] < @window_handle.top + offset_y = (@window_handle.top - event[:pageY]) + else + offset_y = (event[:pageY] - @window_handle.top) + end + new_x = ((event[:pageX] - offset_x) + (event[:pageX] - @last_mouse_x)) + new_y = ((event[:pageY] - offset_y) + (event[:pageY] - @last_mouse_y)) + @window_handle.set_position(new_x,new_y) + @last_mouse_x = event[:pageX] + @last_mouse_y = event[:pageY] + end + end + end + on(:dblclick) do |event| + the_window = self.window + if the_window + special = the_window.special_state() + if special.is_a?(::Hash) + if special[:action] == :rollup + GxG::DISPLAY_DETAILS[:object].window_restore(the_window.uuid) + else + GxG::DISPLAY_DETAILS[:object].window_rollup(the_window.uuid,{:top => the_window.top, :left => the_window.left, :right => the_window.right, :bottom => the_window.bottom}) + end + else + GxG::DISPLAY_DETAILS[:object].window_rollup(the_window.uuid,{:top => the_window.top, :left => the_window.left, :right => the_window.right, :bottom => the_window.bottom}) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousemove) do |event| + @window_handle = self.window + if ((event[:buttons] & 1) == 1) + @mode = :drag + do_drag(event) + else + @mode = :hover + end + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + cell_two[:content] << the_title + row[:content] << cell_one + row[:content] << cell_two + row[:content] << cell_three + table[:content] << row + # + build_list << table + # + interior_component = ::GxG::Database::DetachedHash.new + interior_component.title = "content #{@uuid.to_s}" + interior_component[:component] = "org.gxg.gui.block" + interior_component[:settings] = {} + interior_component[:options] = {:style => {:clear => "both", :padding => "0px", :margin => "0px", :border => "0px", :width => "#{(self.width - (@resize_margin * 2))}px", :height => "#{self.height - (@title_margin + @menu_margin + @resize_margin)}px"}, :states => {:"default-background-color" => true}} + interior_component[:script] = "" + interior_component[:content] = [] + interior_component_uuid = interior_component.uuid() + # + if @scrolling + if @scrolling == true + interior_component[:options][:style][:overflow] = "scroll" + else + if @scrolling == "vertical" + interior_component[:options][:style][:"overflow-y"] = "scroll" + end + if @scrolling == "horizontal" + interior_component[:options][:style][:"overflow-x"] = "scroll" + end + end + else + interior_component[:options][:style][:overflow] = "hidden" + end + # + left_resizer = ::GxG::Database::DetachedHash.new + left_resizer[:component] = "org.gxg.gui.block.table.cell" + left_resizer[:settings] = {} + left_resizer[:options] = {:style => {:width => "2px", :cursor => "w-resize"}} + left_resizer[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + window_handle = self.window + if window_handle + window_handle.set_left(event[:pageX]) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + left_resizer[:content] = [] + # + right_resizer = ::GxG::Database::DetachedHash.new + right_resizer[:component] = "org.gxg.gui.block.table.cell" + right_resizer[:settings] = {} + right_resizer[:options] = {:style => {:width => "2px", :cursor => "e-resize"}} + right_resizer[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + window_handle = self.window + if window_handle + window_handle.set_right(event[:pageX] + 1) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + right_resizer[:content] = [] + # + bottom_resizer = ::GxG::Database::DetachedHash.new + bottom_resizer[:component] = "org.gxg.gui.block.table.cell" + bottom_resizer[:settings] = {} + bottom_resizer[:options] = {:style => {:height => "2px", :cursor => "s-resize"}} + bottom_resizer[:script] = " + def do_drag(event) + window_handle = self.window + if window_handle + if ((event[:buttons] & 1) == 1) + window_handle.set_bottom(event[:pageY] + 4) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + bottom_resizer[:content] = [] + # + sw_resizer = ::GxG::Database::DetachedHash.new + sw_resizer[:component] = "org.gxg.gui.block.table.cell" + sw_resizer[:settings] = {} + sw_resizer[:options] = {:style => {:width => "2px", :height => "2px", :cursor => "sw-resize"}} + sw_resizer[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + window_handle = self.window + if window_handle + window_handle.set_left(event[:pageX] - 2) + window_handle.set_bottom(event[:pageY] + 4) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + sw_resizer[:content] = [] + # + se_resizer = ::GxG::Database::DetachedHash.new + se_resizer[:component] = "org.gxg.gui.block.table.cell" + se_resizer[:settings] = {} + se_resizer[:options] = {:style => {:width => "2px", :height => "2px", :cursor => "se-resize"}} + se_resizer[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + window_handle = self.window + if window_handle + window_handle.set_right(event[:pageX] + 2) + window_handle.set_bottom(event[:pageY] + 4) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + se_resizer[:content] = [] + # + left_menu_resize = ::GxG::Database::DetachedHash.new + left_menu_resize[:component] = "org.gxg.gui.block.table.cell" + left_menu_resize[:settings] = {} + left_menu_resize[:options] = {:style => {:width => "2px", :cursor => "w-resize"}} + left_menu_resize[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + window_handle = self.window + if window_handle + window_handle.set_left(event[:pageX]) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + left_menu_resize[:content] = [] + # + right_menu_resize = ::GxG::Database::DetachedHash.new + right_menu_resize[:component] = "org.gxg.gui.block.table.cell" + right_menu_resize[:settings] = {} + right_menu_resize[:options] = {:style => {:width => "2px", :cursor => "e-resize"}} + right_menu_resize[:script] = " + def do_drag(event) + if ((event[:buttons] & 1) == 1) + window_handle = self.window + if window_handle + window_handle.set_right(event[:pageX] + 1) + end + end + end + on(:mouseup) do |event| + @mode = :hover + the_window = self.window + if the_window + GxG::DISPLAY_DETAILS[:object].window_bring_to_front(the_window.uuid) + end + end + on(:mousedown) do |event| + @mode = :drag + do_drag(event) + end + on(:mouseover) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseleave) do |event| + if @mode == :drag + do_drag(event) + end + end + on(:mouseenter) do |event| + if @mode == :drag + do_drag(event) + end + end + " + right_menu_resize[:content] = [] + # + menu_cell = ::GxG::Database::DetachedHash.new + menu_cell.title = "menu_area #{@uuid.to_s}" + menu_cell[:component] = "org.gxg.gui.block.table.cell" + menu_cell[:settings] = {} + menu_cell[:options] = {:style => {:overflow => "hidden", :height=>"16px",:padding => "0px", :margin => "0px"}} + menu_cell[:script] = "" + menu_cell[:content] = [] + # + menu_row = ::GxG::Database::DetachedHash.new + menu_row.title = "menu_area_row #{@uuid.to_s}" + menu_row[:component] = "org.gxg.gui.block.table.row" + menu_row[:settings] = {} + menu_row[:options] = {:style => {:overflow => "hidden", :clear => "both", :padding => "0px", :width => "100%", :height=>"16px", :margin => "0px", :"vertical-align" => "middle", :"background-color" => "transparent", :"border-bottom" => "1px solid #c2c2c2"}} + menu_row[:script] = "" + menu_row[:content] = [(left_menu_resize),(menu_cell),(right_menu_resize)] + # + content_area = ::GxG::Database::DetachedHash.new + content_area.title = "content_area #{@uuid.to_s}" + content_area[:component] = "org.gxg.gui.block.table.cell" + content_area[:settings] = {} + content_area[:options] = {:style => {:padding => "0px", :margin => "0px", :border => "0px"}} + content_area[:script] = "" + content_area[:content] = [(interior_component)] + # + row_one = ::GxG::Database::DetachedHash.new + row_one[:component] = "org.gxg.gui.block.table.row" + row_one[:settings] = {} + row_one[:options] = {} + row_one[:script] = "" + row_one[:content] = [] + # + row_two = ::GxG::Database::DetachedHash.new + row_two[:component] = "org.gxg.gui.block.table.row" + row_two[:settings] = {} + row_two[:options] = {} + row_two[:script] = "" + row_two[:content] = [] + # + resizing_areas = ::GxG::Database::DetachedHash.new + resizing_areas.title = "window_body #{@uuid.to_s}" + resizing_areas[:component] = "org.gxg.gui.block.table" + resizing_areas[:settings] = {} + resizing_areas[:options] = {:style => {:width => "100%"}, :states => {:"default-background-color" => true}} + resizing_areas[:script] = "" + resizing_areas[:content] = [] + # + # link pieces + if @resize + row_one[:content] = [(left_resizer),(content_area),(right_resizer)] + row_two[:content] = [(sw_resizer),(bottom_resizer),(se_resizer)] + if @menu_reference + resizing_areas[:content] = [(menu_row),(row_one),(row_two)] + else + resizing_areas[:content] = [(row_one),(row_two)] + end + else + if @menu_reference + row_one[:content] = [(content_area)] + resizing_areas[:content] = [(menu_row),(row_one)] + else + row_one[:content] = [(content_area)] + resizing_areas[:content] = [(row_one)] + end + end + # + build_list << resizing_areas + # + @content = nil + page.build_components(self,build_list) + # self.build_interior_components(build_list) + # + the_object = page.find_object_by(interior_component_uuid) + if the_object + @content = the_object + end + # + the_object = page.find_object_by("window_body #{@uuid.to_s}") + if the_object + @window_body = the_object + end + # + the_object = page.find_object_by("title_object #{@uuid.to_s}") + if the_object + @title_object = the_object + end + # + the_object = page.find_object_by("menu_area #{@uuid.to_s}") + if the_object + @menu_area = the_object + @menu_margin = (`#{@menu_area.element}.getBoundingClientRect().bottom - #{@menu_area.element}.getBoundingClientRect().top`.to_i) + # border-collapse: collapse + if @menu_area + @menu_area.parent.parent.gxg_merge_style({:"border-collapse" => "collapse"}) + end + end + GxG::DISPATCHER.post_event do + self.commit_settings + end + self + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.window", ::GxG::Gui::Window) + # + class DialogBox < ::GxG::Gui::Vdom::BaseElement + # + def _before_create + super() + end + # + def uuid() + @uuid + end + # Methods to override in window's script + def before_close(data=nil) + # override in window's script + end + # + def after_close(data=nil) + # override in window's script + end + # + def before_open(data=nil) + # override in window's script + end + # + def after_open(data=nil) + # override in window's script + end + # + def set_application(the_application) + @application = the_application + end + alias :application= :set_application + # + def application() + @application + end + # + def set_window_title(the_title=nil) + if @title_object + if the_title.is_a?(::String) + @window_title = the_title + @title_object.set_text(@window_title) + end + end + end + alias :window_title= :set_window_title + # + def window_title() + if @title_object + @window_title + else + nil + end + end + # + def set_data(data={}) + @data = data + end + alias :data= :set_data + # + def data() + @data + end + # + def set_responder(block=nil) + if block.respond_to?(:call) + @responder = block + end + end + alias :responder= :set_responder + # + def respond(data=nil) + page.dialog_close() + if @responder.respond_to?(:call) + @responder.call(data) + end + end + # Geometry details: + def top() + @top + end + # + def left() + @left + end + # + def right() + @right + end + # + def bottom() + @bottom + end + # + def width() + (@right - @left) + end + # + def height() + (@bottom - @top) + end + # + def position() + {:x => @left, :y => @top} + end + # + def layer() + @layer + end + # + # Initialization and Setup + def initialize(the_parent, the_options) + # FIXME: work around for strange bug in layout_refresh + # ::GxG::DISPLAY_DETAILS[:object].layout_refresh + # bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + # + @special_state = nil + @content = nil + # So, at this point I've decided to use pixels internally, but percentages externally. (interface) + # You can pass Integer (as Pixels), Float, or String Percentage + the_width = nil + if the_options[:width].is_a?(::String) || the_options[:width].to_s.include?(".") + if the_options[:width].is_a?(::String) + the_width = ((`window.innerWidth`.to_f) * (the_options[:width].gsub("%","").to_f / 100.0)) + else + the_width = ((`window.innerWidth`.to_f) * (the_options[:width] / 100.0)) + end + else + if the_options[:width].is_a?(::Numeric) + the_width = the_options[:width].to_f + end + end + if the_width + @left = ((`window.innerWidth`.to_f / 2.0) - (the_width / 2.0)) + @right = ((`window.innerWidth`.to_f / 2.0) + (the_width / 2.0)) + end + # + the_height = nil + if the_options[:height].is_a?(::String) || the_options[:height].to_s.include?(".") + if the_options[:height].is_a?(::String) + the_height = ((`window.innerHeight`.to_f) * (the_options[:height].gsub("%","").to_f / 100.0)) + else + the_height = ((`window.innerHeight`.to_f) * (the_options[:height] / 100.0)) + end + else + if the_options[:height].is_a?(::Numeric) + the_height = the_options[:height].to_f + end + end + if the_height + @top = ((`window.innerHeight`.to_f / 2.0) - (the_height / 2.0)) + @bottom = ((`window.innerHeight`.to_f / 2.0) + (the_height / 2.0)) + end + # + if the_options[:top].is_a?(::String) || the_options[:top].to_s.include?(".") + if the_options[:top].is_a?(::String) + @top = (`window.innerHeight`.to_f * (the_options[:top].gsub("%","").to_f / 100.0)) + else + @top = (`window.innerHeight`.to_f * (the_options[:top] / 100.0)) + end + else + unless the_height + @top = (the_options[:top] || ((`window.innerHeight`.to_i / 2) - 150)) + end + end + if the_options[:left].is_a?(::String) || the_options[:left].to_s.include?(".") + if the_options[:left].is_a?(::String) + @left = (`window.innerWidth`.to_f * (the_options[:left].gsub("%","").to_f / 100.0)) + else + @left = (`window.innerWidth`.to_f * (the_options[:left] / 100.0)) + end + else + unless the_width + @left = (the_options[:left] || ((`window.innerWidth`.to_i / 2) - 300)) + end + end + if the_options[:right].is_a?(::String) || the_options[:right].to_s.include?(".") + if the_options[:right].is_a?(::String) + @right = (`window.innerWidth`.to_f * (the_options[:right].gsub("%","").to_f / 100.0)) + else + @right = (`window.innerWidth`.to_f * (the_options[:right] / 100.0)) + end + else + unless the_width + @right = (the_options[:right] || ((`window.innerWidth`.to_i / 2) + 300)) + end + end + if the_options[:bottom].is_a?(::String) || the_options[:bottom].to_s.include?(".") + if the_options[:bottom].is_a?(::String) + @bottom = (`window.innerHeight`.to_f * (the_options[:bottom].gsub("%","").to_f / 100.0)) + else + @bottom = (`window.innerHeight`.to_f * (the_options[:bottom] / 100.0)) + end + else + unless the_height + @bottom = (the_options[:bottom] || ((`window.innerHeight`.to_i / 2) + 150)) + end + end + # + @window_title = the_options[:window_title] || "Untitled" + @title_object = nil + # + @opacity = (the_options[:opacity] || 0.8) + @background_color = (the_options[:"background-color"] || "rgba(0,0,0,#{@opacity.to_s})") + @layer = 2000 + # + @tracking = {} + @responder = nil + @data = {} + # + super(the_parent, the_options) + # + self.set_attribute(:draggable,false) + # + self + end + # Slight of hand: + alias :original_add_child :add_child + def add_child(component_record) + if @content + @content.add_child(component_record) + else + original_add_child(component_record) + end + end + # + alias :original_children :children + def children() + if @content + @content.children + else + {} + end + end + # + def find_child(the_reference=nil, interior=false) + result = nil + if GxG::valid_uuid?(the_reference) + uuid_check = true + else + uuid_check = false + end + if interior == true + search_queue = self.original_children.values + else + search_queue = self.children.values + end + while search_queue.size > 0 do + entry = search_queue.shift + if entry + if uuid_check + if entry.uuid == the_reference + result = entry + break + end + else + if entry.title == the_reference + result = entry + break + end + end + unless result + entry.children.values.each do |the_child| + search_queue << the_child + end + end + end + end + result + end + # + # Construction + def cascade() + # Initialize Style Settings: + self.merge_style({:position => "absolute", :top => "0px", :left => "0px", :width => "100%", :height => "100%", :"background-color" => @background_color, :"z-index" => @layer}) + mouse_event_capture = " + on(:mouseup,true) do |event| + end + on(:mousedown,true) do |event| + end + on(:mousemove,true) do |event| + end + on(:mouseenter,true) do |event| + end + on(:mouseleave,true) do |event| + end + " + self.set_script(mouse_event_capture) + # Prep build_list + build_list = [] + # + modal_frame = ::GxG::Database::DetachedHash.new + modal_frame.title = "modal_frame #{@uuid.to_s}" + modal_frame[:component] = "org.gxg.gui.block.table" + modal_frame[:settings] = {} + modal_frame[:options] = {:style => {:opacity => 1.0, :clear => "both", :overflow => "hidden", :position => "absolute", :top => "#{@top}px", :left => "#{@left}px", :width => "#{self.width}px", :height=>"#{self.height}px", :padding => "0px", :margin => "0px", :"border-radius" => "5px 5px 5px 5px", :"z-index" => (@layer + 100)}, :states => {:"default-background-color" => true}} + modal_frame[:script] = "" + modal_frame[:content] = [] + # + the_title = ::GxG::Database::DetachedHash.new + the_title.title = "title #{@uuid.to_s}" + the_title[:component] = "org.gxg.gui.label" + the_title[:settings] = {} + the_title[:options] = {:content => @window_title, :style => {:opacity => 1.0, :width => "100%", :"font-size" => "16px", :"text-align" => "center"}} + the_title[:script] = "" + the_title[:content] = [] + # + title_cell = ::GxG::Database::DetachedHash.new + title_cell[:component] = "org.gxg.gui.block.table.cell" + title_cell[:settings] = {} + title_cell[:options] = {:style => {:opacity => 1.0, :width => "100%", :padding => "0px 0px 0px #{((self.width() / 2) - ((@window_title.size / 2) * 8))}px", :margin => "0px", :"vertical-align" => "middle", :"background-color" => "transparent"}} + title_cell[:script] = "" + title_cell[:content] = [(the_title)] + # + title_row = ::GxG::Database::DetachedHash.new + title_row[:component] = "org.gxg.gui.block.table.row" + title_row[:settings] = {} + title_row[:options] = {:style => {:opacity => 1.0, :clear => "both", :padding => "0px", :width => "100%", :height=>"24px", :margin => "0px", :"vertical-align" => "middle", :"background-color" => "#ffffff", :"border-bottom" => "1px solid #c2c2c2"}} + title_row[:script] = "" + title_row[:content] = [(title_cell)] + # + interior_component = ::GxG::Database::DetachedHash.new + interior_component.title = "content #{@uuid.to_s}" + interior_component[:component] = "org.gxg.gui.block.table.cell" + interior_component[:settings] = {} + interior_component[:options] = {:style => {:opacity => 1.0, :clear => "both", :padding => "0px", :margin => "0px", :border => "0px", :width => "#{self.width}px", :height => "#{self.height}px"}, :states => {:"default-background-color" => true}} + interior_component[:script] = "" + interior_component[:content] = [] + # + content_row = ::GxG::Database::DetachedHash.new + content_row[:component] = "org.gxg.gui.block.table.row" + content_row[:settings] = {} + content_row[:options] = {:style => {:opacity => 1.0, :width => "#{self.width}px", :height => "#{self.height - 24}px", :padding => "0px", :margin => "0px", :border => "0px"}} + content_row[:script] = "" + content_row[:content] = [(interior_component)] + # + modal_frame[:content] = [(title_row),(content_row)] + # + build_list << modal_frame + # + @content = nil + page.build_components(self,build_list) + # + the_object = page.find_object_by("content #{@uuid.to_s}") + if the_object + @content = the_object + end + # + the_object = page.find_object_by("title #{@uuid.to_s}") + if the_object + @title_object = the_object + end + # + end + # Hooks for open/close events + def open(details={}) + # override in object script + end + # + def close(details={}) + # override in object script + end + end + ::GxG::Gui::register_component_class(:"org.gxg.gui.window.dialog", ::GxG::Gui::DialogBox) + # + # ### Page supports + module Vdom + class Page + def window_open(build_source=nil, the_application=nil) + if build_source.is_a?(GxG::Database::DetachedHash) + self.build_components(self, [(build_source)], {:application => the_application}) + new_window = self.find_object_by(build_source.uuid) + # + if new_window + if new_window.menu_reference() + the_menu_source = the_application.search_content(new_window.menu_reference()) + if the_menu_source.is_a?(GxG::Database::DetachedHash) + new_window.set_menu(the_menu_source) + else + log_warn("Invalid Menu Resource referenced: #{new_window.menu_reference().inspect}") + end + end + # + self.window_register(new_window.uuid, new_window) + new_window.before_open + new_window.open + new_window.show + new_window.after_open + true + else + log_warn("Unable to locate newly created window object.") + false + end + else + false + end + end + # + def window_close(the_reference=nil, options={}) + if the_reference.is_any?(::String, ::Symbol) + registration = self.get_window_registration(the_reference) + if registration + registration[:window].before_close + registration[:window].close + registration[:window].hide + registration[:window].after_close + if registration[:window].application() + registration[:window].application.unlink_window(the_reference) + registration[:window].set_application(nil) + end + self.window_unregister(the_reference) + registration[:window].destroy + true + else + false + end + else + false + end + end + # + def dialog_open(build_source=nil, the_application=nil, details=nil, &block) + if build_source.is_a?(::GxG::Database::DetachedHash) + if @dialog.is_a?(::GxG::Gui::DialogBox) + @dialog_queue << {:source => build_source, :application => the_application, :details => details, :responder => block} + true + else + page.build_components(self,[(build_source)],{:application => the_application}) + new_window = page.find_object_by(build_source.uuid) + if new_window + new_window.set_title(build_source.title) + if the_application.is_a?(::GxG::Application) + new_window.set_application(the_application) + unless the_application.get_window(build_source.uuid) + the_application.link_window(new_window) + end + end + # Set details if provided + if details + new_window.set_data(details) + end + if block.respond_to?(:call) + new_window.set_responder(block) + end + new_window.before_open() + new_window.open() + new_window.show({:origin => {:opacity => 0.0}, :destination => {:opacity => 1.0}}) + new_window.after_open() + true + else + false + end + end + end + end + # + def dialog_close(options={}) + if @dialog.is_a?(::GxG::Gui::DialogBox) + @dialog.before_close() + close_procedure = Proc.new do + @dialog.close() + @dialog.after_close() + @dialog.every_child do |the_child| + if the_child.is_a?(::GxG::Gui::ApplicationViewport) + the_application = the_child.application + if the_application + the_application.unlink_viewport(the_child.title) + end + end + end + @dialog.destroy + @dialog = nil + if @dialog_queue.size > 0 + entry = @dialog_queue.shift + page.dialog_open(entry[:source], entry[:application], entry[:details], &entry[:responder]) + end + end + if options[:origin] && options[:destination] + @dialog.hide(options, &close_procedure) + else + @dialog.hide({:origin => {:opacity => 1}, :destination => {:opacity => 0}, :options => {:duration => 500}}, &close_procedure) + end + end + true + end + # + # ### Window Layering & Effects + def window_is_in_back?(the_reference=nil) + the_record = self.get_window_registration(the_reference) + if the_record + bottom_layer = ((@window_registry.collect {|uuid,entry| entry[:layer]}).min || 0) + if the_record[:layer] == bottom_layer + true + else + false + end + else + false + end + end + # + def window_is_in_front?(the_reference=nil) + the_record = self.get_window_registration(the_reference) + if the_record + top_layer = ((@window_registry.collect {|uuid,entry| entry[:layer]}).max || 0) + if the_record[:layer] == top_layer + true + else + false + end + else + false + end + end + # + def window_send_to_back(the_reference=nil) + the_record = self.get_window_registration(the_reference) + if the_record + unless self.window_is_in_back?(the_reference) + back_layer = self.window_back_layer() + the_record[:window].gxg_merge_style(:"z-index",(200 + back_layer)) + the_record[:layer] = back_layer + end + else + false + end + end + # + def window_bring_to_front(the_reference=nil, options={}) + the_record = self.get_window_registration(the_reference) + if the_record + unless the_record[:layer] == self.window_top_layer() + front_layer = self.window_next_layer() + the_record[:window].gxg_merge_style(:"z-index",(200 + front_layer)) + the_record[:layer] = front_layer + end + true + else + false + end + end + # Transition Effect Methods (override for custom effects) + def next_minimize_slot() + # slots are allocated from the top / left of the content area: + cell_width = 200 + cell_height = 24 + bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + columns = (0..(bounds[:right] - bounds[:left]).step(cell_width).size) + rows = (0..(bounds[:bottom] - bounds[:top]).step(cell_height).size) + found = nil + (0..(rows - 1)).each do |the_row| + (0..(columns - 1)).each do |the_column| + is_in = false + test_rect = {:top => (bounds[:top] + (the_row * cell_height)), :left => (bounds[:left] + (the_column * cell_width)), :right => (bounds[:left] + (the_column * cell_width) + cell_width), :bottom => (bounds[:top] + (the_row * cell_height) + cell_height)} + @window_registry.values.each do |the_record| + # + this_rect = {:top => 0, :left => 0, :right => 0, :bottom => 0} + this_rect[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + this_rect[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + this_rect[:right] = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + this_rect[:bottom] = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + if (this_rect[:top] == test_rect[:top] && this_rect[:left] == test_rect[:left] && this_rect[:right] == test_rect[:right] && this_rect[:bottom] == test_rect[:bottom]) + is_in = true + break + end + # + end + unless is_in + found = test_rect + break + end + end + if found + break + end + end + found + end + # + def minimize_effect(the_record=nil,&block) + switcher = self.get_window_switcher() + if switcher + origin = {:opacity => 1} + origin[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + origin[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + origin_right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + origin_bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + origin[:width] = (origin_right - origin[:left]) + origin[:height] = (origin_bottom - origin[:top]) + destination = {:opacity => 0} + destination[:top] = `#{switcher.element}.getBoundingClientRect().top`.to_i + destination[:left] = `#{switcher.element}.getBoundingClientRect().right`.to_i + destination_right = `#{switcher.element}.getBoundingClientRect().right`.to_i + destination_bottom = `#{switcher.element}.getBoundingClientRect().top`.to_i + destination[:width] = (destination_right - destination[:left]) + destination[:height] = (destination_bottom - destination[:top]) + conclude = Proc.new { + if block.respond_to?(:call) + block.call(true) + end + } + else + # find a tile spot to land on: + the_slot = self.next_minimize_slot() + origin = {:opacity => 1} + origin[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + origin[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + origin_right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + origin_bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + origin[:width] = (origin_right - origin[:left]) + origin[:height] = (origin_bottom - origin[:top]) + destination = {:opacity => 0.5} + destination[:top] = the_slot[:top] + destination[:left] = the_slot[:left] + destination_right = the_slot[:right] + destination_bottom = the_slot[:bottom] + destination[:width] = (destination_right - destination[:left]) + destination[:height] = (destination_bottom - destination[:top]) + conclude = Proc.new { + self.window_send_to_back(the_record[:window].uuid) + if block.respond_to?(:call) + block.call(true) + end + } + end + animate_content = Proc.new { + top = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + left = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + the_record[:window].set_frame({:top => top, :left => left, :right => right, :bottom => bottom}) + } + the_record[:window].animate({:origin => origin, :destination => destination, :options => {:duration => 500, :animationiteration => animate_content, :animationend => conclude}}) + # + settings = {:action => :minimize, :top => 0, :left => 0, :right => 0, :bottom => 0} + settings[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + settings[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + settings[:right] = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + settings[:bottom] = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + settings + end + # + def unminimize_effect(the_record=nil,&block) + switcher = self.get_window_switcher() + if switcher + origin = {:opacity => 0} + origin[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + origin[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + origin_right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + origin_bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + origin[:width] = (origin_right - origin[:left]) + origin[:height] = (origin_bottom - origin[:top]) + destination = {:opacity => 1} + destination[:top] = the_record[:restore][:settings][:top] + destination[:left] = the_record[:restore][:settings][:left] + destination_right = the_record[:restore][:settings][:right] + destination_bottom = the_record[:restore][:settings][:bottom] + destination[:width] = (destination_right - destination[:left]) + destination[:height] = (destination_bottom - destination[:top]) + else + # + origin = {:opacity => 0.5} + origin[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + origin[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + origin_right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + origin_bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + origin[:width] = (origin_right - origin[:left]) + origin[:height] = (origin_bottom - origin[:top]) + destination = {:opacity => 1} + destination[:top] = the_record[:restore][:settings][:top] + destination[:left] = the_record[:restore][:settings][:left] + destination_right = the_record[:restore][:settings][:right] + destination_bottom = the_record[:restore][:settings][:bottom] + destination[:width] = (destination_right - destination[:left]) + destination[:height] = (destination_bottom - destination[:top]) + end + animate_content = Proc.new { + top = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + left = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + the_record[:window].set_frame({:top => top, :left => left, :right => right, :bottom => bottom}) + } + conclude = Proc.new { + if block.respond_to?(:call) + block.call(true) + end + } + the_record[:window].animate({:origin => origin, :destination => destination, :options => {:duration => 500, :animationiteration => animate_content, :animationend => conclude}}) + # + true + end + # + def maximize_effect(the_record=nil,&block) + bounds = GxG::DISPLAY_DETAILS[:object].layout_content_area() + origin = {} + origin[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + origin[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + origin_right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + origin_bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + origin[:width] = (origin_right - origin[:left]) + origin[:height] = (origin_bottom - origin[:top]) + destination = {} + destination[:top] = bounds[:top] + destination[:left] = bounds[:left] + destination[:width] = (bounds[:right] - destination[:left]) + destination[:height] = (bounds[:bottom] - destination[:top]) + # run animation (which alters the state) + # animationiteration set_frame + animate_content = Proc.new { + top = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + left = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + the_record[:window].set_frame({:top => top, :left => left, :right => right, :bottom => bottom}) + } + conclude = Proc.new { + if block.respond_to?(:call) + block.call(true) + end + } + the_record[:window].animate({:origin => origin, :destination => destination, :options => {:duration => 500, :animationiteration => animate_content, :animationend => conclude}}) + # + settings = {:action => :maximize, :top => 0, :left => 0, :right => 0, :bottom => 0} + settings[:top] = bounds[:top] + settings[:left] = bounds[:left] + settings[:right] = bounds[:right] + settings[:bottom] = bounds[:bottom] + settings + end + # + def unmaximize_effect(the_record=nil,&block) + origin = {} + origin[:top] = the_record[:window].top() + origin[:left] = the_record[:window].left() + origin_right = the_record[:window].right() + origin_bottom = the_record[:window].bottom() + origin[:width] = (origin_right - origin[:left]) + origin[:height] = (origin_bottom - origin[:top]) + destination = {} + destination[:top] = the_record[:restore][:settings][:top] + destination[:left] = the_record[:restore][:settings][:left] + destination_right = the_record[:restore][:settings][:right] + destination_bottom = the_record[:restore][:settings][:bottom] + destination[:width] = (destination_right - destination[:left]) + destination[:height] = (destination_bottom - destination[:top]) + # run animation (which alters the state) + animate_content = Proc.new { + top = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + left = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + right = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + bottom = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + the_record[:window].set_frame({:top => top, :left => left, :right => right, :bottom => bottom}) + } + conclude = Proc.new { + if block.respond_to?(:call) + block.call(true) + end + } + the_record[:window].animate({:origin => origin, :destination => destination, :options => {:duration => 500, :animationiteration => animate_content, :animationend => conclude}}) + true + end + # + def rollup_effect(the_record=nil,&block) + origin = {} + the_top = the_record[:window].top() + origin[:height] = the_record[:window].height() + destination = {} + destination[:height] = 24 + # run animation (which alters the state) + window_body = the_record[:window].find_child("window_body",true) + if window_body + window_body.hide({:origin => {:opacity => 1.0}, :destination => {:opacity => 0.0}}) do + the_record[:window].animate({:origin => origin, :destination => destination}) + if block.respond_to?(:call) + block.call(true) + end + end + end + # + settings = {:action => :rollup, :top => 0, :left => 0, :right => 0, :bottom => 0} + settings[:top] = `#{the_record[:window].element}.getBoundingClientRect().top`.to_i + settings[:left] = `#{the_record[:window].element}.getBoundingClientRect().left`.to_i + settings[:right] = `#{the_record[:window].element}.getBoundingClientRect().right`.to_i + settings[:bottom] = `#{the_record[:window].element}.getBoundingClientRect().bottom`.to_i + settings + end + # + def rolldown_effect(the_record=nil,&block) + origin = {} + origin[:height] = 24 + destination = {} + destination[:height] = (the_record[:restore][:settings][:bottom] - the_record[:restore][:settings][:top]) + # run animation (which alters the state) + conclude = Proc.new { + window_body = the_record[:window].find_child("window_body",true) + if window_body + window_body.show({:origin => {:opacity => 0.0}, :destination => {:opacity => 1.0}}) do + the_record[:window].commit_settings + if block.respond_to?(:call) + block.call(true) + end + end + end + } + the_record[:window].animate({:origin => origin, :destination => destination, :options => {:duration => 500, :animationend => conclude}}) + true + end + # set/restore window state: + def window_restoreable?(the_reference=nil) + the_record = self.get_window_registration(the_reference) + if the_record + if the_record[:restore].is_a?(::Hash) + true + else + false + end + else + false + end + end + # + def window_restore(the_reference=nil, options={},&block) + the_record = self.get_window_registration(the_reference) + if the_record + if the_record[:restore].is_a?(::Hash) + # restore from last :action + case the_record[:restore][:action] + when :minimize + self.unminimize_effect(the_record) do + the_record[:window].clear_special_state(the_record[:restore][:settings]) + # clear the restore record + the_record[:restore] = nil + if block.respond_to?(:call) + block.call(true) + end + end + when :maximize + self.unmaximize_effect(the_record) do + the_record[:window].clear_special_state(the_record[:restore][:settings]) + # clear the restore record + the_record[:restore] = nil + if block.respond_to?(:call) + block.call(true) + end + end + when :rollup + self.rolldown_effect(the_record) do + the_record[:window].clear_special_state(the_record[:restore][:settings]) + # clear the restore record + the_record[:restore] = nil + if block.respond_to?(:call) + block.call(true) + end + end + end + true + else + false + end + else + false + end + end + # + def window_minimize(the_reference=nil, settings={}) + the_record = self.get_window_registration(the_reference) + if the_record + if self.window_restoreable?(the_reference) + self.window_restore(the_reference) do + settings = {:top => the_record[:window].top, :left => the_record[:window].left, :right => the_record[:window].right, :bottom => the_record[:window].bottom} + the_record[:restore] = {:action => :minimize, :settings => settings} + the_record[:window].set_special_state(self.minimize_effect(the_record)) + end + else + the_record[:restore] = {:action => :minimize, :settings => settings} + the_record[:window].set_special_state(self.minimize_effect(the_record)) + end + true + else + false + end + end + # + def window_maximize(the_reference=nil, settings={}) + the_record = self.get_window_registration(the_reference) + if the_record + if self.window_restoreable?(the_reference) + self.window_restore(the_reference) do + settings = {:top => the_record[:window].top, :left => the_record[:window].left, :right => the_record[:window].right, :bottom => the_record[:window].bottom} + the_record[:restore] = {:action => :maximize, :settings => settings} + the_record[:window].set_special_state(self.maximize_effect(the_record)) + self.window_bring_to_front(the_reference) + end + else + the_record[:restore] = {:action => :maximize, :settings => settings} + the_record[:window].set_special_state(self.maximize_effect(the_record)) + self.window_bring_to_front(the_reference) + end + true + else + false + end + end + # + def window_rollup(the_reference=nil, settings={}) + the_record = self.get_window_registration(the_reference) + if the_record + if self.window_restoreable?(the_reference) + self.window_restore(the_reference) do + settings = {:top => the_record[:window].top, :left => the_record[:window].left, :right => the_record[:window].right, :bottom => the_record[:window].bottom} + the_record[:restore] = {:action => :rollup, :settings => settings} + the_record[:window].set_special_state(self.rollup_effect(the_record)) + end + else + the_record[:restore] = {:action => :rollup, :settings => settings} + the_record[:window].set_special_state(self.rollup_effect(the_record)) + end + true + else + false + end + end + # ### Window Registry + def get_window_registration(the_reference=nil) + if the_reference.is_any?(::String, ::Symbol) + # {:window => nil, :layer => 0, :restore => nil} + @window_registry[(the_reference)] + else + nil + end + end + # + def get_window(the_reference=nil) + if the_reference.is_any?(::String, ::Symbol) + registration = self.get_window_registration(the_reference) + if registration + registration[:window] + else + nil + end + else + nil + end + end + # + def window_list() + result = [] + @window_registry.keys.each do |reference| + result << {:uuid => reference, :title => @window_registry[(reference)][:window].title()} + end + result + end + # + def window_repack_layers() + @window_registry = (@window_registry.sort_by {|uuid,entry| entry[:layer]}).to_h + @window_registry.values.each_with_index do |entry, indexer| + entry[:window].gxg_merge_style(:"z-index",(200 + indexer)) + entry[:layer] = indexer + end + true + end + # + def window_top_layer() + ((@window_registry.collect {|uuid,entry| entry[:layer]}).max || 0) + end + # + def window_back_layer() + back_layer = (((@window_registry.collect {|uuid,entry| entry[:layer]}).min || 0) - 1) + if back_layer < 100 + window_repack_layers + back_layer = (((@window_registry.collect {|uuid,entry| entry[:layer]}).min || 0) - 1) + end + back_layer + end + # + def window_next_layer() + next_layer = (((@window_registry.collect {|uuid,entry| entry[:layer]}).max || 0) + 1) + if next_layer > 99 + window_repack_layers + next_layer = (((@window_registry.collect {|uuid,entry| entry[:layer]}).max || 0) + 1) + end + next_layer + end + # + def window_register(the_reference=nil, the_instance=nil) + if the_reference.is_any?(::String, ::Symbol) + if the_instance + # {:window => nil, :layer => 0, :restore => nil} + front_layer = self.window_next_layer() + the_instance.gxg_merge_style(:"z-index",(200 + front_layer)) + @window_registry[(the_reference)] = {:window => the_instance, :layer => front_layer, :restore => nil} + true + else + false + end + else + false + end + end + # + def window_unregister(the_reference=nil) + if the_reference.is_any?(::String, ::Symbol) + @window_registry.delete(the_reference) + true + else + false + end + end + # + # ### Window Switcher Supports + def set_window_switcher(the_switcher=nil) + if the_switcher + @window_switcher = the_switcher + end + end + # + def get_window_switcher() + @window_switcher + end + # + end + end + end +end \ No newline at end of file diff --git a/gxg/web/gxg_xhr.rb b/gxg/web/gxg_xhr.rb new file mode 100644 index 0000000..dab594f --- /dev/null +++ b/gxg/web/gxg_xhr.rb @@ -0,0 +1,741 @@ +# +#require 'gxg_uri' +# +module GxG + module Networking + class Channel + # + end + # + class Connector + # + def remote_uuid() + @remote_uuid + end + # + def open?() + @active + end + # + def close() + the_message = new_message({:operation => "close", :display_path => @display_path}) + the_message[:sender] = @uuid + the_message[:to] = @remote_uuid + self.pull(the_message,{:synchronous => true}) + @active = false + @csrf = nil + if @heart_beat_timer.is_a?(::Hash) + GxG::DISPATCHER.cancel_timer(@heart_beat_timer) + end + end + # + def push(the_message=nil,options={}) + if @active + # Uses PUT method, with data payload + if the_message.is_a?(::GxG::Events::Message) + payload = the_message.body.to_json.to_s + the_xhr = `new XMLHttpRequest` + `#{the_xhr}.open('PUT',#{@host_prefix},true)` + `#{the_xhr}.timeout = #{(options[:timeout] || 5000)}` + `#{the_xhr}.setRequestHeader('X-Requested-With','XMLHttpRequest')` + `#{the_xhr}.setRequestHeader('Accept','application/json')` + `#{the_xhr}.setRequestHeader('X-CSRF-Token', #{@csrf})` + `#{the_xhr}.setRequestHeader('Content-Type', 'application/json')` + # unless (`navigator.userAgent`).include?("Firefox") || (`navigator.userAgent`).include?("Chrome") + # `#{the_xhr}.setRequestHeader('Content-Length', #{payload.size})` + # end + failed = Proc.new do |reason=nil| + the_message.fail(reason) + end + succeeded = Proc.new do + response_code = (`#{the_xhr}.status`).to_i + if response_code < 300 + the_message.succeed(::JSON.parse(`#{the_xhr}.response`,{:symbolize_names => true})) + else + failed.call(response_code) + end + end + `#{the_xhr}.addEventListener('load', #{succeeded})` + `#{the_xhr}.addEventListener('error', #{failed})` + `#{the_xhr}.addEventListener('timeout', #{failed})` + `#{the_xhr}.addEventListener('abort', #{failed})` + `#{the_xhr}.send(#{payload})` + # + true + else + # error? + false + end + end + # + end + # + def pull(the_message=nil,options={:synchronous => false}) + if @active + # Uses GET method, with in-URL query specifiers (no data payload) + if the_message.is_a?(::GxG::Events::Message) + the_query = "" + the_message.body.keys.each do |the_key| + the_query = the_query + ("#{the_key.to_s}=#{the_message.body[(the_key)].to_s}") + unless the_key == the_message.body.keys.last + the_query = the_query + "&" + end + end + the_url = (@host_prefix.to_s + "?" + the_query) + the_xhr = `new XMLHttpRequest` + if options[:synchronous] == true + `#{the_xhr}.open('GET',#{the_url.to_s},false)` + else + `#{the_xhr}.open('GET',#{the_url.to_s},true)` + `#{the_xhr}.timeout = #{(options[:timeout] || 600000)}` + end + `#{the_xhr}.setRequestHeader('X-Requested-With','XMLHttpRequest')` + `#{the_xhr}.setRequestHeader('Accept','application/json')` + failed = Proc.new do |reason=nil| + the_message.fail(reason) + end + succeeded = Proc.new do + response_code = (`#{the_xhr}.status`).to_i + if response_code < 300 + the_message.succeed(::JSON.parse(`#{the_xhr}.response`,{:symbolize_names => true})) + else + failed.call(response_code) + end + end + `#{the_xhr}.addEventListener('load', #{succeeded})` + `#{the_xhr}.addEventListener('error', #{failed})` + `#{the_xhr}.addEventListener('timeout', #{failed})` + `#{the_xhr}.addEventListener('abort', #{failed})` + `#{the_xhr}.send(null)` + # + true + else + # error? + false + end + end + # + end + # + def push_message(the_message=nil) + # TODO: establish a sensible routing scheme and make better use of message :sender and :to in that idea. + end + # + def pull_message(options={}) + # Use to pull a message portion or look for application messages on the server. + end + # + def process_pending(options={}) + # + end + # Support-level methods for pulling/pushing objects or object-deltas. + def push_object_portions(details={}, error_handler=nil, &the_handler) + # supply thread and index to send, if successful, will post event to keep chain going. + # Details: {:thread => "", :index => 0, :portions => 100} + end + # Search The Database(s) + def search_database(details={}, error_handler=nil, &the_handler) + # FOR NOW: limit 100 records returned at a time. + # Use: {:limit => 100, :offset => 0} || {:page => 1} + if details.is_a?(::Hash) && the_handler + self.pull_data({:operation => :search_database, :criteria => details},error_handler) do |data| + if data.is_a?(::Hash) + the_handler.call(data) + end + end + true + else + false + end + end + # High-level methods for pulling/pushing objects or object-deltas. + def destroy_object(the_specifier=nil,error_handler=nil,&the_handler) + if the_specifier && the_handler + self.pull_data({:operation => :destroy_object, :location => the_specifier},error_handler) do |data| + if data.is_a?(::Hash) + the_handler.call(data) + end + end + true + else + false + end + end + # + def push_data(details={}, error_handler=nil,&the_handler) + if details.is_a?(::Hash) && the_handler + content = details.to_json.to_s.encode64 + msg_frame = {:sender => @uuid.to_s, :to => @remote_uuid.to_s, :display_path => @display_path, :operation => "put_portion", :thread => "00000000-0000-4000-0000-000000000000", :index => 99999, :total => 100000, :payload => ""} + portions = ::GxG::apportioned_ranges(content.size,(65536 - msg_frame.to_json.size)) + # + if portions.size > 1 + the_message = new_message({ :operation => "put_data", :display_path => @display_path}) + the_message[:sender] = @uuid + the_message[:to] = @remote_uuid + the_message[:body][:thread] = the_message.id().to_s + # + if the_handler.respond_to?(:call) + the_message.on(:success) do |response| + @buffer.delete(the_message[:body][:thread]) + the_handler.call(response) + end + end + if error_handler.respond_to?(:call) + the_message.on(:fail) do |response| + @buffer.delete(the_message[:body][:thread]) + error_handler.call(response) + end + end + # + @buffer[(the_message[:body][:thread])] = [] + # + portions.each_with_index do |the_range, indexer| + the_msg_portion = msg_frame.clone + the_msg_portion[:thread] = the_message[:body][:thread] + the_msg_portion[:index] = indexer + the_msg_portion[:total] = portions.size + the_msg_portion[:payload] = content[(the_range)] + @buffer[(the_message[:body][:thread])] << the_msg_portion + end + # ----------------------------------------------------- + # Send portions + (0..(portions.size - 1)).each do |the_portion| + ::GxG::DISPATCHER.post_event(:communications) do + if @buffer[(the_message[:body][:thread])].is_a?(::Array) + if @buffer[(the_message[:body][:thread])][(the_portion)].is_a?(::Hash) + # + portion_message = new_message(@buffer[(the_message[:body][:thread])][(the_portion)]) + portion_message[:sender] = @uuid + portion_message[:to] = @remote_uuid + # + portion_message.on(:succeed) do |response| + if the_portion == (@buffer[(the_message[:body][:thread])].size - 1) + if @buffer[(the_message[:body][:thread])] + GxG::CONNECTION.push(the_message) + end + end + end + # + portion_message.on(:fail) do |response| + the_message.fail(response) + end + # + GxG::CONNECTION.push(portion_message) + end + end + end + end + # + else + # single message payload + the_message = new_message({ :operation => "put_data", :display_path => @display_path, :payload => content}) + the_message[:sender] = @uuid + the_message[:to] = @remote_uuid + GxG::CONNECTION.push(the_message) + end + # + true + else + false + end + end + # + def push_object(details={}, error_handler=nil,&the_handler) + if details.is_a?(::Hash) && the_handler + self.push_data({:operation => :put_object, :data => details},error_handler) do |data| + if data.is_a?(::Hash) + the_handler.call(data) + end + end + true + else + false + end + end + # + def pull_data(the_specifier={},error_handler=nil,&the_handler) + if the_specifier && the_handler + the_message = new_message({:operation => "get_data", :thread => nil, :display_path => @display_path, :details => the_specifier.to_json.encode64}) + the_message[:sender] = @uuid + the_message[:to] = @remote_uuid + the_message[:body][:thread] = the_message.id().to_s + if error_handler.respond_to?(:call) + the_message.on(:fail,&error_handler) + end + the_message.on(:success) do |response| + if response.is_a?(::Hash) + if response[:result] == true + case response[:operation] + when "this_portion" + data = response[:payload] + if data.is_a?(::String) + # FIXME: .base64? needs a proper working regex (or go brute force) + # data = Base64::strict_decode64(data) + if data.base64? + data = data.decode64 + end + if data.json? + begin + data = ::JSON::parse(data,{:symbolize_names => true}) + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:data => data}}) + end + end + the_handler.call(data) + else + log_error({:error => Exception.new("Malformed Response Data"), :parameters => {:data => data}}) + end + when "get_portions" + @pending[(response[:thread])] = the_message + @buffers[(response[:thread])] = [] + the_messages = [] + (0..(response[:total] - 1)).each do |indexer| + portion_message = new_message({:operation => "get_portion", :thread => (response[:thread]), :index => indexer, :display_path => @display_path}) + portion_message[:sender] = @uuid + portion_message[:to] = @remote_uuid + # + portion_message.on(:success) do |reply| + @buffers[(response[:thread])] << reply + if @buffers[(response[:thread])].size == response[:total] + already = [] + the_data = "" + until already.size == response[:total] do + (0..(response[:total] - 1)).each do |referencer| + unless already.index(referencer) + the_data = (the_data + (@buffers[(response[:thread])][(referencer)][:payload])) + already << referencer + break + end + end + end + signal_message = new_message({:operation => "got_portions", :thread => response[:thread], :display_path => @display_path}) + signal_message[:sender] = @uuid + signal_message[:to] = @remote_uuid + GxG::CONNECTION.pull(signal_message) + # + @pending.delete(response[:thread]) + @buffers.delete(response[:thread]) + if the_data.size > 0 + # FIXME: .base64? needs a proper working regex (or go brute force) + # the_data = Base64::strict_decode64(the_data) + if the_data.base64? + the_data = the_data.decode64 + end + if the_data.json? + begin + the_data = ::JSON::parse(the_data,{:symbolize_names => true}) + rescue Exception => the_error + log_error({:error => the_error, :parameters => {:data => the_data}}) + end + end + the_handler.call(the_data) + else + log_warn("Empty payload from portions.") + end + end + end + # + portion_message.on(:fail) do |reply| + log_warn("Cancelling Transfer: portion #{indexer} of thread #{response[:thread]}.") + @pending.delete(response[:thread]) + @buffers.delete(response[:thread]) + if error_handler.respond_to?(:call) + error_handler.call(reply) + end + end + # + # GxG::CONNECTION.pull(portion_message) + the_messages << portion_message + # + end + the_messages.each do |a_message| + GxG::CONNECTION.pull(a_message) + end + end + end + end + end + GxG::CONNECTION.pull(the_message) + end + end + # + def pull_object(the_specifier=nil,error_handler=nil,&the_handler) + # Use pull_data({:operation => :get_object, :location => the_specifier}) + if the_specifier && the_handler + # GxG::DISPLAY_DETAILS[:object].set_busy(true) + self.pull_data({:operation => :get_object, :location => the_specifier},error_handler) do |data| + if data.is_a?(::Hash) + the_handler.call(::GxG::Database::process_detached_import(data)) + # GxG::DISPLAY_DETAILS[:object].set_busy(false) + end + end + true + else + false + end + end + # + def get_permissions(the_specifier=nil,error_handler=nil,&the_handler) + # See vfs: :action => 'set_permissions', :path => (location), :alterations => [{:credential => nil, :permissions => {...}}], :revocations => [(a-credential)] + if the_specifier && the_handler + # GxG::DISPLAY_DETAILS[:object].set_busy(true) + self.pull_data({:operation => :get_permissions, :location => the_specifier},error_handler) do |data| + if data.is_a?(::Hash) + the_handler.call(data) + # GxG::DISPLAY_DETAILS[:object].set_busy(false) + end + end + true + else + false + end + end + # + # Libarary Loading Support: + def library_pull(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + self.pull_data({:operation => :get_library}.merge!(details),error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # Format Support: + def format_pull(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_any?(::Array, ::Hash) && the_handler + self.pull_data({:operation => :format_pull, :criteria => details},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # TODO: format_push(data={}) + # Admin: + def admin(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + self.pull_data(({:operation => :admin}.merge!(details)),error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # + # VFS: + def vfs(details={}, error_handler=nil, &the_handler) + # Specify either a :location or :name + if self.open? + if details.is_a?(::Hash) && the_handler + self.pull_data(({:operation => :vfs}.merge!(details)),error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # + # Application Support: + def app_state_push(details={}, error_handler=nil, &the_handler) + if details.is_a?(::Hash) && the_handler + self.push_data({:operation => :app_state_push, :application => details[:application], :data => details[:data]},error_handler) do |data| + if data.is_a?(::Hash) + the_handler.call(data) + end + end + true + else + false + end + end + # + def app_state_pull(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + self.pull_data({:operation => :app_state_pull, :application => details[:application]},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # + def application_open(details={}, error_handler=nil, &the_handler) + # Specify either a :location or :name + if self.open? + if details.is_a?(::Hash) && the_handler + if details[:name] + self.pull_data({:operation => :application_open, :restore => (details[:restore] || false), :name => details[:name]},error_handler) do |data| + the_handler.call(data) + end + else + self.pull_data({:operation => :application_open, :restore => (details[:restore] || false), :location => details[:location]},error_handler) do |data| + the_handler.call(data) + end + end + true + else + false + end + end + end + # + def application_close(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + self.pull_data({:operation => :application_close, :application => details[:application]},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # + def application_list(error_handler=nil, &the_handler) + if self.open? + if the_handler + self.pull_data({:operation => :application_list},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # + def application_menu(error_handler=nil, &the_handler) + if self.open? + if the_handler + self.pull_data({:operation => :application_menu},error_handler) do |data| + the_handler.call(data) + end + true + else + false + end + end + end + # + def entries(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + GxG::DISPLAY_DETAILS[:object].set_busy(true) + self.pull_data({:operation => :entries, :location => details[:location]},error_handler) do |data| + the_handler.call(data) + GxG::DISPLAY_DETAILS[:object].set_busy(false) + end + true + else + false + end + end + end + # + def heartbeat() + if @active + heartbeat_message = new_message({:operation => "heartbeat", :display_path => @display_path}) + heartbeat_message[:sender] = @uuid.to_s + heartbeat_message[:to] = @remote_uuid.to_s + heartbeat_message.on(:success) do |response| + if response.is_a?(::Hash) + # FIXME: possible conflict with proper credential setting. + if response[:result][:status] == "credentialed" + # GxG::DISPLAY_DETAILS[:logged_in] = true + else + # GxG::DISPLAY_DETAILS[:logged_in] = false + end + GxG::DISPLAY_DETAILS[:server_status] = response[:result][:server].to_s.to_sym + end + end + GxG::CONNECTION.pull(heartbeat_message) + end + end + # + # Set Credential: + def update_credential(details={}, error_handler=nil, &the_handler) + if self.open? + if details.is_a?(::Hash) && the_handler + content = details.to_json.to_s.encrypt(@csrf.to_s).encode64 + the_message = new_message({:operation => "update_credential", :display_path => @display_path, :payload => content}) + the_message[:sender] = @uuid + the_message[:to] = @remote_uuid + the_message[:body][:thread] = the_message.id().to_s + the_message.on(:success) do |the_response| + if the_response.is_a?(::Hash) + if the_response[:status] == "credentialed" + GxG::DISPLAY_DETAILS[:logged_in] = true + end + end + the_handler.call(the_response) + end + if error_handler + the_message.on(:fail) do |the_response| + error_handler.call(the_response) + end + end + GxG::CONNECTION.push(the_message) + end + end + end + # + def downgrade_credential(&the_handler) + # ???? + if @active + downgrade_message = new_message({:operation => "downgrade_credential", :display_path => @display_path}) + downgrade_message[:sender] = @uuid.to_s + downgrade_message[:to] = @remote_uuid.to_s + downgrade_message.on(:success) do |response| + # var evt = new CustomEvent('credentialchanged', { detail: "state" }); ?? + # window.dispatchEvent(evt); + if response.is_a?(::Hash) + # FIXME: possible conflict with proper credential setting. + if response[:status] == "uncredentialed" + GxG::DISPLAY_DETAILS[:logged_in] = false + if the_handler.respond_to?(:call) + the_handler.call(response) + end + new_event = `new CustomEvent('credentialchanged', { detail: #{response[:status].to_s} })` + `window.dispatchEvent(#{new_event})` + else + # GxG::DISPLAY_DETAILS[:logged_in] = true + end + # GxG::DISPLAY_DETAILS[:server_status] = response[:server].to_s.to_sym + end + end + GxG::CONNECTION.pull(downgrade_message) + end + end + # + def relative_url() + @relative_url + end + # + def query() + @query + end + # + def article() + @article + end + # + def initialize() + if (`window.location['href']`).include?("https://") + @host_prefix = ("https://" + `window.location['host']`) + else + @host_prefix = ("http://" + `window.location['host']`) + end + @relative_url = "" + # Find Relative URL + page_name = File::basename(`window.location['pathname']`) + subpath_array = (`window.location['pathname']`.split("/")) + found_at = subpath_array.index(page_name) + if found_at.is_a?(::Numeric) + if found_at > 1 + @relative_url = subpath_array[(0..(found_at - 1))].join("/") + end + end + # Process Query if found + @article = nil + @query = {} + query_string = `window.location.href`.split("?")[1] + if query_string + @article = query_string.split("#")[1] + query_string = query_string.split("#")[0] + if query_string.include?("&") + query_array = query_string.split("&") + else + query_array = query_string.split("&") + end + query_array.each do |entry| + buffer = entry.split("=") + @query[(buffer[0].to_s.to_sym)] = buffer[1] + end + else + @article = `window.location.href`.split("#")[1] + end + # + @uuid = GxG.uuid_generate().to_sym + @remote_uuid = nil + @active = true + @display_path = nil + @in_busy = false + @out_busy = false + @pending = {} + @channels = {} + @buffers = {} + @csrf = nil + @the_connector = self + # Make introduction to host and aquire csrf token + the_introduction = new_message({:operation => "introduction", :introducing => @uuid.to_s}) + the_introduction[:sender] = @uuid.to_s + the_introduction[:to] = @host_prefix.to_s + the_introduction.on(:success) do |response| + if response[:result] == "OK" + if response[:csrf] + @csrf = response[:csrf] + end + if response[:status] == "credentialed" + GxG::DISPLAY_DETAILS[:logged_in] = true + else + GxG::DISPLAY_DETAILS[:logged_in] = false + end + # + if response[:display] == "unavailable" + `alert('Display Resource Unavailable')` + else + @display_path = response[:display] + @active = true + # Are we logged in at the server and recovering from a crash ?? + GxG::DISPATCHER.post_event(:display) do + until (`document.readyState`.to_s == "complete") do + sleep 0.5 + end + ::GxG::DISPLAY_DETAILS[:query] = @query + ::GxG::DISPLAY_DETAILS[:article] = @article + ::GxG::DISPLAY_DETAILS[:object] = ::GxG::Gui::Page.new(@display_path) + GxG::DISPATCHER.post_event(:display) do + sleep 1.0 + ::GxG::DISPLAY_DETAILS[:object].layout_refresh + end + end + # + end + # + end + end + the_introduction.on(:fail) do |response| + @active = false + log_error({:error => Exception.new("Communication Error"), :parameters => {:response => response}}) + end + GxG::DISPATCHER.post_event(:communications) do + GxG::CONNECTION.pull(the_introduction) + @heart_beat_timer = GxG::DISPATCHER.add_periodic_timer({:interval => 60.0}) do + GxG::CONNECTION.heartbeat() + end + end + # + `window.addEventListener('unload', do_unload,false);function do_unload() { Opal.GxG.$eval("GxG::CONNECTION.close"); };` + # + self + end + # + end + end +end diff --git a/gxg_web.rb b/gxg_web.rb new file mode 100644 index 0000000..cd4af20 --- /dev/null +++ b/gxg_web.rb @@ -0,0 +1,101 @@ +# Require Core Library: +# require 'opal/full' +require 'opal' +require 'opal/paths' +# +## Require Standard Library: +# require 'benchmark' +require 'bigdecimal' +require 'buffer' +require 'base64' +require 'console' +require 'date' +require 'delegate' +require 'enumerator' +# require 'erb' +require 'forwardable' +require 'iconv' +require 'json' +require 'js' +require 'observer' +require 'opal-parser' +require 'opal-platform' +# require 'ostruct' +require 'pathname' +require 'pp' +require 'promise' +require 'securerandom' +# require 'set' +require 'singleton' +require 'stringio' +require 'strscan' +# require 'template' +require 'thread' +require 'time' +# GxG Requirements: +require 'deps/chronic' +#require 'deps/rexml/document' +#require 'deps/xml-simple' +require 'web/gxg' +#require 'gxg_uri' +require 'web/gxg_augments' +# require 'gxg_datetime' +require 'web/gxg_elements' +require 'web/gxg_events' +require 'web/gxg_database' +require 'web/gxg_libraries' +require 'web/gxg_applications' +# Ferro-Alternative: Foundation Framwork +require 'web/gxg_vdom' +require 'web/gxg_foundation' +require 'web/gxg_layout' +require 'web/gxg_windows' +require 'web/gxg_gui' +require 'web/gxg_comm' +# +module GxG + DISPLAY_DETAILS = {:server_status => :running, :logged_in => false, :host_path => "", :use_ssl => false, :socket => nil, :object => nil, :theme => "default", :query => {}, :article => nil, :mode => :browsing} + LAYOUT = {:"top-left" => {}, :top => {}, :"top-right" => {}, :right => {}, :"bottom-right" => {}, :bottom => {}, :"bottom-left" => {}, :left => {}} + LAYOUT_LIMITS = {:top => 0, :left => 0, :bottom => `window.innerHeight`.to_i, :right => `window.innerWidth`.to_i, :page_width => `window.innerWidth`.to_i, :page_height => `window.innerHeight`.to_i} + APPLICATIONS = {:processes => {}, :libraries => {}, :viewports => {}, :prefetching => false} + ANIMATION = {:animations => {}, :assets => {}} + DB = {:formats => {}} + CONNECTION = ::GxG::Networking::Connector.new + SOCKET_MONITOR = ::GxG::Networking::SocketMonitor.new + THEMES = {} + PAGES = {} + DOCUMENTS = {} +end +# +class Object + private + def page() + ::GxG::DISPLAY_DETAILS[:object] + end + # + def applications() + ::GxG::APPLICATIONS[:processes] + end + # + def libraries() + ::GxG::APPLICATIONS[:libraries] + end + # + def viewports() + ::GxG::APPLICATIONS[:viewports] + end + # + def connector() + ::GxG::CONNECTION + end + # + def sockets() + ::GxG::SOCKET_MONITOR + end + # + def documents() + ::GxG::DOCUMENTS + end + # +end +# \ No newline at end of file diff --git a/update_web.sh b/update_web.sh new file mode 100755 index 0000000..0b3b905 --- /dev/null +++ b/update_web.sh @@ -0,0 +1,5 @@ +#!/bin/bash +source /etc/profile +rm ./gxg.js +opal -I ./gxg -I ./gxg/deps -I ./gxg/standard --compile gxg_web.rb > gxg.js +# minify ./gxg.js > ../src/main/gxg.js