code:
Most writing is basically just plain text with branches for different cases.
Important cases to remember:
-Does the slave have a dick/vagina/tits?
-Does the PC have a dick/tits/vagina?
-If the slave has a dick and you plan to use it, can they get erect on their own?
-Is the PC pregnant?
-Is the slave mindbroken?
-Is the slave blind?
-Is the slave pregnant?
-If you penetrate an orifice, is the slave a virgin in that orifice?
-Can the slave move (i.e. is the slave amputated and/or have massive tits and/or a massive dick)?
-Does the slave like/hate trust/fear you?
-Does the slave have fetishes or personality/sexual quirks/flaws that would impact how they react?

It's important to handle every RELEVANT variation of the above cases. Depending on what you're writing, other cases may also warrant consideration; e.g. a breast-play scene will probably need to account for breast size and lactation a lot more than a generic fuck scene. If a scene only applies to a specific type of slave, you can restrict the cases you account for to the ones that are only possible for that type of slave.

There are, broadly speaking, two main ways to do this:
1. Write the same big block of text and then copy/paste it into each relevant case, and tweak the wording slightly.
 ++Less thinking required
 --LOTS of duplicate text; if you need to change something later, you have to change it in many places
2. Decompose the scene into smaller segments or sections that can each be swapped out for the slave type in question.
 ++MUCH easier maintenance
 --Have to figure out how to phrase the scene in a way that can be broken down into discrete chunks

You can also combine the two cases. For example, you may want a complex, interleaved set of <<if>> conditions for the common cases, and then have big blobs for the corner cases that are hard to handle.

It can also be easier if you split the scene into a "prep", "main", and "finish" section (these would all be the same writing block, just 2-3 <<if>> chains in sequence); that way, you could, for example, take all the various possible cases that aren't really unique, and narrate them so that, in the main section, you don't have to do so much explanatory writing for the scene to make sense overall. Then, if necessary, wrap up with the finish section. (See src/pregmod/fSlaveSelfImpreg.tw for an example of this approach)

All the variables for you and the slave are, generally, held in the variables $PC and $activeSlave, respectively. When the PC and the slave have the same attributes, they usually have the same name, but exactly what they do and mean can vary a little bit. RTFM for details. To access a specific variable, you do $var.attribName, so e.g. pregnancy is checked by $activeSlave.preg or $PC.preg. In rarer cases, you'll be dealing with an indexed entry in the $slaves array; if that's the case, you'd use $slaves[_u] or $slaves[$i] (or whatever) instead of $activeSlave. If a second (or third?) slave is present, it will be stored in another variable, the name of which will depend on the scene; for example, when impregnating a slave with another slave, the sperm donor is in $impregnatrix

Conditions are usually checked by
<<if CONDITION>>
	SHOW THIS TEXT
<<elseif CONDITION2>>
	SHOW THIS TEXT INSTEAD
<<else>>
	IF NEITHER CONDITION IS MET, SHOW THIS
<</if>>

Conditions are usually comparative (i.e. $a < $b or $b == 5 or $c != $d.e) and can be chained together with && (and) or || (or) and grouped with parentheses () - for example:
<<if($a > 1 || ($b == 2 && $c != $a))>>
lets you check that either variable A is greater than one, or both B equals two and C is not equal to A.
There are also a few misc functions that let you check things like whether a slave can get pregnant or whether two slaves are mutually fertile

Variable names are interpolated straight into the text, so if you have a slave named "Beth" then the text
"You fuck $activeSlave.slaveName" becomes "You fuck Beth"
You can also explicitly print a variable by doing <<print _varName>> (for temp variables) or <<print $varName>> (for global variables) which can be useful in various situations; it also lets you print the output of code expressions, like <<print $someNumericVar * 10>>.
If you want to change a variable, you do something like
<<set $activeSlave.devotion += 5>>
Which would, in this case, give a devotion increase of 5.

Color is changed with YOUR @@.colorname;COLORED TEXT@@ HERE

So a random (really stupid) example might be:
<<if $activeSlave.fetish == 'mindbroken'>>
	Special big block of mindbroken text here.
<<elseif $activeSlave.amp == 1>>
	Special big block of amputee text here.
<<if $PC.dick > 0>>
	<<set _wasVirgin = ''>>
	<<if $activeSlave.vagina > -1>>
		<<set _orifice = 'vagina'>>
		<<if $activeSlave.vagina == 0>>
			<<set _wasVirgin = 'vaginal'>>
		<</if>>
	<<else>>
		<<set _orifice = 'ass'>>
		<<if $activeSlave.anus == 0>>
			<<set _wasVirgin = "anal">>
		<</if>>
	<</if>>
	You fuck $activeSlave.slaveName's _orifice.
	<<if _wasVirgin != ''>>
		<<if $activeSlave.devotion > 50>>
			She @@.hotpink;loves@@ losing her <<print _wasVirgin>> virginity to you.
			<<set $activeSlave.devotion += 5>>
		<<else>>
			She @@.mediumorchid;@@dislikes losing her <<print _wasVirgin>> virginity to you.
			<<set $activeSlave.devotion -= 5>>
		<</if>>
	<</if>>
<<elseif $PC.vagina > 0 && $activeSlave.dick > 0>>
	$activeSlave.slaveName
	<<if $activeSlave.devotion > 50>>
		@@.hotpink;lovingly penetrates@@
		<<set $activeSlave.devotion += 5>>
	<<else>>
		indifferently hammers
	<</if>>
	your pussy.
<<elseif $activeSlave.vagina > -1>>
	Lesbian sex here.
<<else>>
	ERROR - PC doesn't have dick or vagina?
<</if>>

If you need to set a temp variable, prefix it with _ instead of $. This way, you don't go cluttering up the world with variables that only matter inside your little writing segment.

You can "hot-test" stuff by text editing your HTML copy of FC, but it's a little more tedious because you have to "escape" double quotes, <, >, and & with " < > and & respectively.

Some hints:
1. Write your logic first, so you don't forget to close tags. In other words, it's better to do
<<if $cond>
	TODO
<<else>>
	TODO
<</if>>
And then fill it in later than it is to end up with a situation where you have a dozen unclosed tags and you can't remember where they are or what they do.

2. For very simple stuff, it's fine to "inline" your stuff. For example, when penetrating a slave, doing "you fuck her <<if $activeSlave.vagina > -1>>pussy<<else>>ass<</if>>" to show "pussy" or "ass" as necessary. However, if you need to do the same comparison a bunch of times, do something like
<<if $activeSlave.vagina > -1>>
	<<set _targetOrifice = "vagina">>
<<else>>
	<<set _targetOrifice = "asshole">>
<</if>>
And then, when you need it, do "you fuck her _targetOrifice" in sixteen different places without having the pain in the ass of copy/pasting the same if/else clause every time.

3. INDENT YOUR LOGIC. USE TABS. I'm serious. Don't question me. It will make EVERYONE hate you, when they have to deal with your code, if it's not indented properly.
This is much easier to read:
<<if $cond1>>
	<<if $cond2>>
		whatever
	<<elseif $cond3>>
		whatever
	<<elseif $cond4>>
		<<if $cond5>>
			whatever
		<<else>>
			<<if $cond6>>
				whatever
			<</if>>
		<</if>>
	<<else>>
		whatever
		<<if $cond7>>
			<<if $cond8>>
				whatever
			<</if>>
		<</if>>
	<</if>>
<</if>>
than this:
<<if $cond1>>
<<if $cond2>>
whatever
<<elseif $cond3>>
whatever
<<elseif $cond4>>
<<if $cond5>>
whatever
<<else>>
<<if $cond6>>
whatever
<</if>>
<</if>>
<<else>>
whatever
<<if $cond7>>
<<if $cond8>>
whatever
<</if>>
<</if>>
<</if>>
<</if>>

4. Proof-read your shit before posting it. Spell-check wouldn't hurt.

repo_name: Dhanze/fc


code:
This brief how-to guides through patching and building sugarcube (https://bitbucket.org/tmedwards/sugarcube)
for the Free Cities. Sugarcube sources can be obtained locally either by cloning its Mercurial
repository or downloading source archive from bitbucket (https://bitbucket.org/tmedwards/sugarcube/downloads/).

Prerequisites (listed NOT in the installation order, please read the list to the end first):
1. Node.js with npm (https://nodejs.org).
2. Mercurial (https://www.mercurial-scm.org/) if you want to clone the sugarcube repository (so it is
optional). Please note that when downloading Windows version, the Mercurial web site lists source
download first (and linked it to the main page), while we need binaries.
3. To build some packages, Node.js requires python and a C/C++ compiler. On Windows you may want to
install Visual Studio (or just the build tools) and Python 2.7 first. As of now, SugarCube does not
depend on any of such packages.

Windows: please choose to make the tools accessible from anywhere by allowing installers to modify
the PATH environment variable.

Retrieving SugarCube sources and preparing build environment for it.
1a. If you opted to use Mercurial, open a terminal window where you want to clone the repository, and
run the following command:
	hg clone https://bitbucket.org/tmedwards/sugarcube
Change working directory into the cloned repository.
Now choose the branch. Let's suppose we want the latest officially released version 2. The branch name
is "v2-release" then:
	hg checkout v2-release
Please note that branch switching in Mercurial requires a clean local tree (without modifications).
To achieve that, run:
	hg update --clean

1b. If you decided not to use Mercurial, you can download a snapshot of a branch or a tag from the
"Downloads" tab of the bitbucket repository page. Unpack it and open a terminal in the directory
with the source files.

2. The last step we need is downloading required JavaScript libraries that are used during the build.
Run the node package manager (npm) in the repository:
	npm install

CAUTION: dependencies list (located in the package.json file in the repository root) may change from
commit to commit and it differs between branches! Make sure to install correct dependencies after switching working branch.

Patching and building SugarCube.

3a. If you opted to use Mercurial, use it to apply the patch:
	hg patch --no-commit <full path to sugarcube-fc-changes.patch>

3b. If you do not use Mercurial:
	patch -p1 < <full path to sugarcube-fc-changes.patch>

Building
Run the build.js script. If you use Git bash, you can run the build.js file directly, otherwise run
it as "node build.js". See "build.js -h" for options, but reasonable sets are the following.
For release version:
	./build.js -b 2
and for the debug version:
	./build.js -b 2 -u -d

Find result files in the dist directory.

APPENDIX Lists required steps very briefly.

1. Clone Sugarcube repo: hg clone https://bitbucket.org/tmedwards/sugarcube
2. Change active directory into the directory of the sugarcube clone.
3. Set active branch to "v2-release": hg checkout v2-release
4. Run npm install in the repo dir.

CAUTION: Requited dependencies change during the project lifetime and vary from branch to branch;
you may need to run npm install again after some time, or after branch/tag change. Try to run it in
case of strange build errors.

The next is done in the directory where SC repo was cloned. Loop over required SugarCube versions:

1. hg update --clean to clean any local changes
2. hg checkout v2.28.2 (any tag or head here, of course)
3. hg patch --no-commit ~/games/fc-pregmod/devNotes/sugarcube\ stuff/sugarcube-fc-changes.patch
4. node build.js -b 2 to build release version
5. cp dist/twine2/sugarcube-2/format.js <whenever you want>
6. node build.js -b 2 -u -d to build debug version
7. cp dist/twine2/sugarcube-2/format.js <whenever you want to place the debug header>


code:
There are two main changes here.

CHANGE 1: Speed up loading
==========================

The first is to speed up loading the file.

Edit the sugarcube header and change:

	{key:"_serialize",value:function(e)
	...
	 return Object.freeze

	 {return LZString.compressToUTF16(JSON.stringify(e))}},{key:"_deserialize",value:function(e){return JSON.parse(LZString.decompressFromUTF16(e))}}]),e}();

Into this

[{key:"_serialize",value:function(e)

/* look here for changes */
{return JSON.stringify(e)}},{key:"_deserialize",value:function(e){return JSON.parse((!e || e[0]=="{")?e:LZString.decompressFromUTF16(e))}}]),e}();
/* changes end here */

return Object.freeze

CHANGE 2: Remember scroll position
==================================

Edit the sugarcube header and change:

	var t=jQuery(this);t.ariaIsDisabled()||(t.is("[aria-pressed]")&&t.attr("aria-pressed","true"===t.attr("aria-pressed")?"false":"true"),e.apply(this,arguments))

into this:

	var t=jQuery(this);
	const dataPassage = t.attr('data-passage');
	const initialDataPassage = window && window.SugarCube && window.SugarCube.State && window.SugarCube.State.passage;
	const savedYOffset = window.pageYOffset;
	t.is("[aria-pressed]")&&t.attr("aria-pressed","true"===t.attr("aria-pressed")?"false":"true"),e.apply(this,arguments);
	const doJump = function(){ window.scrollTo(0, savedYOffset); }
	if ( dataPassage && (window.lastDataPassageLink === dataPassage || initialDataPassage === dataPassage))
		doJump();
	window.lastDataPassageLink = dataPassage;

This uses two separate methods to detect if we are returning to the same page. Just checking sugarcube should actually be sufficient, but no harm in this approach either.

CHANGE 3: Ignore expired array
==============================

	return e?t.history=clone(R):t.delta=O(R),V.length>0&&(t.expired=[].concat(_toConsumableArray(V))),null!==B&&(t.seed=B.seed),t}function n(e,t){

	into

	return e?t.history=clone(R):t.delta=O(R),V.length>0&&(t.expired=[]),null!==B&&(t.seed=B.seed),t}function n(e,t){

CHANGE 4: Save overwriting?
===========================

	d.append(e("load","ui-close",L10n.get("savesLabelLoad"),s,function(e){jQuery(document).one(":dialogclose",function(){

	into

	d.append(e("save","ui-close",L10n.get("savesLabelSave"),s,Save.slots.save),e("load","ui-close",L10n.get("savesLabelLoad"),s,function(e){jQuery(document).one(":dialogclose",function(){

path: devNotes/sugarcube stuff/important for updating sugarcube.txt


code:
diff -r cd5d732c8669 src/lib/jquery-plugins.js
--- a/src/lib/jquery-plugins.js	Fri Mar 09 07:43:39 2018 -0600
+++ b/src/lib/jquery-plugins.js	Fri Jan 25 18:37:15 2019 +0100
@@ -37,6 +37,10 @@
		return function () {
			const $this = jQuery(this);

+			const dataPassage = $this.attr('data-passage');
+			const initialDataPassage = window && window.SugarCube && window.SugarCube.State && window.SugarCube.State.passage;
+			const savedYOffset = window.pageYOffset;
+
			// Toggle "aria-pressed" status, if the attribute exists.
			if ($this.is('[aria-pressed]')) {
				$this.attr('aria-pressed', $this.attr('aria-pressed') === 'true' ? 'false' : 'true');
@@ -44,6 +48,11 @@

			// Call the true handler.
			fn.apply(this, arguments);
+
+			const doJump = function(){ window.scrollTo(0, savedYOffset); }
+			if ( dataPassage && (window.lastDataPassageLink === dataPassage || initialDataPassage === dataPassage))
+				doJump();
+			window.lastDataPassageLink = dataPassage;
		};
	}

diff -r cd5d732c8669 src/lib/simplestore/adapters/webstorage.js
--- a/src/lib/simplestore/adapters/webstorage.js	Fri Mar 09 07:43:39 2018 -0600
+++ b/src/lib/simplestore/adapters/webstorage.js	Fri Jan 25 18:37:15 2019 +0100
@@ -192,11 +192,11 @@
		}

		static _serialize(obj) {
-			return LZString.compressToUTF16(JSON.stringify(obj));
+			return JSON.stringify(obj);
		}

		static _deserialize(str) {
-			return JSON.parse(LZString.decompressFromUTF16(str));
+			return JSON.parse((!str || str[0] == "{") ? str : LZString.decompressFromUTF16(str));
		}
	}

diff -r cd5d732c8669 src/state.js
--- a/src/state.js	Fri Mar 09 07:43:39 2018 -0600
+++ b/src/state.js	Fri Jan 25 18:37:15 2019 +0100
@@ -104,7 +104,7 @@
		}

		if (_expired.length > 0) {
-			stateObj.expired = [..._expired];
+			stateObj.expired = [];
		}

		if (_prng !== null) {
diff -r cd5d732c8669 src/ui.js
--- a/src/ui.js	Fri Mar 09 07:43:39 2018 -0600
+++ b/src/ui.js	Fri Jan 25 18:37:15 2019 +0100
@@ -337,6 +337,7 @@
				if (saves.slots[i]) {
					// Add the load button.
					$tdLoad.append(
+						createButton('save', 'ui-close', L10n.get('savesLabelSave'), i, Save.slots.save),
						createButton('load', 'ui-close', L10n.get('savesLabelLoad'), i, slot => {
							jQuery(document).one(':dialogclose', () => Save.slots.load(slot));
						})


code:
diff -r df721eb4cd55 src/lib/jquery-plugins.js
--- a/src/lib/jquery-plugins.js	Sun Dec 16 04:39:00 2018 -0600
+++ b/src/lib/jquery-plugins.js	Fri Jan 25 00:54:01 2019 +0100
@@ -43,14 +43,9 @@
		return function () {
			const $this = jQuery(this);

-			// Exit if the element is disabled.
-			//
-			// NOTE: This should only be necessary for elements which are not disableable
-			// per the HTML specification as disableable elements should be made inert
-			// automatically.
-			if ($this.ariaIsDisabled()) {
-				return;
-			}
+			const dataPassage = $this.attr('data-passage');
+			const initialDataPassage = window && window.SugarCube && window.SugarCube.State && window.SugarCube.State.passage;
+			const savedYOffset = window.pageYOffset;

			// Toggle "aria-pressed" status, if the attribute exists.
			if ($this.is('[aria-pressed]')) {
@@ -59,6 +54,11 @@

			// Call the true handler.
			fn.apply(this, arguments);
+
+			const doJump = function(){ window.scrollTo(0, savedYOffset); }
+			if ( dataPassage && (window.lastDataPassageLink === dataPassage || initialDataPassage === dataPassage))
+				doJump();
+			window.lastDataPassageLink = dataPassage;
		};
	}

diff -r df721eb4cd55 src/lib/simplestore/adapters/webstorage.js
--- a/src/lib/simplestore/adapters/webstorage.js	Sun Dec 16 04:39:00 2018 -0600
+++ b/src/lib/simplestore/adapters/webstorage.js	Fri Jan 25 00:54:01 2019 +0100
@@ -189,11 +189,11 @@
		}

		static _serialize(obj) {
-			return LZString.compressToUTF16(JSON.stringify(obj));
+			return JSON.stringify(obj);
		}

		static _deserialize(str) {
-			return JSON.parse(LZString.decompressFromUTF16(str));
+			return JSON.parse((!str || str[0] == "{") ? str : LZString.decompressFromUTF16(str));
		}
	}

diff -r df721eb4cd55 src/state.js
--- a/src/state.js	Sun Dec 16 04:39:00 2018 -0600
+++ b/src/state.js	Fri Jan 25 00:54:01 2019 +0100
@@ -104,7 +104,7 @@
		}

		if (_expired.length > 0) {
-			stateObj.expired = [..._expired];
+			stateObj.expired = [];
		}

		if (_prng !== null) {
diff -r df721eb4cd55 src/ui.js
--- a/src/ui.js	Sun Dec 16 04:39:00 2018 -0600
+++ b/src/ui.js	Fri Jan 25 00:54:01 2019 +0100
@@ -334,6 +334,7 @@
				if (saves.slots[i]) {
					// Add the load button.
					$tdLoad.append(
+						createButton('save', 'ui-close', L10n.get('savesLabelSave'), i, Save.slots.save),
						createButton('load', 'ui-close', L10n.get('savesLabelLoad'), i, slot => {
							jQuery(document).one(':dialogclose', () => Save.slots.load(slot));
						})


code:
body {
	overflow-x: hidden;
}

/* makes HR colorflip compatible */
hr {
	background:#ccc;
	border:0;
}

/* clears SugarCube's default transition */
.passage {
	transition: none;
	-webkit-transition: none;
}

.passage-in {
	opacity: 1 !important;
}

/* default is 54em */
#passages {
	max-width: 100%;
}

/* small trick to hide broken images */
img {
	text-indent: -10000px;
}

.imageColumn { /* TODO: this is mostly a copy of imageRef to get its flex features working properly. They can probably be removed from imageRef at some point */
	display: flex;
	flex-direction: column;
	flex-wrap: wrap;
	align-items: flex-start;
	position: relative;
	float: right;
}

.imageRef {
	display: flex;
	flex-direction: column;
	flex-wrap: wrap;
	align-items: flex-start;
	position: relative;
	background-color: rgba(80, 80, 80, 0.5);
	margin: 2px;
}

.tinyImg {
	height: 120px;
	width: 120px;
	float: left;
}

.smlImg {
	height: 150px;
	width: 150px;
	float: left;
}

.smlImg > img, .smlImg > video {
	height: auto;
}

.medImg {
	height: 300px;
	width: 300px;
	float: right;
}

.medImg > img, .medImg > video {
	height: auto;
}

.lrgRender {
	height: 531px;
	width: 531px;
	margin-right: -50px;
	margin-left: -50px;
	float: right;
	z-index: -1;
}

.lrgVector {
	height: 600px;
	width: 600px;
	margin-right: -125px;
	margin-left: -125px;
	float: right;
	z-index: -1;
}

.lrgRender > div.mask {
	width: 150px;
	height: 100%;
	background: linear-gradient(90deg, rgba(17,17,17,1), rgba(17,17,17,0.8) 60%, rgba(17,17,17,0));
	z-index: 1;
	/*position: absolute;*/
}

.lrgRender > img, .lrgRender > video {
	margin-left: -150px;
	height: 531px;
	width: auto;
}

.lrgVector > div.mask {
	width: 150px;
	height: 100%;
	background: linear-gradient(90deg, rgba(17,17,17,1), rgba(17,17,17,0.8) 60%, rgba(17,17,17,0));
	z-index: 1;
}

.lrgVector > img, .lrgVector > video {
	margin-left: -150px;
	height: 600px;
	width: auto;
}

.lrgVector svg {
	width: 336px;
}

object {
	object-fit: scale-down;
	position: absolute;
	top: 0;
	left: 0;
}

img.paperdoll {
	position: absolute;
	top: 0;
	left: 0;
	margin-left: 0;
}

span.plusButton {
	display: inline-block;
	line-height: 25px;
	width: 20px;
	text-align: center;
	border: 1px solid rgba(0, 139, 0, 0.3);
	background: rgba(0, 139, 0, 0.2);
	margin: 2px 0;
}

span.minusButton {
	display: inline-block;
	line-height: 25px;
	width: 20px;
	text-align: center;
	border: 1px solid rgba(184, 0, 0, 0.3);
	background: rgba(184, 0, 0, 0.2);
	margin: 2px 0;
}

span.zeroButton {
	display: inline-block;
	line-height: 25px;
	width: 20px;
	text-align: center;
	border: 1px solid rgba(0, 0, 255, 0.3);
	background: rgba(0, 0, 255, 0.2);
	margin: 2px 0;
}

span.plusButton:hover { background: rgba(0, 139, 0, 0.4); }
span.minusButton:hover { background: rgba(184, 0, 0, 0.4); }
span.zeroButton:hover { background: rgba(0, 0, 255, 0.4); }

span.plusButton > a { display: block; }
span.minusButton > a { display: block; }
span.zeroButton > a { display: block; }

span.plusButton > a:hover { text-decoration: none; }
span.minusButton > a:hover { text-decoration: none; }
span.zeroButton > a:hover { text-decoration: none; }

/* Colors are made as css classes, to allow them to be changed for a light color scheme (for example). */
.link { color: #68D } /* link color */
.aquamarine { color: aquamarine }
.coral { color: coral }
.cyan { color: cyan }
.darkgoldenrod { color: darkgoldenrod }
.darkred { color: darkred }
.darkviolet { color: darkviolet }
.deeppink { color: deeppink }
.steelblue { color: steelblue }
.deepskyblue { color: deepskyblue }
.dodgerblue { color: dodgerblue }
.blue { color: blue }
.gold { color: gold }
.goldenrod { color: goldenrod }
.gray { color: gray }
.green { color: green }
.hotpink { color: hotpink }
.lawngreen { color: lawngreen }
.lightblue { color: lightblue }
.lightcoral { color: lightcoral }
.lightgreen { color: lightgreen }
.lightpink { color: lightpink }
.lightsalmon { color: lightsalmon }
.lime { color: lime }
.limegreen { color: limegreen }
.magenta { color: magenta }
.mediumaquamarine { color: mediumaquamarine }
.mediumorchid { color: mediumorchid }
.mediumseagreen { color: mediumseagreen }
.orange { color: orange }
.orangered { color: orangered }
.orchid { color: orchid }
.pink { color: pink }
.red { color: red }
.seagreen { color: seagreen }
.springgreen { color: springgreen }
.tan { color: tan }
.chocolate { color: chocolate }
.saddlebrown { color: saddlebrown }
.teal { color: teal }
.yellow { color: yellow }
.yellowgreen { color: yellowgreen }

.link a { color: #68D } /* link color */
.aquamarine a { color: aquamarine }
.coral a { color: coral }
.cyan a { color: cyan }
.darkgoldenrod a { color: darkgoldenrod }
.darkred a { color: darkred }
.darkviolet a { color: darkviolet }
.deeppink a { color: deeppink }
.steelblue a { color: steelblue }
.deepskyblue a { color: deepskyblue }
.dodgerblue a { color: dodgerblue }
.blue a { color: blue }
.gold a { color: gold }
.goldenrod a { color: goldenrod }
.gray a { color: gray }
.green a { color: green }
.hotpink a { color: hotpink }
.lawngreen a { color: lawngreen }
.lightblue a { color: lightblue }
.lightcoral a { color: lightcoral }
.lightgreen a { color: lightgreen }
.lightpink a { color: lightpink }
.lightsalmon a { color: lightsalmon }
.lime a { color: lime }
.limegreen a { color: limegreen }
.magenta a { color: magenta }
.mediumaquamarine a { color: mediumaquamarine }
.mediumorchid a { color: mediumorchid }
.mediumseagreen a { color: mediumseagreen }
.orange a { color: orange }
.orangered a { color: orangered }
.orchid a { color: orchid }
.pink a { color: pink }
.red a { color: red }
.seagreen a { color: seagreen }
.springgreen a { color: springgreen }
.tan a { color: tan }
.chocolate a { color: chocolate }
.saddlebrown a { color: saddlebrown }
.teal a { color: teal }
.yellow a { color: yellow }
.yellowgreen a { color: yellowgreen }

/*! <<checkvars>> macro for SugarCube 2.x */
#ui-dialog-body.checkvars{padding:1em}#ui-dialog-body.checkvars h1{font-size:1em;margin-top:0}#ui-dialog-body.checkvars table{border-collapse:collapse;border-spacing:0}#ui-dialog-body.checkvars thead tr{border-bottom:2px solid #444}#ui-dialog-body.checkvars tr:not(:first-child){border-top:1px solid #444}#ui-dialog-body.checkvars td,#ui-dialog-body.checkvars th{padding:.25em 1em}#ui-dialog-body.checkvars td:first-child,#ui-dialog-body.checkvars th:first-child{padding-left:.5em;border-right:1px solid #444}#ui-dialog-body.checkvars td:last-child,#ui-dialog-body.checkvars th:last-child{padding-right:.5em}#ui-dialog-body.checkvars th:first-child{text-align:center}#ui-dialog-body.checkvars td:first-child{font-weight:700;text-align:right}#ui-dialog-body.checkvars td{font-family:monospace,monospace;vertical-align:top;white-space:pre-wrap}#ui-dialog-body.checkvars .scroll-pad{margin:0;padding:0}

/*! <<bugreport>> macro for SugarCube 2.x */
#ui-dialog-body.bugreport #bugreport-info{margin-bottom:1em}#ui-dialog-body.bugreport #bugreport-data{display:block;overflow:auto;font-family:monospace,monospace;background-color:transparent;border:1px solid #444;margin:0;padding:6px;height:auto;min-height:200px;white-space:normal}#ui-dialog-body.bugreport .scroll-pad{margin:0;padding:0}

div.output{
width: 100%;
width: 100vw;
max-width: 100%;
word-break: break-all;
white-space: normal;
}
/* css rules for rules assistant */
.rajs-listitem {
	display: inline-block;
	color: #68D;
	margin-right: 1em;
}
.rajs-listitem:last-of-type { margin-right: 0; }
.rajs-listitem:hover {
	cursor: pointer;
	text-decoration: underline;
}
.rajs-list strong:first-of-type, .rajs-list input {
	margin-right: 2em;
}
.rajs-listitem input {
	margin: 0.25em;
}
.rajs-section h1 {
	border-bottom: 1px solid white;
	cursor: pointer;
}
.rajs-section h1:hover { text-decoration: underline; }

.scroll {
	overflow: auto;
}

/* Accordion 000-250-006 */

button.accordion {
	cursor: pointer;
	padding: 5px;
	width: 100%;
	margin-bottom: 10px;
	border-bottom: 3px double;
	border-right: 3px double;
	border-left: none;
	border-top: none;
	text-align: left;
	outline: none;
	transition: 0.2s;
	background-color: transparent;
}

button.accordion.active, button.accordion:hover {
	background-color: transparent;
}

button.accordion:before {
	content: '\002B';
	color: #777;
	font-weight: bold;
	float: left;
	margin-right: 5px;
}

.unStaffed {
	background-color: transparent;
	padding: .5em .2em;
	margin-bottom: 1em;
	border-bottom: thin inset grey;
}

.unStaffed:before {
	content: '\00D7';
	color: #777;
	font-weight: bold;
	color: red;
	float: left;
	margin-right: 5px;
}

.unStaffed:after {
	content: attr(data-after);
	float: right;
	margin-left: 2em;
	font-weight: strong;
	color: green;
}

button.accordion:after {
	content: attr(data-after);
	float: right;
	margin-left: 2em;
	font-weight: normal;
	color: green;
}

button.accordion.active:before {
	content: "\2212";
}

.accHidden {
	padding: 0 18px;
	margin-top: 5px;
	margin-bottom: 5px;
	max-height: 0;
	overflow: hidden;
}

div.accordion:before {
	content: '\002B';
	color: #777;
	font-weight: bold;
	float: left;
	margin-right: 5px;
}
div.accordion.active:before {
	content: "\2212";
}

.hidden
{
	display:none;
}

div.quicklist.devotion button.mindbroken
{
	background-color:red;
}
div.quicklist.devotion button.very-hateful
{
	background-color:darkviolet;
}
div.quicklist.devotion button.hateful
{
	background-color:darkviolet;
}
div.quicklist.devotion button.resistant
{
	background-color:mediumorchid;
}
div.quicklist.devotion button.ambivalent
{
	background-color: yellow;
	color: #444444;
}
div.quicklist.devotion button.accepting
{
	background-color: hotpink;
}
div.quicklist.devotion button.devoted
{
	background-color: deeppink;
}
div.quicklist.devotion button.worshipful
{
	background-color: magenta;
}
div.quicklist.trust button.mindbroken
{
	background-color:red;
}
div.quicklist.trust button.extremely-terrified
{
	background-color: darkgoldenrod;
}
div.quicklist.trust button.terrified
{
	background-color: goldenrod;
}
div.quicklist.trust button.frightened
{
	background-color: gold;
	color: coral;
}
div.quicklist.trust button.fearful
{
	background-color: yellow;
	color: green;
}
div.quicklist.trust button.hate-careful
{
	background-color: orange;
}
div.quicklist.trust button.careful
{
	background-color: mediumaquamarine;
	color: forestgreen;
}
div.quicklist.trust button.bold
{
	background-color: orangered;
}
div.quicklist.trust button.trusting
{
	background-color: mediumseagreen;
}
div.quicklist.trust button.defiant
{
	background-color: darkred;
}
div.quicklist.trust button.profoundly-trusting
{
	background-color: seagreen;
}
div.quicklist
{
	table-layout: fixed;
	text-align: center;
	border-collapse: separate;
	border-spacing: 2px;
	border-style: hidden;
	empty-cells: hide;
	width: 70%;
}
div.quicklist button
{
	margin-top: 15px;
	margin-right: 20px;
	white-space: nowrap;
}

.optionMacro {
	margin-top: 8px;
	display: inline-block;
}

.optionDescription {
	display: inline-block;
	width: 500px;
	margin-right: 10px;
	line-height: 1.2;
	text-align: right;
}

@media only screen and (max-width: 1200px) {
	.optionDescription {
		display: block;
		width: unset;
		text-align: left;
	}

	.optionMacro {
		margin-top: 30px;
	}

}

.optionMacroOption {
	border: #555 solid 0.5px;
	border-top-width: 0;
	vertical-align: top;
	-moz-user-select: none;
	min-width: 79px; /* 80px - 1px for border */
	display: inline-block;
	text-align: center;
}

.optionComment {
	vertical-align: top;
	color: gray;
	padding-left: 10px;
}

/* But don't add the | after the last one */
.optionMacroOption:last-child {
	border-top-right-radius: 6px;
	border-bottom-right-radius: 6px;
	border-right-width: 0;
}

.optionMacroOption:first-child {
	border-top-left-radius: 6px;
	border-bottom-left-radius: 6px;
}

.optionMacroOption a {
	padding: 0 10px;
	box-sizing: border-box;
	width: 100%;
	display: block;
}

.optionMacroOption:hover {
	background: #2F2F2F;
}

body.lightTheme .optionMacroOption:hover {
	background: #DDDDDD;
}

body.lightTheme .optionMacroSelected {
	background: #DDDDDD;
}

.optionMacroSelected {
	background: #2F2F2F;
	padding: 0 10px;
	min-width: 80px;
	cursor: pointer;
	box-sizing: border-box;
}

.optionValue {
	display: inline-flex;
	flex-wrap: wrap;
}

.optionValue input {
	padding: 3px 3px 3px 10px;
	background: transparent;
	border: #555 solid 0.5px;
	border-top-width: 0;
	min-width: unset;
	width: 140px;
	height: 22px;
}

.optionMacroNumber input {
	width: 50px;
}

.optionValue input:last-child {
	border-top-right-radius: 6px;
	border-bottom-right-radius: 6px;
	border-right-width: 0;
}

.optionValue input:first-child {
	border-top-left-radius: 6px;
	border-bottom-left-radius: 6px;
}

.optionMacroSelected.optionMacroEnable {
	color: rgb(230,255,230);
	background-image: linear-gradient(rgba(0, 255,0, 0.12), transparent);
}

.optionMacroSelected.optionMacroDisable {
	color: rgb(255,240,240);
	background-image: linear-gradient(rgba(255, 100, 0, 0.2), transparent);
}

/* Stylesheet for WardrobeUse.tw */

#passage-wardrobe-use .optionMacro {
	display: inline;
}

#passage-wardrobe-use .optionDescription {
	width: unset;
	text-align: left;
}

#passage-wardrobe-use .optionValue {
	display: flex;
}

#passage-wardrobe-use .optionMacroOption {
	flex-grow: 1;
}

/* Last line shouldn't grow */
#passage-wardrobe-use .optionValue::after {
	flex-grow: 1000000000;
	content: '';
}

#graph .linage {
	fill: none;
	stroke: white;
}
#graph .marriage {
	fill: none;
	stroke: white;
}
#graph .node {
	background-color: lightblue;
	border-style: solid;
	border-width: 1px;
}
#graph .nodeText{
	font: 10px sans-serif;
	margin: 0;
	padding: 0;
	color: black;
}
#graph {
	font: 10px sans-serif;
	margin: -20px 0;
	padding: 0;
	color: black;
}

#graph div {
	border-style: solid;
	border-width: 1px;
}

#graph .XY {
	background-color: lightblue;
}

