=b.SCRIPT.id?r.text():b.DISPLAY:"text"===e&&r.size===b.DISPLAY.size?r=b.TEXT:"script"===e?r=b.SCRIPT:"scriptscript"===e&&(r=b.SCRIPTSCRIPT),r},Qr=function(e,t){var r,n=Jr(e.size,t.style),a=n.fracNum(),i=n.fracDen();r=t.havingStyle(a);var o=bt(e.numer,r,t);if(e.continued){var s=8.5/t.fontMetrics().ptPerEm,l=3.5/t.fontMetrics().ptPerEm;o.height=o.height0?3*c:7*c,d=t.fontMetrics().denom1):(m>0?(u=t.fontMetrics().num2,p=c):(u=t.fontMetrics().num3,p=3*c),d=t.fontMetrics().denom2),h){var w=t.fontMetrics().axisHeight;u-o.depth-(w+.5*m)0&&(t="."===(t=e)?null:t),t};nt({type:"genfrac",names:["\\genfrac"],props:{numArgs:6,allowedInArgument:!0,argTypes:["math","math","size","text","math","math"]},handler:function(e,t){var r,n=e.parser,a=t[4],i=t[5],o=it(t[0]),s="atom"===o.type&&"open"===o.family?rn(o.text):null,l=it(t[1]),h="atom"===l.type&&"close"===l.family?rn(l.text):null,m=Ft(t[2],"size"),c=null;r=!!m.isBlank||(c=m.value).number>0;var u="auto",p=t[3];if("ordgroup"===p.type){if(p.body.length>0){var d=Ft(p.body[0],"textord");u=tn[Number(d.text)]}}else p=Ft(p,"textord"),u=tn[Number(p.text)];return{type:"genfrac",mode:n.mode,numer:a,denom:i,continued:!1,hasBarLine:r,barSize:c,leftDelim:s,rightDelim:h,size:u}},htmlBuilder:Qr,mathmlBuilder:en}),nt({type:"infix",names:["\\above"],props:{numArgs:1,argTypes:["size"],infix:!0},handler:function(e,t){var r=e.parser,n=(e.funcName,e.token);return{type:"infix",mode:r.mode,replaceWith:"\\\\abovefrac",size:Ft(t[0],"size").value,token:n}}}),nt({type:"genfrac",names:["\\\\abovefrac"],props:{numArgs:3,argTypes:["math","size","math"]},handler:function(e,t){var r=e.parser,n=(e.funcName,t[0]),a=function(e){if(!e)throw new Error("Expected non-null, but got "+String(e));return e}(Ft(t[1],"infix").size),i=t[2],o=a.number>0;return{type:"genfrac",mode:r.mode,numer:n,denom:i,continued:!1,hasBarLine:o,barSize:a,leftDelim:null,rightDelim:null,size:"auto"}},htmlBuilder:Qr,mathmlBuilder:en});var nn=function(e,t){var r,n,a=t.style;"supsub"===e.type?(r=e.sup?bt(e.sup,t.havingStyle(a.sup()),t):bt(e.sub,t.havingStyle(a.sub()),t),n=Ft(e.base,"horizBrace")):n=Ft(e,"horizBrace");var i,o=bt(n.base,t.havingBaseStyle(b.DISPLAY)),s=Pt(n,t);if(n.isOver?(i=je.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:o},{type:"kern",size:.1},{type:"elem",elem:s}]},t)).children[0].children[0].children[1].classes.push("svg-align"):(i=je.makeVList({positionType:"bottom",positionData:o.depth+.1+s.height,children:[{type:"elem",elem:s},{type:"kern",size:.1},{type:"elem",elem:o}]},t)).children[0].children[0].children[0].classes.push("svg-align"),r){var l=je.makeSpan(["mord",n.isOver?"mover":"munder"],[i],t);i=n.isOver?je.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:l},{type:"kern",size:.2},{type:"elem",elem:r}]},t):je.makeVList({positionType:"bottom",positionData:l.depth+.2+r.height+r.depth,children:[{type:"elem",elem:r},{type:"kern",size:.2},{type:"elem",elem:l}]},t)}return je.makeSpan(["mord",n.isOver?"mover":"munder"],[i],t)};nt({type:"horizBrace",names:["\\overbrace","\\underbrace"],props:{numArgs:1},handler:function(e,t){var r=e.parser,n=e.funcName;return{type:"horizBrace",mode:r.mode,label:n,isOver:/^\\over/.test(n),base:t[0]}},htmlBuilder:nn,mathmlBuilder:function(e,t){var r=Dt(e.label);return new Mt.MathNode(e.isOver?"mover":"munder",[Nt(e.base,t),r])}}),nt({type:"href",names:["\\href"],props:{numArgs:2,argTypes:["url","original"],allowedInText:!0},handler:function(e,t){var r=e.parser,n=t[1],a=Ft(t[0],"url").url;return r.settings.isTrusted({command:"\\href",url:a})?{type:"href",mode:r.mode,href:a,body:ot(n)}:r.formatUnsupportedCmd("\\href")},htmlBuilder:function(e,t){var r=ut(e.body,t,!1);return je.makeAnchor(e.href,[],r,t)},mathmlBuilder:function(e,t){var r=qt(e.body,t);return r instanceof kt||(r=new kt("mrow",[r])),r.setAttribute("href",e.href),r}}),nt({type:"href",names:["\\url"],props:{numArgs:1,argTypes:["url"],allowedInText:!0},handler:function(e,t){var r=e.parser,n=Ft(t[0],"url").url;if(!r.settings.isTrusted({command:"\\url",url:n}))return r.formatUnsupportedCmd("\\url");for(var a=[],i=0;i0&&(n=Le(e.totalheight,t)-r,n=Number(n.toFixed(2)));var a=0;e.width.number>0&&(a=Le(e.width,t));var i={height:r+n+"em"};a>0&&(i.width=a+"em"),n>0&&(i.verticalAlign=-n+"em");var o=new C(e.src,e.alt,i);return o.height=r,o.depth=n,o},mathmlBuilder:function(e,t){var r=new Mt.MathNode("mglyph",[]);r.setAttribute("alt",e.alt);var n=Le(e.height,t),a=0;if(e.totalheight.number>0&&(a=(a=Le(e.totalheight,t)-n).toFixed(2),r.setAttribute("valign","-"+a+"em")),r.setAttribute("height",n+a+"em"),e.width.number>0){var i=Le(e.width,t);r.setAttribute("width",i+"em")}return r.setAttribute("src",e.src),r}}),nt({type:"kern",names:["\\kern","\\mkern","\\hskip","\\mskip"],props:{numArgs:1,argTypes:["size"],primitive:!0,allowedInText:!0},handler:function(e,t){var r=e.parser,n=e.funcName,a=Ft(t[0],"size");if(r.settings.strict){var i="m"===n[1],o="mu"===a.value.unit;i?(o||r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+n+" supports only mu units, not "+a.value.unit+" units"),"math"!==r.mode&&r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+n+" works only in math mode")):o&&r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+n+" doesn't support mu units")}return{type:"kern",mode:r.mode,dimension:a.value}},htmlBuilder:function(e,t){return je.makeGlue(e.dimension,t)},mathmlBuilder:function(e,t){var r=Le(e.dimension,t);return new Mt.SpaceNode(r)}}),nt({type:"lap",names:["\\mathllap","\\mathrlap","\\mathclap"],props:{numArgs:1,allowedInText:!0},handler:function(e,t){var r=e.parser,n=e.funcName,a=t[0];return{type:"lap",mode:r.mode,alignment:n.slice(5),body:a}},htmlBuilder:function(e,t){var r;"clap"===e.alignment?(r=je.makeSpan([],[bt(e.body,t)]),r=je.makeSpan(["inner"],[r],t)):r=je.makeSpan(["inner"],[bt(e.body,t)]);var n=je.makeSpan(["fix"],[]),a=je.makeSpan([e.alignment],[r,n],t),i=je.makeSpan(["strut"]);return i.style.height=a.height+a.depth+"em",i.style.verticalAlign=-a.depth+"em",a.children.unshift(i),a=je.makeSpan(["thinbox"],[a],t),je.makeSpan(["mord","vbox"],[a],t)},mathmlBuilder:function(e,t){var r=new Mt.MathNode("mpadded",[Nt(e.body,t)]);if("rlap"!==e.alignment){var n="llap"===e.alignment?"-1":"-0.5";r.setAttribute("lspace",n+"width")}return r.setAttribute("width","0px"),r}}),nt({type:"styling",names:["\\(","$"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler:function(e,t){var r=e.funcName,n=e.parser,a=n.mode;n.switchMode("math");var i="\\("===r?"\\)":"$",o=n.parseExpression(!1,i);return n.expect(i),n.switchMode(a),{type:"styling",mode:n.mode,style:"text",body:o}}}),nt({type:"text",names:["\\)","\\]"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler:function(e,t){throw new n("Mismatched "+e.funcName)}});var on=function(e,t){switch(t.style.size){case b.DISPLAY.size:return e.display;case b.TEXT.size:return e.text;case b.SCRIPT.size:return e.script;case b.SCRIPTSCRIPT.size:return e.scriptscript;default:return e.text}};nt({type:"mathchoice",names:["\\mathchoice"],props:{numArgs:4,primitive:!0},handler:function(e,t){return{type:"mathchoice",mode:e.parser.mode,display:ot(t[0]),text:ot(t[1]),script:ot(t[2]),scriptscript:ot(t[3])}},htmlBuilder:function(e,t){var r=on(e,t),n=ut(r,t,!1);return je.makeFragment(n)},mathmlBuilder:function(e,t){var r=on(e,t);return qt(r,t)}});var sn=function(e,t,r,n,a,i,o){var s,l,h;if(e=je.makeSpan([],[e]),t){var m=bt(t,n.havingStyle(a.sup()),n);l={elem:m,kern:Math.max(n.fontMetrics().bigOpSpacing1,n.fontMetrics().bigOpSpacing3-m.depth)}}if(r){var c=bt(r,n.havingStyle(a.sub()),n);s={elem:c,kern:Math.max(n.fontMetrics().bigOpSpacing2,n.fontMetrics().bigOpSpacing4-c.height)}}if(l&&s){var u=n.fontMetrics().bigOpSpacing5+s.elem.height+s.elem.depth+s.kern+e.depth+o;h=je.makeVList({positionType:"bottom",positionData:u,children:[{type:"kern",size:n.fontMetrics().bigOpSpacing5},{type:"elem",elem:s.elem,marginLeft:-i+"em"},{type:"kern",size:s.kern},{type:"elem",elem:e},{type:"kern",size:l.kern},{type:"elem",elem:l.elem,marginLeft:i+"em"},{type:"kern",size:n.fontMetrics().bigOpSpacing5}]},n)}else if(s){var p=e.height-o;h=je.makeVList({positionType:"top",positionData:p,children:[{type:"kern",size:n.fontMetrics().bigOpSpacing5},{type:"elem",elem:s.elem,marginLeft:-i+"em"},{type:"kern",size:s.kern},{type:"elem",elem:e}]},n)}else{if(!l)return e;var d=e.depth+o;h=je.makeVList({positionType:"bottom",positionData:d,children:[{type:"elem",elem:e},{type:"kern",size:l.kern},{type:"elem",elem:l.elem,marginLeft:i+"em"},{type:"kern",size:n.fontMetrics().bigOpSpacing5}]},n)}return je.makeSpan(["mop","op-limits"],[h],n)},ln=["\\smallint"],hn=function(e,t){var r,n,a,i=!1;"supsub"===e.type?(r=e.sup,n=e.sub,a=Ft(e.base,"op"),i=!0):a=Ft(e,"op");var o,s=t.style,h=!1;if(s.size===b.DISPLAY.size&&a.symbol&&!l.contains(ln,a.name)&&(h=!0),a.symbol){var m=h?"Size2-Regular":"Size1-Regular",c="";if("\\oiint"!==a.name&&"\\oiiint"!==a.name||(c=a.name.substr(1),a.name="oiint"===c?"\\iint":"\\iiint"),o=je.makeSymbol(a.name,m,"math",t,["mop","op-symbol",h?"large-op":"small-op"]),c.length>0){var u=o.italic,p=je.staticSvg(c+"Size"+(h?"2":"1"),t);o=je.makeVList({positionType:"individualShift",children:[{type:"elem",elem:o,shift:0},{type:"elem",elem:p,shift:h?.08:0}]},t),a.name="\\"+c,o.classes.unshift("mop"),o.italic=u}}else if(a.body){var d=ut(a.body,t,!0);1===d.length&&d[0]instanceof O?(o=d[0]).classes[0]="mop":o=je.makeSpan(["mop"],d,t)}else{for(var f=[],g=1;g0){for(var s=a.body.map((function(e){var t=e.text;return"string"==typeof t?{type:"textord",mode:e.mode,text:t}:e})),l=ut(s,t.withFont("mathrm"),!0),h=0;h=0?s.setAttribute("height","+"+a+"em"):(s.setAttribute("height",a+"em"),s.setAttribute("depth","+"+-a+"em")),s.setAttribute("voffset",a+"em"),s}});var fn=["\\tiny","\\sixptsize","\\scriptsize","\\footnotesize","\\small","\\normalsize","\\large","\\Large","\\LARGE","\\huge","\\Huge"];nt({type:"sizing",names:fn,props:{numArgs:0,allowedInText:!0},handler:function(e,t){var r=e.breakOnTokenText,n=e.funcName,a=e.parser,i=a.parseExpression(!1,r);return{type:"sizing",mode:a.mode,size:fn.indexOf(n)+1,body:i}},htmlBuilder:function(e,t){var r=t.havingSize(e.size);return dn(e.body,r,t)},mathmlBuilder:function(e,t){var r=t.havingSize(e.size),n=Bt(e.body,r),a=new Mt.MathNode("mstyle",n);return a.setAttribute("mathsize",r.sizeMultiplier+"em"),a}}),nt({type:"smash",names:["\\smash"],props:{numArgs:1,numOptionalArgs:1,allowedInText:!0},handler:function(e,t,r){var n=e.parser,a=!1,i=!1,o=r[0]&&Ft(r[0],"ordgroup");if(o)for(var s="",l=0;lr.height+r.depth+i&&(i=(i+c-r.height-r.depth)/2);var u=l.height-r.height-i-h;r.style.paddingLeft=m+"em";var p=je.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:r,wrapperClasses:["svg-align"]},{type:"kern",size:-(r.height+u)},{type:"elem",elem:l},{type:"kern",size:h}]},t);if(e.index){var d=t.havingStyle(b.SCRIPTSCRIPT),f=bt(e.index,d,t),g=.6*(p.height-p.depth),v=je.makeVList({positionType:"shift",positionData:-g,children:[{type:"elem",elem:f}]},t),y=je.makeSpan(["root"],[v]);return je.makeSpan(["mord","sqrt"],[y,p],t)}return je.makeSpan(["mord","sqrt"],[p],t)},mathmlBuilder:function(e,t){var r=e.body,n=e.index;return n?new Mt.MathNode("mroot",[Nt(r,t),Nt(n,t)]):new Mt.MathNode("msqrt",[Nt(r,t)])}});var gn={display:b.DISPLAY,text:b.TEXT,script:b.SCRIPT,scriptscript:b.SCRIPTSCRIPT};nt({type:"styling",names:["\\displaystyle","\\textstyle","\\scriptstyle","\\scriptscriptstyle"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler:function(e,t){var r=e.breakOnTokenText,n=e.funcName,a=e.parser,i=a.parseExpression(!0,r),o=n.slice(1,n.length-5);return{type:"styling",mode:a.mode,style:o,body:i}},htmlBuilder:function(e,t){var r=gn[e.style],n=t.havingStyle(r).withFont("");return dn(e.body,n,t)},mathmlBuilder:function(e,t){var r=gn[e.style],n=t.havingStyle(r),a=Bt(e.body,n),i=new Mt.MathNode("mstyle",a),o={display:["0","true"],text:["0","false"],script:["1","false"],scriptscript:["2","false"]}[e.style];return i.setAttribute("scriptlevel",o[0]),i.setAttribute("displaystyle",o[1]),i}});var vn=function(e,t){var r=e.base;return r?"op"===r.type?r.limits&&(t.style.size===b.DISPLAY.size||r.alwaysHandleSupSub)?hn:null:"operatorname"===r.type?r.alwaysHandleSupSub&&(t.style.size===b.DISPLAY.size||r.limits)?pn:null:"accent"===r.type?l.isCharacterBox(r.base)?Ut:null:"horizBrace"===r.type&&!e.sub===r.isOver?nn:null:null};at({type:"supsub",htmlBuilder:function(e,t){var r=vn(e,t);if(r)return r(e,t);var n,a,i,o=e.base,s=e.sup,h=e.sub,m=bt(o,t),c=t.fontMetrics(),u=0,p=0,d=o&&l.isCharacterBox(o);if(s){var f=t.havingStyle(t.style.sup());n=bt(s,f,t),d||(u=m.height-f.fontMetrics().supDrop*f.sizeMultiplier/t.sizeMultiplier)}if(h){var g=t.havingStyle(t.style.sub());a=bt(h,g,t),d||(p=m.depth+g.fontMetrics().subDrop*g.sizeMultiplier/t.sizeMultiplier)}i=t.style===b.DISPLAY?c.sup1:t.style.cramped?c.sup3:c.sup2;var v,y=t.sizeMultiplier,x=.5/c.ptPerEm/y+"em",w=null;if(a){var k=e.base&&"op"===e.base.type&&e.base.name&&("\\oiint"===e.base.name||"\\oiiint"===e.base.name);(m instanceof O||k)&&(w=-m.italic+"em")}if(n&&a){u=Math.max(u,i,n.depth+.25*c.xHeight),p=Math.max(p,c.sub2);var S=4*c.defaultRuleThickness;if(u-n.depth-(a.height-p)0&&(u+=M,p-=M)}var z=[{type:"elem",elem:a,shift:p,marginRight:x,marginLeft:w},{type:"elem",elem:n,shift:-u,marginRight:x}];v=je.makeVList({positionType:"individualShift",children:z},t)}else if(a){p=Math.max(p,c.sub1,a.height-.8*c.xHeight);var A=[{type:"elem",elem:a,marginLeft:w,marginRight:x}];v=je.makeVList({positionType:"shift",positionData:p,children:A},t)}else{if(!n)throw new Error("supsub must have either sup or sub.");u=Math.max(u,i,n.depth+.25*c.xHeight),v=je.makeVList({positionType:"shift",positionData:-u,children:[{type:"elem",elem:n,marginRight:x}]},t)}var T=gt(m,"right")||"mord";return je.makeSpan([T],[m,je.makeSpan(["msupsub"],[v])],t)},mathmlBuilder:function(e,t){var r,n=!1;e.base&&"horizBrace"===e.base.type&&!!e.sup===e.base.isOver&&(n=!0,r=e.base.isOver),!e.base||"op"!==e.base.type&&"operatorname"!==e.base.type||(e.base.parentIsSupSub=!0);var a,i=[Nt(e.base,t)];if(e.sub&&i.push(Nt(e.sub,t)),e.sup&&i.push(Nt(e.sup,t)),n)a=r?"mover":"munder";else if(e.sub)if(e.sup){var o=e.base;a=o&&"op"===o.type&&o.limits&&t.style===b.DISPLAY||o&&"operatorname"===o.type&&o.alwaysHandleSupSub&&(t.style===b.DISPLAY||o.limits)?"munderover":"msubsup"}else{var s=e.base;a=s&&"op"===s.type&&s.limits&&(t.style===b.DISPLAY||s.alwaysHandleSupSub)||s&&"operatorname"===s.type&&s.alwaysHandleSupSub&&(s.limits||t.style===b.DISPLAY)?"munder":"msub"}else{var l=e.base;a=l&&"op"===l.type&&l.limits&&(t.style===b.DISPLAY||l.alwaysHandleSupSub)||l&&"operatorname"===l.type&&l.alwaysHandleSupSub&&(l.limits||t.style===b.DISPLAY)?"mover":"msup"}return new Mt.MathNode(a,i)}}),at({type:"atom",htmlBuilder:function(e,t){return je.mathsym(e.text,e.mode,t,["m"+e.family])},mathmlBuilder:function(e,t){var r=new Mt.MathNode("mo",[zt(e.text,e.mode)]);if("bin"===e.family){var n=Tt(e,t);"bold-italic"===n&&r.setAttribute("mathvariant",n)}else"punct"===e.family?r.setAttribute("separator","true"):"open"!==e.family&&"close"!==e.family||r.setAttribute("stretchy","false");return r}});var bn={mi:"italic",mn:"normal",mtext:"normal"};at({type:"mathord",htmlBuilder:function(e,t){return je.makeOrd(e,t,"mathord")},mathmlBuilder:function(e,t){var r=new Mt.MathNode("mi",[zt(e.text,e.mode,t)]),n=Tt(e,t)||"italic";return n!==bn[r.type]&&r.setAttribute("mathvariant",n),r}}),at({type:"textord",htmlBuilder:function(e,t){return je.makeOrd(e,t,"textord")},mathmlBuilder:function(e,t){var r,n=zt(e.text,e.mode,t),a=Tt(e,t)||"normal";return r="text"===e.mode?new Mt.MathNode("mtext",[n]):/[0-9]/.test(e.text)?new Mt.MathNode("mn",[n]):"\\prime"===e.text?new Mt.MathNode("mo",[n]):new Mt.MathNode("mi",[n]),a!==bn[r.type]&&r.setAttribute("mathvariant",a),r}});var yn={"\\nobreak":"nobreak","\\allowbreak":"allowbreak"},xn={" ":{},"\\ ":{},"~":{className:"nobreak"},"\\space":{},"\\nobreakspace":{className:"nobreak"}};at({type:"spacing",htmlBuilder:function(e,t){if(xn.hasOwnProperty(e.text)){var r=xn[e.text].className||"";if("text"===e.mode){var a=je.makeOrd(e,t,"textord");return a.classes.push(r),a}return je.makeSpan(["mspace",r],[je.mathsym(e.text,e.mode,t)],t)}if(yn.hasOwnProperty(e.text))return je.makeSpan(["mspace",yn[e.text]],[],t);throw new n('Unknown type of space "'+e.text+'"')},mathmlBuilder:function(e,t){if(!xn.hasOwnProperty(e.text)){if(yn.hasOwnProperty(e.text))return new Mt.MathNode("mspace");throw new n('Unknown type of space "'+e.text+'"')}return new Mt.MathNode("mtext",[new Mt.TextNode("\xa0")])}});var wn=function(){var e=new Mt.MathNode("mtd",[]);return e.setAttribute("width","50%"),e};at({type:"tag",mathmlBuilder:function(e,t){var r=new Mt.MathNode("mtable",[new Mt.MathNode("mtr",[wn(),new Mt.MathNode("mtd",[qt(e.body,t)]),wn(),new Mt.MathNode("mtd",[qt(e.tag,t)])])]);return r.setAttribute("width","100%"),r}});var kn={"\\text":void 0,"\\textrm":"textrm","\\textsf":"textsf","\\texttt":"texttt","\\textnormal":"textrm"},Sn={"\\textbf":"textbf","\\textmd":"textmd"},Mn={"\\textit":"textit","\\textup":"textup"},zn=function(e,t){var r=e.font;return r?kn[r]?t.withTextFontFamily(kn[r]):Sn[r]?t.withTextFontWeight(Sn[r]):t.withTextFontShape(Mn[r]):t};nt({type:"text",names:["\\text","\\textrm","\\textsf","\\texttt","\\textnormal","\\textbf","\\textmd","\\textit","\\textup"],props:{numArgs:1,argTypes:["text"],allowedInArgument:!0,allowedInText:!0},handler:function(e,t){var r=e.parser,n=e.funcName,a=t[0];return{type:"text",mode:r.mode,body:ot(a),font:n}},htmlBuilder:function(e,t){var r=zn(e,t),n=ut(e.body,r,!0);return je.makeSpan(["mord","text"],n,r)},mathmlBuilder:function(e,t){var r=zn(e,t);return qt(e.body,r)}}),nt({type:"underline",names:["\\underline"],props:{numArgs:1,allowedInText:!0},handler:function(e,t){return{type:"underline",mode:e.parser.mode,body:t[0]}},htmlBuilder:function(e,t){var r=bt(e.body,t),n=je.makeLineSpan("underline-line",t),a=t.fontMetrics().defaultRuleThickness,i=je.makeVList({positionType:"top",positionData:r.height,children:[{type:"kern",size:a},{type:"elem",elem:n},{type:"kern",size:3*a},{type:"elem",elem:r}]},t);return je.makeSpan(["mord","underline"],[i],t)},mathmlBuilder:function(e,t){var r=new Mt.MathNode("mo",[new Mt.TextNode("\u203e")]);r.setAttribute("stretchy","true");var n=new Mt.MathNode("munder",[Nt(e.body,t),r]);return n.setAttribute("accentunder","true"),n}}),nt({type:"vcenter",names:["\\vcenter"],props:{numArgs:1,argTypes:["original"],allowedInText:!1},handler:function(e,t){return{type:"vcenter",mode:e.parser.mode,body:t[0]}},htmlBuilder:function(e,t){var r=bt(e.body,t),n=t.fontMetrics().axisHeight,a=.5*(r.height-n-(r.depth+n));return je.makeVList({positionType:"shift",positionData:a,children:[{type:"elem",elem:r}]},t)},mathmlBuilder:function(e,t){return new Mt.MathNode("mpadded",[Nt(e.body,t)],["vcenter"])}}),nt({type:"verb",names:["\\verb"],props:{numArgs:0,allowedInText:!0},handler:function(e,t,r){throw new n("\\verb ended by end of line instead of matching delimiter")},htmlBuilder:function(e,t){for(var r=An(e),n=[],a=t.havingStyle(t.style.text()),i=0;i0&&(this.undefStack[this.undefStack.length-1][e]=t)}else{var a=this.undefStack[this.undefStack.length-1];a&&!a.hasOwnProperty(e)&&(a[e]=this.current[e])}this.current[e]=t},e}(),Rn={},En=Rn;function Hn(e,t){Rn[e]=t}Hn("\\noexpand",(function(e){var t=e.popToken();return e.isExpandable(t.text)&&(t.noexpand=!0,t.treatAsRelax=!0),{tokens:[t],numArgs:0}})),Hn("\\expandafter",(function(e){var t=e.popToken();return e.expandOnce(!0),{tokens:[t],numArgs:0}})),Hn("\\@firstoftwo",(function(e){return{tokens:e.consumeArgs(2)[0],numArgs:0}})),Hn("\\@secondoftwo",(function(e){return{tokens:e.consumeArgs(2)[1],numArgs:0}})),Hn("\\@ifnextchar",(function(e){var t=e.consumeArgs(3);e.consumeSpaces();var r=e.future();return 1===t[0].length&&t[0][0].text===r.text?{tokens:t[1],numArgs:0}:{tokens:t[2],numArgs:0}})),Hn("\\@ifstar","\\@ifnextchar *{\\@firstoftwo{#1}}"),Hn("\\TextOrMath",(function(e){var t=e.consumeArgs(2);return"text"===e.mode?{tokens:t[0],numArgs:0}:{tokens:t[1],numArgs:0}}));var Ln={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,a:10,A:10,b:11,B:11,c:12,C:12,d:13,D:13,e:14,E:14,f:15,F:15};Hn("\\char",(function(e){var t,r=e.popToken(),a="";if("'"===r.text)t=8,r=e.popToken();else if('"'===r.text)t=16,r=e.popToken();else if("`"===r.text)if("\\"===(r=e.popToken()).text[0])a=r.text.charCodeAt(1);else{if("EOF"===r.text)throw new n("\\char` missing argument");a=r.text.charCodeAt(0)}else t=10;if(t){if(null==(a=Ln[r.text])||a>=t)throw new n("Invalid base-"+t+" digit "+r.text);for(var i;null!=(i=Ln[e.future().text])&&i":"\\dotsb","-":"\\dotsb","*":"\\dotsb",":":"\\dotsb","\\DOTSB":"\\dotsb","\\coprod":"\\dotsb","\\bigvee":"\\dotsb","\\bigwedge":"\\dotsb","\\biguplus":"\\dotsb","\\bigcap":"\\dotsb","\\bigcup":"\\dotsb","\\prod":"\\dotsb","\\sum":"\\dotsb","\\bigotimes":"\\dotsb","\\bigoplus":"\\dotsb","\\bigodot":"\\dotsb","\\bigsqcup":"\\dotsb","\\And":"\\dotsb","\\longrightarrow":"\\dotsb","\\Longrightarrow":"\\dotsb","\\longleftarrow":"\\dotsb","\\Longleftarrow":"\\dotsb","\\longleftrightarrow":"\\dotsb","\\Longleftrightarrow":"\\dotsb","\\mapsto":"\\dotsb","\\longmapsto":"\\dotsb","\\hookrightarrow":"\\dotsb","\\doteq":"\\dotsb","\\mathbin":"\\dotsb","\\mathrel":"\\dotsb","\\relbar":"\\dotsb","\\Relbar":"\\dotsb","\\xrightarrow":"\\dotsb","\\xleftarrow":"\\dotsb","\\DOTSI":"\\dotsi","\\int":"\\dotsi","\\oint":"\\dotsi","\\iint":"\\dotsi","\\iiint":"\\dotsi","\\iiiint":"\\dotsi","\\idotsint":"\\dotsi","\\DOTSX":"\\dotsx"};Hn("\\dots",(function(e){var t="\\dotso",r=e.expandAfterFuture().text;return r in Pn?t=Pn[r]:("\\not"===r.substr(0,4)||r in X.math&&l.contains(["bin","rel"],X.math[r].group))&&(t="\\dotsb"),t}));var Fn={")":!0,"]":!0,"\\rbrack":!0,"\\}":!0,"\\rbrace":!0,"\\rangle":!0,"\\rceil":!0,"\\rfloor":!0,"\\rgroup":!0,"\\rmoustache":!0,"\\right":!0,"\\bigr":!0,"\\biggr":!0,"\\Bigr":!0,"\\Biggr":!0,$:!0,";":!0,".":!0,",":!0};Hn("\\dotso",(function(e){return e.future().text in Fn?"\\ldots\\,":"\\ldots"})),Hn("\\dotsc",(function(e){var t=e.future().text;return t in Fn&&","!==t?"\\ldots\\,":"\\ldots"})),Hn("\\cdots",(function(e){return e.future().text in Fn?"\\@cdots\\,":"\\@cdots"})),Hn("\\dotsb","\\cdots"),Hn("\\dotsm","\\cdots"),Hn("\\dotsi","\\!\\cdots"),Hn("\\dotsx","\\ldots\\,"),Hn("\\DOTSI","\\relax"),Hn("\\DOTSB","\\relax"),Hn("\\DOTSX","\\relax"),Hn("\\tmspace","\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax"),Hn("\\,","\\tmspace+{3mu}{.1667em}"),Hn("\\thinspace","\\,"),Hn("\\>","\\mskip{4mu}"),Hn("\\:","\\tmspace+{4mu}{.2222em}"),Hn("\\medspace","\\:"),Hn("\\;","\\tmspace+{5mu}{.2777em}"),Hn("\\thickspace","\\;"),Hn("\\!","\\tmspace-{3mu}{.1667em}"),Hn("\\negthinspace","\\!"),Hn("\\negmedspace","\\tmspace-{4mu}{.2222em}"),Hn("\\negthickspace","\\tmspace-{5mu}{.277em}"),Hn("\\enspace","\\kern.5em "),Hn("\\enskip","\\hskip.5em\\relax"),Hn("\\quad","\\hskip1em\\relax"),Hn("\\qquad","\\hskip2em\\relax"),Hn("\\tag","\\@ifstar\\tag@literal\\tag@paren"),Hn("\\tag@paren","\\tag@literal{({#1})}"),Hn("\\tag@literal",(function(e){if(e.macros.get("\\df@tag"))throw new n("Multiple \\tag");return"\\gdef\\df@tag{\\text{#1}}"})),Hn("\\bmod","\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}\\mathbin{\\rm mod}\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}"),Hn("\\pod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)"),Hn("\\pmod","\\pod{{\\rm mod}\\mkern6mu#1}"),Hn("\\mod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}{\\rm mod}\\,\\,#1"),Hn("\\pmb","\\html@mathml{\\@binrel{#1}{\\mathrlap{#1}\\kern0.5px#1}}{\\mathbf{#1}}"),Hn("\\newline","\\\\\\relax"),Hn("\\TeX","\\textrm{\\html@mathml{T\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125emX}{TeX}}");var Vn=D["Main-Regular"]["T".charCodeAt(0)][1]-.7*D["Main-Regular"]["A".charCodeAt(0)][1]+"em";Hn("\\LaTeX","\\textrm{\\html@mathml{L\\kern-.36em\\raisebox{"+Vn+"}{\\scriptstyle A}\\kern-.15em\\TeX}{LaTeX}}"),Hn("\\KaTeX","\\textrm{\\html@mathml{K\\kern-.17em\\raisebox{"+Vn+"}{\\scriptstyle A}\\kern-.15em\\TeX}{KaTeX}}"),Hn("\\hspace","\\@ifstar\\@hspacer\\@hspace"),Hn("\\@hspace","\\hskip #1\\relax"),Hn("\\@hspacer","\\rule{0pt}{0pt}\\hskip #1\\relax"),Hn("\\ordinarycolon",":"),Hn("\\vcentcolon","\\mathrel{\\mathop\\ordinarycolon}"),Hn("\\dblcolon",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-.9mu}\\vcentcolon}}{\\mathop{\\char"2237}}'),Hn("\\coloneqq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2254}}'),Hn("\\Coloneqq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2237\\char"3d}}'),Hn("\\coloneq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"3a\\char"2212}}'),Hn("\\Coloneq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"2237\\char"2212}}'),Hn("\\eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2255}}'),Hn("\\Eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"3d\\char"2237}}'),Hn("\\eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2239}}'),Hn("\\Eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"2212\\char"2237}}'),Hn("\\colonapprox",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"3a\\char"2248}}'),Hn("\\Colonapprox",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"2237\\char"2248}}'),Hn("\\colonsim",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"3a\\char"223c}}'),Hn("\\Colonsim",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"2237\\char"223c}}'),Hn("\u2237","\\dblcolon"),Hn("\u2239","\\eqcolon"),Hn("\u2254","\\coloneqq"),Hn("\u2255","\\eqqcolon"),Hn("\u2a74","\\Coloneqq"),Hn("\\ratio","\\vcentcolon"),Hn("\\coloncolon","\\dblcolon"),Hn("\\colonequals","\\coloneqq"),Hn("\\coloncolonequals","\\Coloneqq"),Hn("\\equalscolon","\\eqqcolon"),Hn("\\equalscoloncolon","\\Eqqcolon"),Hn("\\colonminus","\\coloneq"),Hn("\\coloncolonminus","\\Coloneq"),Hn("\\minuscolon","\\eqcolon"),Hn("\\minuscoloncolon","\\Eqcolon"),Hn("\\coloncolonapprox","\\Colonapprox"),Hn("\\coloncolonsim","\\Colonsim"),Hn("\\simcolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),Hn("\\simcoloncolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\dblcolon}"),Hn("\\approxcolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),Hn("\\approxcoloncolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\dblcolon}"),Hn("\\notni","\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u220c}}"),Hn("\\limsup","\\DOTSB\\operatorname*{lim\\,sup}"),Hn("\\liminf","\\DOTSB\\operatorname*{lim\\,inf}"),Hn("\\injlim","\\DOTSB\\operatorname*{inj\\,lim}"),Hn("\\projlim","\\DOTSB\\operatorname*{proj\\,lim}"),Hn("\\varlimsup","\\DOTSB\\operatorname*{\\overline{lim}}"),Hn("\\varliminf","\\DOTSB\\operatorname*{\\underline{lim}}"),Hn("\\varinjlim","\\DOTSB\\operatorname*{\\underrightarrow{lim}}"),Hn("\\varprojlim","\\DOTSB\\operatorname*{\\underleftarrow{lim}}"),Hn("\\gvertneqq","\\html@mathml{\\@gvertneqq}{\u2269}"),Hn("\\lvertneqq","\\html@mathml{\\@lvertneqq}{\u2268}"),Hn("\\ngeqq","\\html@mathml{\\@ngeqq}{\u2271}"),Hn("\\ngeqslant","\\html@mathml{\\@ngeqslant}{\u2271}"),Hn("\\nleqq","\\html@mathml{\\@nleqq}{\u2270}"),Hn("\\nleqslant","\\html@mathml{\\@nleqslant}{\u2270}"),Hn("\\nshortmid","\\html@mathml{\\@nshortmid}{\u2224}"),Hn("\\nshortparallel","\\html@mathml{\\@nshortparallel}{\u2226}"),Hn("\\nsubseteqq","\\html@mathml{\\@nsubseteqq}{\u2288}"),Hn("\\nsupseteqq","\\html@mathml{\\@nsupseteqq}{\u2289}"),Hn("\\varsubsetneq","\\html@mathml{\\@varsubsetneq}{\u228a}"),Hn("\\varsubsetneqq","\\html@mathml{\\@varsubsetneqq}{\u2acb}"),Hn("\\varsupsetneq","\\html@mathml{\\@varsupsetneq}{\u228b}"),Hn("\\varsupsetneqq","\\html@mathml{\\@varsupsetneqq}{\u2acc}"),Hn("\\imath","\\html@mathml{\\@imath}{\u0131}"),Hn("\\jmath","\\html@mathml{\\@jmath}{\u0237}"),Hn("\\llbracket","\\html@mathml{\\mathopen{[\\mkern-3.2mu[}}{\\mathopen{\\char`\u27e6}}"),Hn("\\rrbracket","\\html@mathml{\\mathclose{]\\mkern-3.2mu]}}{\\mathclose{\\char`\u27e7}}"),Hn("\u27e6","\\llbracket"),Hn("\u27e7","\\rrbracket"),Hn("\\lBrace","\\html@mathml{\\mathopen{\\{\\mkern-3.2mu[}}{\\mathopen{\\char`\u2983}}"),Hn("\\rBrace","\\html@mathml{\\mathclose{]\\mkern-3.2mu\\}}}{\\mathclose{\\char`\u2984}}"),Hn("\u2983","\\lBrace"),Hn("\u2984","\\rBrace"),Hn("\\minuso","\\mathbin{\\html@mathml{{\\mathrlap{\\mathchoice{\\kern{0.145em}}{\\kern{0.145em}}{\\kern{0.1015em}}{\\kern{0.0725em}}\\circ}{-}}}{\\char`\u29b5}}"),Hn("\u29b5","\\minuso"),Hn("\\darr","\\downarrow"),Hn("\\dArr","\\Downarrow"),Hn("\\Darr","\\Downarrow"),Hn("\\lang","\\langle"),Hn("\\rang","\\rangle"),Hn("\\uarr","\\uparrow"),Hn("\\uArr","\\Uparrow"),Hn("\\Uarr","\\Uparrow"),Hn("\\N","\\mathbb{N}"),Hn("\\R","\\mathbb{R}"),Hn("\\Z","\\mathbb{Z}"),Hn("\\alef","\\aleph"),Hn("\\alefsym","\\aleph"),Hn("\\Alpha","\\mathrm{A}"),Hn("\\Beta","\\mathrm{B}"),Hn("\\bull","\\bullet"),Hn("\\Chi","\\mathrm{X}"),Hn("\\clubs","\\clubsuit"),Hn("\\cnums","\\mathbb{C}"),Hn("\\Complex","\\mathbb{C}"),Hn("\\Dagger","\\ddagger"),Hn("\\diamonds","\\diamondsuit"),Hn("\\empty","\\emptyset"),Hn("\\Epsilon","\\mathrm{E}"),Hn("\\Eta","\\mathrm{H}"),Hn("\\exist","\\exists"),Hn("\\harr","\\leftrightarrow"),Hn("\\hArr","\\Leftrightarrow"),Hn("\\Harr","\\Leftrightarrow"),Hn("\\hearts","\\heartsuit"),Hn("\\image","\\Im"),Hn("\\infin","\\infty"),Hn("\\Iota","\\mathrm{I}"),Hn("\\isin","\\in"),Hn("\\Kappa","\\mathrm{K}"),Hn("\\larr","\\leftarrow"),Hn("\\lArr","\\Leftarrow"),Hn("\\Larr","\\Leftarrow"),Hn("\\lrarr","\\leftrightarrow"),Hn("\\lrArr","\\Leftrightarrow"),Hn("\\Lrarr","\\Leftrightarrow"),Hn("\\Mu","\\mathrm{M}"),Hn("\\natnums","\\mathbb{N}"),Hn("\\Nu","\\mathrm{N}"),Hn("\\Omicron","\\mathrm{O}"),Hn("\\plusmn","\\pm"),Hn("\\rarr","\\rightarrow"),Hn("\\rArr","\\Rightarrow"),Hn("\\Rarr","\\Rightarrow"),Hn("\\real","\\Re"),Hn("\\reals","\\mathbb{R}"),Hn("\\Reals","\\mathbb{R}"),Hn("\\Rho","\\mathrm{P}"),Hn("\\sdot","\\cdot"),Hn("\\sect","\\S"),Hn("\\spades","\\spadesuit"),Hn("\\sub","\\subset"),Hn("\\sube","\\subseteq"),Hn("\\supe","\\supseteq"),Hn("\\Tau","\\mathrm{T}"),Hn("\\thetasym","\\vartheta"),Hn("\\weierp","\\wp"),Hn("\\Zeta","\\mathrm{Z}"),Hn("\\argmin","\\DOTSB\\operatorname*{arg\\,min}"),Hn("\\argmax","\\DOTSB\\operatorname*{arg\\,max}"),Hn("\\plim","\\DOTSB\\mathop{\\operatorname{plim}}\\limits"),Hn("\\bra","\\mathinner{\\langle{#1}|}"),Hn("\\ket","\\mathinner{|{#1}\\rangle}"),Hn("\\braket","\\mathinner{\\langle{#1}\\rangle}"),Hn("\\Bra","\\left\\langle#1\\right|"),Hn("\\Ket","\\left|#1\\right\\rangle"),Hn("\\angln","{\\angl n}"),Hn("\\blue","\\textcolor{##6495ed}{#1}"),Hn("\\orange","\\textcolor{##ffa500}{#1}"),Hn("\\pink","\\textcolor{##ff00af}{#1}"),Hn("\\red","\\textcolor{##df0030}{#1}"),Hn("\\green","\\textcolor{##28ae7b}{#1}"),Hn("\\gray","\\textcolor{gray}{#1}"),Hn("\\purple","\\textcolor{##9d38bd}{#1}"),Hn("\\blueA","\\textcolor{##ccfaff}{#1}"),Hn("\\blueB","\\textcolor{##80f6ff}{#1}"),Hn("\\blueC","\\textcolor{##63d9ea}{#1}"),Hn("\\blueD","\\textcolor{##11accd}{#1}"),Hn("\\blueE","\\textcolor{##0c7f99}{#1}"),Hn("\\tealA","\\textcolor{##94fff5}{#1}"),Hn("\\tealB","\\textcolor{##26edd5}{#1}"),Hn("\\tealC","\\textcolor{##01d1c1}{#1}"),Hn("\\tealD","\\textcolor{##01a995}{#1}"),Hn("\\tealE","\\textcolor{##208170}{#1}"),Hn("\\greenA","\\textcolor{##b6ffb0}{#1}"),Hn("\\greenB","\\textcolor{##8af281}{#1}"),Hn("\\greenC","\\textcolor{##74cf70}{#1}"),Hn("\\greenD","\\textcolor{##1fab54}{#1}"),Hn("\\greenE","\\textcolor{##0d923f}{#1}"),Hn("\\goldA","\\textcolor{##ffd0a9}{#1}"),Hn("\\goldB","\\textcolor{##ffbb71}{#1}"),Hn("\\goldC","\\textcolor{##ff9c39}{#1}"),Hn("\\goldD","\\textcolor{##e07d10}{#1}"),Hn("\\goldE","\\textcolor{##a75a05}{#1}"),Hn("\\redA","\\textcolor{##fca9a9}{#1}"),Hn("\\redB","\\textcolor{##ff8482}{#1}"),Hn("\\redC","\\textcolor{##f9685d}{#1}"),Hn("\\redD","\\textcolor{##e84d39}{#1}"),Hn("\\redE","\\textcolor{##bc2612}{#1}"),Hn("\\maroonA","\\textcolor{##ffbde0}{#1}"),Hn("\\maroonB","\\textcolor{##ff92c6}{#1}"),Hn("\\maroonC","\\textcolor{##ed5fa6}{#1}"),Hn("\\maroonD","\\textcolor{##ca337c}{#1}"),Hn("\\maroonE","\\textcolor{##9e034e}{#1}"),Hn("\\purpleA","\\textcolor{##ddd7ff}{#1}"),Hn("\\purpleB","\\textcolor{##c6b9fc}{#1}"),Hn("\\purpleC","\\textcolor{##aa87ff}{#1}"),Hn("\\purpleD","\\textcolor{##7854ab}{#1}"),Hn("\\purpleE","\\textcolor{##543b78}{#1}"),Hn("\\mintA","\\textcolor{##f5f9e8}{#1}"),Hn("\\mintB","\\textcolor{##edf2df}{#1}"),Hn("\\mintC","\\textcolor{##e0e5cc}{#1}"),Hn("\\grayA","\\textcolor{##f6f7f7}{#1}"),Hn("\\grayB","\\textcolor{##f0f1f2}{#1}"),Hn("\\grayC","\\textcolor{##e3e5e6}{#1}"),Hn("\\grayD","\\textcolor{##d6d8da}{#1}"),Hn("\\grayE","\\textcolor{##babec2}{#1}"),Hn("\\grayF","\\textcolor{##888d93}{#1}"),Hn("\\grayG","\\textcolor{##626569}{#1}"),Hn("\\grayH","\\textcolor{##3b3e40}{#1}"),Hn("\\grayI","\\textcolor{##21242c}{#1}"),Hn("\\kaBlue","\\textcolor{##314453}{#1}"),Hn("\\kaGreen","\\textcolor{##71B307}{#1}");var Gn={"\\relax":!0,"^":!0,_:!0,"\\limits":!0,"\\nolimits":!0},Un=function(){function e(e,t,r){this.settings=void 0,this.expansionCount=void 0,this.lexer=void 0,this.macros=void 0,this.stack=void 0,this.mode=void 0,this.settings=t,this.expansionCount=0,this.feed(e),this.macros=new On(En,t.macros),this.mode=r,this.stack=[]}var t=e.prototype;return t.feed=function(e){this.lexer=new In(e,this.settings)},t.switchMode=function(e){this.mode=e},t.beginGroup=function(){this.macros.beginGroup()},t.endGroup=function(){this.macros.endGroup()},t.future=function(){return 0===this.stack.length&&this.pushToken(this.lexer.lex()),this.stack[this.stack.length-1]},t.popToken=function(){return this.future(),this.stack.pop()},t.pushToken=function(e){this.stack.push(e)},t.pushTokens=function(e){var t;(t=this.stack).push.apply(t,e)},t.scanArgument=function(e){var t,r,n;if(e){if(this.consumeSpaces(),"["!==this.future().text)return null;t=this.popToken();var a=this.consumeArg(["]"]);n=a.tokens,r=a.end}else{var i=this.consumeArg();n=i.tokens,t=i.start,r=i.end}return this.pushToken(new qn("EOF",r.loc)),this.pushTokens(n),t.range(r,"")},t.consumeSpaces=function(){for(;;){if(" "!==this.future().text)break;this.stack.pop()}},t.consumeArg=function(e){var t=[],r=e&&e.length>0;r||this.consumeSpaces();var a,i=this.future(),o=0,s=0;do{if(a=this.popToken(),t.push(a),"{"===a.text)++o;else if("}"===a.text){if(-1===--o)throw new n("Extra }",a)}else if("EOF"===a.text)throw new n("Unexpected end of input in a macro argument, expected '"+(e&&r?e[s]:"}")+"'",a);if(e&&r)if((0===o||1===o&&"{"===e[s])&&a.text===e[s]){if(++s===e.length){t.splice(-s,s);break}}else s=0}while(0!==o||r);return"{"===i.text&&"}"===t[t.length-1].text&&(t.pop(),t.shift()),t.reverse(),{tokens:t,start:i,end:a}},t.consumeArgs=function(e,t){if(t){if(t.length!==e+1)throw new n("The length of delimiters doesn't match the number of args!");for(var r=t[0],a=0;athis.settings.maxExpand)throw new n("Too many expansions: infinite loop or need to increase maxExpand setting");var i=a.tokens,o=this.consumeArgs(a.numArgs,a.delimiters);if(a.numArgs)for(var s=(i=i.slice()).length-1;s>=0;--s){var l=i[s];if("#"===l.text){if(0===s)throw new n("Incomplete placeholder at end of macro body",l);if("#"===(l=i[--s]).text)i.splice(s+1,1);else{if(!/^[1-9]$/.test(l.text))throw new n("Not a valid argument number",l);var h;(h=i).splice.apply(h,[s,2].concat(o[+l.text-1]))}}}return this.pushTokens(i),i},t.expandAfterFuture=function(){return this.expandOnce(),this.future()},t.expandNextToken=function(){for(;;){var e=this.expandOnce();if(e instanceof qn){if("\\relax"!==e.text&&!e.treatAsRelax)return this.stack.pop();this.stack.pop()}}throw new Error},t.expandMacro=function(e){return this.macros.has(e)?this.expandTokens([new qn(e)]):void 0},t.expandTokens=function(e){var t=[],r=this.stack.length;for(this.pushTokens(e);this.stack.length>r;){var n=this.expandOnce(!0);n instanceof qn&&(n.treatAsRelax&&(n.noexpand=!1,n.treatAsRelax=!1),t.push(this.stack.pop()))}return t},t.expandMacroAsText=function(e){var t=this.expandMacro(e);return t?t.map((function(e){return e.text})).join(""):t},t._getExpansion=function(e){var t=this.macros.get(e);if(null==t)return t;var r="function"==typeof t?t(this):t;if("string"==typeof r){var n=0;if(-1!==r.indexOf("#"))for(var a=r.replace(/##/g,"");-1!==a.indexOf("#"+(n+1));)++n;for(var i=new In(r,this.settings),o=[],s=i.lex();"EOF"!==s.text;)o.push(s),s=i.lex();return o.reverse(),{tokens:o,numArgs:n}}return r},t.isDefined=function(e){return this.macros.has(e)||Tn.hasOwnProperty(e)||X.math.hasOwnProperty(e)||X.text.hasOwnProperty(e)||Gn.hasOwnProperty(e)},t.isExpandable=function(e){var t=this.macros.get(e);return null!=t?"string"==typeof t||"function"==typeof t||!t.unexpandable:Tn.hasOwnProperty(e)&&!Tn[e].primitive},e}(),Yn={"\u0301":{text:"\\'",math:"\\acute"},"\u0300":{text:"\\`",math:"\\grave"},"\u0308":{text:'\\"',math:"\\ddot"},"\u0303":{text:"\\~",math:"\\tilde"},"\u0304":{text:"\\=",math:"\\bar"},"\u0306":{text:"\\u",math:"\\breve"},"\u030c":{text:"\\v",math:"\\check"},"\u0302":{text:"\\^",math:"\\hat"},"\u0307":{text:"\\.",math:"\\dot"},"\u030a":{text:"\\r",math:"\\mathring"},"\u030b":{text:"\\H"}},Wn={"\xe1":"a\u0301","\xe0":"a\u0300","\xe4":"a\u0308","\u01df":"a\u0308\u0304","\xe3":"a\u0303","\u0101":"a\u0304","\u0103":"a\u0306","\u1eaf":"a\u0306\u0301","\u1eb1":"a\u0306\u0300","\u1eb5":"a\u0306\u0303","\u01ce":"a\u030c","\xe2":"a\u0302","\u1ea5":"a\u0302\u0301","\u1ea7":"a\u0302\u0300","\u1eab":"a\u0302\u0303","\u0227":"a\u0307","\u01e1":"a\u0307\u0304","\xe5":"a\u030a","\u01fb":"a\u030a\u0301","\u1e03":"b\u0307","\u0107":"c\u0301","\u010d":"c\u030c","\u0109":"c\u0302","\u010b":"c\u0307","\u010f":"d\u030c","\u1e0b":"d\u0307","\xe9":"e\u0301","\xe8":"e\u0300","\xeb":"e\u0308","\u1ebd":"e\u0303","\u0113":"e\u0304","\u1e17":"e\u0304\u0301","\u1e15":"e\u0304\u0300","\u0115":"e\u0306","\u011b":"e\u030c","\xea":"e\u0302","\u1ebf":"e\u0302\u0301","\u1ec1":"e\u0302\u0300","\u1ec5":"e\u0302\u0303","\u0117":"e\u0307","\u1e1f":"f\u0307","\u01f5":"g\u0301","\u1e21":"g\u0304","\u011f":"g\u0306","\u01e7":"g\u030c","\u011d":"g\u0302","\u0121":"g\u0307","\u1e27":"h\u0308","\u021f":"h\u030c","\u0125":"h\u0302","\u1e23":"h\u0307","\xed":"i\u0301","\xec":"i\u0300","\xef":"i\u0308","\u1e2f":"i\u0308\u0301","\u0129":"i\u0303","\u012b":"i\u0304","\u012d":"i\u0306","\u01d0":"i\u030c","\xee":"i\u0302","\u01f0":"j\u030c","\u0135":"j\u0302","\u1e31":"k\u0301","\u01e9":"k\u030c","\u013a":"l\u0301","\u013e":"l\u030c","\u1e3f":"m\u0301","\u1e41":"m\u0307","\u0144":"n\u0301","\u01f9":"n\u0300","\xf1":"n\u0303","\u0148":"n\u030c","\u1e45":"n\u0307","\xf3":"o\u0301","\xf2":"o\u0300","\xf6":"o\u0308","\u022b":"o\u0308\u0304","\xf5":"o\u0303","\u1e4d":"o\u0303\u0301","\u1e4f":"o\u0303\u0308","\u022d":"o\u0303\u0304","\u014d":"o\u0304","\u1e53":"o\u0304\u0301","\u1e51":"o\u0304\u0300","\u014f":"o\u0306","\u01d2":"o\u030c","\xf4":"o\u0302","\u1ed1":"o\u0302\u0301","\u1ed3":"o\u0302\u0300","\u1ed7":"o\u0302\u0303","\u022f":"o\u0307","\u0231":"o\u0307\u0304","\u0151":"o\u030b","\u1e55":"p\u0301","\u1e57":"p\u0307","\u0155":"r\u0301","\u0159":"r\u030c","\u1e59":"r\u0307","\u015b":"s\u0301","\u1e65":"s\u0301\u0307","\u0161":"s\u030c","\u1e67":"s\u030c\u0307","\u015d":"s\u0302","\u1e61":"s\u0307","\u1e97":"t\u0308","\u0165":"t\u030c","\u1e6b":"t\u0307","\xfa":"u\u0301","\xf9":"u\u0300","\xfc":"u\u0308","\u01d8":"u\u0308\u0301","\u01dc":"u\u0308\u0300","\u01d6":"u\u0308\u0304","\u01da":"u\u0308\u030c","\u0169":"u\u0303","\u1e79":"u\u0303\u0301","\u016b":"u\u0304","\u1e7b":"u\u0304\u0308","\u016d":"u\u0306","\u01d4":"u\u030c","\xfb":"u\u0302","\u016f":"u\u030a","\u0171":"u\u030b","\u1e7d":"v\u0303","\u1e83":"w\u0301","\u1e81":"w\u0300","\u1e85":"w\u0308","\u0175":"w\u0302","\u1e87":"w\u0307","\u1e98":"w\u030a","\u1e8d":"x\u0308","\u1e8b":"x\u0307","\xfd":"y\u0301","\u1ef3":"y\u0300","\xff":"y\u0308","\u1ef9":"y\u0303","\u0233":"y\u0304","\u0177":"y\u0302","\u1e8f":"y\u0307","\u1e99":"y\u030a","\u017a":"z\u0301","\u017e":"z\u030c","\u1e91":"z\u0302","\u017c":"z\u0307","\xc1":"A\u0301","\xc0":"A\u0300","\xc4":"A\u0308","\u01de":"A\u0308\u0304","\xc3":"A\u0303","\u0100":"A\u0304","\u0102":"A\u0306","\u1eae":"A\u0306\u0301","\u1eb0":"A\u0306\u0300","\u1eb4":"A\u0306\u0303","\u01cd":"A\u030c","\xc2":"A\u0302","\u1ea4":"A\u0302\u0301","\u1ea6":"A\u0302\u0300","\u1eaa":"A\u0302\u0303","\u0226":"A\u0307","\u01e0":"A\u0307\u0304","\xc5":"A\u030a","\u01fa":"A\u030a\u0301","\u1e02":"B\u0307","\u0106":"C\u0301","\u010c":"C\u030c","\u0108":"C\u0302","\u010a":"C\u0307","\u010e":"D\u030c","\u1e0a":"D\u0307","\xc9":"E\u0301","\xc8":"E\u0300","\xcb":"E\u0308","\u1ebc":"E\u0303","\u0112":"E\u0304","\u1e16":"E\u0304\u0301","\u1e14":"E\u0304\u0300","\u0114":"E\u0306","\u011a":"E\u030c","\xca":"E\u0302","\u1ebe":"E\u0302\u0301","\u1ec0":"E\u0302\u0300","\u1ec4":"E\u0302\u0303","\u0116":"E\u0307","\u1e1e":"F\u0307","\u01f4":"G\u0301","\u1e20":"G\u0304","\u011e":"G\u0306","\u01e6":"G\u030c","\u011c":"G\u0302","\u0120":"G\u0307","\u1e26":"H\u0308","\u021e":"H\u030c","\u0124":"H\u0302","\u1e22":"H\u0307","\xcd":"I\u0301","\xcc":"I\u0300","\xcf":"I\u0308","\u1e2e":"I\u0308\u0301","\u0128":"I\u0303","\u012a":"I\u0304","\u012c":"I\u0306","\u01cf":"I\u030c","\xce":"I\u0302","\u0130":"I\u0307","\u0134":"J\u0302","\u1e30":"K\u0301","\u01e8":"K\u030c","\u0139":"L\u0301","\u013d":"L\u030c","\u1e3e":"M\u0301","\u1e40":"M\u0307","\u0143":"N\u0301","\u01f8":"N\u0300","\xd1":"N\u0303","\u0147":"N\u030c","\u1e44":"N\u0307","\xd3":"O\u0301","\xd2":"O\u0300","\xd6":"O\u0308","\u022a":"O\u0308\u0304","\xd5":"O\u0303","\u1e4c":"O\u0303\u0301","\u1e4e":"O\u0303\u0308","\u022c":"O\u0303\u0304","\u014c":"O\u0304","\u1e52":"O\u0304\u0301","\u1e50":"O\u0304\u0300","\u014e":"O\u0306","\u01d1":"O\u030c","\xd4":"O\u0302","\u1ed0":"O\u0302\u0301","\u1ed2":"O\u0302\u0300","\u1ed6":"O\u0302\u0303","\u022e":"O\u0307","\u0230":"O\u0307\u0304","\u0150":"O\u030b","\u1e54":"P\u0301","\u1e56":"P\u0307","\u0154":"R\u0301","\u0158":"R\u030c","\u1e58":"R\u0307","\u015a":"S\u0301","\u1e64":"S\u0301\u0307","\u0160":"S\u030c","\u1e66":"S\u030c\u0307","\u015c":"S\u0302","\u1e60":"S\u0307","\u0164":"T\u030c","\u1e6a":"T\u0307","\xda":"U\u0301","\xd9":"U\u0300","\xdc":"U\u0308","\u01d7":"U\u0308\u0301","\u01db":"U\u0308\u0300","\u01d5":"U\u0308\u0304","\u01d9":"U\u0308\u030c","\u0168":"U\u0303","\u1e78":"U\u0303\u0301","\u016a":"U\u0304","\u1e7a":"U\u0304\u0308","\u016c":"U\u0306","\u01d3":"U\u030c","\xdb":"U\u0302","\u016e":"U\u030a","\u0170":"U\u030b","\u1e7c":"V\u0303","\u1e82":"W\u0301","\u1e80":"W\u0300","\u1e84":"W\u0308","\u0174":"W\u0302","\u1e86":"W\u0307","\u1e8c":"X\u0308","\u1e8a":"X\u0307","\xdd":"Y\u0301","\u1ef2":"Y\u0300","\u0178":"Y\u0308","\u1ef8":"Y\u0303","\u0232":"Y\u0304","\u0176":"Y\u0302","\u1e8e":"Y\u0307","\u0179":"Z\u0301","\u017d":"Z\u030c","\u1e90":"Z\u0302","\u017b":"Z\u0307","\u03ac":"\u03b1\u0301","\u1f70":"\u03b1\u0300","\u1fb1":"\u03b1\u0304","\u1fb0":"\u03b1\u0306","\u03ad":"\u03b5\u0301","\u1f72":"\u03b5\u0300","\u03ae":"\u03b7\u0301","\u1f74":"\u03b7\u0300","\u03af":"\u03b9\u0301","\u1f76":"\u03b9\u0300","\u03ca":"\u03b9\u0308","\u0390":"\u03b9\u0308\u0301","\u1fd2":"\u03b9\u0308\u0300","\u1fd1":"\u03b9\u0304","\u1fd0":"\u03b9\u0306","\u03cc":"\u03bf\u0301","\u1f78":"\u03bf\u0300","\u03cd":"\u03c5\u0301","\u1f7a":"\u03c5\u0300","\u03cb":"\u03c5\u0308","\u03b0":"\u03c5\u0308\u0301","\u1fe2":"\u03c5\u0308\u0300","\u1fe1":"\u03c5\u0304","\u1fe0":"\u03c5\u0306","\u03ce":"\u03c9\u0301","\u1f7c":"\u03c9\u0300","\u038e":"\u03a5\u0301","\u1fea":"\u03a5\u0300","\u03ab":"\u03a5\u0308","\u1fe9":"\u03a5\u0304","\u1fe8":"\u03a5\u0306","\u038f":"\u03a9\u0301","\u1ffa":"\u03a9\u0300"},Xn=function(){function e(e,t){this.mode=void 0,this.gullet=void 0,this.settings=void 0,this.leftrightDepth=void 0,this.nextToken=void 0,this.mode="math",this.gullet=new Un(e,t,this.mode),this.settings=t,this.leftrightDepth=0}var t=e.prototype;return t.expect=function(e,t){if(void 0===t&&(t=!0),this.fetch().text!==e)throw new n("Expected '"+e+"', got '"+this.fetch().text+"'",this.fetch());t&&this.consume()},t.consume=function(){this.nextToken=null},t.fetch=function(){return null==this.nextToken&&(this.nextToken=this.gullet.expandNextToken()),this.nextToken},t.switchMode=function(e){this.mode=e,this.gullet.switchMode(e)},t.parse=function(){this.settings.globalGroup||this.gullet.beginGroup(),this.settings.colorIsTextColor&&this.gullet.macros.set("\\color","\\textcolor");var e=this.parseExpression(!1);return this.expect("EOF"),this.settings.globalGroup||this.gullet.endGroup(),e},t.parseExpression=function(t,r){for(var n=[];;){"math"===this.mode&&this.consumeSpaces();var a=this.fetch();if(-1!==e.endOfExpression.indexOf(a.text))break;if(r&&a.text===r)break;if(t&&Tn[a.text]&&Tn[a.text].infix)break;var i=this.parseAtom(r);if(!i)break;"internal"!==i.type&&n.push(i)}return"text"===this.mode&&this.formLigatures(n),this.handleInfixNodes(n)},t.handleInfixNodes=function(e){for(var t,r=-1,a=0;a=0&&this.settings.reportNonstrict("unicodeTextInMathMode",'Latin-1/Unicode text character "'+t[0]+'" used in math mode',e);var s,l=X[this.mode][t].group,h=Bn.range(e);if(U.hasOwnProperty(l)){var m=l;s={type:"atom",mode:this.mode,family:m,loc:h,text:t}}else s={type:l,mode:this.mode,loc:h,text:t};i=s}else{if(!(t.charCodeAt(0)>=128))return null;this.settings.strict&&(w(t.charCodeAt(0))?"math"===this.mode&&this.settings.reportNonstrict("unicodeTextInMathMode",'Unicode text character "'+t[0]+'" used in math mode',e):this.settings.reportNonstrict("unknownSymbol",'Unrecognized Unicode character "'+t[0]+'" ('+t.charCodeAt(0)+")",e)),i={type:"textord",mode:"text",loc:Bn.range(e),text:t}}if(this.consume(),o)for(var c=0;c 0
+ || $(event.target).filter(".fold-unfold").length > 0;
+ if (trigger) {
+ $(">*:not(h2)", this).toggle(400);
+ $(">h2>span.fold-unfold", this).toggleClass("glyphicon-collapse-down glyphicon-collapse-up");
+ event.stopPropagation();
+ }
+});
+$(".solution").each(function() {
+ $(">*:not(h2)", this).toggle();
+ var h2 = $("h2:first", this);
+ h2.append("");
+});
+
+
+// Handle searches.
+// Relies on document having 'meta' element with name 'search-domain'.
+function google_search() {
+ var query = document.getElementById("google-search").value;
+ var domain = $("meta[name=search-domain]").attr("value");
+ window.open("https://www.google.com/search?q=" + query + "+site:" + domain);
+}
+
+// function to shrink the life cycle bar when scrolling
+$(function(){
+ $('#life-cycle').data('size','big');
+});
+
+$(window).scroll(function(){
+ if($(document).scrollTop() > 0)
+ {
+ if($('#life-cycle').data('size') == 'big')
+ {
+ $('#life-cycle').data('size','small');
+ $('#life-cycle').stop().animate({
+ padding: '5px'
+ },100);
+ }
+ }
+ else
+ {
+ if($('#life-cycle').data('size') == 'small')
+ {
+ $('#life-cycle').data('size','big');
+ $('#life-cycle').stop().animate({
+ padding: '15px'
+ },100);
+ }
+ }
+});
diff --git a/bin/boilerplate/AUTHORS b/bin/boilerplate/AUTHORS
new file mode 100644
index 0000000..04e1f5a
--- /dev/null
+++ b/bin/boilerplate/AUTHORS
@@ -0,0 +1 @@
+FIXME: list authors' names and email addresses.
\ No newline at end of file
diff --git a/bin/boilerplate/CITATION b/bin/boilerplate/CITATION
new file mode 100644
index 0000000..56ece3c
--- /dev/null
+++ b/bin/boilerplate/CITATION
@@ -0,0 +1 @@
+FIXME: describe how to cite this lesson.
\ No newline at end of file
diff --git a/bin/boilerplate/CONTRIBUTING.md b/bin/boilerplate/CONTRIBUTING.md
new file mode 100644
index 0000000..8c095d8
--- /dev/null
+++ b/bin/boilerplate/CONTRIBUTING.md
@@ -0,0 +1,151 @@
+# Contributing
+
+[The Carpentries][c-site] ([Software Carpentry][swc-site], [Data Carpentry][dc-site], and [Library Carpentry][lc-site]) are open source projects,
+and we welcome contributions of all kinds:
+new lessons,
+fixes to existing material,
+bug reports,
+and reviews of proposed changes are all welcome.
+
+## Contributor Agreement
+
+By contributing,
+you agree that we may redistribute your work under [our license](LICENSE.md).
+In exchange,
+we will address your issues and/or assess your change proposal as promptly as we can,
+and help you become a member of our community.
+Everyone involved in [The Carpentries][c-site]
+agrees to abide by our [code of conduct](CODE_OF_CONDUCT.md).
+
+## How to Contribute
+
+The easiest way to get started is to file an issue
+to tell us about a spelling mistake,
+some awkward wording,
+or a factual error.
+This is a good way to introduce yourself
+and to meet some of our community members.
+
+1. If you do not have a [GitHub][github] account,
+ you can [send us comments by email][email].
+ However,
+ we will be able to respond more quickly if you use one of the other methods described below.
+
+2. If you have a [GitHub][github] account,
+ or are willing to [create one][github-join],
+ but do not know how to use Git,
+ you can report problems or suggest improvements by [creating an issue][issues].
+ This allows us to assign the item to someone
+ and to respond to it in a threaded discussion.
+
+3. If you are comfortable with Git,
+ and would like to add or change material,
+ you can submit a pull request (PR).
+ Instructions for doing this are [included below](#using-github).
+
+## Where to Contribute
+
+1. If you wish to change this lesson,
+ please work in ,
+ which can be viewed at .
+
+2. If you wish to change the example lesson,
+ please work in ,
+ which documents the format of our lessons
+ and can be viewed at .
+
+3. If you wish to change the template used for workshop websites,
+ please work in .
+ The home page of that repository explains how to set up workshop websites,
+ while the extra pages in
+ provide more background on our design choices.
+
+4. If you wish to change CSS style files, tools,
+ or HTML boilerplate for lessons or workshops stored in `_includes` or `_layouts`,
+ please work in .
+
+## What to Contribute
+
+There are many ways to contribute,
+from writing new exercises and improving existing ones
+to updating or filling in the documentation
+and submitting [bug reports][issues]
+about things that do not work, are not clear, or are missing.
+If you are looking for ideas, please see the 'Issues' tab for
+a list of issues associated with this repository,
+or you may also look at the issues for [Data Carpentry][dc-issues],
+[Software Carpentry][swc-issues], and [Library Carpentry][lc-issues] projects.
+
+Comments on issues and reviews of pull requests are just as welcome:
+we are smarter together than we are on our own.
+Reviews from novices and newcomers are particularly valuable:
+it is easy for people who have been using these lessons for a while
+to forget how impenetrable some of this material can be,
+so fresh eyes are always welcome.
+
+## What *Not* to Contribute
+
+Our lessons already contain more material than we can cover in a typical workshop,
+so we are usually *not* looking for more concepts or tools to add to them.
+As a rule,
+if you want to introduce a new idea,
+you must (a) estimate how long it will take to teach
+and (b) explain what you would take out to make room for it.
+The first encourages contributors to be honest about requirements;
+the second, to think hard about priorities.
+
+We are also not looking for exercises or other material that will only run on one platform.
+Our workshops typically contain a mixture of Windows, macOS, and Linux users;
+in order to be usable,
+our lessons must run equally well on all three.
+
+## Using GitHub
+
+If you choose to contribute via GitHub, you may want to look at
+[How to Contribute to an Open Source Project on GitHub][how-contribute].
+To manage changes, we follow [GitHub flow][github-flow].
+Each lesson has at least two maintainers who review issues and pull requests or encourage others to do so.
+The maintainers are community volunteers and have final say over what gets merged into the lesson.
+To use the web interface for contributing to a lesson:
+
+1. Fork the originating repository to your GitHub profile.
+2. Within your version of the forked repository, move to the `gh-pages` branch and
+create a new branch for each significant change being made.
+3. Navigate to the file(s) you wish to change within the new branches and make revisions as required.
+4. Commit all changed files within the appropriate branches.
+5. Create individual pull requests from each of your changed branches
+to the `gh-pages` branch within the originating repository.
+6. If you receive feedback, make changes using your issue-specific branches of the forked
+repository and the pull requests will update automatically.
+7. Repeat as needed until all feedback has been addressed.
+
+When starting work, please make sure your clone of the originating `gh-pages` branch is up-to-date
+before creating your own revision-specific branch(es) from there.
+Additionally, please only work from your newly-created branch(es) and *not*
+your clone of the originating `gh-pages` branch.
+Lastly, published copies of all the lessons are available in the `gh-pages` branch of the originating
+repository for reference while revising.
+
+## Other Resources
+
+General discussion of [Software Carpentry][swc-site], [Data Carpentry][dc-site], and [Library Carpentry][lc-site]
+happens on the [discussion mailing list][discuss-list],
+which everyone is welcome to join.
+You can also [reach us by email][email].
+
+[email]: mailto:team@carpentries.org
+[dc-issues]: https://github.com/issues?q=user%3Adatacarpentry
+[dc-lessons]: http://datacarpentry.org/lessons/
+[dc-site]: http://datacarpentry.org/
+[discuss-list]: https://carpentries.topicbox.com/groups/discuss
+[github]: https://github.com
+[github-flow]: https://guides.github.com/introduction/flow/
+[github-join]: https://github.com/join
+[how-contribute]: https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github
+[issues]: https://guides.github.com/features/issues/
+[swc-issues]: https://github.com/issues?q=user%3Aswcarpentry
+[swc-lessons]: https://software-carpentry.org/lessons/
+[swc-site]: https://software-carpentry.org/
+[c-site]: https://carpentries.org/
+[lc-site]: https://librarycarpentry.org/
+[lc-issues]: https://github.com/issues?q=user%3Alibrarycarpentry
diff --git a/bin/boilerplate/README.md b/bin/boilerplate/README.md
new file mode 100644
index 0000000..060994a
--- /dev/null
+++ b/bin/boilerplate/README.md
@@ -0,0 +1,40 @@
+# FIXME Lesson title
+
+[](https://swc-slack-invite.herokuapp.com/)
+
+This repository generates the corresponding lesson website from [The Carpentries](https://carpentries.org/) repertoire of lessons.
+
+## Contributing
+
+We welcome all contributions to improve the lesson! Maintainers will do their best to help you if you have any
+questions, concerns, or experience any difficulties along the way.
+
+We'd like to ask you to familiarize yourself with our [Contribution Guide](CONTRIBUTING.md) and have a look at
+the [more detailed guidelines][lesson-example] on proper formatting, ways to render the lesson locally, and even
+how to write new episodes.
+
+Please see the current list of [issues][FIXME] for ideas for contributing to this
+repository. For making your contribution, we use the GitHub flow, which is
+nicely explained in the chapter [Contributing to a Project](http://git-scm.com/book/en/v2/GitHub-Contributing-to-a-Project) in Pro Git
+by Scott Chacon.
+Look for the tag . This indicates that the maintainers will welcome a pull request fixing this issue.
+
+
+## Maintainer(s)
+
+Current maintainers of this lesson are
+
+* FIXME
+* FIXME
+* FIXME
+
+
+## Authors
+
+A list of contributors to the lesson can be found in [AUTHORS](AUTHORS)
+
+## Citation
+
+To cite this lesson, please consult with [CITATION](CITATION)
+
+[lesson-example]: https://carpentries.github.io/lesson-example
diff --git a/bin/boilerplate/_config.yml b/bin/boilerplate/_config.yml
new file mode 100644
index 0000000..f2ca373
--- /dev/null
+++ b/bin/boilerplate/_config.yml
@@ -0,0 +1,120 @@
+#------------------------------------------------------------
+# Values for this lesson.
+#------------------------------------------------------------
+
+# Which carpentry is this ("swc", "dc", "lc", or "cp")?
+# swc: Software Carpentry
+# dc: Data Carpentry
+# lc: Library Carpentry
+# cp: Carpentries (to use for instructor traning for instance)
+# incubator: Carpentries Incubator
+carpentry: "swc"
+
+# Overall title for pages.
+title: "Lesson Title"
+
+# Life cycle stage of the lesson
+# See this page for more details: https://cdh.carpentries.org/the-lesson-life-cycle.html
+# Possible values: "pre-alpha", "alpha", "beta", "stable"
+#
+# Lessons that are going through the transition to the
+# Carpentries Workbench will go through 3 steps:
+# 'transition-step-1': notice indicating a new version
+# 'transition-step-2': notice encouraging to use new version
+# 'transition-step-3': notice indicating the lesson is deprecated,
+# with automated redirect
+life_cycle: "pre-alpha"
+
+# For lessons in the life stages in 'transition-step-1' or later:
+# - 'transition_url' holds the URL for the version of the lesson that
+# uses the Workbench (needed for all 3 steps)
+# - 'transition_date' (in yyyy-mm-dd format) is the date when the lesson
+# will transition to being deprecated. The date only needs to be decided
+# when the lesson is in 'transition-step-2'.
+transition_url:
+transition_date:
+
+#------------------------------------------------------------
+# Generic settings (should not need to change).
+#------------------------------------------------------------
+
+# What kind of thing is this ("workshop" or "lesson")?
+kind: "lesson"
+
+# Magic to make URLs resolve both locally and on GitHub.
+# See https://help.github.com/articles/repository-metadata-on-github-pages/.
+# Please don't change it: / is correct.
+repository: /
+
+# Email address, no mailto:
+email: "team@carpentries.org"
+
+# Sites.
+coc: "https://docs.carpentries.org/topic_folders/policies/code-of-conduct.html"
+amy_site: "https://amy.carpentries.org/"
+carpentries_github: "https://github.com/carpentries"
+carpentries_pages: "https://carpentries.github.io"
+carpentries_site: "https://carpentries.org/"
+dc_site: "https://datacarpentry.org"
+example_repo: "https://github.com/carpentries/lesson-example"
+example_site: "https://carpentries.github.io/lesson-example"
+lc_site: "https://librarycarpentry.org/"
+swc_github: "https://github.com/swcarpentry"
+swc_pages: "https://swcarpentry.github.io"
+swc_site: "https://software-carpentry.org"
+template_repo: "https://github.com/carpentries/styles"
+training_site: "https://carpentries.github.io/instructor-training"
+workshop_repo: "https://github.com/carpentries/workshop-template"
+workshop_site: "https://carpentries.github.io/workshop-template"
+cc_by_human: "https://creativecommons.org/licenses/by/4.0/"
+
+# Surveys.
+pre_survey: "https://carpentries.typeform.com/to/wi32rS#slug="
+post_survey: "https://carpentries.typeform.com/to/UgVdRQ#slug="
+instructor_pre_survey: "https://carpentries.typeform.com/to/QVOarK#slug="
+instructor_post_survey: "https://carpentries.typeform.com/to/cjJ9UP#slug="
+
+# Set to 'true' for instructor training websites only.
+instructor_training: false
+
+# Start time in minutes (0 to be clock-independent, 540 to show a start at 09:00 am).
+start_time: 0
+
+# Specify that things in the episodes collection should be output.
+collections:
+ episodes:
+ output: true
+ permalink: /:path/index.html
+ extras:
+ output: true
+ permalink: /:path/index.html
+
+# Set the default layout for things in the episodes collection.
+defaults:
+ - values:
+ root: .
+ layout: page
+ - scope:
+ path: ""
+ type: episodes
+ values:
+ root: ..
+ layout: episode
+ - scope:
+ path: ""
+ type: extras
+ values:
+ root: ..
+ layout: page
+
+# Files and directories that are not to be copied.
+exclude:
+ - Makefile
+ - bin/
+ - .Rproj.user/
+ - .vendor/
+ - vendor/
+ - .docker-vendor/
+
+# Turn on built-in syntax highlighting.
+highlighter: rouge
diff --git a/bin/boilerplate/_episodes/01-introduction.md b/bin/boilerplate/_episodes/01-introduction.md
new file mode 100644
index 0000000..2e156c2
--- /dev/null
+++ b/bin/boilerplate/_episodes/01-introduction.md
@@ -0,0 +1,15 @@
+---
+title: "Introduction"
+teaching: 0
+exercises: 0
+questions:
+- "Key question (FIXME)"
+objectives:
+- "First learning objective. (FIXME)"
+keypoints:
+- "First key point. Brief Answer to questions. (FIXME)"
+---
+FIXME
+
+{% include links.md %}
+
diff --git a/bin/boilerplate/_extras/about.md b/bin/boilerplate/_extras/about.md
new file mode 100644
index 0000000..5f07f65
--- /dev/null
+++ b/bin/boilerplate/_extras/about.md
@@ -0,0 +1,5 @@
+---
+title: About
+---
+{% include carpentries.html %}
+{% include links.md %}
diff --git a/bin/boilerplate/_extras/discuss.md b/bin/boilerplate/_extras/discuss.md
new file mode 100644
index 0000000..bfc33c5
--- /dev/null
+++ b/bin/boilerplate/_extras/discuss.md
@@ -0,0 +1,6 @@
+---
+title: Discussion
+---
+FIXME
+
+{% include links.md %}
diff --git a/bin/boilerplate/_extras/figures.md b/bin/boilerplate/_extras/figures.md
new file mode 100644
index 0000000..0012c88
--- /dev/null
+++ b/bin/boilerplate/_extras/figures.md
@@ -0,0 +1,79 @@
+---
+title: Figures
+---
+
+{% include base_path.html %}
+{% include manual_episode_order.html %}
+
+
+
+{% comment %} Create anchor for each one of the episodes. {% endcomment %}
+
+{% for lesson_episode in lesson_episodes %}
+ {% if site.episode_order %}
+ {% assign episode = site.episodes | where: "slug", lesson_episode | first %}
+ {% else %}
+ {% assign episode = lesson_episode %}
+ {% endif %}
+
+{% endfor %}
+
+{% include links.md %}
diff --git a/bin/boilerplate/_extras/guide.md b/bin/boilerplate/_extras/guide.md
new file mode 100644
index 0000000..50f266f
--- /dev/null
+++ b/bin/boilerplate/_extras/guide.md
@@ -0,0 +1,6 @@
+---
+title: "Instructor Notes"
+---
+FIXME
+
+{% include links.md %}
diff --git a/bin/boilerplate/index.md b/bin/boilerplate/index.md
new file mode 100644
index 0000000..95ccdbd
--- /dev/null
+++ b/bin/boilerplate/index.md
@@ -0,0 +1,17 @@
+---
+layout: lesson
+root: . # Is the only page that doesn't follow the pattern /:path/index.html
+permalink: index.html # Is the only page that doesn't follow the pattern /:path/index.html
+---
+FIXME: home page introduction
+
+
+
+{% comment %} This is a comment in Liquid {% endcomment %}
+
+> ## Prerequisites
+>
+> FIXME
+{: .prereq}
+
+{% include links.md %}
diff --git a/bin/boilerplate/reference.md b/bin/boilerplate/reference.md
new file mode 100644
index 0000000..8c82616
--- /dev/null
+++ b/bin/boilerplate/reference.md
@@ -0,0 +1,9 @@
+---
+layout: reference
+---
+
+## Glossary
+
+FIXME
+
+{% include links.md %}
diff --git a/bin/boilerplate/setup.md b/bin/boilerplate/setup.md
new file mode 100644
index 0000000..b8c5032
--- /dev/null
+++ b/bin/boilerplate/setup.md
@@ -0,0 +1,7 @@
+---
+title: Setup
+---
+FIXME
+
+
+{% include links.md %}
diff --git a/bin/chunk-options.R b/bin/chunk-options.R
new file mode 100644
index 0000000..8e0d62a
--- /dev/null
+++ b/bin/chunk-options.R
@@ -0,0 +1,70 @@
+# These settings control the behavior of all chunks in the novice R materials.
+# For example, to generate the lessons with all the output hidden, simply change
+# `results` from "markup" to "hide".
+# For more information on available chunk options, see
+# http://yihui.name/knitr/options#chunk_options
+
+library("knitr")
+
+fix_fig_path <- function(pth) file.path("..", pth)
+
+
+## We set the path for the figures globally below, so if we want to
+## customize it for individual episodes, we can append a prefix to the
+## global path. For instance, if we call knitr_fig_path("01-") in the
+## first episode of the lesson, it will generate the figures in
+## `fig/rmd-01-`
+knitr_fig_path <- function(prefix) {
+ new_path <- paste0(opts_chunk$get("fig.path"),
+ prefix)
+ opts_chunk$set(fig.path = new_path)
+}
+
+## We use the rmd- prefix for the figures generated by the lessons so
+## they can be easily identified and deleted by `make clean-rmd`. The
+## working directory when the lessons are generated is the root so the
+## figures need to be saved in fig/, but when the site is generated,
+## the episodes will be one level down. We fix the path using the
+## `fig.process` option.
+
+opts_chunk$set(tidy = FALSE, results = "markup", comment = NA,
+ fig.align = "center", fig.path = "fig/rmd-",
+ fig.process = fix_fig_path,
+ fig.width = 8.5, fig.height = 8.5,
+ fig.retina = 2)
+
+# The hooks below add html tags to the code chunks and their output so that they
+# are properly formatted when the site is built.
+
+hook_in <- function(x, options) {
+ lg <- tolower(options$engine)
+ style <- paste0(".language-", lg)
+
+ stringr::str_c("\n\n~~~\n",
+ paste0(x, collapse="\n"),
+ "\n~~~\n{: ", style, "}\n\n")
+}
+
+hook_out <- function(x, options) {
+ x <- gsub("\n$", "", x)
+ stringr::str_c("\n\n~~~\n",
+ paste0(x, collapse="\n"),
+ "\n~~~\n{: .output}\n\n")
+}
+
+hook_error <- function(x, options) {
+ x <- gsub("\n$", "", x)
+ stringr::str_c("\n\n~~~\n",
+ paste0(x, collapse="\n"),
+ "\n~~~\n{: .error}\n\n")
+}
+
+hook_warning <- function(x, options) {
+ x <- gsub("\n$", "", x)
+ stringr::str_c("\n\n~~~\n",
+ paste0(x, collapse = "\n"),
+ "\n~~~\n{: .warning}\n\n")
+}
+
+knit_hooks$set(source = hook_in, output = hook_out, warning = hook_warning,
+ error = hook_error, message = hook_out)
diff --git a/bin/dependencies.R b/bin/dependencies.R
new file mode 100644
index 0000000..4eeeb21
--- /dev/null
+++ b/bin/dependencies.R
@@ -0,0 +1,107 @@
+install_required_packages <- function(lib = NULL, repos = getOption("repos", default = c(CRAN = "https://cran.rstudio.com/"))) {
+
+ if (is.null(lib)) {
+ lib <- .libPaths()[[1]]
+ }
+
+ message("lib paths: ", paste(lib, collapse = ", "))
+ # Note: RMarkdown is needed for renv to detect packages in Rmd documents.
+ required_pkgs <- c("rprojroot", "desc", "remotes", "renv", "BiocManager", "rmarkdown")
+ installed_pkgs <- rownames(installed.packages(lib.loc = lib))
+ missing_pkgs <- setdiff(required_pkgs, installed_pkgs)
+
+ # The default installation of R will have "@CRAN@" as the default repository,
+ # which directs contrib.url() to either force the user to choose a mirror if
+ # interactive or fail if not. Since we are not interactve, we need to force
+ # the mirror here.
+ if ("@CRAN@" %in% repos) {
+ repos <- c(CRAN = "https://cran.rstudio.com/")
+ }
+
+ if (length(missing_pkgs) != 0) {
+ install.packages(missing_pkgs, lib = lib, repos = repos)
+ }
+}
+
+find_root <- function() {
+
+ cfg <- rprojroot::has_file_pattern("^_config.y*ml$")
+ root <- rprojroot::find_root(cfg)
+
+ root
+}
+
+# set the BiocManager repositories and return a function that resets the default
+# repositories.
+#
+# @example
+# bioc_repos_example <- function() {
+# message("User repos")
+# as.data.frame(getOption("repos"))
+# reset_repos <- use_bioc_repos()
+# on.exit(reset_repos())
+# message("Bioc repos")
+# as.data.frame(getOption("repos"))
+# }
+# bioc_repos_example()
+# as.data.frame(getOption("repos")
+use_bioc_repos <- function() {
+ repos <- getOption("repos")
+ suppressMessages(options(repos = BiocManager::repositories()))
+ function() {
+ options(repos = repos)
+ }
+}
+
+identify_dependencies <- function() {
+
+ root <- find_root()
+
+ reset_repos <- use_bioc_repos()
+ on.exit(reset_repos(), add = TRUE)
+ eps <- file.path(root, "_episodes_rmd")
+ bin <- file.path(root, "bin")
+
+ required_pkgs <- unique(c(
+ ## Packages for episodes
+ renv::dependencies(eps, progress = FALSE, error = "ignored")$Package,
+ ## Packages for tools
+ renv::dependencies(bin, progress = FALSE, error = "ignored")$Package
+ ))
+
+ required_pkgs
+}
+
+create_description <- function(required_pkgs) {
+ d <- desc::description$new("!new")
+ d$set_deps(data.frame(type = "Imports", package = required_pkgs, version = "*"))
+ d$write("DESCRIPTION")
+ # We have to write the description twice to get the hidden dependencies
+ # because renv only considers explicit dependencies.
+ #
+ # This is needed because some of the hidden dependencis will require system
+ # libraries to be configured.
+ suppressMessages(repo <- BiocManager::repositories())
+ deps <- remotes::dev_package_deps(dependencies = TRUE, repos = repo)
+ deps <- deps$package[deps$diff < 0]
+ if (length(deps)) {
+ # only create new DESCRIPTION file if there are dependencies to install
+ d$set_deps(data.frame(type = "Imports", package = deps, version = "*"))
+ d$write("DESCRIPTION")
+ }
+}
+
+install_dependencies <- function(required_pkgs, ...) {
+
+ reset_repos <- use_bioc_repos()
+ on.exit(reset_repos(), add = TRUE)
+
+ create_description(required_pkgs)
+ on.exit(file.remove("DESCRIPTION"), add = TRUE)
+ remotes::install_deps(dependencies = TRUE, ...)
+
+ if (require("knitr") && packageVersion("knitr") < '1.9.19') {
+ stop("knitr must be version 1.9.20 or higher")
+ }
+
+}
diff --git a/bin/generate_md_episodes.R b/bin/generate_md_episodes.R
new file mode 100644
index 0000000..7fb4c5a
--- /dev/null
+++ b/bin/generate_md_episodes.R
@@ -0,0 +1,44 @@
+generate_md_episodes <- function() {
+
+ # avoid ansi color characters from being printed in the output
+ op <- options()
+ on.exit(options(op), add = TRUE)
+ options(crayon.enabled = FALSE)
+ ## get the Rmd file to process from the command line, and generate the path
+ ## for their respective outputs
+ args <- commandArgs(trailingOnly = TRUE)
+ if (!identical(length(args), 2L)) {
+ stop("input and output file must be passed to the script")
+ }
+
+ src_rmd <- args[1]
+ dest_md <- args[2]
+
+ ## knit the Rmd into markdown
+ knitr::knit(src_rmd, output = dest_md)
+
+ # Read the generated md files and add comments advising not to edit them
+ add_no_edit_comment <- function(y) {
+ con <- file(y)
+ mdfile <- readLines(con)
+ if (mdfile[1] != "---")
+ stop("Input file does not have a valid header")
+ mdfile <- append(
+ mdfile,
+ "# Please do not edit this file directly; it is auto generated.",
+ after = 1
+ )
+ mdfile <- append(
+ mdfile,
+ paste("# Instead, please edit", basename(y), "in _episodes_rmd/"),
+ after = 2
+ )
+ writeLines(mdfile, con)
+ close(con)
+ return(paste("Warning added to YAML header of", y))
+ }
+
+ vapply(dest_md, add_no_edit_comment, character(1))
+}
+
+generate_md_episodes()
diff --git a/bin/install_r_deps.sh b/bin/install_r_deps.sh
new file mode 100755
index 0000000..0280f24
--- /dev/null
+++ b/bin/install_r_deps.sh
@@ -0,0 +1 @@
+Rscript -e "source(file.path('bin', 'dependencies.R')); install_required_packages(); install_dependencies(identify_dependencies())"
diff --git a/bin/knit_lessons.sh b/bin/knit_lessons.sh
new file mode 100755
index 0000000..141c136
--- /dev/null
+++ b/bin/knit_lessons.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+# Only try running R to translate files if there are some files present.
+# The Makefile passes in the names of files.
+
+if [ $# -eq 2 ] ; then
+ Rscript -e "source('bin/generate_md_episodes.R')" "$@"
+fi
diff --git a/bin/lesson_check.py b/bin/lesson_check.py
new file mode 100644
index 0000000..86e4249
--- /dev/null
+++ b/bin/lesson_check.py
@@ -0,0 +1,628 @@
+"""
+Check lesson files and their contents.
+"""
+
+
+import os
+import glob
+import re
+import sys
+from argparse import ArgumentParser
+
+# This uses the `__all__` list in `util.py` to determine what objects to import
+# see https://docs.python.org/3/tutorial/modules.html#importing-from-a-package
+from util import *
+from reporter import Reporter
+
+__version__ = '0.3'
+
+# Where to look for source Markdown files.
+SOURCE_DIRS = ['', '_episodes', '_extras']
+
+# Where to look for source Rmd files.
+SOURCE_RMD_DIRS = ['_episodes_rmd']
+
+# Required files: each entry is ('path': YAML_required).
+# FIXME: We do not yet validate whether any files have the required
+# YAML headers, but should in the future.
+# The '%' is replaced with the source directory path for checking.
+# Episodes are handled specially, and extra files in '_extras' are also handled
+# specially. This list must include all the Markdown files listed in the
+# 'bin/initialize' script.
+REQUIRED_FILES = {
+ 'CODE_OF_CONDUCT.md': True,
+ 'CONTRIBUTING.md': False,
+ 'LICENSE.md': True,
+ 'README.md': False,
+ os.path.join('_extras', 'discuss.md'): True,
+ os.path.join('_extras', 'guide.md'): True,
+ 'index.md': True,
+ 'reference.md': True,
+ 'setup.md': True,
+}
+
+# Episode filename pattern.
+P_EPISODE_FILENAME = re.compile(r'(\d\d)-[-\w]+.md$')
+
+# Pattern to match lines ending with whitespace.
+P_TRAILING_WHITESPACE = re.compile(r'\s+$')
+
+# Pattern to match figure references in HTML.
+P_FIGURE_REFS = re.compile(r'
]+src="([^"]+)"[^>]*>')
+
+# Pattern to match internally-defined Markdown links.
+P_INTERNAL_LINK_REF = re.compile(r'\[([^\]]+)\]\[([^\]]+)\]')
+
+# Pattern to match reference links (to resolve internally-defined references).
+P_INTERNAL_LINK_DEF = re.compile(r'^\[([^\]]+)\]:\s*(.+)')
+
+# Pattern to match {% include ... %} statements
+P_INTERNAL_INCLUDE_LINK = re.compile(r'^{% include ([^ ]*) %}$')
+
+# Pattern to match image-only and link-only lines
+P_LINK_IMAGE_LINE = re.compile(r'''
+ [> #]* # any number of '>', '#', and spaces
+ \W{,3} # up to 3 non-word characters
+ !? # ! or nothing
+ \[[^]]+\] # [any text]
+ [([] # ( or [
+ [^])]+ # 1+ characters that are neither ] nor )
+ [])] # ] or )
+ (?:{:[^}]+})? # {:any text} or nothing
+ \W{,3} # up to 3 non-word characters
+ [ ]* # any number of spaces
+ \\?$ # \ or nothing + end of line''', re.VERBOSE)
+
+# What kinds of blockquotes are allowed?
+KNOWN_BLOCKQUOTES = {
+ 'callout',
+ 'caution',
+ 'challenge',
+ 'checklist',
+ 'discussion',
+ 'keypoints',
+ 'objectives',
+ 'prereq',
+ 'quotation',
+ 'solution',
+ 'testimonial',
+ 'warning'
+}
+
+# What kinds of code fragments are allowed?
+# Below we allow all 'language-*' code blocks
+KNOWN_CODEBLOCKS = {
+ 'error',
+ 'output',
+ 'source',
+ 'warning'
+}
+
+# What fields are required in teaching episode metadata?
+TEACHING_METADATA_FIELDS = {
+ ('title', str),
+ ('teaching', int),
+ ('exercises', int),
+ ('questions', list),
+ ('objectives', list),
+ ('keypoints', list)
+}
+
+# What fields are required in break episode metadata?
+BREAK_METADATA_FIELDS = {
+ ('layout', str),
+ ('title', str),
+ ('break', int)
+}
+
+# How long are lines allowed to be?
+# Please keep this in sync with .editorconfig!
+MAX_LINE_LEN = 100
+
+# Contents of _config.yml
+CONFIG = {}
+
+def main():
+ """Main driver."""
+
+ args = parse_args()
+ args.reporter = Reporter()
+
+ global CONFIG
+ config_file = os.path.join(args.source_dir, '_config.yml')
+ CONFIG = load_yaml(config_file)
+ CONFIG["config_file"] = config_file
+
+ life_cycle = CONFIG.get('life_cycle', None)
+ # pre-alpha lessons should report without error
+ if life_cycle == "pre-alpha":
+ args.permissive = True
+
+ check_config(args.reporter)
+ check_source_rmd(args.reporter, args.source_dir, args.parser)
+
+ args.references = read_references(args.reporter, args.reference_path)
+
+ docs = read_all_markdown(args.source_dir, args.parser)
+ check_fileset(args.source_dir, args.reporter, list(docs.keys()))
+ check_unwanted_files(args.source_dir, args.reporter)
+ for filename in list(docs.keys()):
+ checker = create_checker(args, filename, docs[filename])
+ checker.check()
+
+ args.reporter.report()
+ if args.reporter.messages:
+ if args.permissive:
+ print("Problems detected but ignored (permissive mode).")
+ else:
+ print("Problems detected.")
+ sys.exit(1)
+ else:
+ print("No problems found.")
+
+ return
+
+
+def parse_args():
+ """Parse command-line arguments."""
+
+ parser = ArgumentParser(description="""Check episode files in a lesson.""")
+ parser.add_argument('-l', '--linelen',
+ default=False,
+ action="store_true",
+ dest='line_lengths',
+ help='Check line lengths')
+ parser.add_argument('-p', '--parser',
+ default=None,
+ dest='parser',
+ help='path to Markdown parser')
+ parser.add_argument('-r', '--references',
+ default=None,
+ dest='reference_path',
+ help='path to Markdown file of external references')
+ parser.add_argument('-s', '--source',
+ default=os.curdir,
+ dest='source_dir',
+ help='source directory')
+ parser.add_argument('-w', '--whitespace',
+ default=False,
+ action="store_true",
+ dest='trailing_whitespace',
+ help='Check for trailing whitespace')
+ parser.add_argument('--permissive',
+ default=False,
+ action="store_true",
+ dest='permissive',
+ help='Do not raise an error even if issues are detected')
+
+ args, extras = parser.parse_known_args()
+ require(args.parser is not None,
+ 'Path to Markdown parser not provided',
+ True)
+ require(not extras,
+ 'Unexpected trailing command-line arguments "{0}"'.format(extras))
+
+ return args
+
+def check_config(reporter):
+ """Check configuration file."""
+
+ reporter.check_field(CONFIG["config_file"], 'configuration',
+ CONFIG, 'kind', 'lesson')
+ reporter.check_field(CONFIG["config_file"], 'configuration',
+ CONFIG, 'carpentry', ('swc', 'dc', 'lc', 'cp', 'incubator'))
+ reporter.check_field(CONFIG["config_file"], 'configuration', CONFIG, 'title')
+ reporter.check_field(CONFIG["config_file"], 'configuration', CONFIG, 'email')
+
+ for defaults in [
+ {'values': {'root': '.', 'layout': 'page'}},
+ {'values': {'root': '..', 'layout': 'episode'}, 'scope': {'type': 'episodes', 'path': ''}},
+ {'values': {'root': '..', 'layout': 'page'}, 'scope': {'type': 'extras', 'path': ''}}
+ ]:
+ error_text = 'incorrect settings for: root "{0}" layout "{1}"'
+ root = defaults["values"]["root"]
+ layout = defaults["values"]["layout"]
+ error_message = error_text.format(root, layout)
+
+ defaults_test = defaults in CONFIG.get('defaults', [])
+ reporter.check(defaults_test, 'configuration', error_message)
+
+def check_source_rmd(reporter, source_dir, parser):
+ """Check that Rmd episode files include `source: Rmd`"""
+
+ episode_rmd_dir = [os.path.join(source_dir, d) for d in SOURCE_RMD_DIRS]
+ episode_rmd_files = [os.path.join(d, '*.Rmd') for d in episode_rmd_dir]
+ results = {}
+ for pat in episode_rmd_files:
+ for f in glob.glob(pat):
+ data = read_markdown(parser, f)
+ dy = data['metadata']
+ if dy:
+ reporter.check_field(f, 'episode_rmd',
+ dy, 'source', 'Rmd')
+
+def read_references(reporter, ref_path):
+ """Read shared file of reference links, returning dictionary of valid references
+ {symbolic_name : URL}
+ """
+
+ if 'remote_theme' in CONFIG:
+ return {}
+
+ if not ref_path:
+ raise Warning("No filename has been provided.")
+
+ result = {}
+ urls_seen = set()
+
+ with open(ref_path, 'r', encoding='utf-8') as reader:
+ for (num, line) in enumerate(reader, 1):
+
+ # Skip empty lines
+ if len(line.strip()) == 0:
+ continue
+
+ # Skip HTML comments
+ if line.strip().startswith(""):
+ continue
+
+ # Skip Liquid's {% include ... %} lines
+ if P_INTERNAL_INCLUDE_LINK.search(line):
+ continue
+
+ m = P_INTERNAL_LINK_DEF.search(line)
+
+ message = '{}: {} not a valid reference: {}'
+ require(m, message.format(ref_path, num, line.rstrip()))
+
+ name = m.group(1)
+ url = m.group(2)
+
+ message = 'Empty reference at {0}:{1}'
+ require(name, message.format(ref_path, num))
+
+ unique_name = name not in result
+ unique_url = url not in urls_seen
+
+ reporter.check(unique_name,
+ ref_path,
+ 'Duplicate reference name {0} at line {1}',
+ name, num)
+
+ reporter.check(unique_url,
+ ref_path,
+ 'Duplicate definition of URL {0} at line {1}',
+ url, num)
+
+ result[name] = url
+ urls_seen.add(url)
+
+ return result
+
+
+def read_all_markdown(source_dir, parser):
+ """Read source files, returning
+ {path : {'metadata':yaml, 'metadata_len':N, 'text':text, 'lines':[(i, line, len)], 'doc':doc}}
+ """
+
+ all_dirs = [os.path.join(source_dir, d) for d in SOURCE_DIRS]
+ all_patterns = [os.path.join(d, '*.md') for d in all_dirs]
+ result = {}
+ for pat in all_patterns:
+ for filename in glob.glob(pat):
+ data = read_markdown(parser, filename)
+ if data:
+ result[filename] = data
+ return result
+
+
+def check_fileset(source_dir, reporter, filenames_present):
+ """Are all required files present? Are extraneous files present?"""
+
+ # Check files with predictable names.
+ required = [os.path.join(source_dir, p) for p in REQUIRED_FILES]
+ missing = set(required) - set(filenames_present)
+ for m in missing:
+ reporter.add(None, 'Missing required file {0}', m)
+
+ # Check episode files' names.
+ seen = []
+ for filename in filenames_present:
+ if '_episodes' not in filename:
+ continue
+
+ # split path to check episode name
+ base_name = os.path.basename(filename)
+ m = P_EPISODE_FILENAME.search(base_name)
+ if m and m.group(1):
+ seen.append(m.group(1))
+ else:
+ reporter.add(
+ None, 'Episode {0} has badly-formatted filename', filename)
+
+ # Check for duplicate episode numbers.
+ reporter.check(len(seen) == len(set(seen)),
+ None,
+ 'Duplicate episode numbers {0} vs {1}',
+ sorted(seen), sorted(set(seen)))
+
+ # Check that numbers are consecutive.
+ seen = sorted([int(s) for s in seen])
+ clean = True
+ for i in range(len(seen) - 1):
+ clean = clean and ((seen[i+1] - seen[i]) == 1)
+ reporter.check(clean,
+ None,
+ 'Missing or non-consecutive episode numbers {0}',
+ seen)
+
+
+def create_checker(args, filename, info):
+ """Create appropriate checker for file."""
+
+ for (pat, cls) in CHECKERS:
+ if pat.search(filename):
+ return cls(args, filename, **info)
+ return NotImplemented
+
+class CheckBase:
+ """Base class for checking Markdown files."""
+
+ def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
+ """Cache arguments for checking."""
+
+ self.args = args
+ self.reporter = self.args.reporter # for convenience
+ self.filename = filename
+ self.metadata = metadata
+ self.metadata_len = metadata_len
+ self.text = text
+ self.lines = lines
+ self.doc = doc
+
+ self.layout = None
+
+ def check(self):
+ """Run tests."""
+
+ self.check_metadata()
+ self.check_line_lengths()
+ self.check_trailing_whitespace()
+ self.check_blockquote_classes()
+ self.check_codeblock_classes()
+ self.check_defined_link_references()
+
+ def check_metadata(self):
+ """Check the YAML metadata."""
+
+ self.reporter.check(self.metadata is not None,
+ self.filename,
+ 'Missing metadata entirely')
+
+ if self.metadata and (self.layout is not None):
+ self.reporter.check_field(
+ self.filename, 'metadata', self.metadata, 'layout', self.layout)
+
+ def check_line_lengths(self):
+ """Check the raw text of the lesson body."""
+
+ if self.args.line_lengths:
+ over_limit = []
+
+ for (i, l, n) in self.lines:
+ # Report lines that are longer than the suggested
+ # line length limit only if they're not
+ # link-only or image-only lines.
+ if n > MAX_LINE_LEN and not P_LINK_IMAGE_LINE.match(l):
+ over_limit.append(i)
+
+ self.reporter.check(not over_limit,
+ self.filename,
+ 'Line(s) too long: {0}',
+ ', '.join([str(i) for i in over_limit]))
+
+ def check_trailing_whitespace(self):
+ """Check for whitespace at the ends of lines."""
+
+ if self.args.trailing_whitespace:
+ trailing = [
+ i for (i, l, n) in self.lines if P_TRAILING_WHITESPACE.match(l)]
+ self.reporter.check(not trailing,
+ self.filename,
+ 'Line(s) end with whitespace: {0}',
+ ', '.join([str(i) for i in trailing]))
+
+ def check_blockquote_classes(self):
+ """Check that all blockquotes have known classes."""
+
+ for node in self.find_all(self.doc, {'type': 'blockquote'}):
+ cls = self.get_val(node, 'attr', 'class')
+ self.reporter.check(cls in KNOWN_BLOCKQUOTES,
+ (self.filename, self.get_loc(node)),
+ 'Unknown or missing blockquote type {0}',
+ cls)
+
+ def check_codeblock_classes(self):
+ """Check that all code blocks have known classes."""
+
+ for node in self.find_all(self.doc, {'type': 'codeblock'}):
+ cls = self.get_val(node, 'attr', 'class')
+ self.reporter.check(cls is not None and (cls in KNOWN_CODEBLOCKS or
+ cls.startswith('language-')),
+ (self.filename, self.get_loc(node)),
+ 'Unknown or missing code block type {0}',
+ cls)
+
+ def check_defined_link_references(self):
+ """Check that defined links resolve in the file.
+
+ Internally-defined links match the pattern [text][label].
+ """
+
+ result = set()
+ for node in self.find_all(self.doc, {'type': 'text'}):
+ for match in P_INTERNAL_LINK_REF.findall(node['value']):
+ text = match[0]
+ link = match[1]
+ if link not in self.args.references:
+ result.add('"{0}"=>"{1}"'.format(text, link))
+ self.reporter.check(not result,
+ self.filename,
+ 'Internally-defined links may be missing definitions: {0}',
+ ', '.join(sorted(result)))
+
+ def find_all(self, node, pattern, accum=None):
+ """Find all matches for a pattern."""
+
+ assert isinstance(pattern, dict), 'Patterns must be dictionaries'
+ if accum is None:
+ accum = []
+ if self.match(node, pattern):
+ accum.append(node)
+ for child in node.get('children', []):
+ self.find_all(child, pattern, accum)
+ return accum
+
+ def match(self, node, pattern):
+ """Does this node match the given pattern?"""
+
+ for key in pattern:
+ if key not in node:
+ return False
+ val = pattern[key]
+ if isinstance(val, str):
+ if node[key] != val:
+ return False
+ elif isinstance(val, dict):
+ if not self.match(node[key], val):
+ return False
+ return True
+
+ @staticmethod
+ def get_val(node, *chain):
+ """Get value one or more levels down."""
+
+ curr = node
+ for selector in chain:
+ curr = curr.get(selector, None)
+ if curr is None:
+ break
+ return curr
+
+ def get_loc(self, node):
+ """Convenience method to get node's line number."""
+
+ result = self.get_val(node, 'options', 'location')
+ if self.metadata_len is not None:
+ result += self.metadata_len
+ return result
+
+
+class CheckNonJekyll(CheckBase):
+ """Check a file that isn't translated by Jekyll."""
+
+ def check_metadata(self):
+ self.reporter.check(self.metadata is None,
+ self.filename,
+ 'Unexpected metadata')
+
+
+class CheckIndex(CheckBase):
+ """Check the main index page."""
+
+ def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
+ super().__init__(args, filename, metadata, metadata_len, text, lines, doc)
+ self.layout = 'lesson'
+
+ def check_metadata(self):
+ super().check_metadata()
+ self.reporter.check(self.metadata.get('root', '') == '.',
+ self.filename,
+ 'Root not set to "."')
+
+
+class CheckEpisode(CheckBase):
+ """Check an episode page."""
+
+ def check(self):
+ """Run extra tests."""
+
+ super().check()
+ self.check_reference_inclusion()
+
+ def check_metadata(self):
+ super().check_metadata()
+ if self.metadata:
+ if 'layout' in self.metadata:
+ if self.metadata['layout'] == 'break':
+ self.check_metadata_fields(BREAK_METADATA_FIELDS)
+ else:
+ self.reporter.add(self.filename,
+ 'Unknown episode layout "{0}"',
+ self.metadata['layout'])
+ else:
+ self.check_metadata_fields(TEACHING_METADATA_FIELDS)
+
+ def check_metadata_fields(self, expected):
+ """Check metadata fields."""
+ for (name, type_) in expected:
+ if name not in self.metadata:
+ self.reporter.add(self.filename,
+ 'Missing metadata field {0}',
+ name)
+ elif not isinstance(self.metadata[name], type_):
+ self.reporter.add(self.filename,
+ '"{0}" has wrong type in metadata ({1} instead of {2})',
+ name, type(self.metadata[name]), type_)
+
+ def check_reference_inclusion(self):
+ """Check that links file has been included."""
+
+ if 'remote_theme' in CONFIG:
+ return
+
+ if not self.args.reference_path:
+ return
+
+ for (i, last_line, line_len) in reversed(self.lines):
+ if last_line:
+ break
+
+ require(last_line,
+ 'No non-empty lines in {0}'.format(self.filename))
+
+ include_filename = os.path.split(self.args.reference_path)[-1]
+ if include_filename not in last_line:
+ self.reporter.add(self.filename,
+ 'episode does not include "{0}"',
+ include_filename)
+
+
+class CheckReference(CheckBase):
+ """Check the reference page."""
+
+ def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
+ super().__init__(args, filename, metadata, metadata_len, text, lines, doc)
+ self.layout = 'reference'
+
+
+class CheckGeneric(CheckBase):
+ """Check a generic page."""
+
+ def __init__(self, args, filename, metadata, metadata_len, text, lines, doc):
+ super().__init__(args, filename, metadata, metadata_len, text, lines, doc)
+
+
+CHECKERS = [
+ (re.compile(r'CONTRIBUTING\.md'), CheckNonJekyll),
+ (re.compile(r'README\.md'), CheckNonJekyll),
+ (re.compile(r'index\.md'), CheckIndex),
+ (re.compile(r'reference\.md'), CheckReference),
+ # '.' below is what's passed on the command line via '-s' flag
+ (re.compile(os.path.join('.','_episodes', '[^/]*\.md')), CheckEpisode),
+ (re.compile(r'.*\.md'), CheckGeneric)
+]
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/lesson_initialize.py b/bin/lesson_initialize.py
new file mode 100644
index 0000000..79ec05c
--- /dev/null
+++ b/bin/lesson_initialize.py
@@ -0,0 +1,47 @@
+"""Initialize a newly-created repository."""
+
+
+import sys
+import os
+import shutil
+
+BOILERPLATE = (
+ 'AUTHORS',
+ 'CITATION',
+ 'CONTRIBUTING.md',
+ 'README.md',
+ '_config.yml',
+ os.path.join('_episodes', '01-introduction.md'),
+ os.path.join('_extras', 'about.md'),
+ os.path.join('_extras', 'discuss.md'),
+ os.path.join('_extras', 'figures.md'),
+ os.path.join('_extras', 'guide.md'),
+ 'index.md',
+ 'reference.md',
+ 'setup.md',
+)
+
+
+def main():
+ """Check for collisions, then create."""
+
+ # Check.
+ errors = False
+ for path in BOILERPLATE:
+ if os.path.exists(path):
+ print('Warning: {0} already exists.'.format(path), file=sys.stderr)
+ errors = True
+ if errors:
+ print('**Exiting without creating files.**', file=sys.stderr)
+ sys.exit(1)
+
+ # Create.
+ for path in BOILERPLATE:
+ shutil.copyfile(
+ os.path.join('bin', 'boilerplate', path),
+ path
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/markdown_ast.rb b/bin/markdown_ast.rb
new file mode 100755
index 0000000..2ef3f77
--- /dev/null
+++ b/bin/markdown_ast.rb
@@ -0,0 +1,13 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+# Use Kramdown parser to produce AST for Markdown document.
+
+require 'kramdown'
+require 'kramdown-parser-gfm'
+require 'json'
+
+markdown = $stdin.read
+doc = Kramdown::Document.new(markdown, input: 'GFM', hard_wrap: false)
+tree = doc.to_hash_a_s_t
+puts JSON.pretty_generate(tree)
diff --git a/bin/repo_check.py b/bin/repo_check.py
new file mode 100644
index 0000000..6988ca5
--- /dev/null
+++ b/bin/repo_check.py
@@ -0,0 +1,181 @@
+"""
+Check repository settings.
+"""
+
+
+import sys
+import os
+from subprocess import Popen, PIPE
+import re
+from argparse import ArgumentParser
+
+from util import require
+from reporter import Reporter
+
+# Import this way to produce a more useful error message.
+try:
+ import requests
+except ImportError:
+ print('Unable to import requests module: please install requests', file=sys.stderr)
+ sys.exit(1)
+
+
+# Pattern to match Git command-line output for remotes => (user name, project name).
+P_GIT_REMOTE = re.compile(r'upstream\s+(?:https://|git@)github.com[:/]([^/]+)/([^.]+)(\.git)?\s+\(fetch\)')
+
+# Repository URL format string.
+F_REPO_URL = 'https://github.com/{0}/{1}/'
+
+# Pattern to match repository URLs => (user name, project name)
+P_REPO_URL = re.compile(r'https?://github\.com/([^.]+)/([^/]+)/?')
+
+# API URL format string.
+F_API_URL = 'https://api.github.com/repos/{0}/{1}/labels'
+
+# Expected labels and colors.
+EXPECTED = {
+ 'help wanted': 'dcecc7',
+ 'status:in progress': '9bcc65',
+ 'status:changes requested': '679f38',
+ 'status:wait': 'fff2df',
+ 'status:refer to cac': 'ffdfb2',
+ 'status:need more info': 'ee6c00',
+ 'status:blocked': 'e55100',
+ 'status:out of scope': 'eeeeee',
+ 'status:duplicate': 'bdbdbd',
+ 'type:typo text': 'f8bad0',
+ 'type:bug': 'eb3f79',
+ 'type:formatting': 'ac1357',
+ 'type:template and tools': '7985cb',
+ 'type:instructor guide': '00887a',
+ 'type:discussion': 'b2e5fc',
+ 'type:enhancement': '7fdeea',
+ 'type:clarification': '00acc0',
+ 'type:teaching example': 'ced8dc',
+ 'good first issue': 'ffeb3a',
+ 'high priority': 'd22e2e'
+}
+
+
+def main():
+ """
+ Main driver.
+ """
+
+ args = parse_args()
+ reporter = Reporter()
+ repo_url = get_repo_url(args.repo_url)
+ check_labels(reporter, repo_url)
+ reporter.report()
+
+
+def parse_args():
+ """
+ Parse command-line arguments.
+ """
+
+ parser = ArgumentParser(description="""Check repository settings.""")
+ parser.add_argument('-r', '--repo',
+ default=None,
+ dest='repo_url',
+ help='repository URL')
+ parser.add_argument('-s', '--source',
+ default=os.curdir,
+ dest='source_dir',
+ help='source directory')
+
+ args, extras = parser.parse_known_args()
+ require(not extras,
+ 'Unexpected trailing command-line arguments "{0}"'.format(extras))
+
+ return args
+
+
+def get_repo_url(repo_url):
+ """
+ Figure out which repository to query.
+ """
+
+ # Explicitly specified.
+ if repo_url is not None:
+ return repo_url
+
+ # Guess.
+ cmd = 'git remote -v'
+ p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE,
+ close_fds=True, universal_newlines=True, encoding='utf-8')
+ stdout_data, stderr_data = p.communicate()
+ stdout_data = stdout_data.split('\n')
+ matches = [P_GIT_REMOTE.match(line) for line in stdout_data]
+ matches = [m for m in matches if m is not None]
+ require(len(matches) == 1,
+ 'Unexpected output from git remote command: "{0}"'.format(matches))
+
+ username = matches[0].group(1)
+ require(
+ username, 'empty username in git remote output {0}'.format(matches[0]))
+
+ project_name = matches[0].group(2)
+ require(
+ username, 'empty project name in git remote output {0}'.format(matches[0]))
+
+ url = F_REPO_URL.format(username, project_name)
+ return url
+
+
+def check_labels(reporter, repo_url):
+ """
+ Check labels in repository.
+ """
+
+ actual = get_labels(repo_url)
+ extra = set(actual.keys()) - set(EXPECTED.keys())
+
+ reporter.check(not extra,
+ None,
+ 'Extra label(s) in repository {0}: {1}',
+ repo_url, ', '.join(sorted(extra)))
+
+ missing = set(EXPECTED.keys()) - set(actual.keys())
+ reporter.check(not missing,
+ None,
+ 'Missing label(s) in repository {0}: {1}',
+ repo_url, ', '.join(sorted(missing)))
+
+ overlap = set(EXPECTED.keys()).intersection(set(actual.keys()))
+ for name in sorted(overlap):
+ reporter.check(EXPECTED[name].lower() == actual[name].lower(),
+ None,
+ 'Color mis-match for label {0} in {1}: expected {2}, found {3}',
+ name, repo_url, EXPECTED[name], actual[name])
+
+
+def get_labels(repo_url):
+ """
+ Get actual labels from repository.
+ """
+
+ m = P_REPO_URL.match(repo_url)
+ require(
+ m, 'repository URL {0} does not match expected pattern'.format(repo_url))
+
+ username = m.group(1)
+ require(username, 'empty username in repository URL {0}'.format(repo_url))
+
+ project_name = m.group(2)
+ require(
+ username, 'empty project name in repository URL {0}'.format(repo_url))
+
+ url = F_API_URL.format(username, project_name)
+ r = requests.get(url)
+ require(r.status_code == 200,
+ 'Request for {0} failed with {1}'.format(url, r.status_code))
+
+ result = {}
+ for entry in r.json():
+ result[entry['name']] = entry['color']
+ return result
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/reporter.py b/bin/reporter.py
new file mode 100644
index 0000000..550dbf0
--- /dev/null
+++ b/bin/reporter.py
@@ -0,0 +1,75 @@
+import sys
+
+class Reporter:
+ """Collect and report errors."""
+
+ # Marker to show that an expected value hasn't been provided.
+ # (Can't use 'None' because that might be a legitimate value.)
+ _DEFAULT_REPORTER = []
+
+ def __init__(self):
+ """Constructor."""
+ self.messages = []
+
+ def check_field(self, filename, name, values, key, expected=_DEFAULT_REPORTER):
+ """Check that a dictionary has an expected value."""
+
+ if key not in values:
+ self.add(filename, '{0} does not contain {1}', name, key)
+ elif expected is self._DEFAULT_REPORTER:
+ pass
+ elif type(expected) in (tuple, set, list):
+ if values[key] not in expected:
+ self.add(
+ filename, '{0} {1} value {2} is not in {3}', name, key, values[key], expected)
+ elif values[key] != expected:
+ self.add(filename, '{0} {1} is {2} not {3}',
+ name, key, values[key], expected)
+
+ def check(self, condition, location, fmt, *args):
+ """Append error if condition not met."""
+
+ if not condition:
+ self.add(location, fmt, *args)
+
+ def add(self, location, fmt, *args):
+ """Append error unilaterally."""
+
+ self.messages.append((location, fmt.format(*args)))
+
+ @staticmethod
+ def pretty(item):
+ location, message = item
+ if isinstance(location, type(None)):
+ return message
+ elif isinstance(location, str):
+ return location + ': ' + message
+ elif isinstance(location, tuple):
+ return '{0}:{1}: '.format(*location) + message
+
+ print('Unknown item "{0}"'.format(item), file=sys.stderr)
+ return NotImplemented
+
+ @staticmethod
+ def key(item):
+ location, message = item
+ if isinstance(location, type(None)):
+ return ('', -1, message)
+ elif isinstance(location, str):
+ return (location, -1, message)
+ elif isinstance(location, tuple):
+ return (location[0], location[1], message)
+
+ print('Unknown item "{0}"'.format(item), file=sys.stderr)
+ return NotImplemented
+
+ def report(self, stream=sys.stdout):
+ """Report all messages in order."""
+
+ if not self.messages:
+ return
+
+ for m in sorted(self.messages, key=self.key):
+ print(self.pretty(m), file=stream)
+
+
diff --git a/bin/run-make-docker-serve.sh b/bin/run-make-docker-serve.sh
new file mode 100755
index 0000000..1e09178
--- /dev/null
+++ b/bin/run-make-docker-serve.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -o errexit
+set -o pipefail
+set -o nounset
+
+
+bundle install
+bundle update
+exec bundle exec jekyll serve --host 0.0.0.0
diff --git a/bin/test_lesson_check.py b/bin/test_lesson_check.py
new file mode 100644
index 0000000..7a6d603
--- /dev/null
+++ b/bin/test_lesson_check.py
@@ -0,0 +1,19 @@
+import unittest
+
+import lesson_check
+import reporter
+
+
+class TestFileList(unittest.TestCase):
+ def setUp(self):
+ self.reporter = reporter.Reporter() # TODO: refactor reporter class.
+
+ def test_file_list_has_expected_entries(self):
+ # For first pass, simply assume that all required files are present
+
+ lesson_check.check_fileset('', self.reporter, lesson_check.REQUIRED_FILES)
+ self.assertEqual(len(self.reporter.messages), 0)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/bin/util.py b/bin/util.py
new file mode 100644
index 0000000..1398c37
--- /dev/null
+++ b/bin/util.py
@@ -0,0 +1,111 @@
+import sys
+import os
+import json
+from subprocess import Popen, PIPE
+
+# Import this way to produce a more useful error message.
+try:
+ import yaml
+except ImportError:
+ print('Unable to import YAML module: please install PyYAML', file=sys.stderr)
+ sys.exit(1)
+
+__all__ = ['check_unwanted_files', 'load_yaml', 'read_markdown', 'require']
+
+# Files that shouldn't be present.
+UNWANTED_FILES = [
+ '.nojekyll'
+]
+
+def read_markdown(parser, path):
+ """
+ Get YAML and AST for Markdown file, returning
+ {'metadata':yaml, 'metadata_len':N, 'text':text, 'lines':[(i, line, len)], 'doc':doc}.
+ """
+
+ # Split and extract YAML (if present).
+ with open(path, 'r', encoding='utf-8') as reader:
+ body = reader.read()
+ metadata_raw, metadata_yaml, body = split_metadata(path, body)
+
+ # Split into lines.
+ metadata_len = 0 if metadata_raw is None else metadata_raw.count('\n')
+ lines = [(metadata_len+i+1, line, len(line))
+ for (i, line) in enumerate(body.split('\n'))]
+
+ # Parse Markdown.
+ cmd = 'bundle exec ruby {0}'.format(parser)
+ p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE,
+ close_fds=True, universal_newlines=True, encoding='utf-8')
+ stdout_data, stderr_data = p.communicate(body)
+ doc = json.loads(stdout_data)
+
+ return {
+ 'metadata': metadata_yaml,
+ 'metadata_len': metadata_len,
+ 'text': body,
+ 'lines': lines,
+ 'doc': doc
+ }
+
+
+def split_metadata(path, text):
+ """
+ Get raw (text) metadata, metadata as YAML, and rest of body.
+ If no metadata, return (None, None, body).
+ """
+
+ metadata_raw = None
+ metadata_yaml = None
+
+ pieces = text.split('---', 2)
+ if len(pieces) == 3:
+ metadata_raw = pieces[1]
+ text = pieces[2]
+ try:
+ metadata_yaml = yaml.load(metadata_raw, Loader=yaml.SafeLoader)
+ except yaml.YAMLError as e:
+ message = 'Unable to parse YAML header in {0}:\n{1}'
+ print(message.format(path, e), file=sys.stderr)
+
+ return metadata_raw, metadata_yaml, text
+
+
+def load_yaml(filename):
+ """
+ Wrapper around YAML loading so that 'import yaml' is only needed
+ in one file.
+ """
+
+ try:
+ with open(filename, 'r', encoding='utf-8') as reader:
+ return yaml.load(reader, Loader=yaml.SafeLoader)
+ except yaml.YAMLError as e:
+ message = 'ERROR: Unable to load YAML file {0}:\n{1}'
+ print(message.format(filename, e), file=sys.stderr)
+ except (FileNotFoundError, IOError):
+ message = 'ERROR: File {} not found'
+ print(message.format(filename), file=sys.stderr)
+
+ return {}
+
+def check_unwanted_files(dir_path, reporter):
+ """
+ Check that unwanted files are not present.
+ """
+
+ for filename in UNWANTED_FILES:
+ path = os.path.join(dir_path, filename)
+ reporter.check(not os.path.exists(path),
+ path,
+ "Unwanted file found")
+
+
+def require(condition, message, fatal=False):
+ """Fail if condition not met."""
+
+ if not condition:
+ print(message, file=sys.stderr)
+
+ if fatal:
+ sys.exit(1)
diff --git a/bin/workshop_check.py b/bin/workshop_check.py
new file mode 100644
index 0000000..312b1a1
--- /dev/null
+++ b/bin/workshop_check.py
@@ -0,0 +1,419 @@
+'''Check that a workshop's index.html metadata is valid. See the
+docstrings on the checking functions for a summary of the checks.
+'''
+
+
+import sys
+import os
+import re
+from datetime import date
+from util import split_metadata, load_yaml, check_unwanted_files
+from reporter import Reporter
+
+# Metadata field patterns.
+EMAIL_PATTERN = r'[^@]+@[^@]+\.[^@]+'
+HUMANTIME_PATTERN = r'((0?[1-9]|1[0-2]):[0-5]\d(am|pm)(-|to)(0?[1-9]|1[0-2]):[0-5]\d(am|pm))|((0?\d|1\d|2[0-3]):[0-5]\d(-|to)(0?\d|1\d|2[0-3]):[0-5]\d)'
+EVENTBRITE_PATTERN = r'\d{9,10}'
+URL_PATTERN = r'https?://.+'
+
+# Defaults.
+CARPENTRIES = ("dc", "swc", "lc", "cp")
+DEFAULT_CONTACT_EMAIL = 'team@carpentries.org'
+
+USAGE = 'Usage: "workshop_check.py path/to/root/directory"'
+
+# Country and language codes. Note that codes mean different things: 'ar'
+# is 'Arabic' as a language but 'Argentina' as a country.
+
+ISO_COUNTRY = [
+ 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq', 'ar', 'as',
+ 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh',
+ 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz',
+ 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co',
+ 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz',
+ 'ec', 'ee', 'eg', 'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm',
+ 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm',
+ 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn',
+ 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is',
+ 'it', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp',
+ 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt',
+ 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mk', 'ml', 'mm',
+ 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my',
+ 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu',
+ 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr',
+ 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb',
+ 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so',
+ 'sr', 'st', 'sv', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj', 'tk',
+ 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'um',
+ 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws',
+ 'ye', 'yt', 'za', 'zm', 'zw'
+]
+
+ISO_LANGUAGE = [
+ 'aa', 'ab', 'ae', 'af', 'ak', 'am', 'an', 'ar', 'as', 'av', 'ay', 'az',
+ 'ba', 'be', 'bg', 'bh', 'bi', 'bm', 'bn', 'bo', 'br', 'bs', 'ca', 'ce',
+ 'ch', 'co', 'cr', 'cs', 'cu', 'cv', 'cy', 'da', 'de', 'dv', 'dz', 'ee',
+ 'el', 'en', 'eo', 'es', 'et', 'eu', 'fa', 'ff', 'fi', 'fj', 'fo', 'fr',
+ 'fy', 'ga', 'gd', 'gl', 'gn', 'gu', 'gv', 'ha', 'he', 'hi', 'ho', 'hr',
+ 'ht', 'hu', 'hy', 'hz', 'ia', 'id', 'ie', 'ig', 'ii', 'ik', 'io', 'is',
+ 'it', 'iu', 'ja', 'jv', 'ka', 'kg', 'ki', 'kj', 'kk', 'kl', 'km', 'kn',
+ 'ko', 'kr', 'ks', 'ku', 'kv', 'kw', 'ky', 'la', 'lb', 'lg', 'li', 'ln',
+ 'lo', 'lt', 'lu', 'lv', 'mg', 'mh', 'mi', 'mk', 'ml', 'mn', 'mr', 'ms',
+ 'mt', 'my', 'na', 'nb', 'nd', 'ne', 'ng', 'nl', 'nn', 'no', 'nr', 'nv',
+ 'ny', 'oc', 'oj', 'om', 'or', 'os', 'pa', 'pi', 'pl', 'ps', 'pt', 'qu',
+ 'rm', 'rn', 'ro', 'ru', 'rw', 'sa', 'sc', 'sd', 'se', 'sg', 'si', 'sk',
+ 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'ss', 'st', 'su', 'sv', 'sw', 'ta',
+ 'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tn', 'to', 'tr', 'ts', 'tt', 'tw',
+ 'ty', 'ug', 'uk', 'ur', 'uz', 've', 'vi', 'vo', 'wa', 'wo', 'xh', 'yi',
+ 'yo', 'za', 'zh', 'zu'
+]
+
+
+def look_for_fixme(func):
+ """Decorator to fail test if text argument starts with "FIXME"."""
+
+ def inner(arg):
+ if (arg is not None) and \
+ isinstance(arg, str) and \
+ arg.lstrip().startswith('FIXME'):
+ return False
+ return func(arg)
+ return inner
+
+
+@look_for_fixme
+def check_layout(layout):
+ '''"layout" in YAML header must be "workshop".'''
+
+ return layout == 'workshop'
+
+
+@look_for_fixme
+def check_carpentry(layout):
+ '''"carpentry" in YAML header must be "dc", "swc", "lc", or "cp".'''
+
+ return layout in CARPENTRIES
+
+
+@look_for_fixme
+def check_country(country):
+ '''"country" must be a lowercase ISO-3166 two-letter code.'''
+
+ return country in ISO_COUNTRY
+
+
+@look_for_fixme
+def check_language(language):
+ '''"language" must be a lowercase ISO-639 two-letter code.'''
+
+ return language in ISO_LANGUAGE
+
+
+@look_for_fixme
+def check_humandate(date):
+ """
+ 'humandate' must be a human-readable date with a 3-letter month
+ and 4-digit year. Examples include 'Feb 18-20, 2025' and 'Feb 18
+ and 20, 2025'. It may be in languages other than English, but the
+ month name should be kept short to aid formatting of the main
+ Carpentries web site.
+ """
+
+ if ',' not in date:
+ return False
+
+ month_dates, year = date.split(',')
+
+ # The first three characters of month_dates are not empty
+ month = month_dates[:3]
+ if any(char == ' ' for char in month):
+ return False
+
+ # But the fourth character is empty ("February" is illegal)
+ if month_dates[3] != ' ':
+ return False
+
+ # year contains *only* numbers
+ try:
+ int(year)
+ except:
+ return False
+
+ return True
+
+
+@look_for_fixme
+def check_humantime(time):
+ """
+ 'humantime' is a human-readable start and end time for the
+ workshop, such as '09:00 - 16:00'.
+ """
+
+ return bool(re.match(HUMANTIME_PATTERN, time.replace(' ', '')))
+
+
+def check_date(this_date):
+ """
+ 'startdate' and 'enddate' are machine-readable start and end dates
+ for the workshop, and must be in YYYY-MM-DD format, e.g.,
+ '2015-07-01'.
+ """
+
+ # YAML automatically loads valid dates as datetime.date.
+ return isinstance(this_date, date)
+
+
+@look_for_fixme
+def check_latitude_longitude(latlng):
+ """
+ 'latlng' must be a valid latitude and longitude represented as two
+ floating-point numbers separated by a comma.
+ """
+
+ try:
+ lat, lng = latlng.split(',')
+ lat = float(lat)
+ lng = float(lng)
+ return (-90.0 <= lat <= 90.0) and (-180.0 <= lng <= 180.0)
+ except ValueError:
+ return False
+
+
+def check_instructors(instructors):
+ """
+ 'instructor' must be a non-empty comma-separated list of quoted
+ names, e.g. ['First name', 'Second name', ...']. Do not use 'TBD'
+ or other placeholders.
+ """
+
+ # YAML automatically loads list-like strings as lists.
+ return isinstance(instructors, list) and len(instructors) > 0
+
+
+def check_helpers(helpers):
+ """
+ 'helper' must be a comma-separated list of quoted names,
+ e.g. ['First name', 'Second name', ...']. The list may be empty.
+ Do not use 'TBD' or other placeholders.
+ """
+
+ # YAML automatically loads list-like strings as lists.
+ return isinstance(helpers, list) and len(helpers) >= 0
+
+
+@look_for_fixme
+def check_emails(emails):
+ """
+ 'emails' must be a comma-separated list of valid email addresses.
+ The list may be empty. A valid email address consists of characters,
+ an '@', and more characters. It should not contain the default contact
+ """
+
+ # YAML automatically loads list-like strings as lists.
+ if (isinstance(emails, list) and len(emails) >= 0):
+ for email in emails:
+ if ((not bool(re.match(EMAIL_PATTERN, email))) or (email == DEFAULT_CONTACT_EMAIL)):
+ return False
+ else:
+ return False
+
+ return True
+
+
+def check_eventbrite(eventbrite):
+ """
+ 'eventbrite' (the Eventbrite registration key) must be 9 or more
+ digits. It may appear as an integer or as a string.
+ """
+
+ if isinstance(eventbrite, int):
+ return True
+ else:
+ return bool(re.match(EVENTBRITE_PATTERN, eventbrite))
+
+
+@look_for_fixme
+def check_collaborative_notes(collaborative_notes):
+ """
+ 'collaborative_notes' must be a valid URL.
+ """
+
+ return bool(re.match(URL_PATTERN, collaborative_notes))
+
+
+@look_for_fixme
+def check_pass(value):
+ """
+ This test always passes (it is used for 'checking' things like the
+ workshop address, for which no sensible validation is feasible).
+ """
+
+ return True
+
+
+HANDLERS = {
+ 'layout': (True, check_layout, 'layout isn\'t "workshop"'),
+
+ 'carpentry': (True, check_carpentry, 'carpentry isn\'t in ' +
+ ', '.join(CARPENTRIES)),
+
+ 'country': (True, check_country,
+ 'country invalid: must use lowercase two-letter ISO code ' +
+ 'from ' + ', '.join(ISO_COUNTRY)),
+
+ 'language': (False, check_language,
+ 'language invalid: must use lowercase two-letter ISO code' +
+ ' from ' + ', '.join(ISO_LANGUAGE)),
+
+ 'humandate': (True, check_humandate,
+ 'humandate invalid. Please use three-letter months like ' +
+ '"Jan" and four-letter years like "2025"'),
+
+ 'humantime': (True, check_humantime,
+ 'humantime doesn\'t include numbers'),
+
+ 'startdate': (True, check_date,
+ 'startdate invalid. Must be of format year-month-day, ' +
+ 'i.e., 2014-01-31'),
+
+ 'enddate': (False, check_date,
+ 'enddate invalid. Must be of format year-month-day, i.e.,' +
+ ' 2014-01-31'),
+
+ 'latlng': (True, check_latitude_longitude,
+ 'latlng invalid. Check that it is two floating point ' +
+ 'numbers, separated by a comma'),
+
+ 'instructor': (True, check_instructors,
+ 'instructor list isn\'t a valid list of format ' +
+ '["First instructor", "Second instructor",..]'),
+
+ 'helper': (True, check_helpers,
+ 'helper list isn\'t a valid list of format ' +
+ '["First helper", "Second helper",..]'),
+
+ 'email': (True, check_emails,
+ 'contact email list isn\'t a valid list of format ' +
+ '["me@example.org", "you@example.org",..] or contains incorrectly formatted email addresses or ' +
+ '"{0}".'.format(DEFAULT_CONTACT_EMAIL)),
+
+ 'eventbrite': (False, check_eventbrite, 'Eventbrite key appears invalid'),
+
+ 'collaborative_notes': (False, check_collaborative_notes, 'Collaborative Notes URL appears invalid'),
+
+ 'venue': (False, check_pass, 'venue name not specified'),
+
+ 'address': (False, check_pass, 'address not specified')
+}
+
+# REQUIRED is all required categories.
+REQUIRED = {k for k in HANDLERS if HANDLERS[k][0]}
+
+# OPTIONAL is all optional categories.
+OPTIONAL = {k for k in HANDLERS if not HANDLERS[k][0]}
+
+
+def check_blank_lines(reporter, raw):
+ """
+ Blank lines are not allowed in category headers.
+ """
+
+ lines = [(i, x) for (i, x) in enumerate(
+ raw.strip().split('\n')) if not x.strip()]
+ reporter.check(not lines,
+ None,
+ 'Blank line(s) in header: {0}',
+ ', '.join(["{0}: {1}".format(i, x.rstrip()) for (i, x) in lines]))
+
+
+def check_categories(reporter, left, right, msg):
+ """
+ Report differences (if any) between two sets of categories.
+ """
+
+ diff = left - right
+ reporter.check(len(diff) == 0,
+ None,
+ '{0}: offending entries {1}',
+ msg, sorted(list(diff)))
+
+
+def check_file(reporter, path, data):
+ """
+ Get header from file, call all other functions, and check file for
+ validity.
+ """
+
+ # Get metadata as text and as YAML.
+ raw, header, body = split_metadata(path, data)
+
+ # Do we have any blank lines in the header?
+ check_blank_lines(reporter, raw)
+
+ # Look through all header entries. If the category is in the input
+ # file and is either required or we have actual data (as opposed to
+ # a commented-out entry), we check it. If it *isn't* in the header
+ # but is required, report an error.
+ for category in HANDLERS:
+ required, handler, message = HANDLERS[category]
+ if category in header:
+ if required or header[category]:
+ reporter.check(handler(header[category]),
+ None,
+ '{0}\n actual value "{1}"',
+ message, header[category])
+ elif required:
+ reporter.add(None,
+ 'Missing mandatory key "{0}"',
+ category)
+
+ # Check whether we have missing or too many categories
+ seen_categories = set(header.keys())
+ check_categories(reporter, REQUIRED, seen_categories,
+ 'Missing categories')
+ check_categories(reporter, seen_categories, REQUIRED.union(OPTIONAL),
+ 'Superfluous categories')
+
+
+def check_config(reporter, filename):
+ """
+ Check YAML configuration file.
+ """
+
+ config = load_yaml(filename)
+
+ kind = config.get('kind', None)
+ reporter.check(kind == 'workshop',
+ filename,
+ 'Missing or unknown kind of event: {0}',
+ kind)
+
+ carpentry = config.get('carpentry', None)
+ reporter.check(carpentry in ('swc', 'dc', 'lc', 'cp'),
+ filename,
+ 'Missing or unknown carpentry: {0}',
+ carpentry)
+
+
+def main():
+ '''Run as the main program.'''
+
+ if len(sys.argv) != 2:
+ print(USAGE, file=sys.stderr)
+ sys.exit(1)
+
+ root_dir = sys.argv[1]
+ index_file = os.path.join(root_dir, 'index.html')
+ config_file = os.path.join(root_dir, '_config.yml')
+
+ reporter = Reporter()
+ check_config(reporter, config_file)
+ check_unwanted_files(root_dir, reporter)
+ with open(index_file, encoding='utf-8') as reader:
+ data = reader.read()
+ check_file(reporter, index_file, data)
+ reporter.report()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/code/.gitkeep b/code/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/config.yaml b/config.yaml
deleted file mode 100644
index 856948c..0000000
--- a/config.yaml
+++ /dev/null
@@ -1,73 +0,0 @@
-#------------------------------------------------------------
-# Values for this lesson.
-#------------------------------------------------------------
-
-# Which carpentry is this (swc, dc, lc, or cp)?
-# swc: Software Carpentry
-# dc: Data Carpentry
-# lc: Library Carpentry
-# cp: Carpentries (to use for instructor training for instance)
-# incubator: The Carpentries Incubator
-#
-# This option supports custom types so lessons can be branded
-# and themed with your own logo and alt-text (see `carpentry_description`)
-# See https://carpentries.github.io/sandpaper-docs/editing.html#adding-a-custom-logo
-carpentry: 'incubator'
-
-# Alt-text description of the lesson.
-carpentry_description: 'EIC Tutorials'
-
-# Overall title for pages.
-title: 'Understanding the Simulation Output'
-
-# Date the lesson was created (YYYY-MM-DD, this is empty by default)
-created: 2024-01-01
-
-# Comma-separated list of keywords for the lesson
-keywords: 'EIC, ePIC, simulation, DD4hep, EICrecon, reconstruction, uproot, xrootd, edm4eic, edm4hep'
-
-# Life cycle stage of the lesson
-# possible values: pre-alpha, alpha, beta, stable
-life_cycle: 'stable'
-
-# License of the lesson
-license: 'CC-BY 4.0'
-
-# Link to the source repository for this lesson
-source: 'https://github.com/eic/tutorial-understanding-sim-output'
-
-# Default branch of your lesson
-branch: 'main'
-
-# Who to contact if there are any issues
-contact: 'stephen.kay@york.ac.uk'
-
-# Navigation ------------------------------------------------
-#
-# Use the following menu items to specify the order of
-# individual pages in each dropdown section. Leave blank to
-# include all pages in the folder.
-
-# Order of episodes in your lesson
-episodes:
-- 00-setup.md
-- 01-dd4hep.md
-- 02-eicrecon.md
-
-# Information for Learners
-learners:
-- setup.md
-- reference.md
-
-# Information for Instructors
-instructors:
-- instructor-notes.md
-
-# Learner Profiles
-profiles:
-- learner-profiles.md
-
-# Customisation ---------------------------------------------
-#
-# This space below is where custom yaml items (e.g. pinning
-# sandpaper and varnish versions) should live
diff --git a/data/.gitkeep b/data/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/episodes/00-setup.md b/episodes/00-setup.md
deleted file mode 100644
index 0aa73f4..0000000
--- a/episodes/00-setup.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-title: "Set up the Jupyter Notebook Environment"
-teaching: 5
-exercises: 5
----
-
-::::::::::::::::::::::::::::::::::::::::::::: questions
-
-- How to access simulation campaign output with python?
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-::::::::::::::::::::::::::::::::::::::::::::: objectives
-
-- Set up interactive python platform with Jupyter-notebook.
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-## Set up Jupyter-notebook
-
-- on Google Colab (recommended):
- - copy the [example Colab notebook](https://colab.research.google.com/drive/1Wn9guq1aIJ8RUW36HHTkeR7-iPPcoBOw?usp=sharing) to your own google drive.
- - run the first cell under "setup" to install `uproot` and `xrootd` (takes ~10 minutes)
-- on your local environment:
- - if not yet, install `uproot` and `xrootd` with your local package manager, __or__, in jupyter-notebook, run
-
-```bash
-!pip install xrootd
-!pip install uproot
-!pip install fsspec-xrootd
-!pip install particle ## optional
-```
-
-::::::::::::::::::::::::::::::::::::::::::::: callout
-
-Note: we will be using other standard python packages such as `numpy` and `pandas`, which are pre-installed on Colab.
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-::::::::::::::::::::::::::::::::::::::::::::: keypoints
-
-- install uproot and xrootd
-
-:::::::::::::::::::::::::::::::::::::::::::::
diff --git a/episodes/01-dd4hep.md b/episodes/01-dd4hep.md
deleted file mode 100644
index 23637f1..0000000
--- a/episodes/01-dd4hep.md
+++ /dev/null
@@ -1,169 +0,0 @@
----
-title: "Introduction to the DD4hep simulation"
-teaching: 25
-exercises: 15
----
-
-::::::::::::::::::::::::::::::::::::::::::::: questions
-
-- Understand the inputs and outputs of the DD4hep simulation
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-::::::::::::::::::::::::::::::::::::::::::::: objectives
-
-- Event generation
-- Detector description
-- MCParticles and detector hits
-- Simulation campaign files
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-## DD4hep simulation
-
-This Geant4-based simulation package propagates particles through magnetic field and materials. Particles and detector hits for each event are saved in the output rootfiles.
-
-### __Input 1: Event generation__
-
-The collision event at ePIC, including the beam particles, vertices, and outgoing particles, are typically generated with a dedicated event generator, e.g. PYTHIA8 for specific physics channels. The outputs are provided to the DD4hep simulation in [HEPMC3 format](https://arxiv.org/pdf/1912.08005).
-
-One can also use the DD4hep's particle gun to generate outgoing particles with given vertex and distribution, see the [Simulations Using npsim and Geant4 tutorial](https://eic.github.io/tutorial-simulations-using-npsim-and-geant4/) on `ddsim`.
-
-### __Input 2: Detector description__
-
-The ePIC detector description in DD4hep is maintained in the [eic/epic repository on GitHub](https://github.com/eic/epic/). On the bottom of each sub-detector compact file under `epic/compact`, the `readout` block specifies how the detector hits are saved in the output rootfile.
-
-Below is an example from `epic/compact/tracking/vertex_barrel.xml`:
-
-```xml
-
-
-
- system:8,layer:4,module:12,sensor:2,x:32:-16,y:-16
-
-
-```
-
-All hits from this silicon vertex barrel detector, including their position, energy deposit, time, will be stored under the branch `VertexBarrelHits` in output.
-Each detector hit also comes with an assigned 64-bit cell ID, with the last 32 bits from right to left representing the hit location in a 0.020 x 0.020 mm mesh grid. This __segmentation__ often represents the detector granularity (in this case, the silicon pixel sensor size) that will be used later for hit digitization.
-
-### __Output__
-
-The `event` tree in the simulation output contains
-
-- `MCParticles`: records the truth info of primary and secondary particles
-- Individual branches for signals from various sub-detector systems e.g. `VertexBarrelHits`
-
-::::::::::::::::::::::::::::::::::::::::::::: challenge
-
-## Exercise 1.1: access simulation campaign rootfiles
-
-The simulation campaign [dataset documentation](https://eic.github.io/epic-prod/documentation/default_datasets.html) documents the available datasets and version information.
-
-- __Browse the directory__
-
- For the stand-alone `xrdfs` command, see the [previous Analysis tutorial](https://eic.github.io/tutorial-analysis/). Here we will proceed with the python interface:
-
- ```python
- from XRootD import client
- # Create XRootD client
- eic_server = 'root://dtn-eic.jlab.org/'
- fs = client.FileSystem(eic_server)
- # List directory contents
- fpath = '/volatile/eic/EPIC/RECO/26.02.0/epic_craterlake/SINGLE/e-/10GeV/130to177deg/'
- status, files = fs.dirlist(fpath)
- # Print files
- if status.ok:
- print(files.size)
- for entry in files:
- print(entry.name)
- else:
- print(f"Error: {status.message}")
- ```
-
-- __Open a simulation campaign file__
-
- ```python
- fname = eic_server+fpath+'e-_10GeV_130to177deg.0000.eicrecon.edm4eic.root'
- tree_name = "events"
- # tree_name = "podio_metadata"
- tree = ur.open(fname)[tree_name]
- print(f"Read {fname}:{tree_name}. \n {tree.num_entries} events in total")
- ```
-
-::::::::::::::: solution
-
-`fs.dirlist` returns the list of files available in the campaign directory, and `ur.open(...)[tree_name]`
-opens the `events` tree and reports the number of entries. If the directory listing fails, check that
-the campaign version in `fpath` still exists on the server (campaigns roll over and older versions are
-removed).
-
-:::::::::::::::
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-::::::::::::::::::::::::::::::::::::::::::::: challenge
-
-## Exercise 1.2: inspect available branches in a rootfile
-
-- use `tree.keys(filter_name="*",recursive=False)` to display all branches
-- extract a given branch to dataframe
-
-```python
-bname = "MCParticles"
-df = tree[bname].array(library="ak")
-df = ak.to_dataframe(df)
-print(df)
-```
-
-::::::::::::::: solution
-
-`tree.keys(...)` lists the top-level branches (e.g. `MCParticles` and the per-detector hit
-collections). Reading `MCParticles` into an awkward array and converting to a dataframe gives one row
-per particle, with columns such as `MCParticles.PDG`, `MCParticles.generatorStatus`, and the momentum
-components.
-
-:::::::::::::::
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-::::::::::::::::::::::::::::::::::::::::::::: challenge
-
-## Exercise 1.3: extract momentum distribution of primary electrons
-
-```python
-# select electrons
-from particle import Particle
-part = Particle.from_name("e-")
-pdg_id = part.pdgid.abspid
-condition1 = df["MCParticles.PDG"]==pdg_id
-# select primary particles
-condition2 = df["MCParticles.generatorStatus"]==1
-# extract momentum and plot
-# all electrons
-df_new = df[condition1]
-mom = np.sqrt(df_new["MCParticles.momentum.x"]**2+df_new["MCParticles.momentum.y"]**2+df_new["MCParticles.momentum.z"]**2)
-bins = np.arange(0,20)
-_ = plt.hist(mom,bins=bins,alpha=0.5)
-# primary electrons
-df_new = df[condition1&condition2]
-mom = np.sqrt(df_new["MCParticles.momentum.x"]**2+df_new["MCParticles.momentum.y"]**2+df_new["MCParticles.momentum.z"]**2)
-_ = plt.hist(mom,bins=bins,histtype="step", color='r')
-```
-
-::::::::::::::: solution
-
-The first histogram (filled) shows the momentum of all electrons, including secondaries; the second
-(red outline), obtained by additionally requiring `generatorStatus==1`, shows only the primary
-electrons. The primary distribution is a subset of the all-electron distribution, peaked near the
-generated beam/scattered-electron momentum.
-
-:::::::::::::::
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-::::::::::::::::::::::::::::::::::::::::::::: keypoints
-
-- event generator --`dd4hep`--> simulated hits and particles
-
-:::::::::::::::::::::::::::::::::::::::::::::
diff --git a/episodes/02-eicrecon.md b/episodes/02-eicrecon.md
deleted file mode 100644
index 269e876..0000000
--- a/episodes/02-eicrecon.md
+++ /dev/null
@@ -1,210 +0,0 @@
----
-title: "Reconstruction workflow"
-teaching: 25
-exercises: 15
----
-
-::::::::::::::::::::::::::::::::::::::::::::: questions
-
-- Understand the EICrecon workflow
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-::::::::::::::::::::::::::::::::::::::::::::: objectives
-
-- Hit digitization
-- data model
-- Reconstruction algorithms
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-## Reconstruction workflow
-
-The ePIC reconstruction framework `EICrecon` is maintained in the [eic/eicrecon repository on GitHub](https://github.com/eic/eicrecon/). It processes simulated hits from various detectors to reconstruct trajectory, PID, etc, and eventually reconstruct the simulated particle and physics observables at the vertex. In this section, we will use __track reconstruction__ as an example. Please refer to the [Lehigh reconstruction workfest presentations](https://indico.bnl.gov/event/20727/sessions/7433/#20240725) for the reconstruction workflow of other systems.
-
-Each reconstruction step involves 3 components:
-
-- the algorithm,
-- the JOmniFactory where algorithm and data type are declared,
-- the factory generator to execute the algorithm.
-
-### __Digitization__
-
-All simulated detector hits are digitized to reflect certain detector specs e.g. spatial and time resolution, and energy threshold. For example, the `VertexBarrelHits` from simulation are digitized through the `SiliconTrackerDigi` factory in `EICrecon/src/detectors/BVTX/BVTX.cc`:
-
-```c++
- // Digitization
- app->Add(new JOmniFactoryGeneratorT(
- "SiBarrelVertexRawHits", // factory name
- {
- "VertexBarrelHits" // input
- },
- {
- "SiBarrelVertexRawHits", // outputs
- "SiBarrelVertexRawHitAssociations"
- },
- {
- .threshold = 0.54 * dd4hep::keV, // configurations
- },
- app
- ));
-```
-
-The actual algorithm locates at `EICrecon/src/algorithms/digi/SiliconTrackerDigi.cc`, with its input and output specified in `SiliconTrackerDigi_factory.h`:
-
-```c++
- class SiliconTrackerDigi_factory : public JOmniFactory {
-
- public:
- using AlgoT = eicrecon::SiliconTrackerDigi;
- private:
- std::unique_ptr m_algo;
-
- PodioInput m_sim_hits_input {this};
-
- PodioOutput m_raw_hits_output {this};
- PodioOutput m_assoc_output {this};
-
- ParameterRef m_threshold {this, "threshold", config().threshold};
- ParameterRef m_timeResolution {this, "timeResolution", config().timeResolution};
- ...
-```
-
-By comparing the two blocks of code above, we can see that the digitized hits, `SiBarrelVertexRawHits`, are stored in the data type `RawTrackerHit` that is defined in the [edm4eic data model](https://github.com/eic/EDM4eic/blob/main/edm4eic.yaml):
-
-```yaml
-edm4eic::RawTrackerHit:
- Description: "Raw (digitized) tracker hit"
- Author: "W. Armstrong, S. Joosten"
- Members:
- - uint64_t cellID // The detector specific (geometrical) cell id from segmentation
- - int32_t charge
- - int32_t timeStamp
-```
-
-In addition, the one-to-one relation between the sim hit and its digitized hit is stored as an `MCRecoTrackerHitAssociation` object:
-
-```yaml
-edm4eic::MCRecoTrackerHitAssociation:
- Description: "Association between a RawTrackerHit and a SimTrackerHit"
- Author: "C. Dilks, W. Deconinck"
- Members:
- - float weight // weight of this association
- OneToOneRelations:
- - edm4eic::RawTrackerHit rawHit // reference to the digitized hit
- - edm4hep::SimTrackerHit simHit // reference to the simulated hit
-```
-
-which is filled in `SiliconTrackerDigi.cc`:
-
-```c++
- auto hitassoc = associations->create();
- hitassoc.setWeight(1.0);
- hitassoc.setRawHit(item.second);
- hitassoc.setSimHit(sim_hit);
-```
-
-::::::::::::::::::::::::::::::::::::::::::::: challenge
-
-## Exercise 2.1
-
-Please find other detector systems that use `SiliconTrackerDigi` for digitization.
-
-::::::::::::::: solution
-
-Search the detector setup files under `EICrecon/src/detectors/` for `SiliconTrackerDigi_factory`.
-Besides the barrel vertex tracker (`BVTX`), the other silicon tracker subsystems (e.g. the silicon
-barrel and endcap trackers) use the same digitization factory, each with its own input hit collection
-and threshold configuration.
-
-:::::::::::::::
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-### __Track Reconstruction__
-
-By default, we use the Combinatorial Kalman Filter from the ACTS library to handle track finding and fitting. This happens in the `CKFTracking` factory.
-
-::::::::::::::::::::::::::::::::::::::::::::: challenge
-
-## Exercise 2.2
-
-Please find the inputs and outputs of `CKFTracking`, and draw a flow chart from `CentralTrackerMeasurements` to `CentralTrackVertices`.
-
-::::::::::::::: solution
-
-Look up the `CKFTracking` factory in the tracking detector setup (`EICrecon/src/global/tracking/`).
-Its input is the collection of 2D measurements (`CentralTrackerMeasurements`) and its outputs are the
-track candidates/trajectories that are then turned into track parameters and, downstream, the vertices.
-The flow chart follows the chain measurements → trajectories → track parameters → vertices.
-
-:::::::::::::::
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-### __Reconstruction output__
-
-- __events__ tree:
- - `MCParticles` and detector sim hits are copied from simulation output to recon output
- - outputs from each step of recon algorithms must be either an `edm4hep` or `edm4eic` object if you want to save them in recon output
- - the default list of saved objects in recon output is defined in `EICrecon/src/services/io/podio/JEventProcessorPODIO.cc`. It can be configured on the command line.
-- __podio_metadata__ tree:
- - `events___CollectionTypeInfo` provides a lookup table between output collection name and IDs (its `collectionID` and `name` members line up index-by-index; older files exposed this as `events___idTable`).
-
-::::::::::::::::::::::::::::::::::::::::::::: challenge
-
-## Exercise 2.3
-
-The __vector member__ or __relation__ of a given data collection is saved in a separate branch that starts with "_".
-
-- Please use
-
- ```python
- tree.keys(filter_name="_CentralCKFTrajectories*", recursive=False)
- ```
-
- to list those members in `CentralCKFTrajectories`
-- for a given event, the vector member `_CentralCKFTrajectories_measurementChi2` provides a list of chi2 for each measurement hit respectively. If multiple trajectories are found for one event, you can use `CentralCKFTrajectories.measurementChi2_begin` to locate the start index of a given trajectory (subentry).
-
-::::::::::::::: solution
-
-The `tree.keys(...)` call returns the branches prefixed with `_CentralCKFTrajectories_`, including
-`_CentralCKFTrajectories_measurementChi2`. To read the chi2 values belonging to a particular
-trajectory, use the `measurementChi2_begin`/`measurementChi2_end` indices of that trajectory as the
-slice into the flat `_CentralCKFTrajectories_measurementChi2` vector.
-
-:::::::::::::::
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-::::::::::::::::::::::::::::::::::::::::::::: challenge
-
-## Exercise 2.4
-
-`CentralTrackerMeasurements` saves all available space points for tracking as 2D measurements attached to representing surfaces.
-
-- Please use the relation `_CentralTrackerMeasurements_hits` to trace back to the original detector hit collection (hint: use the collection ID lookup table), and obtain the 3D coordinate of the hit.
-
-::::::::::::::: solution
-
-Each entry of `_CentralTrackerMeasurements_hits` is an object ID holding a collection ID
-(`_CentralTrackerMeasurements_hits.collectionID`) and an index (`_CentralTrackerMeasurements_hits.index`).
-Use the `events___CollectionTypeInfo` branch in the `podio_metadata` tree to build a
-`collectionID -> name` map (zip its `.collectionID` and `.name` members), map the collection ID back to
-the original hit collection name, then index into that collection to read the hit's 3D position.
-
-:::::::::::::::
-
-:::::::::::::::::::::::::::::::::::::::::::::
-
-## What's next
-
-- Generate your own simulation and reconstruction rootfiles: [Simulations Using npsim and Geant4 tutorial](https://eic.github.io/tutorial-simulations-using-npsim-and-geant4/)
-- Contribute to reconstruction algorithms: [Reconstruction Algorithms tutorial](https://eic.github.io/tutorial-reconstruction-algorithms/)
-- Develop analysis benchmarks: [Developing Benchmarks tutorial](https://eic.github.io/tutorial-developing-benchmarks/)
-
-::::::::::::::::::::::::::::::::::::::::::::: keypoints
-
-- simulated hits--`EICrecon`--> reconstructed quantities
-
-:::::::::::::::::::::::::::::::::::::::::::::
diff --git a/fig/.gitkeep b/fig/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/files/.gitkeep b/files/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/index.md b/index.md
index 640c621..f4dbb37 100644
--- a/index.md
+++ b/index.md
@@ -1,28 +1,20 @@
---
-site: sandpaper::sandpaper_site
+layout: lesson
+root: . # Is the only page that doesn't follow the pattern /:path/index.html
+permalink: index.html # Is the only page that doesn't follow the pattern /:path/index.html
---
-
-Welcome to the EIC Tutorial on understanding the simulation output!
-
-This tutorial explains how the ePIC simulation and reconstruction output is structured, and how to
-access and inspect it with Python (`uproot`/`xrootd`) and the `edm4hep`/`edm4eic` data models.
-
-::::::::::::::::::::::::::::::::::::::::::::: prereq
-
-## Prerequisites
-
-Knowledge of CERN ROOT, and the ability to run a Jupyter notebook through Google Colab, or a local
-environment with `uproot` and `xrootd` installed.
-
-This tutorial follows other tutorials in the EIC series:
-
-- [Setting Up Your Environment](https://eic.github.io/tutorial-setting-up-environment/)
-- [DD4hep Simulation](https://eic.github.io/tutorial-simulations-using-npsim-and-geant4/)
-- [Analysis](https://eic.github.io/tutorial-analysis/)
-
-Further information:
-
-- [Geometry Development](https://eic.github.io/tutorial-geometry-development-using-dd4hep/), section 1
-- [Reconstruction Algorithms](https://eic.github.io/tutorial-reconstruction-algorithms/), sections 1, 6, and 7
-
-:::::::::::::::::::::::::::::::::::::::::::::
+> ## Prerequisites
+> Knowledge of CERN ROOT. And able to run jupyter-notebook through Google Colab, or set up your own local environment with uproot and xrootd installed.
+>
+> This tutorial follows other tutorial in the EIC series:
+> - [Setting Up Your Environment](https://eic.github.io/tutorial-setting-up-environment/)
+> - [DD4hep Simulation](https://eic.github.io/tutorial-simulations-using-npsim-and-geant4/)
+> - [Analysis](https://eic.github.io/tutorial-analysis/)
+>
+> Further information:
+>
+> - [Geometry Development](https://eic.github.io/tutorial-geometry-development-using-dd4hep/), section 1
+> - [Reconstruction Algorithms](https://eic.github.io/tutorial-reconstruction-algorithms/) , section 1, 6, and 7
+{: .prereq}
+
+{% include links.md %}
diff --git a/instructors/instructor-notes.md b/instructors/instructor-notes.md
deleted file mode 100644
index 0f80b8e..0000000
--- a/instructors/instructor-notes.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-title: "Instructor Notes"
----
-
-This tutorial follows the environment, simulation, and analysis tutorials in the EIC series. It
-assumes learners already have working knowledge of CERN ROOT and Python.
-
-## Before the session
-
-- Ask learners to complete the [Setup](../learners/setup.md) page in advance, in particular the
- installation of `uproot` and `xrootd`. On Google Colab the setup cell takes about 10 minutes to
- run, so it should ideally be done before the live session.
-- The lesson streams simulation-campaign files over `xrootd`. Check that the campaign paths in the
- episodes are still accessible before teaching (campaigns roll over and older ones are removed).
-
-## Timing
-
-The lesson is about 55 minutes of teaching plus exercises. Most of the exercises are hands-on
-inspection of the simulation and reconstruction output in the notebook environment.
diff --git a/learners/reference.md b/learners/reference.md
deleted file mode 100644
index 67bb9e6..0000000
--- a/learners/reference.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-title: 'Reference'
----
-
-## Glossary
-
-EIC
-: Electron-Ion Collider
-
-DD4hep
-: The detector-description and Geant4-based simulation toolkit used to propagate particles through
- the ePIC detector geometry and produce simulated hits.
-
-EICrecon
-: The ePIC reconstruction framework, which processes simulated hits into reconstructed quantities
- such as tracks and particles.
-
-edm4hep / edm4eic
-: The PODIO-based event data models used to store the ePIC simulation and reconstruction output.
diff --git a/learners/setup.md b/learners/setup.md
deleted file mode 100644
index 4aca7bd..0000000
--- a/learners/setup.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-title: Setup
----
-
-If you have not done so already, please follow the instructions on the
-[Setting Up Your Environment](https://eic.github.io/tutorial-setting-up-environment/) tutorial well
-before the start of the tutorial to ensure your system is ready.
-
-For this tutorial you will additionally need a way to run Python interactively with `uproot` and
-`xrootd` available. See the first episode,
-[Set up the Jupyter Notebook Environment](../episodes/00-setup.md), for the recommended options
-(Google Colab or a local environment).
diff --git a/links.md b/links.md
deleted file mode 100644
index 4c5cd2f..0000000
--- a/links.md
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-[pandoc]: https://pandoc.org/MANUAL.html
-[r-markdown]: https://rmarkdown.rstudio.com/
-[rstudio]: https://www.rstudio.com/
-[carpentries-workbench]: https://carpentries.github.io/sandpaper-docs/
-
diff --git a/profiles/learner-profiles.md b/profiles/learner-profiles.md
deleted file mode 100644
index b7462fa..0000000
--- a/profiles/learner-profiles.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: 'Learner Profiles'
----
-
-This lesson targets people who already have some experience with CERN ROOT and Python and who are
-starting to work with ePIC simulation and reconstruction output — students, postdocs, and
-researchers in the ePIC collaboration who need to understand how the DD4hep and EICrecon output
-files are structured and how to read them with `uproot`/`xrootd` and the `edm4hep`/`edm4eic` data
-models.
diff --git a/reference.md b/reference.md
new file mode 100644
index 0000000..d6fe4ff
--- /dev/null
+++ b/reference.md
@@ -0,0 +1,9 @@
+---
+layout: reference
+---
+
+## Glossary
+
+EIC: Electron-Ion Collider
+
+{% include links.md %}
diff --git a/setup.md b/setup.md
new file mode 100644
index 0000000..7ac5396
--- /dev/null
+++ b/setup.md
@@ -0,0 +1,6 @@
+---
+title: Setup
+---
+If you have not done so already, please follow the instructions [here](https://eic.github.io/tutorial-setting-up-environment/setup.html) well before the start of the tutorial to ensure your system is ready.
+
+{% include links.md %}
diff --git a/site/README.md b/site/README.md
deleted file mode 100644
index 0a00291..0000000
--- a/site/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-This directory contains rendered lesson materials. Please do not edit files
-here.