diff --git a/helper.py b/helper.py
index 4d41158..7c0cc7e 100755
--- a/helper.py
+++ b/helper.py
@@ -16,12 +16,19 @@
from io import BytesIO
from PIL import Image
+from hopper.client import *
+from hopper.common import *
+
+PIPE_DIRECTORY = "/home/pi/pipes"
+
+HOPPER_CLIENT = HopperClient()
+LOG_PIPE_NAME = PipeName((PipeType.RECEIVING, "log", "helper"), PIPE_DIRECTORY)
+HOPPER_CLIENT.open_pipe(LOG_PIPE_NAME, delete=True, create=True)
CONNECTIONS = set()
# The following sets up the asynchronous waiting for file change
picture_watcher = aionotify.Watcher()
-log_watcher = aionotify.Watcher()
img_static_path = "/home/pi/shepherd/shepherd/static/"
img_input_file = img_static_path + "image.jpg"
@@ -34,13 +41,17 @@
log_static_path = "/media/RobotUSB/"
log_input_file = log_static_path + "logs.txt"
+log_buffer = []
+ERASE_ESCAPE_SEQUENCE = "\033[2J"
+
file_open_attempts = 10
wait_between_attempts = 0.1
picture_watcher.watch(
alias="image", path=img_input_file, flags=aionotify.Flags.MODIFY
) # sets up watcher
-log_watcher.watch(alias="logs", path=log_input_file, flags=aionotify.Flags.MODIFY)
+
+
def shrink_image(img):
@@ -110,46 +121,25 @@ async def wait_for_picture_change():
async def wait_for_log_change():
loop = asyncio.get_event_loop()
+
+ websockets.broadcast(CONNECTIONS, ERASE_ESCAPE_SEQUENCE + "\n")
- bypass = False # so first image is not ignored.
- while not os.path.exists(log_input_file):
- await asyncio.sleep(0.5) # twiddle thumbs :)
- if not bypass:
- bypass = True
- await log_watcher.setup(loop)
- print("Log change watcher is running.")
+ while True:
+ d = HOPPER_CLIENT.read(LOG_PIPE_NAME)
- while True: # for all events
- if not bypass:
- event = await log_watcher.get_event() # blocks until file changed
- else:
- bypass = False # reset bypass
+ if d is not None:
+ ds = d.decode("utf-8")
+ if ERASE_ESCAPE_SEQUENCE in ds:
+ log_buffer.clear()
+ websockets.broadcast(CONNECTIONS, ERASE_ESCAPE_SEQUENCE + "\n")
+ print("Received erase sequence")
+ else:
+ websockets.broadcast(CONNECTIONS, "[LOGS]" + ds)
+ log_buffer.append("[LOGS]" + ds)
+ print("[LOGS]" + ds, end="")
- with open(log_input_file, "r") as l:
- old_logs = l.read()
- for c in range(file_open_attempts):
- await asyncio.sleep(wait_between_attempts) # give it time to write the file.
- try: # this runs until the bot has finished writing the logs
- with open(log_input_file, "r") as l:
- new_logs = l.read()
- print("Opened logs successfully")
- break
- except:
- print("Error opening logs: attempt \#" + str(c))
-
- if c >= 9:
- continue # error with this file, go back and wait for next change.
-
- new_logs.replace(old_logs, "") # only new logs remain.
- index = len(new_logs) - len(old_logs)
- old_logs = new_logs
- new_logs = new_logs[index:]
+ await asyncio.sleep(0.1)
- websockets.broadcast(CONNECTIONS, "[LOGS]" + new_logs) # sends new logs.
- print("Logs broadcast.")
-
- # politely stops watching file system.
- log_watcher.close()
loop.stop()
loop.close()
@@ -157,6 +147,7 @@ async def wait_for_log_change():
async def register(websocket): # Runs every time someone connects
CONNECTIONS.add(websocket)
print("Someone has connected to the websocket.")
+
for c in range(file_open_attempts):
time.sleep(wait_between_attempts) # give it time to write the file.
try: # this runs until the bot has finished writing the image
@@ -172,7 +163,12 @@ async def register(websocket): # Runs every time someone connects
if not bypass:
img = shrink_image(img)
img_b64 = im_2_b64(img).decode()
- await websocket.send(img_b64)
+ await websocket.send("[CAMERA]" + img_b64)
+
+ # Send previous logs
+ for l in log_buffer:
+ await websocket.send(l)
+
try:
await websocket.wait_closed()
finally:
@@ -185,6 +181,5 @@ async def main():
wait_for_picture_change(), wait_for_log_change()
) # runs the file change checker and webserver at the same time.
-
asyncio.run(main())
print("Goodbye.")
diff --git a/runner/enums.py b/runner/enums.py
new file mode 100644
index 0000000..834dac9
--- /dev/null
+++ b/runner/enums.py
@@ -0,0 +1,12 @@
+from enum import Enum
+
+class State(Enum):
+ # Once shepherd is up, we are by definition ready to run code, so
+ # there's no need for a "booting" state.
+ ready = object()
+ running = object()
+ post_run = object()
+
+class Mode(Enum):
+ dev = "dev"
+ comp = "comp"
\ No newline at end of file
diff --git a/runner/reaper.py b/runner/reaper.py
new file mode 100644
index 0000000..f2008e0
--- /dev/null
+++ b/runner/reaper.py
@@ -0,0 +1,56 @@
+from enums import State
+import threading
+import errno
+
+class Reaper:
+ @staticmethod
+ def reap(state, user_code, output_file, reason="", reap_grace_time=5):
+ if reason is None:
+ print("Reaping user code")
+ else:
+ print("Reaping user code ({})".format(reason))
+ if state != State.running:
+ print("Warning: told to stop code, but state is {}, not State.running!".format(state))
+ try:
+ user_code.terminate()
+ except OSError as e:
+ if e.errno == errno.ESRCH: # No such process
+ pass
+ else:
+ raise
+ if user_code.poll() is None:
+ butcher_thread = threading.Timer(reap_grace_time, Reaper.butcher, [user_code])
+ butcher_thread.daemon = True
+ butcher_thread.start()
+ try:
+ user_code.communicate()
+ except Exception as e:
+ print("death: Caught an error while killing user code, sod Python's I/O handling...")
+ print("death: The error was: {}: {}".format(type(e), e))
+ butcher_thread.cancel()
+ if output_file is not None:
+ try:
+ output_file.write("\n==== END OF ROUND ====\n\n")
+ except Exception:
+ pass
+ try:
+ output_file.close()
+ except Exception as e:
+ print("death: Caught an error while closing user code's output.")
+ print("death: The error was: {}: {}".format(type(e).__name__, e))
+
+ print("Done reaping user code")
+ return State.post_run
+
+ @staticmethod
+ def butcher(user_code):
+ if user_code.poll() is None:
+ print("Butchering user code")
+ try:
+ user_code.kill()
+ except OSError as e:
+ if e.errno == errno.ESRCH: # No such process
+ pass
+ else:
+ raise
+ print("Done butchering user code")
diff --git a/runner/start.py b/runner/start.py
new file mode 100644
index 0000000..16ca83f
--- /dev/null
+++ b/runner/start.py
@@ -0,0 +1,253 @@
+#!/usr/bin/python3
+
+import os, sys
+import RPi.GPIO as GPIO
+import json
+import time
+import subprocess, threading
+import atexit
+from pytz import utc
+from pathlib import Path
+
+from enums import Mode, State
+from reaper import Reaper
+
+from hopper.client import *
+from hopper.common import *
+
+ROBOT_LIB_LOCATION = "/home/pi/robot"
+
+def load_package_paths():
+ if not os.path.exists(ROBOT_LIB_LOCATION):
+ raise ImportError(f"Cannot find robot library!")
+
+ sys.path.insert(0, ROBOT_LIB_LOCATION)
+
+class Runner:
+ ROUND_LENGTH = 180
+ REAP_GRACE_TIME = 5
+ OUTPUT_FILE_PATH = "/media/RobotUSB/logs.txt"
+
+ # Tell the WebSocket handler to clear its buffer
+ ERASE_ESCAPE_SEQUENCE = b'\033[2J'
+
+ USER_PIPE_NAME = None
+ FLASK_PIPE_NAME = None
+ LOG_PIPE_NAME = None
+ HOPPER_CLIENT = None
+
+ PIPE_DIRECTORY = "/home/pi/pipes"
+
+ START_BUTTON_BOUNCE_TIME=1000
+ START_BUTTON_PIN = 26
+
+ GAME_CONTROL_PATH = Path("/media/ArenaUSB")
+
+ MODE = None
+ ZONE = None
+ STATE = None
+
+ REAPER_TIMER = None
+ DISABLE_REAPER = None
+ REAP_TIME = None
+
+ USERCODE = None
+ OUTPUT_FILE = None
+
+ USER_CODE_PATH = "/home/pi/usercode"
+ USER_CODE_ENTRYPOINT_NAME = "main.py"
+ USER_CODE_ENTRYPOINT_PATH = os.path.join(USER_CODE_PATH,USER_CODE_ENTRYPOINT_NAME)
+
+ USER_CODE_LOG_PIPE_NAME = None
+
+ RUNNING = False
+
+ def __init__(self):
+ os.makedirs(self.USER_CODE_PATH, exist_ok=True)
+ os.chown(self.USER_CODE_PATH, 1000, 1000) # pi:pi
+
+ self.HOPPER_CLIENT = HopperClient()
+
+ self.USER_PIPE_NAME = PipeName((PipeType.INPUT, "start-button", "starter"), self.PIPE_DIRECTORY)
+ self.HOPPER_CLIENT.open_pipe(self.USER_PIPE_NAME, delete=True, create=True)
+
+ self.FLASK_PIPE_NAME = PipeName((PipeType.OUTPUT, "starter", "starter"), self.PIPE_DIRECTORY)
+ self.HOPPER_CLIENT.open_pipe(self.FLASK_PIPE_NAME, delete=True, create=True, blocking=True)
+
+ self.LOG_PIPE_NAME = PipeName((PipeType.INPUT, "log", "starter"), self.PIPE_DIRECTORY)
+ self.HOPPER_CLIENT.open_pipe(self.LOG_PIPE_NAME, delete=True, create=True)
+
+ self.__load_start_graphic()
+ self.__init_gpio()
+
+ robot_reset.reset()
+ self.__reset_state()
+ self.__start_usercode()
+ self.__set_reaper_at_exit()
+
+ def __set_reaper_at_exit(self):
+ atexit.register(self.__reap)
+
+ def __reap(self, reason=""):
+ Reaper.reap(self.STATE, self.USERCODE, self.OUTPUT_FILE, reason=reason, reap_grace_time=self.REAP_GRACE_TIME)
+
+ def __reset_state(self):
+ self.STATE = State.ready # The state of the user code.
+ self.ZONE = None # The robot's home zone, an integer from 0 to 3.
+ self.MODE = None # The robot's mode (development or competition), used for marker recognition.
+ self.DISABLE_REAPER = None # Whether the reaper will kill the user code or not.
+ self.REAPER_TIMER = None # The threading.Timer object that controls the reaper.
+ self.REAP_TIME = None # The time at which the user code will be killed.
+ self.USERCODE = None # A subprocess.Popen object representing the running user code.
+ self.OUTPUT_FILE = None # The file to which output from the user code goes.
+
+ def __start_usercode(self):
+ # Send the erase escape sequence to clear remote logs
+ self.HOPPER_CLIENT.write(self.LOG_PIPE_NAME, self.ERASE_ESCAPE_SEQUENCE)
+
+ environment = dict(os.environ)
+ environment["PYTHONPATH"] = ROBOT_LIB_LOCATION
+ # Start the user code.
+ self.USERCODE = subprocess.Popen(
+ [
+ # python -u /path/to/the_code.py
+ sys.executable, "-u", self.USER_CODE_ENTRYPOINT_PATH,
+ ],
+ stderr=subprocess.STDOUT,
+ bufsize=1, # Line-buffered
+ close_fds="posix" in sys.builtin_module_names, # Only if we're not on Windows
+ env=environment,
+ )
+ user_code_wait_thread = threading.Thread(target=self.__user_code_wait)
+ user_code_wait_thread.daemon = True
+ user_code_wait_thread.start()
+
+ def __user_code_wait(self):
+ exit_code = self.USERCODE.wait()
+ if exit_code == 1:
+ self.__round_end()
+
+ def __round_end(self):
+ self.__reap(reason="end of round")
+ robot_reset.reset()
+ time.sleep(0.5)
+
+ def __init_gpio(self):
+ GPIO.setmode(GPIO.BCM)
+ GPIO.setup(self.START_BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
+
+ GPIO.add_event_detect(self.START_BUTTON_PIN, GPIO.FALLING, callback=self.__gpio_start, bouncetime=self.START_BUTTON_BOUNCE_TIME)
+
+ def __load_start_graphic(self):
+ teamname_file = Path('/home/pi/teamname.txt')
+ if teamname_file.exists():
+ teamname_jpg = teamname_file.read_text().replace('\n', '') +'.jpg'
+ else:
+ teamname_jpg = 'none'
+
+ # Pick a start imapge in order of preference :
+ # 1) We have a team corner image on the USB
+ # 2) The team have uploaded their own image to the robot
+ # 3) We have a generic corner image on the USB
+ # 4) The game image
+ start_graphic = self.GAME_CONTROL_PATH / teamname_jpg
+ if not start_graphic.exists():
+ # attempt to find the team specific corner graphic from the ArenaUSB
+ start_graphic = Path('/home/pi/shepherd/robotsrc/team_logo.jpg')
+ if not start_graphic.exists():
+ # attempt to find the default corner graphic from ArenaUSB
+ start_graphic = self.GAME_CONTROL_PATH / 'Corner.jpg'
+ if not start_graphic.exists():
+ # finally look for a game specific logo
+ start_graphic = Path('/home/pi/game_logo.jpg')
+ if start_graphic.exists():
+ # if ANY of the above paths generate a useful image, copy it into the web "static" files like an animal who doesn't understand the word static
+ # if this all fails then the user will see the last image the camera took
+ static_graphic = Path('/home/pi/shepherd/shepherd/static/image.jpg')
+ static_graphic.write_bytes(start_graphic.read_bytes())
+
+ def __gpio_start(self, _):
+ zone = "0"
+ if (self.GAME_CONTROL_PATH / 'zone1.txt').exists():
+ zone = "1"
+ elif (self.GAME_CONTROL_PATH / 'zone2.txt').exists():
+ zone = "2"
+ elif (self.GAME_CONTROL_PATH / 'zone3.txt').exists():
+ zone = "3"
+
+ self.__start({
+ "mode": "comp",
+ "zone": int(zone)
+ })
+
+ def __start(self, params):
+ self.MODE = Mode[params["mode"]]
+ self.ZONE = int(params["zone"])
+
+ if self.STATE == State.ready:
+ self.STATE = State.running
+
+ start_args = json.dumps({
+ "mode": self.MODE.value,
+ "zone": self.ZONE,
+ "arena": "A",
+ })
+
+ # Put the JSON configuration in the pipe
+ self.HOPPER_CLIENT.write(self.USER_PIPE_NAME, start_args.encode("utf-8"))
+
+ if self.MODE == Mode.comp:
+ self.REAPER_TIMER = threading.Timer(self.ROUND_LENGTH, self.__round_end)
+ # If we get told to exit, there's no point waiting around for the round to finish.
+ self.REAPER_TIMER.daemon = True
+ self.REAPER_TIMER.start()
+ print("Started the robot! It will stop automatically in {} seconds.".format(self.ROUND_LENGTH))
+ else:
+ print("Started the robot! It will not stop automatically.")
+
+ def __stop(self):
+ if self.STATE == State.ready:
+ print("The robot has not run yet, can't stop it before it's started.")
+ elif self.STATE == State.running:
+ try:
+ self.REAPER_TIMER.cancel()
+ except AttributeError: # probably because reaper_timer is None
+ pass
+ self.__round_end()
+ print("Stopped the robot!")
+ elif self.STATE == State.post_run:
+ print("Code already ran, can't stop it")
+ else:
+ raise Exception("This can't happen")
+
+ def __upload(self):
+ if self.REAPER_TIMER is not None:
+ self.REAPER_TIMER.cancel()
+
+ self.__reap("new code upload")
+
+ robot_reset.reset()
+ self.__reset_state()
+ self.__start_usercode()
+
+ def run(self):
+ self.RUNNING = True
+ while (1):
+ b = self.HOPPER_CLIENT.read(self.FLASK_PIPE_NAME)
+ if b != None and len(b) > 0:
+ s = b.decode("utf-8").strip("\n ")
+ d = json.loads(s)
+ if d["request"] == "start":
+ self.__start(d["params"])
+ elif d["request"] == "stop":
+ self.__stop()
+ elif d["request"] == "upload":
+ self.__upload()
+
+if __name__ == "__main__":
+ load_package_paths()
+
+ import robot.reset as robot_reset
+
+ r = Runner()
+ r.run()
diff --git a/sheepsrc b/sheepsrc
index 77a0830..62bd33a 160000
--- a/sheepsrc
+++ b/sheepsrc
@@ -1 +1 @@
-Subproject commit 77a08300caa79c7182fdbe6b5c2b2dabad6d23c2
+Subproject commit 62bd33a42e46e4399d5efc5dd14caa1d5c95bc20
diff --git a/shepherd/__init__.py b/shepherd/__init__.py
index 6784fe6..bb60a8f 100644
--- a/shepherd/__init__.py
+++ b/shepherd/__init__.py
@@ -15,8 +15,6 @@
from shepherd.blueprints import upload, run, pyls, editor, staticroutes
-START_BUTTON_PIN = 26 # GPIO 26, pin 37
-
syslogger = logging.getLogger()
syslogger.addHandler(SysLogHandler('/dev/log'))
@@ -29,76 +27,11 @@
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 * 1024 # 64 MiB
-# app.config["SHEPHERD_USER_CODE_PATH"] = os.path.join("/", "opt", "shepherd")
-app.config["SHEPHERD_USER_CODE_PATH"] = os.path.join(os.getcwd(), "usercode")
+
app.config["SHEPHERD_USER_CODE_ENTRYPOINT_NAME"] = "main.py"
-app.config["SHEPHERD_USER_CODE_ENTRYPOINT_PATH"] = os.path.join(app.config["SHEPHERD_USER_CODE_PATH"], app.config["SHEPHERD_USER_CODE_ENTRYPOINT_NAME"])
-try:
- os.mkdir(app.config["SHEPHERD_USER_CODE_PATH"])
-except OSError as e:
- if e.errno == errno.EEXIST and os.path.isdir(app.config["SHEPHERD_USER_CODE_PATH"]):
- pass
- else:
- raise e
-
-
-# Avoid running the user code twice.
-if (not app.debug) or os.environ.get("WERKZEUG_RUN_MAIN"):
- run.init(app)
- GPIO.setmode(GPIO.BCM)
- GPIO.setup(START_BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
-
- # Teamname should be set on a per brain basis before shipping
- # Its purpose is to allow the setting of specific graphics for help identifing teams in the arena.
- # Graphics are loaded from the ArenaUSB stick if available, or standard graphics from the stick are used.
- # this used to be in rc.local, but the looks of shame and dissapointment got the better of me
-
- game_control_path = Path('/media/ArenaUSB')
-
- teamname_file = Path('/home/pi/teamname.txt')
- if teamname_file.exists():
- teamname_jpg = teamname_file.read_text().replace('\n', '') +'.jpg'
- else:
- teamname_jpg = 'none'
-
- # Pick a start imapge in order of preference :
- # 1) We have a team corner image on the USB
- # 2) The team have uploaded their own image to the robot
- # 3) We have a generic corner image on the USB
- # 4) The game image
- start_graphic = game_control_path / teamname_jpg
- if not start_graphic.exists():
- # attempt to find the team specific corner graphic from the ArenaUSB
- start_graphic = Path('robotsrc/team_logo.jpg')
- if not start_graphic.exists():
- # attempt to find the default corner graphic from ArenaUSB
- start_graphic = game_control_path / 'Corner.jpg'
- if not start_graphic.exists():
- # finally look for a game specific logo
- start_graphic = Path('/home/pi/game_logo.jpg')
- if start_graphic.exists():
- # if ANY of the above paths generate a useful image, copy it into the web "static" files like an animal who doesn't understand the word static
- # if this all fails then the user will see the last image the camera took
- static_graphic = Path('shepherd/static/image.jpg')
- static_graphic.write_bytes(start_graphic.read_bytes())
-
- def _start(channel):
- # Set the zone based on files in game_contol_path, defaulting to zone 0
- zone = "0"
- if (game_control_path / 'zone1.txt').exists():
- zone = "1"
- elif (game_control_path / 'zone2.txt').exists():
- zone = "2"
- elif (game_control_path / 'zone3.txt').exists():
- zone = "3"
- # this is the weirdest calling convention
- ctx = app.test_request_context(data={
- "zone": zone,
- "mode": "competition",
- })
- with ctx:
- run.start()
- GPIO.add_event_detect(START_BUTTON_PIN, GPIO.FALLING, callback=_start, bouncetime=3000)
+app.config["SHEPHERD_USER_CODE_PATH"] = "/home/pi/usercode/"
+
+run.init()
app.register_blueprint(upload.blueprint, url_prefix="/upload")
app.register_blueprint(run.blueprint, url_prefix="/run")
diff --git a/shepherd/blueprints/editor/0.bundle.js b/shepherd/blueprints/editor/0.bundle.js
new file mode 100644
index 0000000..1c05526
--- /dev/null
+++ b/shepherd/blueprints/editor/0.bundle.js
@@ -0,0 +1 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([[0],{565:function(e,n,t){"use strict";t.r(n),t.d(n,"conf",(function(){return i})),t.d(n,"language",(function(){return r}));var i={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"[",close:"]"},{open:"{",close:"}"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{markers:{start:new RegExp("^\\s*#pragma\\s+region\\b"),end:new RegExp("^\\s*#pragma\\s+endregion\\b")}}},r={defaultToken:"",tokenPostfix:".cpp",brackets:[{token:"delimiter.curly",open:"{",close:"}"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"delimiter.square",open:"[",close:"]"},{token:"delimiter.angle",open:"<",close:">"}],keywords:["abstract","amp","array","auto","bool","break","case","catch","char","class","const","constexpr","const_cast","continue","cpu","decltype","default","delegate","delete","do","double","dynamic_cast","each","else","enum","event","explicit","export","extern","false","final","finally","float","for","friend","gcnew","generic","goto","if","in","initonly","inline","int","interface","interior_ptr","internal","literal","long","mutable","namespace","new","noexcept","nullptr","__nullptr","operator","override","partial","pascal","pin_ptr","private","property","protected","public","ref","register","reinterpret_cast","restrict","return","safe_cast","sealed","short","signed","sizeof","static","static_assert","static_cast","struct","switch","template","this","thread_local","throw","tile_static","true","try","typedef","typeid","typename","union","unsigned","using","virtual","void","volatile","wchar_t","where","while","_asm","_based","_cdecl","_declspec","_fastcall","_if_exists","_if_not_exists","_inline","_multiple_inheritance","_pascal","_single_inheritance","_stdcall","_virtual_inheritance","_w64","__abstract","__alignof","__asm","__assume","__based","__box","__builtin_alignof","__cdecl","__clrcall","__declspec","__delegate","__event","__except","__fastcall","__finally","__forceinline","__gc","__hook","__identifier","__if_exists","__if_not_exists","__inline","__int128","__int16","__int32","__int64","__int8","__interface","__leave","__m128","__m128d","__m128i","__m256","__m256d","__m256i","__m64","__multiple_inheritance","__newslot","__nogc","__noop","__nounwind","__novtordisp","__pascal","__pin","__pragma","__property","__ptr32","__ptr64","__raise","__restrict","__resume","__sealed","__single_inheritance","__stdcall","__super","__thiscall","__try","__try_cast","__typeof","__unaligned","__unhook","__uuidof","__value","__virtual_inheritance","__w64","__wchar_t"],operators:["=",">","<","!","~","?",":","==","<=",">=","!=","&&","||","++","--","+","-","*","/","&","|","^","%","<<",">>",">>>","+=","-=","*=","/=","&=","|=","^=","%=","<<=",">>=",">>>="],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/,"number.float"],[/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/,"number.float"],[/0[xX][0-9a-fA-F']*[0-9a-fA-F](@integersuffix)/,"number.hex"],[/0[0-7']*[0-7](@integersuffix)/,"number.octal"],[/0[bB][0-1']*[0-1](@integersuffix)/,"number.binary"],[/\d[\d']*\d(@integersuffix)/,"number"],[/\d(@integersuffix)/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string"],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@doccomment"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],doccomment:[[/[^\/*]+/,"comment.doc"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],raw:[[/(.*)(\))(?:([^ ()\\\t]*))(\")/,{cases:{"$3==$S2":["string.raw","string.raw.end","string.raw.end",{token:"string.raw.end",next:"@pop"}],"@default":["string.raw","string.raw","string.raw","string.raw"]}}],[/.*/,"string.raw"]],include:[[/(\s*)(<)([^<>]*)(>)/,["","keyword.directive.include.begin","string.include.identifier",{token:"keyword.directive.include.end",next:"@pop"}]],[/(\s*)(")([^"]*)(")/,["","keyword.directive.include.begin","string.include.identifier",{token:"keyword.directive.include.end",next:"@pop"}]]]}}}}]);
\ No newline at end of file
diff --git a/shepherd/blueprints/editor/10.bundle.js b/shepherd/blueprints/editor/10.bundle.js
new file mode 100644
index 0000000..ed92154
--- /dev/null
+++ b/shepherd/blueprints/editor/10.bundle.js
@@ -0,0 +1 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([[10],{568:function(e,t,n){"use strict";n.r(t),n.d(t,"conf",(function(){return r})),n.d(t,"language",(function(){return i}));var r={wordPattern:/(#?-?\d*\.\d\w*%?)|((::|[@#.!:])?[\w-?]+%?)|::|[@#.!:]/g,comments:{blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}",notIn:["string","comment"]},{open:"[",close:"]",notIn:["string","comment"]},{open:"(",close:")",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string","comment"]},{open:"'",close:"'",notIn:["string","comment"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{markers:{start:new RegExp("^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/"),end:new RegExp("^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/")}}},i={defaultToken:"",tokenPostfix:".css",ws:"[ \t\n\r\f]*",identifier:"-?-?([a-zA-Z]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))([\\w\\-]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))*",brackets:[{open:"{",close:"}",token:"delimiter.bracket"},{open:"[",close:"]",token:"delimiter.bracket"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],tokenizer:{root:[{include:"@selector"}],selector:[{include:"@comments"},{include:"@import"},{include:"@strings"},["[@](keyframes|-webkit-keyframes|-moz-keyframes|-o-keyframes)",{token:"keyword",next:"@keyframedeclaration"}],["[@](page|content|font-face|-moz-document)",{token:"keyword"}],["[@](charset|namespace)",{token:"keyword",next:"@declarationbody"}],["(url-prefix)(\\()",["attribute.value",{token:"delimiter.parenthesis",next:"@urldeclaration"}]],["(url)(\\()",["attribute.value",{token:"delimiter.parenthesis",next:"@urldeclaration"}]],{include:"@selectorname"},["[\\*]","tag"],["[>\\+,]","delimiter"],["\\[",{token:"delimiter.bracket",next:"@selectorattribute"}],["{",{token:"delimiter.bracket",next:"@selectorbody"}]],selectorbody:[{include:"@comments"},["[*_]?@identifier@ws:(?=(\\s|\\d|[^{;}]*[;}]))","attribute.name","@rulevalue"],["}",{token:"delimiter.bracket",next:"@pop"}]],selectorname:[["(\\.|#(?=[^{])|%|(@identifier)|:)+","tag"]],selectorattribute:[{include:"@term"},["]",{token:"delimiter.bracket",next:"@pop"}]],term:[{include:"@comments"},["(url-prefix)(\\()",["attribute.value",{token:"delimiter.parenthesis",next:"@urldeclaration"}]],["(url)(\\()",["attribute.value",{token:"delimiter.parenthesis",next:"@urldeclaration"}]],{include:"@functioninvocation"},{include:"@numbers"},{include:"@name"},["([<>=\\+\\-\\*\\/\\^\\|\\~,])","delimiter"],[",","delimiter"]],rulevalue:[{include:"@comments"},{include:"@strings"},{include:"@term"},["!important","keyword"],[";","delimiter","@pop"],["(?=})",{token:"",next:"@pop"}]],warndebug:[["[@](warn|debug)",{token:"keyword",next:"@declarationbody"}]],import:[["[@](import)",{token:"keyword",next:"@declarationbody"}]],urldeclaration:[{include:"@strings"},["[^)\r\n]+","string"],["\\)",{token:"delimiter.parenthesis",next:"@pop"}]],parenthizedterm:[{include:"@term"},["\\)",{token:"delimiter.parenthesis",next:"@pop"}]],declarationbody:[{include:"@term"},[";","delimiter","@pop"],["(?=})",{token:"",next:"@pop"}]],comments:[["\\/\\*","comment","@comment"],["\\/\\/+.*","comment"]],comment:[["\\*\\/","comment","@pop"],[/[^*/]+/,"comment"],[/./,"comment"]],name:[["@identifier","attribute.value"]],numbers:[["-?(\\d*\\.)?\\d+([eE][\\-+]?\\d+)?",{token:"attribute.value.number",next:"@units"}],["#[0-9a-fA-F_]+(?!\\w)","attribute.value.hex"]],units:[["(em|ex|ch|rem|vmin|vmax|vw|vh|vm|cm|mm|in|px|pt|pc|deg|grad|rad|turn|s|ms|Hz|kHz|%)?","attribute.value.unit","@pop"]],keyframedeclaration:[["@identifier","attribute.value"],["{",{token:"delimiter.bracket",switchTo:"@keyframebody"}]],keyframebody:[{include:"@term"},["{",{token:"delimiter.bracket",next:"@selectorbody"}],["}",{token:"delimiter.bracket",next:"@pop"}]],functioninvocation:[["@identifier\\(",{token:"attribute.value",next:"@functionarguments"}]],functionarguments:[["\\$@identifier@ws:","attribute.name"],["[,]","delimiter"],{include:"@term"},["\\)",{token:"attribute.value",next:"@pop"}]],strings:[['~?"',{token:"string",next:"@stringenddoublequote"}],["~?'",{token:"string",next:"@stringendquote"}]],stringenddoublequote:[["\\\\.","string"],['"',{token:"string",next:"@pop"}],[/[^\\"]+/,"string"],[".","string"]],stringendquote:[["\\\\.","string"],["'",{token:"string",next:"@pop"}],[/[^\\']+/,"string"],[".","string"]]}}}}]);
\ No newline at end of file
diff --git a/shepherd/blueprints/editor/11.bundle.js b/shepherd/blueprints/editor/11.bundle.js
new file mode 100644
index 0000000..6ac5dd7
--- /dev/null
+++ b/shepherd/blueprints/editor/11.bundle.js
@@ -0,0 +1 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([[11],{569:function(e,n,s){"use strict";s.r(n),s.d(n,"conf",(function(){return t})),s.d(n,"language",(function(){return o}));var t={brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},o={defaultToken:"",tokenPostfix:".dockerfile",instructions:/FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|ARG|VOLUME|LABEL|USER|WORKDIR|COPY|CMD|STOPSIGNAL|SHELL|HEALTHCHECK|ENTRYPOINT/,instructionAfter:/ONBUILD/,variableAfter:/ENV/,variable:/\${?[\w]+}?/,tokenizer:{root:[{include:"@whitespace"},{include:"@comment"},[/(@instructionAfter)(\s+)/,["keyword",{token:"",next:"@instructions"}]],["","keyword","@instructions"]],instructions:[[/(@variableAfter)(\s+)([\w]+)/,["keyword","",{token:"variable",next:"@arguments"}]],[/(@instructions)/,"keyword","@arguments"]],arguments:[{include:"@whitespace"},{include:"@strings"},[/(@variable)/,{cases:{"@eos":{token:"variable",next:"@popall"},"@default":"variable"}}],[/\\/,{cases:{"@eos":"","@default":""}}],[/./,{cases:{"@eos":{token:"",next:"@popall"},"@default":""}}]],whitespace:[[/\s+/,{cases:{"@eos":{token:"",next:"@popall"},"@default":""}}]],comment:[[/(^#.*$)/,"comment","@popall"]],strings:[[/'$/,"string","@popall"],[/'/,"string","@stringBody"],[/"$/,"string","@popall"],[/"/,"string","@dblStringBody"]],stringBody:[[/[^\\\$']/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}],[/\\./,"string.escape"],[/'$/,"string","@popall"],[/'/,"string","@pop"],[/(@variable)/,"variable"],[/\\$/,"string"],[/$/,"string","@popall"]],dblStringBody:[[/[^\\\$"]/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}],[/\\./,"string.escape"],[/"$/,"string","@popall"],[/"/,"string","@pop"],[/(@variable)/,"variable"],[/\\$/,"string"],[/$/,"string","@popall"]]}}}}]);
\ No newline at end of file
diff --git a/shepherd/blueprints/editor/12.bundle.js b/shepherd/blueprints/editor/12.bundle.js
new file mode 100644
index 0000000..300d535
--- /dev/null
+++ b/shepherd/blueprints/editor/12.bundle.js
@@ -0,0 +1 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([[12],{570:function(e,n,t){"use strict";t.r(n),t.d(n,"conf",(function(){return s})),t.d(n,"language",(function(){return o}));var s={comments:{lineComment:"//",blockComment:["(*","*)"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{markers:{start:new RegExp("^\\s*//\\s*#region\\b|^\\s*\\(\\*\\s*#region(.*)\\*\\)"),end:new RegExp("^\\s*//\\s*#endregion\\b|^\\s*\\(\\*\\s*#endregion\\s*\\*\\)")}}},o={defaultToken:"",tokenPostfix:".fs",keywords:["abstract","and","atomic","as","assert","asr","base","begin","break","checked","component","const","constraint","constructor","continue","class","default","delegate","do","done","downcast","downto","elif","else","end","exception","eager","event","external","extern","false","finally","for","fun","function","fixed","functor","global","if","in","include","inherit","inline","interface","internal","land","lor","lsl","lsr","lxor","lazy","let","match","member","mod","module","mutable","namespace","method","mixin","new","not","null","of","open","or","object","override","private","parallel","process","protected","pure","public","rec","return","static","sealed","struct","sig","then","to","true","tailcall","trait","try","type","upcast","use","val","void","virtual","volatile","when","while","with","yield"],symbols:/[=>\]/,"annotation"],[/^#(if|else|endif)/,"keyword"],[/[{}()\[\]]/,"@brackets"],[/[<>](?!@symbols)/,"@brackets"],[/@symbols/,"delimiter"],[/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/,"number.float"],[/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/,"number.float"],[/0x[0-9a-fA-F]+LF/,"number.float"],[/0x[0-9a-fA-F]+(@integersuffix)/,"number.hex"],[/0b[0-1]+(@integersuffix)/,"number.bin"],[/\d+(@integersuffix)/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"""/,"string",'@string."""'],[/"/,"string",'@string."'],[/\@"/,{token:"string.quote",next:"@litstring"}],[/'[^\\']'B?/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\r\n]+/,""],[/\(\*(?!\))/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\*]+/,"comment"],[/\*\)/,"comment","@pop"],[/\*/,"comment"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/("""|"B?)/,{cases:{"$#==$S2":{token:"string",next:"@pop"},"@default":"string"}}]],litstring:[[/[^"]+/,"string"],[/""/,"string.escape"],[/"/,{token:"string.quote",next:"@pop"}]]}}}}]);
\ No newline at end of file
diff --git a/shepherd/blueprints/editor/13.bundle.js b/shepherd/blueprints/editor/13.bundle.js
new file mode 100644
index 0000000..eee0f7b
--- /dev/null
+++ b/shepherd/blueprints/editor/13.bundle.js
@@ -0,0 +1 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([[13],{571:function(e,n,o){"use strict";o.r(n),o.d(n,"conf",(function(){return t})),o.d(n,"language",(function(){return s}));var t={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"`",close:"`",notIn:["string"]},{open:'"',close:'"',notIn:["string"]},{open:"'",close:"'",notIn:["string","comment"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"`",close:"`"},{open:'"',close:'"'},{open:"'",close:"'"}]},s={defaultToken:"",tokenPostfix:".go",keywords:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var","bool","true","false","uint8","uint16","uint32","uint64","int8","int16","int32","int64","float32","float64","complex64","complex128","byte","rune","uint","int","uintptr","string","nil"],operators:["+","-","*","/","%","&","|","^","<<",">>","&^","+=","-=","*=","/=","%=","&=","|=","^=","<<=",">>=","&^=","&&","||","<-","++","--","==","<",">","=","!","!=","<=",">=",":=","...","(",")","","]","{","}",",",";",".",":"],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/\d*\d+[eE]([\-+]?\d+)?/,"number.float"],[/\d*\.\d+([eE][\-+]?\d+)?/,"number.float"],[/0[xX][0-9a-fA-F']*[0-9a-fA-F]/,"number.hex"],[/0[0-7']*[0-7]/,"number.octal"],[/0[bB][0-1']*[0-1]/,"number.binary"],[/\d[\d']*/,"number"],[/\d/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string"],[/`/,"string","@rawstring"],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@doccomment"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],doccomment:[[/[^\/*]+/,"comment.doc"],[/\/\*/,"comment.doc.invalid"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],rawstring:[[/[^\`]/,"string"],[/`/,"string","@pop"]]}}}}]);
\ No newline at end of file
diff --git a/shepherd/blueprints/editor/14.bundle.js b/shepherd/blueprints/editor/14.bundle.js
new file mode 100644
index 0000000..9f6e43c
--- /dev/null
+++ b/shepherd/blueprints/editor/14.bundle.js
@@ -0,0 +1 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([[14],{572:function(e,t,n){"use strict";n.r(t),n.d(t,"conf",(function(){return i})),n.d(t,"language",(function(){return m}));var a="undefined"==typeof monaco?self.monaco:monaco,r=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],i={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:["{{!--","--}}"]},brackets:[["\x3c!--","--\x3e"],["<",">"],["{{","}}"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"<",close:">"},{open:'"',close:'"'},{open:"'",close:"'"}],onEnterRules:[{beforeText:new RegExp("<(?!(?:"+r.join("|")+"))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$","i"),afterText:/^<\/(\w[\w\d]*)\s*>$/i,action:{indentAction:a.languages.IndentAction.IndentOutdent}},{beforeText:new RegExp("<(?!(?:"+r.join("|")+"))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$","i"),action:{indentAction:a.languages.IndentAction.Indent}}]},m={defaultToken:"",tokenPostfix:"",tokenizer:{root:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.root"}],[/)/,["delimiter.html","tag.html","delimiter.html"]],[/(<)(script)/,["delimiter.html",{token:"tag.html",next:"@script"}]],[/(<)(style)/,["delimiter.html",{token:"tag.html",next:"@style"}]],[/(<)([:\w]+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/(<\/)(\w+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/,"delimiter.html"],[/\{/,"delimiter.html"],[/[^<{]+/]],doctype:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.comment"}],[/[^>]+/,"metatag.content.html"],[/>/,"metatag.html","@pop"]],comment:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.comment"}],[/-->/,"comment.html","@pop"],[/[^-]+/,"comment.content.html"],[/./,"comment.content.html"]],otherTag:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.otherTag"}],[/\/?>/,"delimiter.html","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.script"}],[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],scriptAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterType"}],[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.scriptEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],style:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.style"}],[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],styleAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterType"}],[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.styleEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],handlebarsInSimpleState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3"}],{include:"handlebarsRoot"}],handlebarsInEmbeddedState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3",nextEmbedded:"$S3"}],{include:"handlebarsRoot"}],handlebarsRoot:[[/[#/][^\s}]+/,"keyword.helper.handlebars"],[/else\b/,"keyword.helper.handlebars"],[/[\s]+/],[/[^}]/,"variable.parameter.handlebars"]]}}}}]);
\ No newline at end of file
diff --git a/shepherd/blueprints/editor/15.bundle.js b/shepherd/blueprints/editor/15.bundle.js
new file mode 100644
index 0000000..746e201
--- /dev/null
+++ b/shepherd/blueprints/editor/15.bundle.js
@@ -0,0 +1 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([[15],{573:function(e,t,n){"use strict";n.r(t),n.d(t,"conf",(function(){return r})),n.d(t,"language",(function(){return d}));var i="undefined"==typeof monaco?self.monaco:monaco,o=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],r={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:["\x3c!--","--\x3e"]},brackets:[["\x3c!--","--\x3e"],["<",">"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:'"',close:'"'},{open:"'",close:"'"},{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"}],onEnterRules:[{beforeText:new RegExp("<(?!(?:"+o.join("|")+"))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$","i"),afterText:/^<\/([_:\w][_:\w-.\d]*)\s*>$/i,action:{indentAction:i.languages.IndentAction.IndentOutdent}},{beforeText:new RegExp("<(?!(?:"+o.join("|")+"))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$","i"),action:{indentAction:i.languages.IndentAction.Indent}}],folding:{markers:{start:new RegExp("^\\s*\x3c!--\\s*#region\\b.*--\x3e"),end:new RegExp("^\\s*\x3c!--\\s*#endregion\\b.*--\x3e")}}},d={defaultToken:"",tokenPostfix:".html",ignoreCase:!0,tokenizer:{root:[[/)/,["delimiter","tag","","delimiter"]],[/(<)(script)/,["delimiter",{token:"tag",next:"@script"}]],[/(<)(style)/,["delimiter",{token:"tag",next:"@style"}]],[/(<)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/(<\/)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/,"delimiter"],[/[^<]+/]],doctype:[[/[^>]+/,"metatag.content"],[/>/,"metatag","@pop"]],comment:[[/-->/,"comment","@pop"],[/[^-]+/,"comment.content"],[/./,"comment.content"]],otherTag:[[/\/?>/,"delimiter","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],scriptAfterType:[[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/>/,{token:"delimiter",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]],style:[[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],styleAfterType:[[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/>/,{token:"delimiter",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]]}}}}]);
\ No newline at end of file
diff --git a/shepherd/blueprints/editor/16.bundle.js b/shepherd/blueprints/editor/16.bundle.js
new file mode 100644
index 0000000..8607fc5
--- /dev/null
+++ b/shepherd/blueprints/editor/16.bundle.js
@@ -0,0 +1 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([[16],{574:function(e,n,s){"use strict";s.r(n),s.d(n,"conf",(function(){return o})),s.d(n,"language",(function(){return t}));var o={comments:{lineComment:"#"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},t={defaultToken:"",tokenPostfix:".ini",escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[[/^\[[^\]]*\]/,"metatag"],[/(^\w+)(\s*)(\=)/,["key","","delimiter"]],{include:"@whitespace"},[/\d+/,"number"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/"/,"string",'@string."'],[/'/,"string","@string.'"]],whitespace:[[/[ \t\r\n]+/,""],[/^\s*[#;].*$/,"comment"]],string:[[/[^\\"']+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/["']/,{cases:{"$#==$S2":{token:"string",next:"@pop"},"@default":"string"}}]]}}}}]);
\ No newline at end of file
diff --git a/shepherd/blueprints/editor/17.bundle.js b/shepherd/blueprints/editor/17.bundle.js
new file mode 100644
index 0000000..4fff031
--- /dev/null
+++ b/shepherd/blueprints/editor/17.bundle.js
@@ -0,0 +1 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([[17],{575:function(e,t,o){"use strict";o.r(t),o.d(t,"conf",(function(){return n})),o.d(t,"language",(function(){return s}));var n={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"},{open:"<",close:">"}],folding:{markers:{start:new RegExp("^\\s*//\\s*(?:(?:#?region\\b)|(?: "+e+"
\n":"'+(o?e:u(e,!0))+"
"},s.prototype.blockquote=function(e){return""+(o?e:u(e,!0))+"\n"+e+"
\n"},s.prototype.html=function(e){return e},s.prototype.heading=function(e,t,o){return this.options.headerIds?"
\n":"
\n"},s.prototype.list=function(e,t,o){var n=t?"ol":"ul";return"<"+n+(t&&1!==o?' start="'+o+'"':"")+">\n"+e+""+n+">\n"},s.prototype.listitem=function(e){return"\n\n"+e+"\n"+t+"
\n"},s.prototype.tablerow=function(e){return"\n"+e+" \n"},s.prototype.tablecell=function(e,t){var o=t.header?"th":"td";return(t.align?"<"+o+' align="'+t.align+'">':"<"+o+">")+e+""+o+">\n"},s.prototype.strong=function(e){return""+e+""},s.prototype.em=function(e){return""+e+""},s.prototype.codespan=function(e){return""+e+""},s.prototype.br=function(){return this.options.xhtml?"
":"
"},s.prototype.del=function(e){return""+e+""},s.prototype.link=function(e,t,o){if(this.options.sanitize){try{var n=decodeURIComponent(c(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return o}if(0===n.indexOf("javascript:")||0===n.indexOf("vbscript:")||0===n.indexOf("data:"))return o}this.options.baseUrl&&!p.test(e)&&(e=d(this.options.baseUrl,e));try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return o}var i='"+o+""},s.prototype.image=function(e,t,o){this.options.baseUrl&&!p.test(e)&&(e=d(this.options.baseUrl,e));var n='":">"},s.prototype.text=function(e){return e},a.prototype.strong=a.prototype.em=a.prototype.codespan=a.prototype.del=a.prototype.text=function(e){return e},a.prototype.link=a.prototype.image=function(e,t,o){return""+o},a.prototype.br=function(){return""},l.parse=function(e,t){return new l(t).parse(e)},l.prototype.parse=function(e){this.inline=new r(e.links,this.options),this.inlineText=new r(e.links,m({},this.options,{renderer:new a})),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},l.prototype.next=function(){return this.token=this.tokens.pop()},l.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},l.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},l.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,c(this.inlineText.output(this.token.text)));case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,o,n,i="",r="";for(o="",e=0;e
"+u(e.message+"",!0)+"";throw e}}f.exec=f,v.options=v.setOptions=function(e){return m(v.defaults,e),v},v.getDefaults=function(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:new s,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tables:!0,xhtml:!1}},v.defaults=v.getDefaults(),v.Parser=l,v.parser=l.parse,v.Renderer=s,v.TextRenderer=a,v.Lexer=o,v.lexer=o.lex,v.InlineLexer=r,v.inlineLexer=r.output,v.parse=v,n=v}).call(void 0);var l=n;n.Parser,n.parser,n.Renderer,n.TextRenderer,n.Lexer,n.lexer,n.InlineLexer,n.inlineLexer,n.parse;function u(e){var t=e.inline?"span":"div",o=document.createElement(t);return e.className&&(o.className=e.className),o}function c(e,t){void 0===t&&(t={});var o=u(t);return o.textContent=e,o}function h(e,t){void 0===t&&(t={});var o=u(t);return function e(t,o,n){var r;if(2===o.type)r=document.createTextNode(o.content);else if(3===o.type)r=document.createElement("b");else if(4===o.type)r=document.createElement("i");else if(5===o.type&&n){var s=document.createElement("a");s.href="#",n.disposeables.push(i.j(s,"click",(function(e){n.callback(String(o.index),e)}))),r=s}else 7===o.type?r=document.createElement("br"):1===o.type&&(r=t);t!==r&&t.appendChild(r);Array.isArray(o.children)&&o.children.forEach((function(t){e(r,t,n)}))}(o,function(e){var t={type:1,children:[]},o=0,n=t,i=[],r=new g(e);for(;!r.eos();){var s=r.next(),a="\\"===s&&0!==p(r.peek());if(a&&(s=r.next()),a||0===p(s)||s!==r.peek())if("\n"===s)2===n.type&&(n=i.pop()),n.children.push({type:7});else if(2!==n.type){var l={type:2,content:s};n.children.push(l),i.push(n),n=l}else n.content+=s;else{r.advance(),2===n.type&&(n=i.pop());var u=p(s);if(n.type===u||5===n.type&&6===u)n=i.pop();else{var c={type:u,children:[]};5===u&&(c.index=o,o++),n.children.push(c),i.push(n),n=c}}}2===n.type&&(n=i.pop());i.length;return t}(e),t.actionHandler),o}function d(e,t){void 0===t&&(t={});var o,n=u(t),c=new Promise((function(e){return o=e})),h=new l.Renderer;h.image=function(e,t,o){var n=[];if(e){var i=e.split("|").map((function(e){return e.trim()}));e=i[0];var r=i[1];if(r){var s=/height=(\d+)/.exec(r),a=/width=(\d+)/.exec(r),l=s&&s[1],u=a&&a[1],c=isFinite(parseInt(u)),h=isFinite(parseInt(l));c&&n.push('width="'+u+'"'),h&&n.push('height="'+l+'"')}}var d=[];return e&&d.push('src="'+e+'"'),o&&d.push('alt="'+o+'"'),t&&d.push('title="'+t+'"'),n.length&&(d=d.concat(n)),"
"+e+"
"},t.codeBlockRenderer&&(h.code=function(e,o){var i=t.codeBlockRenderer(o,e),a=r.b.nextId(),l=Promise.all([i,c]).then((function(e){var t=e[0],o=n.querySelector('div[data-code="'+a+'"]');o&&(o.innerHTML=t)})).catch((function(e){}));return t.codeBlockRenderCallback&&l.then(t.codeBlockRenderCallback),''+n.escape(u.substring(d,m))+"",d=m}r=c.endState}return o+="
| language | '+Object(s.escape)(d.languageIdentifier.language)+" | ",a+='
| token type | '+this._tokenTypeToString(d.tokenType)+" | ",a+='
| font style | '+this._fontStyleToString(d.fontStyle)+" | ",a+='
| foreground | '+g.a.Format.CSS.formatHex(d.foreground)+" | ",a+='
| background | '+g.a.Format.CSS.formatHex(d.background)+" | ",a+="