#graph div.XX {
	background-color: pink;
}

#graph div.unknown {
	background-color: gray;
}

#graph div.unknownXY {
	background-color: #808080;
}

#graph div.unknownXX {
	background-color: #808080;
}

#graph .you {
	background-color: red;
}

#graph .emphasis {
	font-style: italic;
	font-weight: bold;
	margin: 0;
	background: #ffffff88;
	border: 2px solid Gold;
	white-space: nowrap;
}

#editFamily {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
}

#editFamily #familyTable {
}

path: devNotes/twine CSS


code:
#!/bin/sh

# setup:
# add this to crontab:
# */15 * * * * cd ~/FC/fc-pregmod && git pull --ff-only origin pregmod-master > ~/FC/git_pull.log 2>&1
# and do "ln -s ~/FC/fc-pregmod/devTools/BuildAndIPFSify.sh .git/hooks/post-merge"
# NOTE: you may need to define XDG_RUNTIME_DIR in your crontab

# TODO: add logic to figure out if we should use ipfs-cluster instead of using the local instance directly.
#			if we use ipfs-cluster we probably don't need to warm ipfs.io's cache.

rm bin/*.html

# if this script is used as the post-merge hook then
# a) git pull has pulled something
# b) $PWD is fc-pregmod
if [ "$(basename "$0")" != "post-merge" ]; then
	# cd to fc-pregmod based on where this script is
	cd "$(readlink -f "$(dirname "$0")")/.." || exit 1
	git pull
	# if there are new html files then git probably pulled something and ran this script as a post-merge hook
	if ls bin/*.html; then
		exit 0
	fi
fi

# If we've done this before then unpin the previous hash so IPFS can GC it if it needs to
if [ -r ../IPFS_hash.txt ]; then
	ipfs pin rm --recursive=true "$(cut -d : -f 2 ../IPFS_hash.txt | tr -d ' ')"
fi

./compile || exit 1

# Keep the build time from changing the hash of the file
sed -Ei -e '/^  \* Built on .+$/d' bin/*.html

# add the date of the last commit to the file, but don't use colons because Windows (still?) doesn't like them
mv bin/*.html "bin/FC pregmod $(git log -1 --format='%cd' --date='format:%F %H-%M').html"

# include the unembedded vector art
ipfs_hash="$(ipfs add -w -Q -r bin/*.html resources)"
echo "IPFS Folder Hash: ${ipfs_hash}" > ../IPFS_hash.txt
ipfs name publish "$ipfs_hash"

# when it's done it will print something like "Published to $your_pubkey: /ipfs/$ipfs_hash"
# You can view the folder at http://127.0.0.1:8080/ipns/$your_pubkey

# make ipfs.io cache the files
# $XDG_RUNTIME_DIR SHOULD be defined, but there are cases where it wouldn't be
if [ -z "${XDG_RUNTIME_DIR+x}" ]; then
	echo "\$XDG_RUNTIME_DIR is unset, bailing"
	exit 2
fi

# throw it into a file so we can loop over lines, not "strings delimited by whitespace"
find resources bin -print | grep -ve '.gitignore' | sed -e 's|bin/||' | grep -Ee '.+\.svg' -e '.html' > "${XDG_RUNTIME_DIR}/files.list"

# ipfs PeerID, it's user specific
PeerID="$(ipfs config show | grep -e 'PeerID' | cut -d: -f 2 | tr -d ' "')"
while IFS= read -r item
do
	echo "https://ipfs.io/ipns/${PeerID}/${item}"
done < "${XDG_RUNTIME_DIR}/files.list" | xargs --max-procs=10 --max-args=1 --replace curl --silent --show-error --range 0-499 --output /dev/null '{}'

rm "${XDG_RUNTIME_DIR}/files.list"


code:
#!/bin/sh
mega-login $1 $2;echo 'New clone? 0:y 1:n 2:na';read Opt #Requirements:MEGAcmd,minify,git. U=$1 P=$2 rDir=$3 lDir=$4 repo=$5
while true;do cd $4;gen=0
	if [ $Opt == 0 ];then mkdir -p $4&&git clone -q --depth 1 $5 $4&&cd $4&&if [[ ! `mega-ls $3|cut -c29-32|paste -sd,` =~ `git log|head -1|cut -c8-11` ]];then gen=1;fi #arraryCheck:stackoverflow.com/a/15394738
	elif [[ $Opt > 0||$gen < 1 ]];then git fetch -q&&if [ `git rev-list ...origin --count` -gt 0 ];then git reset -q --hard&&git pull -q&&gen=1;fi ;fi #gitCheck:stackoverflow.com/a/17192101
	if [[ $Opt > 1||$gen > 0 ]];then minify -rao $4 $4&&./compile --insane&&cd bin/&&rm FC-*;mv * FC-pregmod-`git log -1 --format=%cd --date=format:%m-%d-%Y-%H-%M`-`git log|head -1|cut -c8-11`.html&&mega-put -c * $3&&mega-rm $3`mega-ls $3|sort -r|tail -n +11`;fi
Opt=-1;clear;sleep 15m;done


code:
#!/usr/bin/env python3
import fileinput
import re
import sys

WARNING = '\033[93m'
ENDC = '\033[0m'

def myprint(*args):
	print(WARNING, fileinput.filename() + ":", ENDC,*args)

def yield_line_and_islastline(f):
	global filename
	global linenumber
	try:
		prevline = next(f)
		filename = fileinput.filename()
		linenumber = fileinput.filelineno()
	except StopIteration:
		return
	for line in f:
		yield (prevline, f.isfirstline())
		filename = fileinput.filename()
		linenumber = fileinput.filelineno()
		prevline = line
	yield prevline, True

pattern = re.compile(r'(<<(\/?) *(if|for|else|switch|case|replace|link)[^<>]*)')

tagfound = []
try:
	for line, isLastLine in yield_line_and_islastline(fileinput.input()):
		for (whole,end,tag) in re.findall(pattern,line):
			if tag == "else" or tag == 'case':
				if len(tagfound) == 0:
					myprint("Found", tag, "but with no opening tag:")
					myprint("  ", linenumber,":", whole)
					fileinput.nextfile()
				lasttag = tagfound[-1]
				if (tag == "else" and lasttag["tag"] != "if") or (tag == "case" and lasttag["tag"] != "switch"):
					myprint("Mismatched else: Opening tag was:")
					myprint("  ",lasttag["linenumber"],":", lasttag["whole"])
					myprint("But this tag was:")
					myprint("  ",linenumber,":", whole)
					fileinput.nextfile()
					break
			elif end != '/':
				tagfound.append({"whole": whole, "linenumber":linenumber,"tag":tag})
			else:
				if len(tagfound) == 0:
					myprint("Found closing tag but with no opening tag:")
					myprint("  ", linenumber,":", whole)
					fileinput.nextfile()
					break
				lasttag = tagfound.pop()
				if lasttag["tag"] != tag:
					myprint("Mismatched tag: Opening tag was:")
					myprint("  ",lasttag["linenumber"],":", lasttag["whole"])
					myprint("Closing tag was:")
					myprint("  ",linenumber,":", whole)
					fileinput.nextfile()
					break

		if isLastLine:
			if len(tagfound) != 0:
				myprint("End of file found but", len(tagfound), ("tag hasn't" if len(tagfound)==1 else "tags haven't"), "been closed:")
			for tag in tagfound:
				myprint("  ", tag["linenumber"],":", tag["whole"])
			tagfound = []
except UnicodeDecodeError as e:
	myprint(e)
	print("   Hint: In Linux, you can get more details about Unicode errors by running:")
	print("	 isutf8", fileinput.filename())
	print("   :Note it might be caused by ", filename)


code:
#!/bin/sh

#converts files in the current dir

# collect files with [script] tag
FILES=`grep '\[script\]' -H -R * | cut -f 1 -d ':'`
for f in $FILES; do
	# create new name. Old name can have any of the following suffixes:
	# _JS.tw  JS.tw .tw
	nf="${f%_JS.tw}"
	nf="${nf%JS.tw}"
	nf="${nf%.tw}"
	# we might end up with an empty name, then set dir name as the file name
	[ -z "$(basename $nf)" -o "${nf: -1}" = "/" ] && nf="${nf}$(basename ${nf})"
	# append the new suffix
	nf="${nf}.js"
	echo "renaming ${f} -> ${nf}"
	# rename the file
	git mv "${f}" "${nf}" || echo "renaming ${f} failed"
	# strip Twee header from it
	sed -i 1d "${nf}" || echo "stripping Twee header failed for ${nf}"
	# strip leading empty line '/./,$!d'
	sed -i '/./,$!d' "${nf}" || echo "Stripping leading empty blank lines failed for ${nf}"
done


code:
@echo off
pushd %~dp0
embed_favicon.py
popd
pause


code:
#!/usr/bin/env python3

'''
Script for embedding favicons into a HTML Header.

Script file is expected to reside in devTools directory.

Note: This does not actually check the image file's contents for size detection.

Usage:
python3 embed_favicon.py [FC_pregmod.html]
'''

import sys
import os
import re
import base64

# file extensions eligible for use as favicons and their mimetype
ext2mimetype = {
	'.png': 'image/png',
	'.ico': 'image/x-icon'
}

# reads a file, turns it into a data uri
def data_uri_from_file(filename, mimetype):
	data = open(filename, 'rb').read()
	base64data = base64.b64encode(data).decode('ascii')
	out = 'data:%s;base64,%s' % (mimetype, base64data)
	return out

if __name__ == "__main__":
	# find project root directory path
	# (script file is expected to reside in devTools)
	project_root_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
	html_path = ''
	if (len(sys.argv) > 1):
		html_path = sys.argv[1]
	else:
		# path to bin/FC_pregmod.html
		html_path = os.path.join(
			project_root_path,
			'bin/FC_pregmod.html'
		)
	# path to directory containing all favicons to embed
	favicons_source_path = os.path.join(
		project_root_path,
		'resources/raster/favicon/'
	)
	# walk directory for all files
	favicons_paths = [
		os.path.join(dirpath, filename)
		for dirpath, dirnames, filenames in os.walk(favicons_source_path)
		for filename in filenames
	]
	# ignore files with unknown extensions
	favicons_paths = [f for f in favicons_paths
					  if f[-4:] in ext2mimetype.keys()]

	# prepare embedded data
	size_from_filename = re.compile(r'([0-9]+)\....$')
	favicons_html = []
	for fp in favicons_paths:
		print('Found favicon source file "%s".' % (fp))
		# get mimetype by file extension
		mimetype = ext2mimetype[fp[-4:]]
		if (mimetype == 'image/x-icon'):
			# assume sizes in ico
			sizes = '16x16 32x32 64x64'
		else:
			# guess icon size from file name
			size = size_from_filename.search(fp).group(1)
			sizes = '%sx%s' % (size, size)
		data = data_uri_from_file(fp, mimetype)
		favicons_html.append(
			# prepare html with favicon data embedded
			'<link rel="icon" type="%s" sizes="%s" href="%s">\n' % (
				mimetype, sizes, data
			)
		)

	# modify header file
	with open(html_path, 'r+', encoding='utf-8') as hf:
		print('Rewriting "%s"...' % (html_path))
		lines_in = hf.readlines()  # read whole file
		lines_out = []
		for line in lines_in:
			# embed favicons into head
			if (line.startswith('</head>')):
				lines_out.extend(favicons_html)
			# remove all currently embedded favicons
			if (not (line.startswith('<link') and 'icon' in line)):
				lines_out.append(line)
		hf.seek(0)  # move to beginning of file
		hf.write(''.join(lines_out))  # overwrite with new data
		hf.truncate()  # remove trailing old data

	print('Finished.')


code:
<!-- Added to the <head> of the html file -->
<link rel="shortcut icon" type="image/x-icon" href="resources/raster/favicon/arcologyVector.ico">


code:
#Add files or folders to be excluded.
#empty lines will match on ALL paths, so if you need one do it like this:
#
src/art/
src/gui/svgFilters.tw
src/pregmod/editGenetics.tw

path: devTools/javaSanityCheck/excluded


code:
#Allowed HTML tags
#Effectively everything that is allowed in a these statements like this:
#<tag> or <tag ???>
#when ;1 is specified it expects a matching closing tag like this: </tag>
#Do not add Twine Tags here.
#Characters outside of ASCII scope are not supported.
#
#included to reduce false positives until better solution
!--
http://www.gnu.org/licenses/
-400
#html tags
a;1
b;1
blockquote;1
body;1
br;0
button;1
caption;1
center;1
dd;1
div;1
dl;1
dt;1
h1;1
h2;1
h3;1
h4;1
hr;0
html;1
i;1
img;0
input;0
li;1
option;1
script;1
select;1
span;1
strong;1
style;1
table;1
td;1
th;1
tr;1
tt;1
ul;1

path: devTools/javaSanityCheck/htmlTags


code:
Full sources for easy import into IDE can be found here:
https://gitgud.io/Arkerthan/twine-sanitycheck


code:
package org.arkerthan.sanityCheck;

/**
 * @author Arkerthan
 */
public class DisallowedTagException extends RuntimeException {

	public DisallowedTagException(String tag) {
		super(tag);
	}
}


code:
package org.arkerthan.sanityCheck;

import org.arkerthan.sanityCheck.element.AngleBracketElement;
import org.arkerthan.sanityCheck.element.CommentElement;
import org.arkerthan.sanityCheck.element.Element;
import org.arkerthan.sanityCheck.element.KnownElement;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

/**
 * @author Arkerthan
 * @version 1.0
 */
public class Main {

	public static TagSearchTree<Tag> htmlTags, twineTags;
	private static String currentFile;
	private static int currentLine, currentPosition;
	private static Stack<Element> stack;
	private static List<SyntaxError> errors = new LinkedList<>();
	private static String[] excluded;

	public static void main(String[] args) {

		//setup
		setupExclude();
		setupHtmlTags();
		setupTwineTags();
		Path workingDir = Paths.get("").toAbsolutePath();

		//actual sanityCheck
		runSanityCheckInDirectory(workingDir, new File("src/"));

		//output errors
		for (SyntaxError e :
				errors) {
			System.out.println(e.getError());
		}
	}

	/**
	 * Goes through the whole directory including subdirectories and runs
	 * {@link Main#sanityCheck(Path)} on all .tw files
	 *
	 * @param dir to be checked
	 */
	private static void runSanityCheckInDirectory(Path workingDir, File dir) {
		//subdirectories are checked recursively

		try {
			for (File file : dir.listFiles()) {
				if (file.isFile()) { //run sanityCheck if file is a .tw file
					String path = file.getAbsolutePath();
					if (path.endsWith(".tw")) {
						sanityCheck(workingDir.relativize(file.toPath()));
					}
				} else if (file.isDirectory()) {
					runSanityCheckInDirectory(workingDir, file.getAbsoluteFile());
				}
			}
		} catch (NullPointerException e) {
			e.printStackTrace();
			System.err.println("Couldn't read directory " + currentFile);
			System.exit(-1);
		}
	}

	/**
	 * Runs the sanity check for one file. Does not run if file is excluded.
	 *
	 * @param path file to be checked
	 */
	private static void sanityCheck(Path path) {
		File file = path.toFile();

		// replace this with a known encoding if possible
		Charset encoding = Charset.defaultCharset();

		if (!excluded(file.getPath())) {
			currentFile = file.getPath();
			currentLine = 1;
			stack = new Stack<>();

			//actually opening and reading the file
			try (InputStream in = new FileInputStream(file);
				 Reader reader = new InputStreamReader(in, encoding);
				 // buffer for efficiency
				 Reader buffer = new BufferedReader(reader)) {
				handleCharacters(buffer);
			} catch (IOException e) {
				e.printStackTrace();
				System.err.println("Couldn't read " + file);
			}
		}
	}

	/**
	 * sets up a {@link TagSearchTree<Tag>} for fast access of HTML tags later
	 */
	private static void setupHtmlTags() {
		//load HTML tags into a list
		List<Tag> TagsList = loadTags("devTools/javaSanityCheck/htmlTags");

		//turn List into alphabetical search tree
		try {
			htmlTags = new TagSearchTree<>(TagsList);
		} catch (ArrayIndexOutOfBoundsException e) {
			System.err.println("Illegal Character in devTools/javaSanityCheck/htmlTags");
			System.exit(-1);
		}
	}

	/**
	 * sets up a {@link TagSearchTree<Tag>} for fast access of twine tags later
	 */
	private static void setupTwineTags() {
		//load twine tags into a list
		List tagsList = loadTags("devTools/javaSanityCheck/twineTags");

		//turn List into alphabetical search tree
		try {
			twineTags = new TagSearchTree<>(tagsList);
		} catch (ArrayIndexOutOfBoundsException e) {
			System.err.println("Illegal Character in devTools/javaSanityCheck/twineTags");
			System.exit(-1);
		}
	}

	/**
	 * Loads a list of tags from a file
	 *
	 * @param filePath file to load tags from
	 * @return loaded tags
	 */
	private static List<Tag> loadTags(String filePath) {
		List<Tag> tagsList = new LinkedList<>();
		try {
			Files.lines(new File(filePath).toPath()).map(String::trim)
					.filter(s -> !s.startsWith("#"))
					.forEach(s -> tagsList.add(parseTag(s)));
		} catch (IOException e) {
			System.err.println("Couldn't read " + filePath);
		}
		return tagsList;
	}

	/**
	 * Turns a string into a Tag
	 * ";1" at the end of the String indicates that the tag needs to be closed later
	 *
	 * @param s tag as String
	 * @return tag as Tag
	 */
	private static Tag parseTag(String s) {
		String[] st = s.split(";");
		if (st.length > 1 && st[1].equals("1")) {
			return new Tag(st[0], false);
		}
		return new Tag(st[0], true);
	}

	/**
	 * sets up the excluded files array.
	 */
	private static void setupExclude() {
		//load excluded files
		List<String> excludedList = new ArrayList<>();
		try {
			Files.lines(new File("devTools/javaSanityCheck/excluded").toPath()).map(String::trim)
					.filter(s -> !s.startsWith("#"))
					.forEach(excludedList::add);
		} catch (IOException e) {
			System.err.println("Couldn't read devTools/javaSanityCheck/excluded");
		}

		//turn excluded files into an array and change path to windows style if needed
		if (isWindows()) {
			excluded = new String[excludedList.size()];
			int i = 0;
			for (String s :
					excludedList) {
				excluded[i++] = s.replaceAll("/", "\\\\");
			}
		} else {
			excluded = excludedList.toArray(new String[0]);
		}
	}

	/**
	 * @return whether OS is Windows or not
	 */
	private static boolean isWindows() {
		return (System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"));
	}

	/**
	 * checks if a file or directory is excluded from the sanity check
	 *
	 * @param s file/directory to be checked
	 * @return whether it is excluded or not
	 */
	private static boolean excluded(String s) {
		for (String ex :
				excluded) {
			if (s.startsWith(ex)) return true;
		}
		return false;
	}

	/**
	 * Reads the file character by character.
	 *
	 * @param reader reader that is read
	 * @throws IOException thrown if the file can't be read
	 */
	private static void handleCharacters(Reader reader) throws IOException {
		int r;
		while ((r = reader.read()) != -1) {
			char c = (char) r;
			handleCharacter(c);
		}
	}

	/**
	 * Handles a single character
	 *
	 * @param c next character
	 */
	private static void handleCharacter(char c) {
		//updating position
		currentPosition++;
		if (c == '\n') {
			currentLine++;
			currentPosition = 1;
		}

		//try applying to the innermost element
		if (!stack.empty()) {
			int change;
			try {
				change = stack.peek().handleChar(c);
			} catch (SyntaxError e) {
				change = e.getChange();
				addError(e);
			}

			//change greater 0 means the innermost element did some work
			if (change > 0) {
				//2 means the Element is complete
				if (change == 2) {
					//remove the topmost element from stack since it is complete
					stack.pop();
					return;
				}
				//3 means the Element is complete and part of a two or more tag system
				if (change == 3) {
					//remove the topmost element from stack since it is complete
					KnownElement k = stack.pop().getKnownElement();
					//if KnownElement k is closing another element, check if there is one and remove it
					if (k.isClosing()) {
						if (stack.empty()) { //there are no open elements at all
							addError(new SyntaxError("Closed tag " + k.getShortDescription() + " without " +
									"having any open tags.", -2));
						} else if (stack.peek() instanceof KnownElement) {
							//get opening tag
							KnownElement kFirst = (KnownElement) stack.pop();
							//check if closing element matches the opening element
							if (!kFirst.isMatchingElement(k)) {
								addError(new SyntaxError("Opening tag " + kFirst.getShortDescription() +
										" does not match closing tag " + k.getShortDescription() + ".", -2));
							}
						} else {
							//There closing tag inside another not Known element: <div </html>
							addError(new SyntaxError("Closing tag " + k.getShortDescription() + " inside " +
									"another tag " + stack.peek().getShortDescription() + " without opening first.",
									-2, true));
						}
					}
					//check if the element needs to be closed by another
					if (k.isOpening()) {
						stack.push(k);
					}
					return;
				}
				//means the element couldn't do anything with it and is finished
				if (change == 4) {
					stack.pop();
				} else {
					return;
				}
			}
		}

		//innermost element was uninterested, trying to find matching element
		switch (c) {
			//case '@':
			//stack.push(new AtElement(currentLine, currentPosition));
			//break;
			case '<':
				stack.push(new AngleBracketElement(currentLine, currentPosition));
				break;
			//case '>':
			//addError(new SyntaxError("Dangling \">\", current innermost: " + (stack.empty() ? "null" : stack.peek().getShortDescription()), -2));
			//break;
			case '/':
				stack.push(new CommentElement(currentLine, currentPosition));
				break;
			//case '(':
			//	stack.push(new BracketElement(currentLine, currentPosition));
		}
	}

	/**
	 * add an error to the error list
	 *
	 * @param e new error
	 */
	private static void addError(SyntaxError e) {
		e.setFile(currentFile);
		e.setLine(currentLine);
		e.setPosition(currentPosition);
		errors.add(e);
	}
}


code:
package org.arkerthan.sanityCheck;

/**
 * @author Arkerthan
 */
public class SyntaxError extends Exception {
	private String file;
	private int line, position;
	private String description;
	private int change; //see Element for values; -2 means not thrown
	private boolean warning = false;

	/**
	 * @param description description of error
	 * @param change	  state change as specified in Element
	 */
	public SyntaxError(String description, int change) {
		this.description = description;
		this.change = change;
	}

	/**
	 * @param description description of error
	 * @param change	  state change as specified in Element
	 * @param warning	 whether it is a warning or an error
	 */
	public SyntaxError(String description, int change, boolean warning) {
		this(description, change);
		this.warning = warning;
	}

	/**
	 * @param file at which the error occurred
	 */
	public void setFile(String file) {
		this.file = file;
	}

	/**
	 * @param line in which the error occurred
	 */
	public void setLine(int line) {
		this.line = line;
	}

	/**
	 * @param position at which the error occurred
	 */
	public void setPosition(int position) {
		this.position = position;
	}

	/**
	 * @return error message
	 */
	public String getError() {
		String s = warning ? "Warning: " : "Error: ";
		return s + file + ": " + line + ":" + position + " : " + description;
	}

	/**
	 * @return change that happened in Element before it was thrown. -1 if not thrown.
	 */
	public int getChange() {
		return change;
	}
}


code:
package org.arkerthan.sanityCheck;

/**
 * @author Arkerthan
 */
public class Tag {
	public final String tag;
	public final boolean single;

	public Tag(String tag, boolean single) {
		this.tag = tag;
		this.single = single;
	}
}


code:
package org.arkerthan.sanityCheck;

import java.util.List;

/**
 * @param <E> Tag class to be stored
 * @author Arkerthan
 * <p>
 * Tag SearchTree stores Tags in an alphabetical search tree.
 * Once created the search tree can't be changed anymore.
 */
public class TagSearchTree<E extends Tag> {
	private static final int SIZE = 128;
	private final TagSearchTree<E>[] branches;
	private E element = null;
	private String path;

	/**
	 * creates a new empty TagSearchTree
	 */
	private TagSearchTree() {
		branches = new TagSearchTree[SIZE];
	}

	/**
	 * Creates a new filled TagSearchTree
	 *
	 * @param list Tags to be inserted
	 */
	public TagSearchTree(List<E> list) {
		this();
		for (E e : list) {
			this.add(e, 0);
		}
	}

	/**
	 * adds a new Tag to the TagSearchTree
	 *
	 * @param e	 Tag to be stored
	 * @param index index of relevant char for adding in tag
	 */
	private void add(E e, int index) {
		//set the path to here
		path = e.tag.substring(0, index);
		//checks if tag has to be stored here or further down
		if (e.tag.length() == index) {
			element = e;
		} else {
			//store tag in correct branch
			char c = e.tag.charAt(index);
			if (branches[c] == null) {
				branches[c] = new TagSearchTree<>();
			}
			branches[c].add(e, index + 1);
		}
	}

	/**
	 * @param c character of branch needed
	 * @return branch or null if branch doesn't exist
	 */
	public TagSearchTree<E> getBranch(char c) {
		if (c >= SIZE) return null;
		return branches[c];
	}

	/**
	 * @return stored Tag, null if empty
	 */
	public E getElement() {
		return element;
	}

	/**
	 * @return path inside full tree to get to this Branch
	 */
	public String getPath() {
		return path;
	}
}


code:
package org.arkerthan.sanityCheck;

/**
 * @author Arkerthan
 */
public class UnknownStateException extends RuntimeException {

	public UnknownStateException(int state) {
		super(String.valueOf(state));
	}
}


code:
package org.arkerthan.sanityCheck.element;

import org.arkerthan.sanityCheck.*;

import java.util.Arrays;
import java.util.List;

/**
 * @author Arkerthan
 */
public class AngleBracketElement extends Element {
	private static final List<String> logicTags = Arrays.asList("if", "elseif", "else", "switch", "case", "default");
	private int state = 0;
	/*
	 0 - initial: <
	 TWINE
	 1 - <<
	-1 - <</
	 2 - trying to complete twine tag: <<tag ???>>
	-2 - trying to complete twine tag: <</tag>>
	 3 - waiting for >>
	-3 - expecting > from 3
	 4 - waiting for >> with KnownElement
	-4 - expecting > from 4
	 5 - expecting >>
	-5 - expecting >
	 6 - expecting > with KnownElement opening; comparison?
	-6 - expecting > with KnownElement closing

	 HTML
	-9 - </
	10 - trying to complete HTML tag: <tag ???>
   -10 - trying to complete HTML tag: </tag>
	11 - waiting for >
   -11 - expecting >
	12 - waiting for > with KnownElement
	 */

	private TagSearchTree<Tag> tree;

	public AngleBracketElement(int line, int pos) {
		super(line, pos);
	}

	@Override
	public int handleChar(char c) throws SyntaxError {
		switch (state) {
			case 0:
				switch (c) {
					case '<':
						state = 1;
						return 1;
					case '/':
						state = -9;
						return 1;
					case '>':// empty <>
					case ' ':// assume comparison
					case '='://	"		"
					case '3':// a heart: <3
						return 2;
					default:
						try {
							state = 10;
							tree = Main.htmlTags;
							return handleOpeningHTML(c);
						} catch (SyntaxError e) {
							state = 1;
							throw new SyntaxError("Opening \"<\" missing, found " + c, 1);
						}
				}
			case 1:
				if (c == '<') {
					throw new SyntaxError("Too many \"<\".", 1);
				} else if (c == '>') {
					state = 3;
					throw new SyntaxError("Empty Statement?", 1);
				} else if (c == '/') {
					state = -1;
					return 1;
				}
				state = 2;
				tree = Main.twineTags;
				return handleOpeningTwine(c);
			case -1:
				if (c == '>') {
					throw new SyntaxError("Empty Statement?", 2, true);
				}
				state = -2;
				tree = Main.twineTags;
				return handleClosingTwine(c);

			case 2:
				return handleOpeningTwine(c);
			case -2:
				return handleClosingTwine(c);
			case 3:
				if (c == '>') {
					state = -3;
					return 1;
				}
				break;
			case -3:
				if (c == '>') {
					return 2;
				} else if (c == ' ' || c == '=') { // assuming comparison
					state = 3;
					return 1;
				} else {
					throw new SyntaxError("Closing \">\" missing, opened tag at [" + line + ":" + pos + "]", 2);
				}
			case 4:
				if (c == '>') {
					state = -4;
					return 1;
				}
				break;
			case -4:
				if (c == '>') {
					return 3;
				} else if (c == ' ' || c == '=') { // assuming comparison
					state = 4;
					return 1;
				} else {
					throw new SyntaxError("Closing \">\" missing, opened tag at[" + line + ":" + pos + "]", 2);
				}
			case 5:
				if (c == '>') {
					state = -5;
					return 1;
				} else {
					throw new SyntaxError("Closing \">\" missing, opened tag at [" + line + ":" + pos + "]", 2);
				}
			case -5:
				if (c == '>') {
					return 2;
				}
				throw new SyntaxError("Closing \">\" missing, opened tag at [" + line + ":" + pos + "]", 2);
			case 6:
				if (c == '>') {
					return 3;
				} else if (c == ' ' || c == '=') {
					state = 3;
					return 1;
				} else {
					throw new SyntaxError("Closing \">\" missing, opened tag at [" + line + ":" + pos + "]", 3);
				}
			case -6:
				if (c == '>') {
					return 3;
				}
				throw new SyntaxError("Closing \">\" missing, opened tag at [" + line + ":" + pos + "]", 3);

			case -9:
				if (c == '>') {
					throw new SyntaxError("Empty Statement?", 2, true);
				}
				state = -10;
				tree = Main.htmlTags;
				return handleClosingHTML(c);
			case 10:
				return handleOpeningHTML(c);
			case -10:
				return handleClosingHTML(c);
			case 11:
				if (c == '>')
					return 2;
				if (c == '@') //@ inside HTML tags is allowed
					return 1;
				break;
			case -11:
				if (c == '>')
					return 2;
				throw new SyntaxError("Closing \">\" missing [2]", 2);
			case 12:
				if (c == '>')
					return 3;
				if (c == '@') //@ inside HTML tags is allowed
					return 1;
				break;
			default:
				throw new UnknownStateException(state);
		}
		return 0;
	}

	private int handleOpeningHTML(char c) throws SyntaxError {
		if (c == ' ') {
			state = 11;
			if (tree.getElement() == null) {
				throw new SyntaxError("Unknown HTML tag", 1);
			}
			if (!tree.getElement().single) {
				k = new KnownHtmlElement(line, pos, true, tree.getElement().tag);
				state = 12;
				return 1;
			}
			return 1;
		}
		if (c == '>') {
			if (tree.getElement() == null) {
				throw new SyntaxError("Unknown HTML tag", 2);
			}
			if (!tree.getElement().single) {
				k = new KnownHtmlElement(line, pos, true, tree.getElement().tag);
				return 3;
			}
			return 2;
		}

		tree = tree.getBranch(c);
		if (tree == null) {
			state = 11;
			throw new SyntaxError("Unknown HTML tag or closing \">\" missing, found " + c, 1);
		}

		return 1;
	}

	private int handleClosingHTML(char c) throws SyntaxError {
		if (c == '>') {
			if (tree.getElement() == null) {
				throw new SyntaxError("Unknown HTML tag: " + tree.getPath(), 2);
			}
			if (tree.getElement().single) {
				throw new SyntaxError("Single HTML tag used as closing Tag: " + tree.getElement().tag, 2);
			}
			k = new KnownHtmlElement(line, pos, false, tree.getElement().tag);
			return 3;
		}

		tree = tree.getBranch(c);
		if (tree == null) {
			state = -11;
			throw new SyntaxError("Unknown HTML tag or closing \">\" missing, found " + c, 1);
		}

		return 1;
	}

	private int handleOpeningTwine(char c) throws SyntaxError {
		if (c == ' ') {
			state = 3;
			if (tree.getElement() == null) {
				//assuming not listed means widget until better solution
				return 1;
			}
			if (!tree.getElement().single) {
				if (logicTags.contains(tree.getElement().tag)) {
					k = new KnownLogicElement(line, pos, tree.getElement().tag, false);
				} else {
					k = new KnownTwineElement(line, pos, true, tree.getElement().tag);
				}
				state = 4;
				return 1;
			}
			return 1;
		}
		if (c == '>') {
			state = -5;
			if (tree.getElement() == null) {
				//assuming not listed means widget until better solution
				return 1;
			}
			if (!tree.getElement().single) {
				if (logicTags.contains(tree.getElement().tag)) {
					k = new KnownLogicElement(line, pos, tree.getElement().tag, false);
				} else {
					k = new KnownTwineElement(line, pos, true, tree.getElement().tag);
				}
				state = 6;
				return 1;
			}
			return 2;
		}

		tree = tree.getBranch(c);
		if (tree == null) {
			//assuming not listed means widget until better solution
			state = 3;
		}

		return 1;
	}

	private int handleClosingTwine(char c) throws SyntaxError {
		if (c == '>') {
			if (tree.getElement() == null) {
				throw new SyntaxError("Unknown Twine tag: " + tree.getPath(), 2);
			}
			if (tree.getElement().single) {
				throw new SyntaxError("Single Twine tag used as closing Tag: " + tree.getElement().tag, 2);
			}
			if (logicTags.contains(tree.getElement().tag)) {
				k = new KnownLogicElement(line, pos, tree.getElement().tag, true);
			} else {
				k = new KnownTwineElement(line, pos, false, tree.getElement().tag);
			}
			state = -6;
			return 1;
		}

		tree = tree.getBranch(c);
		if (tree == null) {
			state = 3;
			throw new SyntaxError("Unknown Twine closing tag or closing \">>\" missing, found " + c, 1);
		}

		return 1;
	}

	@Override
	public String getShortDescription() {
		StringBuilder builder = new StringBuilder();
		builder.append(getPositionAsString()).append(" ");
		switch (state) {
			case 0:
				builder.append("<");
				break;
			//TWINE
			case 1:
				builder.append("<<");
				break;
			case -1:
				builder.append("<</");
				break;
			case 2:
				builder.append("<<").append(tree.getPath());
				break;
			case -2:
				builder.append("<</").append(tree.getPath());
				break;
			case 3:
				builder.append("<<???");
				break;
			case -3:
				builder.append("<<???>");
				break;
			case 4:
				builder.append("<<").append(tree.getPath()).append(" ???");
				break;
			case -4:
				builder.append("<<").append(tree.getPath()).append(" ???>");
				break;
			case 5:
				builder.append("<<???");
				break;
			case -5:
				builder.append("<<").append(tree == null ? "???" : tree.getPath()).append(">");
				break;
			case 6:
				builder.append("<<").append(tree.getPath()).append(" ???>");
				break;
			case -6:
				builder.append("<</").append(tree.getPath()).append(">");
				break;
			//HTML
			case -9:
				builder.append("</");
				break;
			case 10:
				builder.append("<").append(tree.getPath());
				break;
			case -10:
				builder.append("</").append(tree.getPath());
				break;
			case 11:
				builder.append("<?").append(tree == null ? "???" : tree.getPath());
				break;
			case -11:
				builder.append("</").append(tree == null ? "???" : tree.getPath());
				break;
			case 12:
				builder.append("<").append(tree.getPath()).append(" ???");
			default:
				throw new UnknownStateException(state);
		}
		return builder.toString();
	}
}


code:
package org.arkerthan.sanityCheck.element;

import org.arkerthan.sanityCheck.SyntaxError;
import org.arkerthan.sanityCheck.UnknownStateException;

/**
 * @author Arkerthan
 */
public class AtElement extends Element {
	private int state = 0;
	// 0 = @
	// 1 = @@
	// 2 = @@.
	// 3 = @@.a -- @@.ab -- @@.abc
	// 4 = @@.abc;abc
	// 5 = @@.abc;abc@

	// example: @@.red;some text@@

	public AtElement(int line, int pos) {
		super(line, pos);
	}

	@Override
	public int handleChar(char c) throws SyntaxError {
		switch (state) {
			case 0:
				state = 1;
				if (c == '@') {
					return 1;
				} else {
					if (c == '.') {
						state = 2;
					}
					throw new SyntaxError("Opening \"@\" missing.", 1);
				}
			case 1:
				if (c == '.') {
					state = 2;
					return 1;
				} else {
					state = 4;
					throw new SyntaxError("\".\" missing, found \"" + c + "\". This might also indicate a " +
							"missing closure in the previous color code.", 0, true);
				}
			case 2:
				state = 3;
				if (Character.isAlphabetic(c)) {
					return 1;
				} else {
					throw new SyntaxError("Identifier might be wrong.", 1, true);
				}
			case 3:
				if (c == ';') {
					state = 4;
					return 1;
				} else if (c == ' ') {
					state = 4;
					throw new SyntaxError("\";\" missing or wrong space.", 1);
				}
				break;
			case 4:
				if (c == '@') {
					state = 5;
					return 1;
				}
				break;
			case 5:
				if (c == '@') {
					return 2;
				} else {
					throw new SyntaxError("Closing \"@\" missing.", 2);
				}
			default:
				throw new UnknownStateException(state);
		}
		return 0;
	}

	@Override
	public String getShortDescription() {
		StringBuilder builder = new StringBuilder();
		builder.append(getPositionAsString()).append(" ");
		switch (state) {
			case 0:
				builder.append("@");
				break;
			case 1:
				builder.append("@@");
				break;
			case 2:
				builder.append("@@.");
				break;
			case 3:
				builder.append("@@.???");
				break;
			case 4:
				builder.append("@@???");
				break;
			case 5:
				builder.append("@@???@");
				break;
			default:
				throw new UnknownStateException(state);
		}
		return builder.toString();
	}
}


code:
package org.arkerthan.sanityCheck.element;

import org.arkerthan.sanityCheck.SyntaxError;

/**
 * @author Arkerthan
 */
public class BracketElement extends Element {
	//int state = 0;

	public BracketElement(int line, int pos) {
		super(line, pos);
	}

	@Override
	public int handleChar(char c) throws SyntaxError {
		if (c == ')') {
			return 2;
		} else {
			return 0;
		}
	}

	@Override
	public String getShortDescription() {
		return getPositionAsString() + " (???";
	}
}


code:
package org.arkerthan.sanityCheck.element;

import org.arkerthan.sanityCheck.SyntaxError;
import org.arkerthan.sanityCheck.UnknownStateException;

/**
 * @author Arkerthan
 */
public class CommentElement extends Element {
	private int state = 0;
	/*
	0 - /
	1 - /*???
	2 - /*???*
	3 - /%???
	4 - /%???%
	 */

	/**
	 * @param line line in which comment starts
	 * @param pos  position in line where comment starts
	 */
	public CommentElement(int line, int pos) {
		super(line, pos);
	}

	@Override
	public int handleChar(char c) throws SyntaxError {
		switch (state) {
			case 0:
				if (c == '*') {
					state = 1;
				} else if (c == '%') {
					state = 3;
				} else if (c == '>') {
					throw new SyntaxError("XHTML style closure", 4, true);
				} else {
					return 4;
				}
				break;
			case 1:
				if (c == '*') {
					state = 2;
				}
				break;
			case 2:
				if (c == '/') {
					return 2;
				} else if (c == '*') {
					return 1;
				}
				state = 1;
				break;
			case 3:
				if (c == '%') {
					state = 4;
				}
				break;
			case 4:
				if (c == '/') {
					return 2;
				}
				state = 3;
				break;
			default:
				throw new UnknownStateException(state);
		}
		return 1;
	}

	@Override
	public String getShortDescription() {
		return getPositionAsString() + "comment";
	}
}


code:
package org.arkerthan.sanityCheck.element;

import org.arkerthan.sanityCheck.SyntaxError;

/**
 * @author Arkerthan
 */
public abstract class Element {
	protected KnownElement k;
	protected int line, pos;

	/**
	 * @param line Line the instance was created
	 * @param pos  Position in line the instance was created
	 */
	protected Element(int line, int pos) {
		this.line = line;
		this.pos = pos;
	}

	/**
	 * Parses a Char and returns an int depending on the state of the element
	 * 0 - the Element did nothing
	 * 1 - the Element changed state
	 * 2 - the Element is finished
	 * 3 - the Element is finished and a KnownHtmlElement was generated
	 * 4 - the Element is finished and the char is still open for use
	 *
	 * @param c char to be parsed
	 * @return state change
	 * @throws SyntaxError thrown when an syntax error is detected
	 */
	public abstract int handleChar(char c) throws SyntaxError;

	/**
	 * @return the constructed KnownElement. null if none was constructed yet.
	 */
	public KnownElement getKnownElement() {
		return k;
	}

	/**
	 * Returns the line and position of the Element in the file it was created in.
	 *
	 * @return position of Element in file as String
	 */
	public String getPositionAsString() {
		return "[" + line + ":" + pos + "]";
	}

	/**
	 * @return a short description usually based on state and position of the Element
	 */
	public abstract String getShortDescription();
}


code:
package org.arkerthan.sanityCheck.element;

import org.arkerthan.sanityCheck.SyntaxError;

/**
 * @author Arkerthan
 */
public abstract class KnownElement extends Element {

	/**
	 * @param line at which it begins
	 * @param pos  at which it begins
	 */
	public KnownElement(int line, int pos) {
		super(line, pos);
	}

	/**
	 * @return true, if it needs another Known Element to close it.
	 */
	public abstract boolean isOpening();

	/**
	 * @return true if it closes another Element.
	 */
	public abstract boolean isClosing();

	/**
	 * @param k Element to be checked
	 * @return true if given Element closes Element
	 */
	public abstract boolean isMatchingElement(KnownElement k);

	@Override
	public int handleChar(char c) throws SyntaxError {
		return 0;
	}
}


code:
package org.arkerthan.sanityCheck.element;

/**
 * @author Arkerthan
 */
public class KnownHtmlElement extends KnownElement {

	private boolean opening;
	private String statement;

	/**
	 * @param line	  at which it begins
	 * @param pos	   at which it begins
	 * @param opening   if it opens a tag: <tag>  or closes it: </tag>
	 * @param statement statement inside the tag
	 */
	public KnownHtmlElement(int line, int pos, boolean opening, String statement) {
		super(line, pos);
		this.opening = opening;
		this.statement = statement;
	}

	@Override
	public String getShortDescription() {
		StringBuilder builder = new StringBuilder();
		builder.append(getPositionAsString()).append(" <");
		if (!opening) {
			builder.append("/");
		}
		return builder.append(statement).append(">").toString();
	}

	@Override
	public boolean isOpening() {
		return opening;
	}

	@Override
	public boolean isClosing() {
		return !opening;
	}

	@Override
	public boolean isMatchingElement(KnownElement k) {
		if (k instanceof KnownHtmlElement) {
			return ((KnownHtmlElement) k).statement.equals(this.statement);
		}
		return false;
	}
}


code:
package org.arkerthan.sanityCheck.element;

import org.arkerthan.sanityCheck.DisallowedTagException;
import org.arkerthan.sanityCheck.UnknownStateException;

import java.util.Arrays;
import java.util.List;

/**
 * @author Arkerthan
 */
public class KnownLogicElement extends KnownElement {
	private static final List<String> allowedTags = Arrays.asList("if", "elseif", "else");
	private final int state;
	private boolean last;
	/*
	0 - if
	1 - elseif
	2 - else
	3 - switch
	4 - case
	5 - default
	 */

	public KnownLogicElement(int line, int pos, String tag, boolean last) {
		this(line, pos, tag);
		this.last = last;
	}

	public KnownLogicElement(int line, int pos, String tag) {
		super(line, pos);
		switch (tag) {
			case "if":
				state = 0;
				break;
			case "elseif":
				state = 1;
				break;
			case "else":
				state = 2;
				break;
			case "switch":
				state = 3;
				break;
			case "case":
				state = 4;
				break;
			case "default":
				state = 5;
				break;
			default:
				throw new DisallowedTagException(tag);
		}
		last = false;
	}

	@Override
	public boolean isOpening() {
		return !last;
	}

	@Override
	public boolean isClosing() {
		return (state != 0 && state != 3) || last;
	}

	@Override
	public boolean isMatchingElement(KnownElement k) {
		if (!(k instanceof KnownLogicElement)) {
			return false;
		}
		KnownLogicElement l = (KnownLogicElement) k;
		switch (state) {
			case 0:
			case 1:
				return l.state == 1 || l.state == 2 || (l.state == 0 && l.last);
			case 2:
				return l.state == 0 && l.last;
			case 3:
			case 4:
				return l.state == 3 || l.state == 4;
			case 5:
				return l.state == 3 && l.last;
			default:
				throw new UnknownStateException(state);
		}
	}

	@Override
	public String getShortDescription() {
		StringBuilder builder = new StringBuilder();
		builder.append(getPositionAsString()).append(" <<");
		if (last) {
			builder.append('/');
		}
		switch (state) {
			case 0:
				builder.append("if");
				break;
			case 1:
				builder.append("elseif");
				break;
			case 2:
				builder.append("else");
				break;
			case 3:
				builder.append("switch");
				break;
			case 4:
				builder.append("case");
				break;
			case 5:
				builder.append("default");
				break;
			default:
				throw new UnknownStateException(state);
		}
		return builder.append(">>").toString();
	}
}


code:
package org.arkerthan.sanityCheck.element;

/**
 * @author Arkerthan
 */
public class KnownTwineElement extends KnownElement {

	private boolean opening;
	private String statement;

	/**
	 * @param line	  at which it begins
	 * @param pos	   at which it begins
	 * @param opening   if it opens a tag: <<tag>>  or closes it: <</tag>>
	 * @param statement statement inside the tag
	 */
	public KnownTwineElement(int line, int pos, boolean opening, String statement) {
		super(line, pos);
		this.opening = opening;
		this.statement = statement;
	}

	@Override
	public String getShortDescription() {
		StringBuilder builder = new StringBuilder();
		builder.append(getPositionAsString()).append(" <<");
		if (!opening) {
			builder.append("/");
		}
		return builder.append(statement).append(">>").toString();
	}

	@Override
	public boolean isOpening() {
		return opening;
	}

	@Override
	public boolean isClosing() {
		return !opening;
	}

	@Override
	public boolean isMatchingElement(KnownElement k) {
		if (k instanceof KnownTwineElement) {
			return ((KnownTwineElement) k).statement.equals(this.statement);
		}
		return false;
	}
}


code:
#Allowed twine tags
#Effectively everything that is allowed in a statements like this:
#<<tag>> or <<tag ???>>
#when ;1 is specified it expects a matching closing tag like this: <</tag>>
#Do not add HTML Tags here.
#Characters outside of ASCII scope are not supported.
#
#twine tags
capture;1
continue;0
for;1
foreach;1
goto;0
htag;1
include;0
link;1
nobr;1
options;1
print;0
replace;1
run;0
script;1
set;0
silently;1
span;1
textbox;0
timed;1
unset;0
widget;1
=;0
#
# Twine logic ### DO NOT TOUCH ###
if;1
elseif;1
else;1
switch;1
case;1
default;0

path: devTools/javaSanityCheck/twineTags


code:
@echo off
:: Generates devNotes/twineCSS.txt from all .css files in src/ subdir

:: See if we can find a git installation
setlocal enabledelayedexpansion

for %%k in (HKCU HKLM) do (
	for %%w in (\ \Wow6432Node\) do (
		for /f "skip=2 delims=: tokens=1*" %%a in ('reg query "%%k\SOFTWARE%%wMicrosoft\Windows\CurrentVersion\Uninstall\Git_is1" /v InstallLocation 2^> nul') do (
			for /f "tokens=3" %%z in ("%%a") do (
				set GIT=%%z:%%b
				set GITFOUND=yes
				goto FOUND
			)
		)
	)
)

:FOUND
if %GITFOUND% == yes (
	set "PATH=%GIT%bin;%PATH%"
	bash --login -c ./makeTwineCSSPassage.sh
)


code:
#!/bin/sh
# Generates devNotes/twine CSS.txt from all .css files in src/ subdir

# Joins all .css files from the current dir (recursive) into a Twee [script] passage
# arguments:
# $1: root repo dir
# $2: output file name
collectCSSForTwine() {
	local files=$(find . -iname '*.css' -print)
	files=$(echo "$files" | sort)
	echo "" > "$2"
	for f in $files; do
		echo -e "\n/* ${f} */\n" >> "$2"
		cat "$f" >> "$2"
	done
}

ROOT_REPO_DIR="$(git rev-parse --show-toplevel)"
cd "${ROOT_REPO_DIR}"/src
collectCSSForTwine "${ROOT_REPO_DIR}" "${ROOT_REPO_DIR}/devNotes/twine CSS.txt"


code:
@echo off
:: Generates devNotes/twineJS.txt from all .js files in src/ subdir

:: See if we can find a git installation
setlocal enabledelayedexpansion

for %%k in (HKCU HKLM) do (
	for %%w in (\ \Wow6432Node\) do (
		for /f "skip=2 delims=: tokens=1*" %%a in ('reg query "%%k\SOFTWARE%%wMicrosoft\Windows\CurrentVersion\Uninstall\Git_is1" /v InstallLocation 2^> nul') do (
			for /f "tokens=3" %%z in ("%%a") do (
				set GIT=%%z:%%b
				set GITFOUND=yes
				goto FOUND
			)
		)
	)
)

:FOUND
if %GITFOUND% == yes (
	set "PATH=%GIT%bin;%PATH%"
	bash --login -c ./makeTwineJSPassage.sh
)


code:
#!/bin/sh
# Generates devNotes/twine JS.txt from all .js files in src/ subdir

# Joins all .js files from the current dir (recursive) into a Twee [script] passage
# arguments:
# $1: root repo dir
# $2: output file name
collectJSForTwine() {
	local files=$(find . -path ./art/assistantArt.js -prune -o -name '*.js' -print)
	files=$(echo "$files" | sort)
	echo "" > "$2"
	for f in $files; do
		echo -e "\n/* ${f} */\n" >> "$2"
		sed -nf "$1"/devTools/stripComments.sed "$f" >> "$2"
	done
}

ROOT_REPO_DIR="$(git rev-parse --show-toplevel)"
cd "${ROOT_REPO_DIR}"/src
collectJSForTwine "${ROOT_REPO_DIR}" "${ROOT_REPO_DIR}/devNotes/twine JS.txt"