Squint: Light-weight ClojureScript dialect
Preparatory release before adding immutable + persistent collections in squint.immutable
tl;dr: added a lot of protocols and made sure core functions work properly with them.
clojure.set dispatches through the collection protocols: results keep the input's type, membership tests against a protocol set are value-based, and rename-keys/map-invert no longer mutate a recordIStack, IIndexed, IVector, IWriter and IPrintWithWriter protocols, write-all, and an ITransientVector -pop! slot; nth, peek, pop, pop!, subvec, vec, vector?, sequential?, set?, map?, seq, = and printing dispatch to custom collection typesequiv, hash, hash-ordered-coll, hash-unordered-coll and the IHash protocol. hash follows equiv: plain mutable objects and arrays hash by referenceIEncodeJS protocol; clj->js dispatches through it so a custom type controls its own conversionIMeta and IWithMeta protocols; meta and with-meta dispatch through them and the internal meta symbol property is gone:require-global and :refer-global to ns, binding globals loaded via a script tag to consts without emitting an import:as-alias in ns :require like CLJS: no runtime import, only a compile-time alias so a namespaced keyword such as ::alias/x resolvesiterable to reject a class instance that has no native iterator or ISeqable, so reduce, into, remove and friends no longer expose an object's internal fields, matching seqable? and CLJSseq of a js/Map or sorted-map to return a non-map sequence of entries, like a plain map and like CLJS, instead of the map itselfILookup. A seq, list or opaque object now throws when called through juxt, comp and friends, like a non-IFn in CLJSdescendants to throw on a constructor, like CLJS, instead of returning nilident?, simple-ident?, qualified-ident?, simple-symbol?, qualified-symbol?, num, byte and shortfnext, nfirst, nthnext, nthrest, parse-boolean, char, pop!, print-str, println-str, prn-str, random-sample, float?, reversible?, / as a value and associative? through the IAssociative protocolISet, IEditableCollection, ITransientCollection, ITransientAssociative, ITransientMap and ITransientSet protocols. disj, transient, conj!, assoc!, dissoc!, disj! and persistent! dispatch to them on custom typesdefrecord, record? and the IRecord marker protocol. Records store their fields as own string-keyed properties and implement the map-facing protocols, so keyword lookup, keys, seq, assoc, conj and = work through the regular core functions. assoc keeps the record type, dissoc of a basis field gives a plain map, printing gives #TypeName{:a 1}merge on a record or other -conj type without an empty representationmerge, keys, vals, update-keys and update-vals to dispatch through the map protocols on custom types, instead of mutating copies or reading internal propertiesinto to reduce through -conj on a type implementing ICollection instead of mutating a plain copyassoc-in and update-in to read intermediate levels through -lookup, so nested updates work on types implementing the map protocolsILookup, IAssociative, IMap, ICounted, IKVReduce, ICollection, IEmptyableCollection and IEquiv protocols. get, assoc, contains?, find, dissoc, count, reduce-kv, conj, empty and = dispatch to them on custom types. Plain objects and arrays keep their fast pathsassoc and assoc! called without a value for the last key: throws at runtime, and at compile time for a literal callget-validator and set-validator!def or deftype inside a function being added to the module exports, which made the module fail to loadtake-nth to throw on a non-number step instead of looping foreverempty to return nil on non-collections and class instances instead of throwing, like CLJSassoc! on a vector to validate the index like assoc, instead of writing a sparse arrayparents, ancestors and descendants to return nil on an invalid hierarchy instead of throwingnil and to base types such as number and string via extend-typederef, reset!, swap!, add-watch and remove-watch dispatch only through the protocols. The internal _deref and _reset_BANG_ props are no longer consulted: implement IDeref, IReset, ISwap or IWatchable insteadIReset, ISwap and IWatchable protocols, satisfied by atoms. reset!, swap!, add-watch and remove-watch dispatch to them on custom types, so a reagent-style reactive atom can be a deftypedeftype methods to receive this, so protocol methods with more than one arity workcljs.analyzer.api/resolve at compile time now sees vars of built-in library namespaces such as clojure.string, through an alias or fully qualifiedclojure.string/escapeclojure.string/blank? to return false on a non-string instead of throwing, like CLJSpeek to throw on a non-stack such as a set or map, like CLJS= to compare regexes by identity, like CLJS, so two equal regex literals are not equalcons to throw eagerly on a non-seqable tail such as a number, like CLJS. seq throws the same is not ISeqable error on non-seqables such as a class instance without iteratorISeqable protocol with -seq: a type extended to it seqs through its -seq method in seq, vec, cons and everything built on them, and seqable? returns true for itsome-fn to return the last predicate result such as false instead of nil, and to treat 0 and "" as truthy, like CLJSmin-key and max-key to compare pairwise like CLJS, correcting the result when keys are NaN or equalmerge with a non-collection first arg to return it or throw on more args, like CLJS, instead of failing to copy itconj and merge on a map throw when a vector arg is not a pair, like CLJSnth to throw on a non-number index, pop to throw on a non-stack, keys and vals to throw on a non-map, and hash-map to throw on a missing value, like CLJSseq to throw on a function, (conj! coll) to return the coll unchanged, and conj on a map to accept only map entries or seqables of map entries, like CLJSdisj to preserve metadatatake, drop, take-last and drop-last assert a number count, like CLJSIAtom marker protocol and the IDeref protocol with -deref, both satisfied by atoms. deref and @ dispatch to -deref on types extended via extend-type, like CLJSmissing-protocol and throw its clear error on every protocol dispatch miss, including custom protocols, like CLJS:meta and :validator options of atom, like CLJS. The validator runs on reset! and swap!, not at creation, matching current CLJS behaviorreset! to return the new valueassoc on a vector to validate the index, like CLJS: out of bounds or a non-number key throws, index count appends. assoc on false also throws: only nil puns to an empty mapdissoc to throw on a non-map collection such as a vector, set or list, like CLJSget-in and select-keys to treat a nil path or key seq as empty, like CLJS, and select-keys to always return a map(cycle nil) and (cycle []) to give an empty seq instead of throwing or looping, cycle a map by its entries, and throw eagerly on a non-iterable, like CLJSNaN? to coerce its argument like CLJS js/isNaN, so (NaN? "foo") is truehash-set, sorted?, char?, rseq, parse-uuid, force, clojure.string/reverse and the typed array constructors int-array, long-array, float-array, double-array and object-arrayrandom-uuid to return a UUID instance, so uuid? is true for it, like CLJSuuid lowercases its argument, like CLJSdoall returns the realized seq itself instead of a vector, like CLJScljs.core.UUID resolves to the core module member, like in CLJS= to compare a plain object map and a js/Map by entries, so the two map reps are equal when their contents arederive, underive, isa?, parents, ancestors and descendants accept a hierarchy written as a plain map, like {:parents {} :ancestors {} :descendants {}}derive and underive with an explicit hierarchy throw on a malformed one, and the global-hierarchy derive requires a namespace-qualified tag and parent, like CLJScljs.analyzer.api/resolve at compile time now sees built-in macros such as and, when-let and bit-and, and user macros, returning :macro true for them, like CLJS. A name in :refer-clojure :exclude no longer resolves to core. A local binding shadows all of these and resolves with :local trueassoc! regression on non-plain objects such as a class instance or a null-prototype object.indexOf on a lazy seq now uses reference equality like a JS array, not value equality. This diverges from CLJS but keeps = out of any bundle that only builds lazy seqs, shrinking a conj bundle from 3801 to 2215 bytes:squint/compile-time opt-in mechanism for macro/compile-time namespaces. See doc/compile-time.md.:require-macros) now expands: the macro's own def no longer shadows the macro lookup or triggers an alias-collision renamedefmacro is compile-time only: no longer emitted to the runtime module, and :refering a macro no longer emits a runtime import for it, matching CLJScountmin and max to propagate NaN like CLJS, correcting a regression in 0.14.199 where the 2-arg comparison dropped it. Also stop inlining the 2-arg call, since CLJS does not inline min/max:refer-clojure :exclude now suppresses the core macro, and declare registers a forward reference so a core-macro-named var such as exists? resolves to the varmin and max to compare with </> and return one of the values, so nil acts like zero and NaN propagates, like CLJS, instead of Math.min/Math.max coercing nil to 0min/max call with simple operands to a comparison, like CLJSThese changes were all made to improve compatibility with dialect tests in clojure-test-suite.
sorted-map, sorted-map-by and sorted-set-by, keeping entries in sorted ordersubvec throw on an out-of-bounds range, a nil index or a non-vector, and coerce indices to integers, like CLJSpop throw on an empty vector or list, like Clojure and CLJShash-map and array-map, both building a plain map like a map literaleven? and odd? throw on a non-integer argument, like Clojure and CLJStake-last to return nil for an empty or nil collection, like CLJSsequential? to return false for sets, maps and strings, instead of treating every iterable as sequentialseq on a string to return its characters and on a set to return its elements, like CLJSpeek and pop on a list to use its front, like CLJS, and (pop nil) to return nilkeys and vals to return nil for an empty or nil map, like CLJSmerge to return nil when called with no maps or only nil maps, like CLJS(partition n step pad coll) to emit the final partial partition even when pad is empty or nil, like CLJS, and no longer throw on a nil padadd-watch and remove-watch to return the reference, like CLJScat transducer appending a nil on completion, so transduce with cat or mapcat no longer adds a trailing nil(conj nil) to return nil, like CLJS, instead of an empty listcontains? on a string to test integer indices, so (contains? "abc" 0) is true, like CLJSLazySeq, so (instance? LazySeq (concat ...)) and similar return truewith-meta on a lazy seq or cons, attaching metadata without forcing realizationboolean to coerce every value except nil and false to truefnil to replace only nil arguments and to use per-position defaults for higher aritiescons to compare equal to vectors and lists and to print as a listrepeat return a lazy seq, so it compares equal to vectors and lists<, >, <=, >= to return true= to compare js/Date values, so distinct #inst literals are not equalprintln in cljs.testfloat and double core functionsare to substitute bindings into the test form like CLJS, so assertions such as thrown? dispatch correctly(+) and (*) and unary (- x)clojure.core and cljs.core to the core module so they can be required, including aliased fns and macros (c/inc, c/->)repeatedly to return an empty seq for count 0 and throw on a non-number countrealized? for delays and lazy seqs#uuid literals with a UUID type#inst literals as js/Date and add inst?cljs.test/assert-expr with defmethod, like CLJSclojure.string/replace-firstdelete) now correctly shadows the special formconj + SortedSet DCE issue... spread:extend-via-metadata is presentdissoc and conj!squint.edn / clojure.edn, a minimal EDN reader in 300-ish lines of JS.*print-fn*, *print-err-fn* and *print-newline* dynamic vars, similar to CLJSprintln is no longer a synonym for console.log and now prints through *print-fn* like CLJSdouble?, vary-meta, symbol core fnswith-out-strprint, prcoll? returned true for functions. Use aset, aget for raw property access on non-objects.core.js more suitable for treeshaking when only a few functions from it are used.pr-str and prn no longer misreport a shared reference as circular. An object referenced from two places now prints in full at each occurrence, while genuine cycles still print as #object[circular].thrown? to work with :default like CLJS(vec (list ..)) edge case(let [x {:a 1}] (x :a)) works.indexOf on lazy seqs, matching CLJS:deps in squint.edn, resolved via clojure -Spath, added to :paths implicitly. Squint only supports :git/sha and :local/root libraries for now.warn-on-lazy-reusae! now prints a deprecation warning and does nothing. It will be removed in the future.= compares two lazy seqs of the same type element-wise instead of as plain objects, so e.g. (= (concat nil ["a"]) (concat nil ["a"])) is truepersistent! no longer freezes its argument, so persistent structures stay extensible for symbol-keyed metadatavswap! and vreset! return the new value, like Clojurenth on an out-of-bounds index without a default now throws, like Clojuredisj on nil returns nil instead of throwingname now returns the part after the / for a namespaced keyword/symbol (e.g. (name :foo/bar) -> "bar"), consistent with namespace so they round-trip. Since keywords are strings in squint this also applies to slash-containing strings (differs from Clojure string name).namespace uses the same / threshold as name, so a leading / yields a nil namespace instead of an empty string and (str (namespace x) "/" (name x)) round-trips.var? (always false; squint has no first-class vars)binding. An earmuffed def (e.g. *foo*) compiles to a mutable box {value ...}; references read .value, set! assigns it, and binding saves/sets/restores it. Because only the box's property is mutated (never the import binding), this works across separately-compiled ESM modules.defprotocol: support the :extend-via-metadata option; protocol methods fall back to an impl on the receiver's metadata keyed by the fully-qualified method name:require-macros accepts a bare symbol libspec (not only a vector).cljc/.cljs namespace required via an :as alias (without :refer) now expand (alias resolves to the macro namespace)keyword?, simple-keyword?, qualified-keyword? (keywords are strings in squint, so these are string-based) and unchecked-intmap-entry?; map entries are tagged so they are distinguishable from plain vectorsreifykeyword, symbol?, namespace, unchecked-inc-int, unchecked-dec-int, unchecked-add-int (keywords/symbols are strings in squint)[x.y :as r] + (let [r ...] (r/f r))); they now resolve to the full-namespace import instead of the local= distinguishing null from undefined; both are nil in CLJS, so (= js/undefined nil) and e.g. (= (keys nil) nil) now return true.^:foo) on vector, map, set and fn literals as runtime metadata, matching CLJS ((meta ^:foo [1 2]) -> {:foo true}). Reader location keys, compiler directives (:tag/:async/:gen) and squint-internal keys are not included; edamame now records location under :line/:column.= not treating sequential collections of different concrete types as equal; (= '(1 2) [1 2]) and lazy-seq/vector comparisons now return true, matching CLJS. Equal-typed collections keep their existing fast paths.fn whose name munges (e.g. dispatch!) emitting an invalid self-reference (dispatch!.cljs$...) in its dispatcher; the munged name is now used.def/defn whose name shadows a :require alias or :refer producing colliding JS identifiers (import * as foo / import { foo } + var foo). The var is now renamed to a fresh gensym and exported under its real name; a bare foo is the var, foo/bar stays the alias.clojure.walk (walk, prewalk, postwalk, prewalk-replace, postwalk-replace, keywordize-keys, stringify-keys, and the *-demo helpers)empty, conj, assoc, into and other copy-based operations now carry metadata, matching Clojure (previously metadata was dropped on the freshly built structure); with-meta/copy now also support lists#html now escapes literal string attributes#html now does not emit true for a boolean-true attribute, but emits bare attribute#html now gates CSS output on style attribute nameorg.babashka.cli/completions command. Print an install snippet with e.g. squint org.babashka.cli/completions snippet --shell zsh (bash, zsh, fish, powershell and nushell are supported)--help/-h, argument validation and error messages are now provided by babashka.cli's dispatch (standard Usage/Commands/Options help, errors to stderr). Builds on #691 (@lread)info/eldoc/lookup, complete (including js/ interop completion) and load-file are now implemented.parse-long returning out-of-range values instead of nil (the safe-integer upper bound was a no-op due to a chained comparison)select-keys dropping keys mapped to nil (loose != undefined matched null); nil-valued keys are now kept, matching Clojureclojure.string/split limit semantics: a positive limit now caps the number of splits and keeps the remainder (e.g. (str/split "a-b-c-d" #"-" 2) -> ["a" "b-c-d"]) instead of truncating like JS String.split; limit 0 discards trailing empties, negative keeps themcompare throwing on booleans; (compare false true) -> -1 and (sort [true false]) no longer throws, matching CLJSclojure.set/intersection and union now use .size (not .length) for their set-size optimization, which was previously dead code (undefined > undefined); results were already correct, this restores the intended performanceseqable? returning false for maps; (seqable? {:a 1}) -> true, matching CLJS (seq already worked on objects)nth returning the not-found default for an in-bounds element that is undefined (e.g. sparse / object-array / JS-interop arrays); it now decides found-ness by the index bound, not the valueparse-double not trimming leading/trailing whitespace ((parse-double " 3.14 ") -> 3.14); the whitespace character class in the regexes was double-escaped and matched literal backslashes instead of control charspr-str / prn now print Infinity, -Infinity and NaN as ##Inf, ##-Inf and ##NaN, matching CLJS (str is unchanged)dedupe (seq and transducer arities)distinct?, any?, ifn?, list*#<Promise 1> / #<Promise rejected ..> / #<Promise pending> instead of silently unwrapping them. Also surfaces a Promise wrapper in the playground (resolved value still inspectable).str wrapping known-numeric infix expressions in ??'', which esbuild flagged as suspicious-nullish-coalescing. (@willcohen):macros option silently ignored when passed to compileString / compileStringEx from JavaScript callers (string keys were keyword-ized by clj-ize-opts and no longer matched the symbol-keyed macro lookups in compile*). (@willcohen)squint.compiler/compile* and squint.compiler/transpile* which accept either a string or a sequence of pre-parsed forms, skipping the forms -> string -> forms roundtrip for SSR use cases. The previous names compile-string* / transpile-string* are retained as deprecated aliases.#html / #jsx were erased when an attrs map was present without a :class key (e.g. [:div.myclass {}]).Add multimethod support (#806):
defmulti, defmethod, get-method, methods,
remove-method, remove-all-methods, prefer-method, prefers, and
hierarchy ops isa?, derive, underive, make-hierarchy, parents,
ancestors, descendants.
cljs.test/report is now a multimethod, extensible via defmethod:
(defmethod report [:cljs.test/default :begin-test-var] [m] ...)
test-var now fires :begin-test-var / :end-test-var events.
await in async functions, in anticipation of CLJS next.
The legacy js-await and js/await forms continue to work as
aliases for now and may be deprecated in a future version.cljs.test / clojure.test support: deftest, is, testing,
are, use-fixtures, async, run-tests.with-meta now preserves callability when applied to a function.cljc files via :require (no need for :require-macros)resolve-macro-ns for cherry namespace resolutionsquint.math file to package.json-0.0)squint.math, also available as clojure.math namespace (a direct port of the original)compare-and-swap!, swap-vals! and reset-vals! (@tonsky)dotimes with _ binding (@tonsky)shuffle not working on lazy sequences (@tonsky):require-macros with :refer now accumulate instead of overwriting (@willcohen)"node:fs", etc. to read config files for conditional compilationlet over defn to capture values.js-yield and js-yield* in expression positionsome? as macrovolatile!, vswap!, vreset!pr-str, prn etc now print EDN (with the idea that you can paste it back into your program)#js/Map reader that reads a JavaScript Map from a Clojure map (maps are printed like this with pr-str too)mapvdoseq can't be used in expression contextalength as macroidentical? callsnat-int?, neg-int?, pos-int? (@eNotchy)randnull and undefined in #html#html escape fixassoc calls, e.g. produced with ->auto-transient)str even moreRemove debug output from compilation
=, and, and not= even morenot= on undefined and false should return trueassoc, assoc! and get when object argument can be inferred or is type hinted with ^objectstr using macro that compiles into template strings + ?? '' for null/undefinedtake-last should return nil or empty seq for negative numberskeys and vals should work on js/Mapmap-indexed and keep-indexed lazy= when using it on numbers, strings or keyword literalsCljdoc chose squint for its small bundle sizes and easy migration off of TypeScript towards CLJS
= to a deep-equals implementation that works on primitives, objects, Arrays, Maps and Setsparse-doubleassoc-in on nil or undefineddissoc on nil or undefined:import-maps support in squint.edn (just literal replacements, prefixes not supported yet)while didn't compile correctlyclojure.string/includes?ClassCastException in statement function when passed Code records:with option in require, e.g. :with {:type :json}not= as functionrandom-uuid (@rafaeldelboni)[:$ ...] tagtrampoline (@rafaeldelboni)throw in expression positionnil:context expr in compile-string:context expr in set! expressionreturn:require + :rename + allow renamed value to be used in other :require clausenull when written in else branch of if#jsx and #htmldefclass: elide constructor when not provideddefclasstake-last(zero? "0") should return falsedeftype field name munging problemfn args don't get munged (@grayrest)| and ifclojure.string functions lower-case, upper-case, capitalize (@plexus)remove return a transducer when no collection is providedset? function (@jonasseglare)aset--paths with squint.edn config properlymapcat return a transducer if no collections are provided (@jonasseglare)clj->js to not process custom classesclj->js to realize lazy seqs into arraysnilsquint.edn configurations with
command line options, take 2squint.edn configurations with command line options.--extension and --paths via CLIchokidar to v4 which reduces the number of dependencies#html must now be explicitly passed as map literal: (let [m {:color :green}] #html [:div {:style {:& m}}]). Fixes issue when using lit-html in combination with classMap. See demo--repl opts to watch subcommand in CLIrun on Windowsnot: wrap argument in parens#html improvements, support :& for spreading propsshuffle core function random distribution and performancesdefclass in REPLnil #html rendering issueconst in let expressions, which esbuild optimizes better^:=> metadata:rename in :requiredefclass to override squint built-ins^:js. This benefits working with vitest and playwright.:output-dir. This behavior is now explicit with :copy-resources, see docs.#html reader for producing HTML literals using hiccup. See docs and playground example.simple-benchmarkdoseq expression with set! in function return positionnull in statement position^:gen + js-yield + js-yield* to write JS generator functions. See playgroundupdate-keys and update-vals= as reified function:refermemoize, filter transducer arity, peek, popdefclass^:async Object method in defclassjs* + // and /* */rem, nnext, str/end-with?, str/index-of and str/last-index-ofjs*) before imports (for JSDoc, Next.js "use client", etc)compileString in JS APIset/union, and smallest first for set/intersectionsorted-set, long, abs, keep-indexed transducer aritychildren function in tree-seq may return nilclojure.set/jointree-seq, flatten, seq? and sequential?clojure.set functions select, rename-keys, rename, project, and map-invert (@PEZ)reduce-kv with js/Map inputclojure.set functions: difference, union, subset?, and superset? (@PEZ)Symbol.iterable be destructureable even if it is not instance of Objectclojure.setreductionsbit-shift-left and more bit- related macrosnot with respect to truthinessreduce without initial value + empty coll, it should call f()not-emptyinto + set + xform>, < etc to be used as HOFsstr/split with respect to trailing empty split elementsintMath can be used without js/ prefixcompileStringEx for playground, preserves namespace transitionsjs-ininto + xformsort on stringsmapcat and concatreversemap + transduce supportfor in REPL modeforjs-mod and quot#jsx tagsre-findcondp macrocompare as default compare function in sort (which fixes numerical sorting)assoc! to be called on arbitrary classes (regression)get to call get method when present.str/split-linespartition-byparse-longsort-byasetcoercive-= as alias for ==js-deletemin-key and max-key and improve testsmin-key and max-keydefonce in REPL-modedoseq and for when binding name clashes with core varsquint compile or squint watchselect-keys is called with nilcljs.pprint (@PEZ)npx squint nrepl-server :port 1888assoc-in! should not mutate objects in the middle if they already existlazy-seq body just oncemin and maxmod, object?getclojure.string's triml, trimr, replaceexamples/vite-react by adding public/index.htmlfind, bounded-count, boolean?, merge-with, meta, with-meta, int?, ex-message, ex-cause, ex-info:pre and :post support in fnnumber?docstring in def:paths in a more robust fashionsquint.edn support, see docswatch subcommand to watch :paths from squint.ednlet variable names in JS more deterministic, which helps hot reloading in React(:require [foo.bar]) from :pathsbit-and and bit-orlib/squint.core.umd.js which defines a global squint.core object (easy to use in browsers, see docs)subs, fn?, re-seqsquint.edn with :paths to resolve macros from (via :require-macros)and and or with respect to CLJS truthinessnull, undefined and false are non-truthy, 0 and "" are truthy.dotimessquint repl improvementscatch to core var.cljc filescompileStringinstance? in return positionto-array, str/starts-with?compareinto-arrayinto-array, some-fn, keep-indexedlazy-seq when body returns nilmax, min, every-predreduce-kvrand-nth, aclone, add-watch, remove-watchdefclass. See doc/defclass.md.and and or without argumentsdoseq in return position emits invalid codejs-objfor and doseq (@lilactown):as alias with hyphen in namespace require libspecAdd preliminary Node.js API in node.js
{:& more :foo :bar} syntax in JSX to spread the more map into the props, inspired by helixzero? pos? and neg? core functionsboolean core function exportboolean core function+ symbol import by bumping shadow-cljs@babel/preset-react dependency to dev dependency.mjs. Use --extension js to change this to .js.--extension option to set extension for JS files.mjs to .jsCan you improve this documentation? These fine people already did:
Michiel Borkent, Peter Strömberg, jonasseglare, Rafael Delboni, Nikita Prokopov, Will Cohen, Phil Baker, Arne Brasseur, Brandon Stubbs, Karl Guertin, Clément, zachcp, Cora Sutton, Jack Rusher & Alex DavisEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |