diff --git a/.hgignore b/.hgignore index c9637c8..c065e98 100644 --- a/.hgignore +++ b/.hgignore @@ -1,5 +1,4 @@ .*\.pyc$ -build .hypothesis .pytest_cache .vscode diff --git a/docs/VUI-docs/build/doctrees/core.doctree b/docs/VUI-docs/build/doctrees/core.doctree new file mode 100644 index 0000000..555cc59 Binary files /dev/null and b/docs/VUI-docs/build/doctrees/core.doctree differ diff --git a/docs/VUI-docs/build/doctrees/display.doctree b/docs/VUI-docs/build/doctrees/display.doctree new file mode 100644 index 0000000..42869cb Binary files /dev/null and b/docs/VUI-docs/build/doctrees/display.doctree differ diff --git a/docs/VUI-docs/build/doctrees/environment.pickle b/docs/VUI-docs/build/doctrees/environment.pickle new file mode 100644 index 0000000..af23363 Binary files /dev/null and b/docs/VUI-docs/build/doctrees/environment.pickle differ diff --git a/docs/VUI-docs/build/doctrees/index.doctree b/docs/VUI-docs/build/doctrees/index.doctree new file mode 100644 index 0000000..e5883d0 Binary files /dev/null and b/docs/VUI-docs/build/doctrees/index.doctree differ diff --git a/docs/VUI-docs/build/doctrees/main.doctree b/docs/VUI-docs/build/doctrees/main.doctree new file mode 100644 index 0000000..efb93de Binary files /dev/null and b/docs/VUI-docs/build/doctrees/main.doctree differ diff --git a/docs/VUI-docs/build/doctrees/persist_task.doctree b/docs/VUI-docs/build/doctrees/persist_task.doctree new file mode 100644 index 0000000..9f6ff13 Binary files /dev/null and b/docs/VUI-docs/build/doctrees/persist_task.doctree differ diff --git a/docs/VUI-docs/build/doctrees/stack_viewer.doctree b/docs/VUI-docs/build/doctrees/stack_viewer.doctree new file mode 100644 index 0000000..c1b5fba Binary files /dev/null and b/docs/VUI-docs/build/doctrees/stack_viewer.doctree differ diff --git a/docs/VUI-docs/build/doctrees/text_viewer.doctree b/docs/VUI-docs/build/doctrees/text_viewer.doctree new file mode 100644 index 0000000..23d65e0 Binary files /dev/null and b/docs/VUI-docs/build/doctrees/text_viewer.doctree differ diff --git a/docs/VUI-docs/build/doctrees/viewer.doctree b/docs/VUI-docs/build/doctrees/viewer.doctree new file mode 100644 index 0000000..cdd8f21 Binary files /dev/null and b/docs/VUI-docs/build/doctrees/viewer.doctree differ diff --git a/docs/VUI-docs/build/html/.buildinfo b/docs/VUI-docs/build/html/.buildinfo new file mode 100644 index 0000000..9ae7c5c --- /dev/null +++ b/docs/VUI-docs/build/html/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 045f1325f6d2a1aed4dff11fe7e98c72 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/VUI-docs/build/html/_images/Joy-VUI-screenshot.PNG b/docs/VUI-docs/build/html/_images/Joy-VUI-screenshot.PNG new file mode 100644 index 0000000..53f7bf1 Binary files /dev/null and b/docs/VUI-docs/build/html/_images/Joy-VUI-screenshot.PNG differ diff --git a/docs/VUI-docs/build/html/_images/packages_Vui.png b/docs/VUI-docs/build/html/_images/packages_Vui.png new file mode 100644 index 0000000..a1e8936 Binary files /dev/null and b/docs/VUI-docs/build/html/_images/packages_Vui.png differ diff --git a/docs/VUI-docs/build/html/_modules/index.html b/docs/VUI-docs/build/html/_modules/index.html new file mode 100644 index 0000000..20e40d1 --- /dev/null +++ b/docs/VUI-docs/build/html/_modules/index.html @@ -0,0 +1,105 @@ + + + + +
+ + +
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Thun is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Thun. If not see <http://www.gnu.org/licenses/>.
+#
+'''
+
+Core
+=====================
+
+The core module defines a bunch of system-wide "constants" (some colors
+and PyGame event groups), the message classes for Oberon-style message
+passing, a "world" class that holds the main context for the system, and
+a mainloop class that manages the, uh, main loop (the PyGame event queue.)
+
+'''
+from sys import stderr
+from traceback import format_exc
+import pygame
+from joy.joy import run
+from joy.utils.stack import stack_to_string
+
+
+COMMITTER = 'Joy <auto-commit@example.com>'
+
+
+BLACK = FOREGROUND = 0, 0, 0
+GREY = 127, 127, 127
+WHITE = BACKGROUND = 255, 255, 255
+BLUE = 100, 100, 255
+GREEN = 70, 200, 70
+
+
+MOUSE_EVENTS = frozenset({
+ pygame.MOUSEMOTION,
+ pygame.MOUSEBUTTONDOWN,
+ pygame.MOUSEBUTTONUP
+ })
+'PyGame mouse events.'
+
+ARROW_KEYS = frozenset({
+ pygame.K_UP,
+ pygame.K_DOWN,
+ pygame.K_LEFT,
+ pygame.K_RIGHT
+ })
+'PyGame arrow key events.'
+
+
+TASK_EVENTS = tuple(range(pygame.USEREVENT, pygame.NUMEVENTS))
+'Keep track of all possible task events.'
+
+AVAILABLE_TASK_EVENTS = set(TASK_EVENTS)
+'Task IDs that have not been assigned to a task.'
+
+ALLOWED_EVENTS = [pygame.QUIT, pygame.KEYUP, pygame.KEYDOWN]
+ALLOWED_EVENTS.extend(MOUSE_EVENTS)
+ALLOWED_EVENTS.extend(TASK_EVENTS)
+'Event "mask" for PyGame event queue, we are only interested in these event types.'
+
+
+ERROR = -1
+PENDING = 0
+SUCCESS = 1
+# 'Message status codes... dunno if this is a good idea or not...
+
+
+[docs]class Message(object):
+ '''Message base class. Contains ``sender`` field.'''
+ def __init__(self, sender):
+ self.sender = sender
+
+
+[docs]class CommandMessage(Message):
+ '''For commands, adds ``command`` field.'''
+ def __init__(self, sender, command):
+ Message.__init__(self, sender)
+ self.command = command
+
+
+[docs]class ModifyMessage(Message):
+ '''
+ For when resources are modified, adds ``subject`` and ``details``
+ fields.
+ '''
+ def __init__(self, sender, subject, **details):
+ Message.__init__(self, sender)
+ self.subject = subject
+ self.details = details
+
+
+[docs]class OpenMessage(Message):
+ '''
+ For when resources are modified, adds ``name``, content_id``,
+ ``status``, and ``traceback`` fields.
+ '''
+ def __init__(self, sender, name):
+ Message.__init__(self, sender)
+ self.name = name
+ self.content_id = self.thing = None
+ self.status = PENDING
+ self.traceback = None
+
+
+[docs]class PersistMessage(Message):
+ '''
+ For when resources are modified, adds ``content_id`` and ``details``
+ fields.
+ '''
+ def __init__(self, sender, content_id, **details):
+ Message.__init__(self, sender)
+ self.content_id = content_id
+ self.details = details
+
+
+
+
+
+# Joy Interpreter & Context
+
+
+[docs]class World(object):
+ '''
+ This object contains the system context, the stack, dictionary, a
+ reference to the display broadcast method, and the log.
+ '''
+
+ def __init__(self, stack_id, stack_holder, dictionary, notify, log):
+ self.stack_holder = stack_holder
+ self.dictionary = dictionary
+ self.notify = notify
+ self.stack_id = stack_id
+ self.log = log.lines
+ self.log_id = log.content_id
+
+[docs] def handle(self, message):
+ '''
+ Deal with updates to the stack and commands.
+ '''
+ if (isinstance(message, ModifyMessage)
+ and message.subject is self.stack_holder
+ ):
+ self._log_lines('', '%s <-' % self.format_stack())
+ if not isinstance(message, CommandMessage):
+ return
+ c, s, d = message.command, self.stack_holder[0], self.dictionary
+ self._log_lines('', '-> %s' % (c,))
+ self.stack_holder[0], _, self.dictionary = run(c, s, d)
+ mm = ModifyMessage(self, self.stack_holder, content_id=self.stack_id)
+ self.notify(mm)
+
+ def _log_lines(self, *lines):
+ self.log.extend(lines)
+ self.notify(ModifyMessage(self, self.log, content_id=self.log_id))
+
+ def format_stack(self):
+ try:
+ return stack_to_string(self.stack_holder[0])
+ except:
+ print >> stderr, format_exc()
+ return str(self.stack_holder[0])
+
+
+[docs]def push(sender, item, notify, stack_name='stack.pickle'):
+ '''
+ Helper function to push an item onto the system stack with message.
+ '''
+ om = OpenMessage(sender, stack_name)
+ notify(om)
+ if om.status == SUCCESS:
+ om.thing[0] = item, om.thing[0]
+ notify(ModifyMessage(sender, om.thing, content_id=om.content_id))
+ return om.status
+
+
+[docs]def open_viewer_on_string(sender, content, notify):
+ '''
+ Helper function to open a text viewer on a string.
+ Typically used to show tracebacks.
+ '''
+ push(sender, content, notify)
+ notify(CommandMessage(sender, 'good_viewer_location open_viewer'))
+
+
+# main loop
+
+
+[docs]class TheLoop(object):
+ '''
+ The main loop manages tasks and the PyGame event queue
+ and framerate clock.
+ '''
+
+ FRAME_RATE = 24
+
+ def __init__(self, display, clock):
+ self.display = display
+ self.clock = clock
+ self.tasks = {}
+ self.running = False
+
+[docs] def install_task(self, F, milliseconds):
+ '''
+ Install a task to run every so many milliseconds.
+ '''
+ try:
+ task_event_id = AVAILABLE_TASK_EVENTS.pop()
+ except KeyError:
+ raise RuntimeError('out of task ids')
+ self.tasks[task_event_id] = F
+ pygame.time.set_timer(task_event_id, milliseconds)
+ return task_event_id
+
+[docs] def remove_task(self, task_event_id):
+ '''
+ Remove an installed task.
+ '''
+ assert task_event_id in self.tasks, repr(task_event_id)
+ pygame.time.set_timer(task_event_id, 0)
+ del self.tasks[task_event_id]
+ AVAILABLE_TASK_EVENTS.add(task_event_id)
+
+ def __del__(self):
+ # Best effort to cancel all running tasks.
+ for task_event_id in self.tasks:
+ pygame.time.set_timer(task_event_id, 0)
+
+[docs] def run_task(self, task_event_id):
+ '''
+ Give a task its time to shine.
+ '''
+ task = self.tasks[task_event_id]
+ try:
+ task()
+ except:
+ traceback = format_exc()
+ self.remove_task(task_event_id)
+ print >> stderr, traceback
+ print >> stderr, 'TASK removed due to ERROR', task
+ open_viewer_on_string(self, traceback, self.display.broadcast)
+
+[docs] def loop(self):
+ '''
+ The actual main loop machinery.
+
+ Maintain a ``running`` flag, pump the PyGame event queue and
+ handle the events (dispatching to the display), tick the clock.
+
+ When the loop is exited (by clicking the window close button or
+ pressing the ``escape`` key) it broadcasts a ``ShutdownMessage``.
+ '''
+ self.running = True
+ while self.running:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ self.running = False
+ elif event.type == pygame.KEYUP and event.key == pygame.K_ESCAPE:
+ self.running = False
+ elif event.type in self.tasks:
+ self.run_task(event.type)
+ else:
+ self.display.dispatch_event(event)
+ pygame.display.update()
+ self.clock.tick(self.FRAME_RATE)
+ self.display.broadcast(ShutdownMessage(self))
+
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Thun is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Thun. If not see <http://www.gnu.org/licenses/>.
+#
+'''
+
+Display
+=================
+
+This module implements a simple visual display system modeled on Oberon.
+
+Refer to Chapter 4 of the Project Oberon book for more information.
+
+There is a Display object that manages a pygame surface and N vertical
+tracks each of which manages zero or more viewers.
+'''
+from copy import copy
+from sys import stderr
+from traceback import format_exc
+import pygame
+from .core import (
+ open_viewer_on_string,
+ GREY,
+ MOUSE_EVENTS,
+ )
+from .viewer import Viewer
+from joy.vui import text_viewer
+
+
+[docs]class Display(object):
+ '''
+ Manage tracks and viewers on a screen (Pygame surface.)
+
+ The size and number of tracks are defined by passing in at least two
+ ratios, e.g. Display(screen, 1, 4, 4) would create three tracks, one
+ small one on the left and two larger ones of the same size, each four
+ times wider than the left one.
+
+ All tracks take up the whole height of the display screen. Tracks
+ manage zero or more Viewers. When you "grow" a viewer a new track is
+ created that overlays or hides one or two existing tracks, and when
+ the last viewer in an overlay track is closed the track closes too
+ and reveals the hidden tracks (and their viewers, if any.)
+
+ In order to facilitate command underlining while mouse dragging the
+ lookup parameter must be a function that accepts a string and returns
+ a Boolean indicating whether that string is a valid Joy function name.
+ Typically you pass in the __contains__ method of the Joy dict. This
+ is a case of breaking "loose coupling" to gain efficiency, as otherwise
+ we would have to e.g. send some sort of lookup message to the
+ World context object, going through the whole Display.broadcast()
+ machinery, etc. Not something you want to do on each MOUSEMOTION
+ event.
+ '''
+
+ def __init__(self, screen, lookup, *track_ratios):
+ self.screen = screen
+ self.w, self.h = screen.get_width(), screen.get_height()
+ self.lookup = lookup
+ self.focused_viewer = None
+ self.tracks = [] # (x, track)
+ self.handlers = [] # Non-viewers that should receive messages.
+ # Create the tracks.
+ if not track_ratios: track_ratios = 1, 4
+ x, total = 0, sum(track_ratios)
+ for ratio in track_ratios[:-1]:
+ track_width = self.w * ratio / total
+ assert track_width >= 10 # minimum width 10 pixels
+ self._open_track(x, track_width)
+ x += track_width
+ self._open_track(x, self.w - x)
+
+ def _open_track(self, x, w):
+ '''Helper function to create the pygame surface and Track.'''
+ track_surface = self.screen.subsurface((x, 0, w, self.h))
+ self.tracks.append((x, Track(track_surface)))
+
+[docs] def open_viewer(self, x, y, class_):
+ '''
+ Open a viewer of class_ at the x, y location on the display,
+ return the viewer.
+ '''
+ track = self._track_at(x)[0]
+ V = track.open_viewer(y, class_)
+ V.focus(self)
+ return V
+
+[docs] def close_viewer(self, viewer):
+ '''Close the viewer.'''
+ for x, track in self.tracks:
+ if track.close_viewer(viewer):
+ if not track.viewers and track.hiding:
+ i = self.tracks.index((x, track))
+ self.tracks[i:i + 1] = track.hiding
+ assert sorted(self.tracks) == self.tracks
+ for _, exposed_track in track.hiding:
+ exposed_track.redraw()
+ if viewer is self.focused_viewer:
+ self.focused_viewer = None
+ break
+
+[docs] def change_viewer(self, viewer, y, relative=False):
+ '''
+ Adjust the top of the viewer to a new y within the boundaries of
+ its neighbors.
+
+ If relative is False new_y should be in screen coords, else new_y
+ should be relative to the top of the viewer.
+ '''
+ for _, track in self.tracks:
+ if track.change_viewer(viewer, y, relative):
+ break
+
+[docs] def grow_viewer(self, viewer):
+ '''
+ Cause the viewer to take up its whole track or, if it does
+ already, take up another track, up to the whole screen.
+
+ This is the inverse of closing a viewer. "Growing" a viewer
+ actually creates a new copy and a new track to hold it. The old
+ tracks and viewers are retained, and they get restored when the
+ covering track closes, which happens automatically when the last
+ viewer in the covering track is closed.
+ '''
+ for x, track in self.tracks:
+ for _, V in track.viewers:
+ if V is viewer:
+ return self._grow_viewer(x, track, viewer)
+
+ def _grow_viewer(self, x, track, viewer):
+ '''Helper function to "grow" a viewer.'''
+ new_viewer = None
+
+ if viewer.h < self.h:
+ # replace the track with a new track that contains
+ # a copy of the viewer at full height.
+ new_track = Track(track.surface) # Reuse it, why not?
+ new_viewer = copy(viewer)
+ new_track._grow_by(new_viewer, 0, self.h - viewer.h)
+ new_track.viewers.append((0, new_viewer))
+ new_track.hiding = [(x, track)]
+ self.tracks[self.tracks.index((x, track))] = x, new_track
+
+ elif viewer.w < self.w:
+ # replace two tracks
+ i = self.tracks.index((x, track))
+ try: # prefer the one on the right
+ xx, xtrack = self.tracks[i + 1]
+ except IndexError:
+ i -= 1 # okay, the one on the left
+ xx, xtrack = self.tracks[i]
+ hiding = [(xx, xtrack), (x, track)]
+ else:
+ hiding = [(x, track), (xx, xtrack)]
+ # We know there has to be at least one other track because it
+ # there weren't then that implies that the one track takes up
+ # the whole display screen (the only way you can get just one
+ # track is by growing a viewer to cover the whole screen.)
+ # Ergo, viewer.w == self.w, so this branch doesn't run.
+ new_x = min(x, xx)
+ new_w = track.w + xtrack.w
+ r = new_x, 0, new_w, self.h
+ new_track = Track(self.screen.subsurface(r))
+ new_viewer = copy(viewer)
+ r = 0, 0, new_w, self.h
+ new_viewer.resurface(new_track.surface.subsurface(r))
+ new_track.viewers.append((0, new_viewer))
+ new_track.hiding = hiding
+ self.tracks[i:i + 2] = [(new_x, new_track)]
+ new_viewer.draw()
+
+ return new_viewer
+
+ def _move_viewer(self, to, rel_y, viewer, _x, y):
+ '''
+ Helper function to move (really copy) a viewer to a new location.
+ '''
+ h = to.split(rel_y)
+ new_viewer = copy(viewer)
+ if not isinstance(to, Track):
+ to = next(T for _, T in self.tracks
+ for _, V in T.viewers
+ if V is to)
+ new_viewer.resurface(to.surface.subsurface((0, y, to.w, h)))
+ to.viewers.append((y, new_viewer))
+ to.viewers.sort() # bisect.insort() would be overkill here.
+ new_viewer.draw()
+ self.close_viewer(viewer)
+
+ def _track_at(self, x):
+ '''
+ Return the track at x along with the track-relative x coordinate,
+ raise ValueError if x is off-screen.
+ '''
+ for track_x, track in self.tracks:
+ if x < track_x + track.w:
+ return track, x - track_x
+ raise ValueError('x outside display: %r' % (x,))
+
+[docs] def at(self, x, y):
+ '''
+ Return the viewer (which can be a Track) at the x, y location,
+ along with the relative-to-viewer-surface x and y coordinates.
+ If there is no viewer at the location the Track will be returned
+ instead.
+ '''
+ track, x = self._track_at(x)
+ viewer, y = track.viewer_at(y)
+ return viewer, x, y
+
+[docs] def iter_viewers(self):
+ '''
+ Iterate through all viewers yielding (viewer, x, y) three-tuples.
+ The x and y coordinates are screen pixels of the top-left corner
+ of the viewer.
+ '''
+ for x, T in self.tracks:
+ for y, V in T.viewers:
+ yield V, x, y
+
+[docs] def done_resizing(self):
+ '''
+ Helper method called directly by ``MenuViewer.mouse_up()`` to (hackily)
+ update the display when done resizing a viewer.
+ '''
+ for _, track in self.tracks: # This should be done by a Message?
+ if track.resizing_viewer:
+ track.resizing_viewer.draw()
+ track.resizing_viewer = None
+ break
+
+[docs] def broadcast(self, message):
+ '''
+ Broadcast a message to all viewers (except the sender) and all
+ registered handlers.
+ '''
+ for _, track in self.tracks:
+ track.broadcast(message)
+ for handler in self.handlers:
+ handler(message)
+
+[docs] def redraw(self):
+ '''
+ Redraw all tracks (which will redraw all viewers.)
+ '''
+ for _, track in self.tracks:
+ track.redraw()
+
+[docs] def focus(self, viewer):
+ '''
+ Set system focus to a given viewer (or no viewer if a track.)
+ '''
+ if isinstance(viewer, Track):
+ if self.focused_viewer: self.focused_viewer.unfocus()
+ self.focused_viewer = None
+ elif viewer is not self.focused_viewer:
+ if self.focused_viewer: self.focused_viewer.unfocus()
+ self.focused_viewer = viewer
+ viewer.focus(self)
+
+[docs] def dispatch_event(self, event):
+ '''
+ Display event handling.
+ '''
+ try:
+ if event.type in {pygame.KEYUP, pygame.KEYDOWN}:
+ self._keyboard_event(event)
+ elif event.type in MOUSE_EVENTS:
+ self._mouse_event(event)
+ else:
+ print >> stderr, (
+ 'received event %s Use pygame.event.set_allowed().'
+ % pygame.event.event_name(event.type)
+ )
+ # Catch all exceptions and open a viewer.
+ except:
+ err = format_exc()
+ print >> stderr, err # To be safe just print it right away.
+ open_viewer_on_string(self, err, self.broadcast)
+
+ def _keyboard_event(self, event):
+ if event.key == pygame.K_PAUSE and event.type == pygame.KEYUP:
+ # At least on my keyboard the break/pause key sends K_PAUSE.
+ # The main use of this is to open a TextViewer if you
+ # accidentally close all the viewers, so you can recover.
+ raise KeyboardInterrupt('break')
+ if not self.focused_viewer:
+ return
+ if event.type == pygame.KEYUP:
+ self.focused_viewer.key_up(self, event.key, event.mod)
+ elif event.type == pygame.KEYDOWN:
+ self.focused_viewer.key_down(
+ self, event.unicode, event.key, event.mod)
+
+ def _mouse_event(self, event):
+ V, x, y = self.at(*event.pos)
+
+ if event.type == pygame.MOUSEMOTION:
+ if not isinstance(V, Track):
+ V.mouse_motion(self, x, y, *(event.rel + event.buttons))
+
+ elif event.type == pygame.MOUSEBUTTONDOWN:
+ if event.button == 1:
+ self.focus(V)
+ V.mouse_down(self, x, y, event.button)
+
+ else:
+ assert event.type == pygame.MOUSEBUTTONUP
+
+ # Check for moving viewer.
+ if (event.button == 2
+ and self.focused_viewer
+ and V is not self.focused_viewer
+ and V.MINIMUM_HEIGHT < y < V.h - self.focused_viewer.MINIMUM_HEIGHT
+ ):
+ self._move_viewer(V, y, self.focused_viewer, *event.pos)
+
+ else:
+ V.mouse_up(self, x, y, event.button)
+
+[docs] def init_text(self, pt, x, y, filename):
+ '''
+ Open and return a ``TextViewer`` on a given file (which must be present
+ in the ``JOYHOME`` directory.)
+ '''
+ viewer = self.open_viewer(x, y, text_viewer.TextViewer)
+ viewer.content_id, viewer.lines = pt.open(filename)
+ viewer.draw()
+ return viewer
+
+
+[docs]class Track(Viewer):
+ '''
+ Manage a vertical strip of the display, and the viewers on it.
+ '''
+
+ def __init__(self, surface):
+ Viewer.__init__(self, surface)
+ self.viewers = [] # (y, viewer)
+ self.hiding = None
+ self.resizing_viewer = None
+ self.draw()
+
+[docs] def split(self, y):
+ '''
+ Split the Track at the y coordinate and return the height
+ available for a new viewer. Tracks manage a vertical strip of
+ the display screen so they don't resize their surface when split.
+ '''
+ h = self.viewers[0][0] if self.viewers else self.h
+ assert h > y
+ return h - y
+
+[docs] def draw(self, rect=None):
+ '''Draw the track onto its surface, clearing all content.
+
+ If rect is passed only draw to that area. This supports e.g.
+ closing a viewer that then exposes part of the track.
+ '''
+ self.surface.fill(GREY, rect=rect)
+
+[docs] def viewer_at(self, y):
+ '''
+ Return the viewer at y along with the viewer-relative y coordinate,
+ if there's no viewer at y return this track and y.
+ '''
+ for viewer_y, viewer in self.viewers:
+ if viewer_y < y <= viewer_y + viewer.h:
+ return viewer, y - viewer_y
+ return self, y
+
+[docs] def open_viewer(self, y, class_):
+ '''Open and return a viewer of class at y.'''
+ # Todo: if y coincides with some other viewer's y replace it.
+ viewer, viewer_y = self.viewer_at(y)
+ h = viewer.split(viewer_y)
+ new_viewer = class_(self.surface.subsurface((0, y, self.w, h)))
+ new_viewer.draw()
+ self.viewers.append((y, new_viewer))
+ self.viewers.sort() # Could use bisect module but how many
+ # viewers will you ever have?
+ return new_viewer
+
+[docs] def close_viewer(self, viewer):
+ '''Close the viewer, reuse the freed space.'''
+ for y, V in self.viewers:
+ if V is viewer:
+ self._close_viewer(y, V)
+ return True
+ return False
+
+ def _close_viewer(self, y, viewer):
+ '''Helper function to do the actual closing.'''
+ i = self.viewers.index((y, viewer))
+ del self.viewers[i]
+ if i: # The previous viewer gets the space.
+ previous_y, previous_viewer = self.viewers[i - 1]
+ self._grow_by(previous_viewer, previous_y, viewer.h)
+ else: # This track gets the space.
+ self.draw((0, y, self.w, viewer.surface.get_height()))
+ viewer.close()
+
+ def _grow_by(self, viewer, y, h):
+ '''Grow a viewer (located at y) by height h.
+
+ This might seem like it should be a method of the viewer, but
+ the viewer knows nothing of its own y location on the screen nor
+ the parent track's surface (to make a new subsurface) so it has
+ to be a method of the track, which has both.
+ '''
+ h = viewer.surface.get_height() + h
+ try:
+ surface = self.surface.subsurface((0, y, self.w, h))
+ except ValueError: # subsurface rectangle outside surface area
+ pass
+ else:
+ viewer.resurface(surface)
+ if h <= viewer.last_touch[1]: viewer.last_touch = 0, 0
+ viewer.draw()
+
+[docs] def change_viewer(self, viewer, new_y, relative=False):
+ '''
+ Adjust the top of the viewer to a new y within the boundaries of
+ its neighbors.
+
+ If relative is False new_y should be in screen coords, else new_y
+ should be relative to the top of the viewer.
+ '''
+ for old_y, V in self.viewers:
+ if V is viewer:
+ if relative: new_y += old_y
+ if new_y != old_y: self._change_viewer(new_y, old_y, V)
+ return True
+ return False
+
+ def _change_viewer(self, new_y, old_y, viewer):
+ new_y = max(0, min(self.h, new_y))
+ i = self.viewers.index((old_y, viewer))
+ if new_y < old_y: # Enlarge self, shrink upper neighbor.
+ if i:
+ previous_y, previous_viewer = self.viewers[i - 1]
+ if new_y - previous_y < self.MINIMUM_HEIGHT:
+ return
+ previous_viewer.resizing = 1
+ h = previous_viewer.split(new_y - previous_y)
+ previous_viewer.resizing = 0
+ self.resizing_viewer = previous_viewer
+ else:
+ h = old_y - new_y
+ self._grow_by(viewer, new_y, h)
+
+ else: # Shink self, enlarge upper neighbor.
+ # Enforce invariant.
+ try:
+ h, _ = self.viewers[i + 1]
+ except IndexError: # No next viewer.
+ h = self.h
+ if h - new_y < self.MINIMUM_HEIGHT:
+ return
+
+ # Change the viewer and adjust the upper viewer or track.
+ h = new_y - old_y
+ self._grow_by(viewer, new_y, -h) # grow by negative height!
+ if i:
+ previous_y, previous_viewer = self.viewers[i - 1]
+ previous_viewer.resizing = 1
+ self._grow_by(previous_viewer, previous_y, h)
+ previous_viewer.resizing = 0
+ self.resizing_viewer = previous_viewer
+ else:
+ self.draw((0, old_y, self.w, h))
+
+ self.viewers[i] = new_y, viewer
+ # self.viewers.sort() # Not necessary, invariant holds.
+ assert sorted(self.viewers) == self.viewers
+
+[docs] def broadcast(self, message):
+ '''
+ Broadcast a message to all viewers on this track (except the sender.)
+ '''
+ for _, viewer in self.viewers:
+ if viewer is not message.sender:
+ viewer.handle(message)
+
+[docs] def redraw(self):
+ '''Redraw the track and all of its viewers.'''
+ self.draw()
+ for _, viewer in self.viewers:
+ viewer.draw()
+
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Thun is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Thun. If not see <http://www.gnu.org/licenses/>.
+#
+'''
+
+Main Module
+======================================
+
+Pulls everything together.
+
+'''
+import os, sys, traceback
+import pygame
+from joy.library import initialize, DefinitionWrapper, SimpleFunctionWrapper
+from joy.vui import core, display, persist_task
+
+
+FULLSCREEN = '-f' in sys.argv
+
+
+JOY_HOME = os.environ.get('JOY_HOME')
+if JOY_HOME is None:
+ JOY_HOME = os.path.expanduser('~/.thun')
+ if not os.path.isabs(JOY_HOME):
+ raise ValueError('what directory?')
+
+
+[docs]def load_definitions(pt, dictionary):
+ '''Load definitions from ``definitions.txt``.'''
+ lines = pt.open('definitions.txt')[1]
+ for line in lines:
+ if '==' in line:
+ DefinitionWrapper.add_def(line, dictionary)
+
+
+[docs]def load_primitives(home, name_space):
+ '''Load primitives from ``library.py``.'''
+ fn = os.path.join(home, 'library.py')
+ if os.path.exists(fn):
+ execfile(fn, name_space)
+
+
+[docs]def init():
+ '''
+ Initialize the system.
+
+ * Init PyGame
+ * Create main window
+ * Start the PyGame clock
+ * Set the event mask
+ * Create the PersistTask
+
+ '''
+ print 'Initializing Pygame...'
+ pygame.init()
+ print 'Creating window...'
+ if FULLSCREEN:
+ screen = pygame.display.set_mode()
+ else:
+ screen = pygame.display.set_mode((1024, 768))
+ clock = pygame.time.Clock()
+ pygame.event.set_allowed(None)
+ pygame.event.set_allowed(core.ALLOWED_EVENTS)
+ pt = persist_task.PersistTask(JOY_HOME)
+ return screen, clock, pt
+
+
+[docs]def init_context(screen, clock, pt):
+ '''
+ More initialization
+
+ * Create the Joy dictionary
+ * Create the Display
+ * Open the log, menu, and scratch text viewers, and the stack pickle
+ * Start the main loop
+ * Create the World object
+ * Register PersistTask and World message handlers with the Display
+ * Load user function definitions.
+
+ '''
+ D = initialize()
+ d = display.Display(
+ screen,
+ D.__contains__,
+ *((144 - 89, 144, 89) if FULLSCREEN else (89, 144))
+ )
+ log = d.init_text(pt, 0, 0, 'log.txt')
+ tho = d.init_text(pt, 0, d.h / 3, 'menu.txt')
+ t = d.init_text(pt, d.w / 2, 0, 'scratch.txt')
+ loop = core.TheLoop(d, clock)
+ stack_id, stack_holder = pt.open('stack.pickle')
+ world = core.World(stack_id, stack_holder, D, d.broadcast, log)
+ loop.install_task(pt.task_run, 10000) # save files every ten seconds
+ d.handlers.append(pt.handle)
+ d.handlers.append(world.handle)
+ load_definitions(pt, D)
+ return locals()
+
+
+[docs]def error_guard(loop, n=10):
+ '''
+ Run a loop function, retry for ``n`` exceptions.
+ Prints tracebacks on ``sys.stderr``.
+ '''
+ error_count = 0
+ while error_count < n:
+ try:
+ loop()
+ break
+ except:
+ traceback.print_exc(file=sys.stderr)
+ error_count += 1
+
+
+[docs]class FileFaker(object):
+ '''Pretends to be a file object but writes to log instead.'''
+
+ def __init__(self, log):
+ self.log = log
+
+
+
+ def flush(self):
+ pass
+
+
+[docs]def main(screen, clock, pt):
+ '''
+ Main function.
+
+ * Call ``init_context()``
+ * Load primitives
+ * Create an ``evaluate`` function that lets you just eval some Python code
+ * Redirect ``stdout`` to the log using a ``FileFaker`` object, and...
+ * Start the main loop.
+ '''
+ name_space = init_context(screen, clock, pt)
+ load_primitives(pt.home, name_space.copy())
+
+ @SimpleFunctionWrapper
+ def evaluate(stack):
+ '''Evaluate the Python code text on the top of the stack.'''
+ code, stack = stack
+ exec code in name_space.copy()
+ return stack
+
+ name_space['D']['evaluate'] = evaluate
+
+
+ sys.stdout, old_stdout = FileFaker(name_space['log']), sys.stdout
+ try:
+ error_guard(name_space['loop'].loop)
+ finally:
+ sys.stdout = old_stdout
+
+ return name_space['d']
+
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Thun is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Thun. If not see <http://www.gnu.org/licenses/>.
+#
+'''
+
+Persist Task
+===========================
+
+This module deals with persisting the "resources" (text files and the
+stack) to the git repo in the ``JOY_HOME`` directory.
+
+'''
+import os, pickle, traceback
+from collections import Counter
+from dulwich.errors import NotGitRepository
+from dulwich.repo import Repo
+from joy.vui import core, init_joy_home
+
+
+[docs]def open_repo(repo_dir=None, initialize=False):
+ '''
+ Open, or create, and return a Dulwich git repo object for the given
+ directory. If the dir path doesn't exist it will be created. If it
+ does exist but isn't a repo the result depends on the ``initialize``
+ argument. If it is ``False`` (the default) a ``NotGitRepository``
+ exception is raised, otherwise ``git init`` is effected in the dir.
+ '''
+ if not os.path.exists(repo_dir):
+ os.makedirs(repo_dir, 0700)
+ return init_repo(repo_dir)
+ try:
+ return Repo(repo_dir)
+ except NotGitRepository:
+ if initialize:
+ return init_repo(repo_dir)
+ raise
+
+
+[docs]def init_repo(repo_dir):
+ '''
+ Initialize a git repository in the directory. Stage and commit all
+ files (toplevel, not those in subdirectories if any) in the dir.
+ '''
+ repo = Repo.init(repo_dir)
+ init_joy_home.initialize(repo_dir)
+ repo.stage([
+ fn
+ for fn in os.listdir(repo_dir)
+ if os.path.isfile(os.path.join(repo_dir, fn))
+ ])
+ repo.do_commit('Initial commit.', committer=core.COMMITTER)
+ return repo
+
+
+[docs]def make_repo_relative_path_maker(repo):
+ '''
+ Helper function to return a function that returns a path given a path,
+ that's relative to the repository.
+ '''
+ c = repo.controldir()
+ def repo_relative_path(path):
+ return os.path.relpath(path, os.path.commonprefix((c, path)))
+ return repo_relative_path
+
+
+[docs]class Resource(object):
+ '''
+ Handle the content of a text files as a list of lines, deal with
+ saving it and staging the changes to a repo.
+ '''
+
+ def __init__(self, filename, repo_relative_filename, thing=None):
+ self.filename = filename
+ self.repo_relative_filename = repo_relative_filename
+ self.thing = thing or self._from_file(open(filename))
+
+ def _from_file(self, f):
+ return f.read().splitlines()
+
+ def _to_file(self, f):
+ for line in self.thing:
+ print >> f, line
+
+[docs] def persist(self, repo):
+ '''
+ Save the lines to the file and stage the file in the repo.
+ '''
+ with open(self.filename, 'w') as f:
+ os.chmod(self.filename, 0600)
+ self._to_file(f)
+ f.flush()
+ os.fsync(f.fileno())
+ # For goodness's sake, write it to the disk already!
+ repo.stage([self.repo_relative_filename])
+
+
+[docs]class PickledResource(Resource):
+ '''
+ A ``Resource`` subclass that uses ``pickle`` on its file/thing.
+ '''
+
+ def _from_file(self, f):
+ return [pickle.load(f)]
+
+ def _to_file(self, f):
+ pickle.dump(self.thing[0], f)
+
+
+[docs]class PersistTask(object):
+ '''
+ This class deals with saving changes to the git repo.
+ '''
+
+ LIMIT = 10
+ MAX_SAVE = 10
+
+ def __init__(self, home):
+ self.home = home
+ self.repo = open_repo(home)
+ self._r = make_repo_relative_path_maker(self.repo)
+ self.counter = Counter()
+ self.store = {}
+
+[docs] def open(self, name):
+ '''
+ Look up the named file in home and return its content_id and data.
+ '''
+ fn = os.path.join(self.home, name)
+ content_id = name # hash(fn)
+ try:
+ resource = self.store[content_id]
+ except KeyError:
+ R = PickledResource if name.endswith('.pickle') else Resource
+ resource = self.store[content_id] = R(fn, self._r(fn))
+ return content_id, resource.thing
+
+[docs] def handle(self, message):
+ '''
+ Handle messages, dispatch to ``handle_FOO()`` methods.
+ '''
+ if isinstance(message, core.OpenMessage):
+ self.handle_open(message)
+ elif isinstance(message, core.ModifyMessage):
+ self.handle_modify(message)
+ elif isinstance(message, core.PersistMessage):
+ self.handle_persist(message)
+ elif isinstance(message, core.ShutdownMessage):
+ for content_id in self.counter:
+ self.store[content_id].persist(self.repo)
+ self.commit('shutdown')
+
+[docs] def handle_open(self, message):
+ '''
+ Foo.
+ '''
+ try:
+ message.content_id, message.thing = self.open(message.name)
+ except:
+ message.traceback = traceback.format_exc()
+ message.status = core.ERROR
+ else:
+ message.status = core.SUCCESS
+
+[docs] def handle_modify(self, message):
+ '''
+ Foo.
+ '''
+ try:
+ content_id = message.details['content_id']
+ except KeyError:
+ return
+ if not content_id:
+ return
+ self.counter[content_id] += 1
+ if self.counter[content_id] > self.LIMIT:
+ self.persist(content_id)
+ self.commit('due to activity')
+
+[docs] def handle_persist(self, message):
+ '''
+ Foo.
+ '''
+ try:
+ resource = self.store[message.content_id]
+ except KeyError:
+ resource = self.handle_persist_new(message)
+ resource.persist(self.repo)
+ self.commit('by request from %r' % (message.sender,))
+
+[docs] def handle_persist_new(self, message):
+ '''
+ Foo.
+ '''
+ name = message.content_id
+ check_filename(name)
+ fn = os.path.join(self.home, name)
+ thing = message.details['thing']
+ R = PickledResource if name.endswith('.pickle') else Resource # !!! refactor!
+ resource = self.store[name] = R(fn, self._r(fn), thing)
+ return resource
+
+[docs] def persist(self, content_id):
+ '''
+ Persist a resource.
+ '''
+ del self.counter[content_id]
+ self.store[content_id].persist(self.repo)
+
+[docs] def task_run(self):
+ '''
+ Stage any outstanding changes.
+ '''
+ if not self.counter:
+ return
+ for content_id, _ in self.counter.most_common(self.MAX_SAVE):
+ self.persist(content_id)
+ self.commit()
+
+[docs] def commit(self, message='auto-commit'):
+ '''
+ Commit.
+ '''
+ return self.repo.do_commit(message, committer=core.COMMITTER)
+
+[docs] def scan(self):
+ '''
+ Return a sorted list of all the files in the home dir.
+ '''
+ return sorted([
+ fn
+ for fn in os.listdir(self.home)
+ if os.path.isfile(os.path.join(self.home, fn))
+ ])
+
+
+[docs]def check_filename(name):
+ '''
+ Sanity checks for filename.
+ '''
+ # TODO: improve this...
+ if len(name) > 64:
+ raise ValueError('bad name %r' % (name,))
+ left, dot, right = name.partition('.')
+ if not left.isalnum() or dot and not right.isalnum():
+ raise ValueError('bad name %r' % (name,))
+
+
+if __name__ == '__main__':
+ JOY_HOME = os.path.expanduser('~/.thun')
+ pt = PersistTask(JOY_HOME)
+ content_id, thing = pt.open('stack.pickle')
+ pt.persist(content_id)
+ print pt.counter
+ mm = core.ModifyMessage(None, None, content_id=content_id)
+ pt.handle(mm)
+ print pt.counter
+
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Thun is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Thun. If not see <http://www.gnu.org/licenses/>.
+#
+'''
+
+Stack Viewer
+=================
+
+'''
+from joy.utils.stack import expression_to_string, iter_stack
+from joy.vui import core, text_viewer
+
+
+MAX_WIDTH = 64
+
+
+[docs]def fsi(item):
+ '''Format Stack Item'''
+ if isinstance(item, tuple):
+ res = '[%s]' % expression_to_string(item)
+ elif isinstance(item, str):
+ res = '"%s"' % item
+ else:
+ assert not isinstance(item, unicode), repr(item)
+ res = str(item)
+ if len(res) > MAX_WIDTH:
+ return res[:MAX_WIDTH - 3] + '...'
+ return res
+
+
+[docs]class StackViewer(text_viewer.TextViewer):
+
+ def __init__(self, surface):
+ super(StackViewer, self).__init__(surface)
+ self.stack_holder = None
+ self.content_id = 'stack viewer'
+
+ def _attach(self, display):
+ if self.stack_holder:
+ return
+ om = core.OpenMessage(self, 'stack.pickle')
+ display.broadcast(om)
+ if om.status != core.SUCCESS:
+ raise RuntimeError('stack unavailable')
+ self.stack_holder = om.thing
+
+ def _update(self):
+ self.lines[:] = map(fsi, iter_stack(self.stack_holder[0])) or ['']
+
+ def focus(self, display):
+ self._attach(display)
+ super(StackViewer, self).focus(display)
+
+ def handle(self, message):
+ if (isinstance(message, core.ModifyMessage)
+ and message.subject is self.stack_holder
+ ):
+ self._update()
+ self.draw_body()
+
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of Thun
+#
+# Thun is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Thun is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Thun. If not see <http://www.gnu.org/licenses/>.
+#
+'''
+
+Text Viewer
+=================
+
+'''
+import string
+import pygame
+from joy.utils.stack import expression_to_string
+from joy.vui.core import (
+ ARROW_KEYS,
+ BACKGROUND as BG,
+ FOREGROUND as FG,
+ CommandMessage,
+ ModifyMessage,
+ OpenMessage,
+ SUCCESS,
+ push,
+ )
+from joy.vui import viewer, font_data
+#reload(viewer)
+
+
+MenuViewer = viewer.MenuViewer
+
+
+SELECTION_COLOR = 235, 255, 0, 32
+SELECTION_KEYS = {
+ pygame.K_F1,
+ pygame.K_F2,
+ pygame.K_F3,
+ pygame.K_F4,
+ }
+STACK_CHATTER_KEYS = {
+ pygame.K_F5,
+ pygame.K_F6,
+ pygame.K_F7,
+ pygame.K_F8,
+ }
+
+
+def _is_command(display, word):
+ return display.lookup(word) or word.isdigit() or all(
+ not s or s.isdigit() for s in word.split('.', 1)
+ ) and len(word) > 1
+
+
+def format_stack_item(content):
+ if isinstance(content, tuple):
+ return '[%s]' % expression_to_string(content)
+ return str(content)
+
+
+class Font(object):
+
+ IMAGE = pygame.image.load(font_data.data, 'Iosevka12.BMP')
+ LOOKUP = (string.ascii_letters +
+ string.digits +
+ '''@#$&_~|`'"%^=-+*/\\<>[]{}(),.;:!?''')
+
+ def __init__(self, char_w=8, char_h=19, line_h=19):
+ self.char_w = char_w
+ self.char_h = char_h
+ self.line_h = line_h
+
+ def size(self, text):
+ return self.char_w * len(text), self.line_h
+
+ def render(self, text):
+ surface = pygame.Surface(self.size(text))
+ surface.fill(BG)
+ x = 0
+ for ch in text:
+ if not ch.isspace():
+ try:
+ i = self.LOOKUP.index(ch)
+ except ValueError:
+ # render a lil box...
+ r = (x + 1, self.line_h / 2 - 3,
+ self.char_w - 2, self.line_h / 2)
+ pygame.draw.rect(surface, FG, r, 1)
+ else:
+ iy, ix = divmod(i, 26)
+ ix *= self.char_w
+ iy *= self.char_h
+ area = ix, iy, self.char_w, self.char_h
+ surface.blit(self.IMAGE, (x, 0), area)
+ x += self.char_w
+ return surface
+
+ def __contains__(self, char):
+ assert len(char) == 1, repr(char)
+ return char in self.LOOKUP
+
+
+FONT = Font()
+
+
+[docs]class TextViewer(MenuViewer):
+
+ MINIMUM_HEIGHT = FONT.line_h + 3
+ CLOSE_TEXT = FONT.render('close')
+ GROW_TEXT = FONT.render('grow')
+
+ class Cursor(object):
+
+ def __init__(self, viewer):
+ self.v = viewer
+ self.x = self.y = 0
+ self.w, self.h = 2, FONT.line_h
+ self.mem = pygame.Surface((self.w, self.h))
+ self.can_fade = False
+
+ def set_to(self, x, y):
+ self.fade()
+ self.x, self.y = x, y
+ self.draw()
+
+ def draw(self):
+ r = self.x * FONT.char_w, self.screen_y(), self.w, self.h
+ self.mem.blit(self.v.body_surface, (0, 0), r)
+ self.v.body_surface.fill(FG, r)
+ self.can_fade = True
+
+ def fade(self):
+ if self.can_fade:
+ dest = self.x * FONT.char_w, self.screen_y()
+ self.v.body_surface.blit(self.mem, dest)
+ self.can_fade = False
+
+ def screen_y(self, row=None):
+ if row is None: row = self.y
+ return (row - self.v.at_line) * FONT.line_h
+
+ def up(self, _mod):
+ if self.y:
+ self.fade()
+ self.y -= 1
+ self.x = min(self.x, len(self.v.lines[self.y]))
+ self.draw()
+
+ def down(self, _mod):
+ if self.y < len(self.v.lines) - 1:
+ self.fade()
+ self.y += 1
+ self.x = min(self.x, len(self.v.lines[self.y]))
+ self.draw()
+ self._check_scroll()
+
+ def left(self, _mod):
+ if self.x:
+ self.fade()
+ self.x -= 1
+ self.draw()
+ elif self.y:
+ self.fade()
+ self.y -= 1
+ self.x = len(self.v.lines[self.y])
+ self.draw()
+ self._check_scroll()
+
+ def right(self, _mod):
+ if self.x < len(self.v.lines[self.y]):
+ self.fade()
+ self.x += 1
+ self.draw()
+ elif self.y < len(self.v.lines) - 1:
+ self.fade()
+ self.y += 1
+ self.x = 0
+ self.draw()
+ self._check_scroll()
+
+ def _check_scroll(self):
+ if self.y < self.v.at_line:
+ self.v.scroll_down()
+ elif self.y > self.v.at_line + self.v.h_in_lines:
+ self.v.scroll_up()
+
+ def __init__(self, surface):
+ self.cursor = self.Cursor(self)
+ MenuViewer.__init__(self, surface)
+ self.lines = ['']
+ self.content_id = None
+ self.at_line = 0
+ self.bg = BG
+ self.command = self.command_rect = None
+ self._sel_start = self._sel_end = None
+
+ def resurface(self, surface):
+ self.cursor.fade()
+ MenuViewer.resurface(self, surface)
+
+ w, h = self.CLOSE_TEXT.get_size()
+ self.close_rect = pygame.rect.Rect(self.w - 2 - w, 1, w, h)
+ w, h = self.GROW_TEXT.get_size()
+ self.grow_rect = pygame.rect.Rect(1, 1, w, h)
+
+ self.body_surface = surface.subsurface(self.body_rect)
+ self.line_w = self.body_rect.w / FONT.char_w + 1
+ self.h_in_lines = self.body_rect.h / FONT.line_h - 1
+ self.command_rect = self.command = None
+ self._sel_start = self._sel_end = None
+
+ def handle(self, message):
+ if super(TextViewer, self).handle(message):
+ return
+ if (isinstance(message, ModifyMessage)
+ and message.subject is self.lines
+ ):
+ # TODO: check self.at_line
+ self.draw_body()
+
+ # Drawing
+
+ def draw_menu(self):
+ #MenuViewer.draw_menu(self)
+ self.surface.blit(self.GROW_TEXT, (1, 1))
+ self.surface.blit(self.CLOSE_TEXT,
+ (self.w - 2 - self.close_rect.w, 1))
+ if self.content_id:
+ self.surface.blit(FONT.render('| ' + self.content_id),
+ (self.grow_rect.w + FONT.char_w + 3, 1))
+ self.surface.fill( # light grey background
+ (196, 196, 196),
+ (0, 0, self.w - 1, self.MINIMUM_HEIGHT),
+ pygame.BLEND_MULT
+ )
+
+ def draw_body(self):
+ MenuViewer.draw_body(self)
+ ys = xrange(0, self.body_rect.height, FONT.line_h)
+ ls = self.lines[self.at_line:self.at_line + self.h_in_lines + 2]
+ for y, line in zip(ys, ls):
+ self.draw_line(y, line)
+
+ def draw_line(self, y, line):
+ surface = FONT.render(line[:self.line_w])
+ self.body_surface.blit(surface, (0, y))
+
+ def _redraw_line(self, row):
+ try: line = self.lines[row]
+ except IndexError: line = ' ' * self.line_w
+ else:
+ n = self.line_w - len(line)
+ if n > 0: line = line + ' ' * n
+ self.draw_line(self.cursor.screen_y(row), line)
+
+ # General Functionality
+
+ def focus(self, display):
+ self.cursor.v = self
+ self.cursor.draw()
+
+ def unfocus(self):
+ self.cursor.fade()
+
+ def scroll_up(self):
+ if self.at_line < len(self.lines) - 1:
+ self._fade_command()
+ self._deselect()
+ self._sel_start = self._sel_end = None
+ self.at_line += 1
+ self.body_surface.scroll(0, -FONT.line_h)
+ row = self.h_in_lines + self.at_line
+ self._redraw_line(row)
+ self._redraw_line(row + 1)
+ self.cursor.draw()
+
+ def scroll_down(self):
+ if self.at_line:
+ self._fade_command()
+ self._deselect()
+ self._sel_start = self._sel_end = None
+ self.at_line -= 1
+ self.body_surface.scroll(0, FONT.line_h)
+ self._redraw_line(self.at_line)
+ self.cursor.draw()
+
+ def command_down(self, display, x, y):
+ if self.command_rect and self.command_rect.collidepoint(x, y):
+ return
+ self._fade_command()
+ line, column, _row = self.at(x, y)
+ word_start = line.rfind(' ', 0, column) + 1
+ word_end = line.find(' ', column)
+ if word_end == -1: word_end = len(line)
+ word = line[word_start:word_end]
+ if not _is_command(display, word):
+ return
+ r = self.command_rect = pygame.Rect(
+ word_start * FONT.char_w, # x
+ y / FONT.line_h * FONT.line_h, # y
+ len(word) * FONT.char_w, # w
+ FONT.line_h # h
+ )
+ pygame.draw.line(self.body_surface, FG, r.bottomleft, r.bottomright)
+ self.command = word
+
+ def command_up(self, display):
+ if self.command:
+ command = self.command
+ self._fade_command()
+ display.broadcast(CommandMessage(self, command))
+
+ def _fade_command(self):
+ self.command = None
+ r, self.command_rect = self.command_rect, None
+ if r:
+ pygame.draw.line(self.body_surface, BG, r.bottomleft, r.bottomright)
+
+[docs] def at(self, x, y):
+ '''
+ Given screen coordinates return the line, row, and column of the
+ character there.
+ '''
+ row = self.at_line + y / FONT.line_h
+ try:
+ line = self.lines[row]
+ except IndexError:
+ row = len(self.lines) - 1
+ line = self.lines[row]
+ column = len(line)
+ else:
+ column = min(x / FONT.char_w, len(line))
+ return line, column, row
+
+ # Event Processing
+
+ def body_click(self, display, x, y, button):
+ if button == 1:
+ _line, column, row = self.at(x, y)
+ self.cursor.set_to(column, row)
+ elif button == 2:
+ if pygame.KMOD_SHIFT & pygame.key.get_mods():
+ self.scroll_up()
+ else:
+ self.scroll_down()
+ elif button == 3:
+ self.command_down(display, x, y)
+ elif button == 4: self.scroll_down()
+ elif button == 5: self.scroll_up()
+
+ def menu_click(self, display, x, y, button):
+ if MenuViewer.menu_click(self, display, x, y, button):
+ return True
+
+ def mouse_up(self, display, x, y, button):
+ if MenuViewer.mouse_up(self, display, x, y, button):
+ return True
+ elif button == 3 and self.body_rect.collidepoint(x, y):
+ self.command_up(display)
+
+ def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2):
+ if MenuViewer.mouse_motion(self, display, x, y, rel_x, rel_y,
+ button0, button1, button2):
+ return True
+ if (button0
+ and display.focused_viewer is self
+ and self.body_rect.collidepoint(x, y)
+ ):
+ bx, by = self.body_rect.topleft
+ _line, column, row = self.at(x - bx, y - by)
+ self.cursor.set_to(column, row)
+ elif button2 and self.body_rect.collidepoint(x, y):
+ bx, by = self.body_rect.topleft
+ self.command_down(display, x - bx, y - by)
+
+
+
+ def key_down(self, display, uch, key, mod):
+
+ if key in SELECTION_KEYS:
+ self._selection_key(display, key, mod)
+ return
+ if key in STACK_CHATTER_KEYS:
+ self._stack_chatter_key(display, key, mod)
+ return
+ if key in ARROW_KEYS:
+ self._arrow_key(key, mod)
+ return
+
+ line, i = self.lines[self.cursor.y], self.cursor.x
+ modified = ()
+ if key == pygame.K_RETURN:
+ self._return_key(mod, line, i)
+ modified = True
+ elif key == pygame.K_BACKSPACE:
+ modified = self._backspace_key(mod, line, i)
+ elif key == pygame.K_DELETE:
+ modified = self._delete_key(mod, line, i)
+ elif key == pygame.K_INSERT:
+ modified = self._insert_key(display, mod, line, i)
+ elif uch and uch in FONT or uch == ' ':
+ self._printable_key(uch, mod, line, i)
+ modified = True
+ else:
+ print '%r %i %s' % (uch, key, bin(mod))
+
+ if modified:
+ # The selection is fragile.
+ self._deselect()
+ self._sel_start = self._sel_end = None
+ message = ModifyMessage(
+ self, self.lines, content_id=self.content_id)
+ display.broadcast(message)
+
+ def _stack_chatter_key(self, display, key, mod):
+ if key == pygame.K_F5:
+ if mod & pygame.KMOD_SHIFT:
+ command = 'roll<'
+ else:
+ command = 'swap'
+ elif key == pygame.K_F6:
+ if mod & pygame.KMOD_SHIFT:
+ command = 'roll>'
+ else:
+ command = 'dup'
+ elif key == pygame.K_F7:
+ if mod & pygame.KMOD_SHIFT:
+ command = 'tuck'
+ else:
+ command = 'over'
+## elif key == pygame.K_F8:
+## if mod & pygame.KMOD_SHIFT:
+## command = ''
+## else:
+## command = ''
+ else:
+ return
+ display.broadcast(CommandMessage(self, command))
+
+ # Selection Handling
+
+ def _selection_key(self, display, key, mod):
+ self.cursor.fade()
+ self._deselect()
+ if key == pygame.K_F1: # set sel start
+ self._sel_start = self.cursor.y, self.cursor.x
+ self._update_selection()
+ elif key == pygame.K_F2: # set sel end
+ self._sel_end = self.cursor.y, self.cursor.x
+ self._update_selection()
+ elif key == pygame.K_F3: # copy
+ if mod & pygame.KMOD_SHIFT:
+ self._parse_selection(display)
+ else:
+ self._copy_selection(display)
+ self._update_selection()
+ elif key == pygame.K_F4: # cut or delete
+ if mod & pygame.KMOD_SHIFT:
+ self._delete_selection(display)
+ else:
+ self._cut_selection(display)
+ self.cursor.draw()
+
+ def _deselect(self):
+ if self._has_selection():
+ srow, erow = self._sel_start[0], self._sel_end[0]
+ # Just erase the whole selection.
+ for r in range(min(srow, erow), max(srow, erow) + 1):
+ self._redraw_line(r)
+
+ def _copy_selection(self, display):
+ if push(self, self._get_selection(), display.broadcast) == SUCCESS:
+ return True
+## om = OpenMessage(self, 'stack.pickle')
+## display.broadcast(om)
+## if om.status == SUCCESS:
+## selection = self._get_selection()
+## om.thing[0] = selection, om.thing[0]
+## display.broadcast(ModifyMessage(
+## self, om.thing, content_id=om.content_id))
+
+ def _parse_selection(self, display):
+ if self._has_selection():
+ if self._copy_selection(display):
+ display.broadcast(CommandMessage(self, 'parse'))
+
+ def _cut_selection(self, display):
+ if self._has_selection():
+ if self._copy_selection(display):
+ self._delete_selection(display)
+
+ def _delete_selection(self, display):
+ if not self._has_selection():
+ return
+ self.cursor.fade()
+ srow, scolumn, erow, ecolumn = self._selection_coords()
+ if srow == erow:
+ line = self.lines[srow]
+ self.lines[srow] = line[:scolumn] + line[ecolumn:]
+ else:
+ left = self.lines[srow][:scolumn]
+ right = self.lines[erow][ecolumn:]
+ self.lines[srow:erow + 1] = [left + right]
+ self.draw_body()
+ self.cursor.set_to(srow, scolumn)
+ display.broadcast(ModifyMessage(
+ self, self.lines, content_id=self.content_id))
+
+ def _has_selection(self):
+ return (self._sel_start
+ and self._sel_end
+ and self._sel_start != self._sel_end)
+
+ def _get_selection(self):
+ '''Return the current selection if any as a single string.'''
+ if not self._has_selection():
+ return ''
+ srow, scolumn, erow, ecolumn = self._selection_coords()
+ if srow == erow:
+ return str(self.lines[srow][scolumn:ecolumn])
+ lines = []
+ assert srow < erow
+ while srow <= erow:
+ line = self.lines[srow]
+ e = ecolumn if srow == erow else len(line)
+ lines.append(line[scolumn:e])
+ scolumn = 0
+ srow += 1
+ return str('\n'.join(lines))
+
+ def _selection_coords(self):
+ (srow, scolumn), (erow, ecolumn) = (
+ min(self._sel_start, self._sel_end),
+ max(self._sel_start, self._sel_end)
+ )
+ return srow, scolumn, erow, ecolumn
+
+ def _update_selection(self):
+ if self._sel_start is None and self._sel_end:
+ self._sel_start = self._sel_end
+ elif self._sel_end is None and self._sel_start:
+ self._sel_end = self._sel_start
+ assert self._sel_start and self._sel_end
+ if self._sel_start != self._sel_end:
+ for rect in self._iter_selection_rectangles():
+ self.body_surface.fill(
+ SELECTION_COLOR,
+ rect,
+ pygame.BLEND_RGBA_MULT
+ )
+
+ def _iter_selection_rectangles(self, ):
+ srow, scolumn, erow, ecolumn = self._selection_coords()
+ if srow == erow:
+ yield (
+ scolumn * FONT.char_w,
+ self.cursor.screen_y(srow),
+ (ecolumn - scolumn) * FONT.char_w,
+ FONT.line_h
+ )
+ return
+ lines = self.lines[srow:erow + 1]
+ assert len(lines) >= 2
+ first_line = lines[0]
+ yield (
+ scolumn * FONT.char_w,
+ self.cursor.screen_y(srow),
+ (len(first_line) - scolumn) * FONT.char_w,
+ FONT.line_h
+ )
+ yield (
+ 0,
+ self.cursor.screen_y(erow),
+ ecolumn * FONT.char_w,
+ FONT.line_h
+ )
+ if len(lines) > 2:
+ for line in lines[1:-1]:
+ srow += 1
+ yield (
+ 0,
+ self.cursor.screen_y(srow),
+ len(line) * FONT.char_w,
+ FONT.line_h
+ )
+
+ # Key Handlers
+
+ def _printable_key(self, uch, _mod, line, i):
+ line = line[:i] + uch + line[i:]
+ self.lines[self.cursor.y] = line
+ self.cursor.fade()
+ self.cursor.x += 1
+ self.draw_line(self.cursor.screen_y(), line)
+ self.cursor.draw()
+
+ def _backspace_key(self, _mod, line, i):
+ res = False
+ if i:
+ line = line[:i - 1] + line[i:]
+ self.lines[self.cursor.y] = line
+ self.cursor.fade()
+ self.cursor.x -= 1
+ self.draw_line(self.cursor.screen_y(), line + ' ')
+ self.cursor.draw()
+ res = True
+ elif self.cursor.y:
+ y = self.cursor.y
+ left, right = self.lines[y - 1:y + 1]
+ self.lines[y - 1:y + 1] = [left + right]
+ self.cursor.x = len(left)
+ self.cursor.y -= 1
+ self.draw_body()
+ self.cursor.draw()
+ res = True
+ return res
+
+ def _delete_key(self, _mod, line, i):
+ res = False
+ if i < len(line):
+ line = line[:i] + line[i + 1:]
+ self.lines[self.cursor.y] = line
+ self.cursor.fade()
+ self.draw_line(self.cursor.screen_y(), line + ' ')
+ self.cursor.draw()
+ res = True
+ elif self.cursor.y < len(self.lines) - 1:
+ y = self.cursor.y
+ left, right = self.lines[y:y + 2]
+ self.lines[y:y + 2] = [left + right]
+ self.draw_body()
+ self.cursor.draw()
+ res = True
+ return res
+
+ def _arrow_key(self, key, mod):
+ if key == pygame.K_UP: self.cursor.up(mod)
+ elif key == pygame.K_DOWN: self.cursor.down(mod)
+ elif key == pygame.K_LEFT: self.cursor.left(mod)
+ elif key == pygame.K_RIGHT: self.cursor.right(mod)
+
+ def _return_key(self, _mod, line, i):
+ self.cursor.fade()
+ # Ignore the mods for now.
+ n = self.cursor.y
+ self.lines[n:n + 1] = [line[:i], line[i:]]
+ self.cursor.y += 1
+ self.cursor.x = 0
+ if self.cursor.y > self.at_line + self.h_in_lines:
+ self.scroll_up()
+ else:
+ self.draw_body()
+ self.cursor.draw()
+
+ def _insert_key(self, display, mod, _line, _i):
+ om = OpenMessage(self, 'stack.pickle')
+ display.broadcast(om)
+ if om.status != SUCCESS:
+ return
+ stack = om.thing[0]
+ if stack:
+ content = format_stack_item(stack[0])
+ if self.insert(content):
+ if mod & pygame.KMOD_SHIFT:
+ display.broadcast(CommandMessage(self, 'pop'))
+ return True
+
+ def insert(self, content):
+ assert isinstance(content, basestring), repr(content)
+ if content:
+ self.cursor.fade()
+ row, column = self.cursor.y, self.cursor.x
+ line = self.lines[row]
+ lines = (line[:column] + content + line[column:]).splitlines()
+ self.lines[row:row + 1] = lines
+ self.draw_body()
+ self.cursor.y = row + len(lines) - 1
+ self.cursor.x = len(lines[-1]) - len(line) + column
+ self.cursor.draw()
+ return True
+
+ def append(self, content):
+ self.cursor.fade()
+ self.cursor.y = len(self.lines) - 1
+ self.cursor.x = len(self.lines[self.cursor.y])
+ self.insert(content)
+
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Simon Forman
+#
+# This file is part of joy.py
+#
+# joy.py is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# joy.py is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with joy.py. If not see <http://www.gnu.org/licenses/>.
+#
+'''
+
+Viewer
+=================
+
+'''
+import pygame
+from joy.vui.core import BACKGROUND, FOREGROUND
+
+
+[docs]class Viewer(object):
+ '''
+ Base Viewer class
+ '''
+
+ MINIMUM_HEIGHT = 11
+
+ def __init__(self, surface):
+ self.resurface(surface)
+ self.last_touch = 0, 0
+
+ def resurface(self, surface):
+ self.w, self.h = surface.get_width(), surface.get_height()
+ self.surface = surface
+
+[docs] def split(self, y):
+ '''
+ Split the viewer at the y coordinate (which is relative to the
+ viewer's surface and must be inside it somewhere) and return the
+ remaining height. The upper part of the viewer remains (and gets
+ redrawn on a new surface) and the lower space is now available
+ for e.g. a new viewer.
+ '''
+ assert y >= self.MINIMUM_HEIGHT
+ new_viewer_h = self.h - y
+ self.resurface(self.surface.subsurface((0, 0, self.w, y)))
+ if y <= self.last_touch[1]: self.last_touch = 0, 0
+ self.draw()
+ return new_viewer_h
+
+ def handle(self, message):
+ assert self is not message.sender
+ pass
+
+[docs] def draw(self):
+ '''Draw the viewer onto its surface.'''
+ self.surface.fill(BACKGROUND)
+ x, y, h = self.w - 1, self.MINIMUM_HEIGHT, self.h - 1
+ # Right-hand side.
+ pygame.draw.line(self.surface, FOREGROUND, (x, 0), (x, h))
+ # Between header and body.
+ pygame.draw.line(self.surface, FOREGROUND, (0, y), (x, y))
+ # Bottom.
+ pygame.draw.line(self.surface, FOREGROUND, (0, h), (x, h))
+
+
+
+ def focus(self, display):
+ pass
+
+ def unfocus(self):
+ pass
+
+ # Event handling.
+
+ def mouse_down(self, display, x, y, button):
+ self.last_touch = x, y
+
+ def mouse_up(self, display, x, y, button):
+ pass
+
+ def mouse_motion(self, display, x, y, dx, dy, button0, button1, button2):
+ pass
+
+ def key_up(self, display, key, mod):
+ if key == pygame.K_q and mod & pygame.KMOD_CTRL: # Ctrl-q
+ display.close_viewer(self)
+ return True
+ if key == pygame.K_g and mod & pygame.KMOD_CTRL: # Ctrl-g
+ display.grow_viewer(self)
+ return True
+
+ def key_down(self, display, uch, key, mod):
+ pass
+
+
+[docs]class MenuViewer(Viewer):
+
+ '''
+ MenuViewer class
+ '''
+
+ MINIMUM_HEIGHT = 26
+
+ def __init__(self, surface):
+ Viewer.__init__(self, surface)
+ self.resizing = 0
+ self.bg = 100, 150, 100
+
+ def resurface(self, surface):
+ Viewer.resurface(self, surface)
+ n = self.MINIMUM_HEIGHT - 2
+ self.close_rect = pygame.rect.Rect(self.w - 2 - n, 1, n, n)
+ self.grow_rect = pygame.rect.Rect(1, 1, n, n)
+ self.body_rect = pygame.rect.Rect(
+ 0, self.MINIMUM_HEIGHT + 1,
+ self.w - 1, self.h - self.MINIMUM_HEIGHT - 2)
+
+[docs] def draw(self):
+ '''Draw the viewer onto its surface.'''
+ Viewer.draw(self)
+ if not self.resizing:
+ self.draw_menu()
+ self.draw_body()
+
+ def draw_menu(self):
+ # menu buttons
+ pygame.draw.rect(self.surface, FOREGROUND, self.close_rect, 1)
+ pygame.draw.rect(self.surface, FOREGROUND, self.grow_rect, 1)
+
+ def draw_body(self):
+ self.surface.fill(self.bg, self.body_rect)
+
+ def mouse_down(self, display, x, y, button):
+ Viewer.mouse_down(self, display, x, y, button)
+ if y <= self.MINIMUM_HEIGHT:
+ self.menu_click(display, x, y, button)
+ else:
+ bx, by = self.body_rect.topleft
+ self.body_click(display, x - bx, y - by, button)
+
+ def body_click(self, display, x, y, button):
+ if button == 1:
+ self.draw_an_a(x, y)
+
+ def menu_click(self, display, x, y, button):
+ if button == 1:
+ self.resizing = 1
+ elif button == 3:
+ if self.close_rect.collidepoint(x, y):
+ display.close_viewer(self)
+ return True
+ elif self.grow_rect.collidepoint(x, y):
+ display.grow_viewer(self)
+ return True
+
+ def mouse_up(self, display, x, y, button):
+
+ if button == 1 and self.resizing:
+ if self.resizing == 2:
+ self.resizing = 0
+ self.draw()
+ display.done_resizing()
+ self.resizing = 0
+ return True
+
+ def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2):
+ if self.resizing and button0:
+ self.resizing = 2
+ display.change_viewer(self, rel_y, relative=True)
+ return True
+ else:
+ self.resizing = 0
+ #self.draw_an_a(x, y)
+
+ def key_up(self, display, key, mod):
+ if Viewer.key_up(self, display, key, mod):
+ return True
+
+ def draw_an_a(self, x, y):
+ # Draw a crude letter A.
+ lw, lh = 10, 14
+ try: surface = self.surface.subsurface((x - lw, y - lh, lw, lh))
+ except ValueError: return
+ draw_a(surface, blend=1)
+
+
+[docs]class SomeViewer(MenuViewer):
+
+ def __init__(self, surface):
+ MenuViewer.__init__(self, surface)
+
+ def resurface(self, surface):
+ MenuViewer.resurface(self, surface)
+
+ def draw_menu(self):
+ MenuViewer.draw_menu(self)
+
+ def draw_body(self):
+ pass
+
+ def body_click(self, display, x, y, button):
+ pass
+
+ def menu_click(self, display, x, y, button):
+ if MenuViewer.menu_click(self, display, x, y, button):
+ return True
+
+ def mouse_up(self, display, x, y, button):
+ if MenuViewer.mouse_up(self, display, x, y, button):
+ return True
+
+ def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2):
+ if MenuViewer.mouse_motion(self, display, x, y, rel_x, rel_y,
+ button0, button1, button2):
+ return True
+
+ def key_down(self, display, uch, key, mod):
+ try:
+ print chr(key),
+ except ValueError:
+ pass
+
+
+# Note that Oberon book says that if you split at the exact top of a viewer
+# it should close, and I think this implies the new viewer gets the old
+# viewer's whole height. I haven't implemented that yet, so the edge-case
+# in the code is broken by "intent" for now..
+
+
+def draw_a(surface, color=FOREGROUND, blend=False):
+ w, h = surface.get_width() - 2, surface.get_height() - 2
+ pygame.draw.aalines(surface, color, False, (
+ (1, h), (w / 2, 1), (w, h), (1, h / 2)
+ ), blend)
+' + _('Hide Search Matches') + '
') + .appendTo($('#searchbox')); + } + }, + + /** + * init the domain index toggle buttons + */ + initIndexTable : function() { + var togglers = $('img.toggler').click(function() { + var src = $(this).attr('src'); + var idnum = $(this).attr('id').substr(7); + $('tr.cg-' + idnum).toggle(); + if (src.substr(-9) === 'minus.png') + $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); + else + $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); + }).css('display', ''); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { + togglers.click(); + } + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords : function() { + $('#searchbox .highlight-link').fadeOut(300); + $('span.highlighted').removeClass('highlighted'); + }, + + /** + * make the url absolute + */ + makeURL : function(relativeURL) { + return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; + }, + + /** + * get the current relative url + */ + getCurrentURL : function() { + var path = document.location.pathname; + var parts = path.split(/\//); + $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { + if (this === '..') + parts.pop(); + }); + var url = parts.join('/'); + return path.substring(url.lastIndexOf('/') + 1, path.length - 1); + }, + + initOnKeyListeners: function() { + $(document).keyup(function(event) { + var activeElementType = document.activeElement.tagName; + // don't navigate when in search box or textarea + if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT') { + switch (event.keyCode) { + case 37: // left + var prevHref = $('link[rel="prev"]').prop('href'); + if (prevHref) { + window.location.href = prevHref; + return false; + } + case 39: // right + var nextHref = $('link[rel="next"]').prop('href'); + if (nextHref) { + window.location.href = nextHref; + return false; + } + } + } + }); + } +}; + +// quick alias for translations +_ = Documentation.gettext; + +$(document).ready(function() { + Documentation.init(); +}); diff --git a/docs/VUI-docs/build/html/_static/documentation_options.js b/docs/VUI-docs/build/html/_static/documentation_options.js new file mode 100644 index 0000000..bee34fe --- /dev/null +++ b/docs/VUI-docs/build/html/_static/documentation_options.js @@ -0,0 +1,10 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '0.1', + LANGUAGE: 'None', + COLLAPSE_INDEX: false, + FILE_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, +}; \ No newline at end of file diff --git a/docs/VUI-docs/build/html/_static/down-pressed.png b/docs/VUI-docs/build/html/_static/down-pressed.png new file mode 100644 index 0000000..5756c8c Binary files /dev/null and b/docs/VUI-docs/build/html/_static/down-pressed.png differ diff --git a/docs/VUI-docs/build/html/_static/down.png b/docs/VUI-docs/build/html/_static/down.png new file mode 100644 index 0000000..1b3bdad Binary files /dev/null and b/docs/VUI-docs/build/html/_static/down.png differ diff --git a/docs/VUI-docs/build/html/_static/file.png b/docs/VUI-docs/build/html/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/docs/VUI-docs/build/html/_static/file.png differ diff --git a/docs/VUI-docs/build/html/_static/jquery-3.2.1.js b/docs/VUI-docs/build/html/_static/jquery-3.2.1.js new file mode 100644 index 0000000..d2d8ca4 --- /dev/null +++ b/docs/VUI-docs/build/html/_static/jquery-3.2.1.js @@ -0,0 +1,10253 @@ +/*! + * jQuery JavaScript Library v3.2.1 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2017-03-20T18:59Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var document = window.document; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var concat = arr.concat; + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + + + + function DOMEval( code, doc ) { + doc = doc || document; + + var script = doc.createElement( "script" ); + + script.text = code; + doc.head.appendChild( script ).parentNode.removeChild( script ); + } +/* global Symbol */ +// Defining this global in .eslintrc.json would create a danger of using the global +// unguarded in another place, it seems safer to define global only for this module + + + +var + version = "3.2.1", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }, + + // Support: Android <=4.0 only + // Make sure we trim BOM and NBSP + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + + // Return all the elements in a clean array + if ( num == null ) { + return slice.call( this ); + } + + // Return just the one element from the set + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + + if ( copyIsArray ) { + copyIsArray = false; + clone = src && Array.isArray( src ) ? src : []; + + } else { + clone = src && jQuery.isPlainObject( src ) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isFunction: function( obj ) { + return jQuery.type( obj ) === "function"; + }, + + isWindow: function( obj ) { + return obj != null && obj === obj.window; + }, + + isNumeric: function( obj ) { + + // As of jQuery 3.0, isNumeric is limited to + // strings and numbers (primitives or objects) + // that can be coerced to finite numbers (gh-2662) + var type = jQuery.type( obj ); + return ( type === "number" || type === "string" ) && + + // parseFloat NaNs numeric-cast false positives ("") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + !isNaN( obj - parseFloat( obj ) ); + }, + + isPlainObject: function( obj ) { + var proto, Ctor; + + // Detect obvious negatives + // Use toString instead of jQuery.type to catch host objects + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + // Objects with no prototype (e.g., `Object.create( null )`) are plain + if ( !proto ) { + return true; + } + + // Objects with prototype are plain iff they were constructed by a global Object function + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + + /* eslint-disable no-unused-vars */ + // See https://github.com/eslint/eslint/issues/6125 + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + type: function( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; + }, + + // Evaluates a script in a global context + globalEval: function( code ) { + DOMEval( code ); + }, + + // Convert dashed to camelCase; used by the css and data modules + // Support: IE <=9 - 11, Edge 12 - 13 + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // Support: Android <=4.0 only + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + now: Date.now, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +} ); + +function isArrayLike( obj ) { + + // Support: real iOS 8.2 only (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = jQuery.type( obj ); + + if ( type === "function" || jQuery.isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.3.3 + * https://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2016-08-08 + */ +(function( window ) { + +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf as it's faster than native + // https://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[i] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), + + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + // CSS escapes + // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox<24 + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + high < 0 ? + // BMP codepoint + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, + fcssescape = function( ch, asCodePoint ) { + if ( asCodePoint ) { + + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }, + + disabledAncestor = addCombinator( + function( elem ) { + return elem.disabled === true && ("form" in elem || "label" in elem); + }, + { dir: "parentNode", next: "legend" } + ); + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { + + // ID selector + if ( (m = match[1]) ) { + + // Document context + if ( nodeType === 9 ) { + if ( (elem = context.getElementById( m )) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && (elem = newContext.getElementById( m )) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( (m = match[3]) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !compilerCache[ selector + " " ] && + (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + + if ( nodeType !== 1 ) { + newContext = context; + newSelector = selector; + + // qSA looks outside Element context, which is not what we want + // Thanks to Andrew Dupont for this workaround technique + // Support: IE <=8 + // Exclude object elements + } else if ( context.nodeName.toLowerCase() !== "object" ) { + + // Capture the context ID, setting it first if necessary + if ( (nid = context.getAttribute( "id" )) ) { + nid = nid.replace( rcssescape, fcssescape ); + } else { + context.setAttribute( "id", (nid = expando) ); + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[i] = "#" + nid + " " + toSelector( groups[i] ); + } + newSelector = groups.join( "," ); + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key + " " ] = value); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created element and returns a boolean result + */ +function assert( fn ) { + var el = document.createElement("fieldset"); + + try { + return !!fn( el ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + // release memory in IE + el = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + a.sourceIndex - b.sourceIndex; + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for :enabled/:disabled + * @param {Boolean} disabled true for :disabled; false for :enabled + */ +function createDisabledPseudo( disabled ) { + + // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable + return function( elem ) { + + // Only certain elements can match :enabled or :disabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled + if ( "form" in elem ) { + + // Check for inherited disabledness on relevant non-disabled elements: + // * listed form-associated elements in a disabled fieldset + // https://html.spec.whatwg.org/multipage/forms.html#category-listed + // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled + // * option elements in a disabled optgroup + // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled + // All such elements have a "form" property. + if ( elem.parentNode && elem.disabled === false ) { + + // Option elements defer to a parent optgroup if present + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + // Support: IE 6 - 11 + // Use the isDisabled shortcut property to check for disabled fieldset ancestors + return elem.isDisabled === disabled || + + // Where there is no isDisabled, check manually + /* jshint -W018 */ + elem.isDisabled !== !disabled && + disabledAncestor( elem ) === disabled; + } + + return elem.disabled === disabled; + + // Try to winnow out elements that can't be disabled before trusting the disabled property. + // Some victims get caught in our net (label, legend, menu, track), but it shouldn't + // even exist on them, let alone have a boolean value. + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + // Remaining elements are neither :enabled nor :disabled + return false; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9-11, Edge + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + if ( preferredDoc !== document && + (subWindow = document.defaultView) && subWindow.top !== subWindow ) { + + // Support: IE 11, Edge + if ( subWindow.addEventListener ) { + subWindow.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( subWindow.attachEvent ) { + subWindow.attachEvent( "onunload", unloadHandler ); + } + } + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert(function( el ) { + el.className = "i"; + return !el.getAttribute("className"); + }); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function( el ) { + el.appendChild( document.createComment("") ); + return !el.getElementsByTagName("*").length; + }); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programmatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function( el ) { + docElem.appendChild( el ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + }); + + // ID filter and find + if ( support.getById ) { + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + + // Support: IE 6 - 7 only + // getElementById is not reliable as a find shortcut + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + // Verify the id attribute + node = elem.getAttributeNode("id"); + if ( node && node.value === id ) { + return [ elem ]; + } + + // Fall back on getElementsByName + elems = context.getElementsByName( id ); + i = 0; + while ( (elem = elems[i++]) ) { + node = elem.getAttributeNode("id"); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See https://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( (support.qsa = rnative.test( document.querySelectorAll )) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( el ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // https://bugs.jquery.com/ticket/12359 + docElem.appendChild( el ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( el.querySelectorAll("[msallowcapture^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !el.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push("~="); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !el.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibling-combinator selector` fails + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push(".#.+[+~]"); + } + }); + + assert(function( el ) { + el.innerHTML = "" + + ""; + + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement("input"); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( el.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( el.querySelectorAll(":enabled").length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: IE9-11+ + // IE's :disabled selector does not pick up the children of disabled fieldsets + docElem.appendChild( el ).disabled = true; + if ( el.querySelectorAll(":disabled").length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + el.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( el ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( el, "*" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( el, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + return a === document ? -1 : + b === document ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + if ( support.matchesSelector && documentIsHTML && + !compilerCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch (e) {} + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; +}; + +Sizzle.escape = function( sel ) { + return (sel + "").replace( rcssescape, fcssescape ); +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( (elem = results[i++]) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + while ( (node = elem[i++]) ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[6] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + // Use previously-cached element index if available + if ( useCache ) { + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + // Don't keep the element (issue #299) + input[0] = null; + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifier + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": createDisabledPseudo( false ), + "disabled": createDisabledPseudo( true ), + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( (tokens = []) ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + }); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + skip = combinator.next, + key = skip || dir, + checkNonElements = base && key === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + return false; + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {}); + + if ( skip && skip === elem.nodeName.toLowerCase() ) { + elem = elem[ dir ] || elem; + } else if ( (oldCache = uniqueCache[ key ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); + } else { + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ key ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { + return true; + } + } + } + } + } + return false; + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; + + if ( outermost ) { + outermostContext = context === document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: