=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
deleted file mode 100644
index 04e1f5a..0000000
--- a/bin/boilerplate/AUTHORS
+++ /dev/null
@@ -1 +0,0 @@
-FIXME: list authors' names and email addresses.
\ No newline at end of file
diff --git a/bin/boilerplate/CITATION b/bin/boilerplate/CITATION
deleted file mode 100644
index 56ece3c..0000000
--- a/bin/boilerplate/CITATION
+++ /dev/null
@@ -1 +0,0 @@
-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
deleted file mode 100644
index 8c095d8..0000000
--- a/bin/boilerplate/CONTRIBUTING.md
+++ /dev/null
@@ -1,151 +0,0 @@
-# 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
deleted file mode 100644
index 060994a..0000000
--- a/bin/boilerplate/README.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# 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
deleted file mode 100644
index f2ca373..0000000
--- a/bin/boilerplate/_config.yml
+++ /dev/null
@@ -1,120 +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 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
deleted file mode 100644
index 2e156c2..0000000
--- a/bin/boilerplate/_episodes/01-introduction.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-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
deleted file mode 100644
index 5f07f65..0000000
--- a/bin/boilerplate/_extras/about.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: About
----
-{% include carpentries.html %}
-{% include links.md %}
diff --git a/bin/boilerplate/_extras/discuss.md b/bin/boilerplate/_extras/discuss.md
deleted file mode 100644
index bfc33c5..0000000
--- a/bin/boilerplate/_extras/discuss.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Discussion
----
-FIXME
-
-{% include links.md %}
diff --git a/bin/boilerplate/_extras/figures.md b/bin/boilerplate/_extras/figures.md
deleted file mode 100644
index 0012c88..0000000
--- a/bin/boilerplate/_extras/figures.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-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
deleted file mode 100644
index 50f266f..0000000
--- a/bin/boilerplate/_extras/guide.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: "Instructor Notes"
----
-FIXME
-
-{% include links.md %}
diff --git a/bin/boilerplate/index.md b/bin/boilerplate/index.md
deleted file mode 100644
index 95ccdbd..0000000
--- a/bin/boilerplate/index.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-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
deleted file mode 100644
index 8c82616..0000000
--- a/bin/boilerplate/reference.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-layout: reference
----
-
-## Glossary
-
-FIXME
-
-{% include links.md %}
diff --git a/bin/boilerplate/setup.md b/bin/boilerplate/setup.md
deleted file mode 100644
index b8c5032..0000000
--- a/bin/boilerplate/setup.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-title: Setup
----
-FIXME
-
-
-{% include links.md %}
diff --git a/bin/chunk-options.R b/bin/chunk-options.R
deleted file mode 100644
index 8e0d62a..0000000
--- a/bin/chunk-options.R
+++ /dev/null
@@ -1,70 +0,0 @@
-# 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
deleted file mode 100644
index 4eeeb21..0000000
--- a/bin/dependencies.R
+++ /dev/null
@@ -1,107 +0,0 @@
-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
deleted file mode 100644
index 7fb4c5a..0000000
--- a/bin/generate_md_episodes.R
+++ /dev/null
@@ -1,44 +0,0 @@
-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
deleted file mode 100755
index 0280f24..0000000
--- a/bin/install_r_deps.sh
+++ /dev/null
@@ -1 +0,0 @@
-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
deleted file mode 100755
index 141c136..0000000
--- a/bin/knit_lessons.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/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
deleted file mode 100644
index 86e4249..0000000
--- a/bin/lesson_check.py
+++ /dev/null
@@ -1,628 +0,0 @@
-"""
-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
deleted file mode 100644
index 79ec05c..0000000
--- a/bin/lesson_initialize.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""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
deleted file mode 100755
index 2ef3f77..0000000
--- a/bin/markdown_ast.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/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
deleted file mode 100644
index 6988ca5..0000000
--- a/bin/repo_check.py
+++ /dev/null
@@ -1,181 +0,0 @@
-"""
-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
deleted file mode 100644
index 550dbf0..0000000
--- a/bin/reporter.py
+++ /dev/null
@@ -1,75 +0,0 @@
-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
deleted file mode 100755
index 1e09178..0000000
--- a/bin/run-make-docker-serve.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/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
deleted file mode 100644
index 7a6d603..0000000
--- a/bin/test_lesson_check.py
+++ /dev/null
@@ -1,19 +0,0 @@
-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
deleted file mode 100644
index 1398c37..0000000
--- a/bin/util.py
+++ /dev/null
@@ -1,111 +0,0 @@
-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
deleted file mode 100644
index 312b1a1..0000000
--- a/bin/workshop_check.py
+++ /dev/null
@@ -1,419 +0,0 @@
-'''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
deleted file mode 100644
index e69de29..0000000
diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..7e0dba5
--- /dev/null
+++ b/config.yaml
@@ -0,0 +1,75 @@
+#------------------------------------------------------------
+# 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: 'Finding and Accessing EIC Simulation Files'
+
+# Date the lesson was created (YYYY-MM-DD, this is empty by default)
+created: 2026-01-01
+
+# Comma-separated list of keywords for the lesson
+keywords: 'EIC, ePIC, Rucio, xrootd, simulation campaign, data management, file access, DID, metadata'
+
+# 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-file-access'
+
+# 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:
+- 01-introduction.md
+- 02-rucio_usage.md
+- 03-use_cases.md
+
+# Information for Learners
+learners:
+- setup.md
+- reference.md
+- metadata-tags.md
+- xrootd.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
deleted file mode 100644
index e69de29..0000000
diff --git a/_episodes/01-introduction.md b/episodes/01-introduction.md
similarity index 52%
rename from _episodes/01-introduction.md
rename to episodes/01-introduction.md
index d3a95ea..241cb4e 100644
--- a/_episodes/01-introduction.md
+++ b/episodes/01-introduction.md
@@ -1,34 +1,42 @@
---
title: "Introduction"
teaching: 10
-exercises:
-questions:
-- "How are EIC/ePIC simulation outputs organised?"
-objectives:
-- "Understand how the simulation output is organised"
-- "Find out how to request a new simulation"
-- "Discover the tools that are available to browse and access the simulation output"
-keypoints:
-- "Simulation campaigns run on a regular (monthly basis)"
-- "Input requests **must** be formatted in a specific way and meet certain pre-requisites"
-- "Rucio is the primary way to browse and access simulated EIC/ePIC data"
+exercises: 0
---
-# Simulation Campaigns
+::::::::::::::::::::::::::::::::::::::::::::: questions
+
+- How are EIC/ePIC simulation outputs organised?
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: objectives
+
+- Understand how the simulation output is organised
+- Find out how to request a new simulation
+- Discover the tools that are available to browse and access the simulation output
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+## Simulation Campaigns
Simulations of a range of physics processes in the ePIC detector are typically run on a monthly basis by the Production Working Group. Information on simulation campaigns can be found on the [Production Working Group pages](https://eic.github.io/epic-prod/). This includes details of files produced in previous campaigns.
-A list of current request from Detector Subsystem Co-ordinators and the Physics Analysis Co-ordinators can be found [here](https://docs.google.com/spreadsheets/d/1BJeq3AYwefNC9m3palH6T0SHMxmRmHpOzLTSa_6SZIU/edit?usp=sharing).
+A list of current requests from Detector Subsystem Co-ordinators and the Physics Analysis Co-ordinators can be found in the [simulation requests spreadsheet](https://docs.google.com/spreadsheets/d/1BJeq3AYwefNC9m3palH6T0SHMxmRmHpOzLTSa_6SZIU/edit?usp=sharing).
Campaigns are designated by a standardised format - **YY.MM.Ver**
+
- YY - Year the campaign ran, e.g. 26 is 2026
- MM - Month the campaign ran, e.g. 02 is February
- Ver - Version of the campaign, starts from 0. May have different versions
These are linked to specific software releases following the same format.
-> **Note that campaigns more than ~6 months old will not directly be accessible using the methods we will explore in this tutorial.**
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+**Note that campaigns more than ~6 months old will not directly be accessible using the methods we will explore in this tutorial.**
+
+:::::::::::::::::::::::::::::::::::::::::::::
Various types of files are produced as part of the simulation campaign as we will discuss in the next section. The files you may wish to access will differ depending upon your use case. In this tutorial, we will explore a few different common use cases and the types of files you may want in each.
@@ -36,37 +44,49 @@ Various types of files are produced as part of the simulation campaign as we wil
If you would like to submit a new request to a future campaign for a dataset that is not in production, please follow the following process:
-1. Coordinate with your physics or detector working group and the detector subsystem or physics analysis co-ordinators to add your request to the overview spreadsheet and assign a priority.
+1. Coordinate with your physics or detector working group and the detector subsystem or physics analysis co-ordinators to add your request to the overview spreadsheet and assign a priority.
2. Generate the Monte-Carlo input for your new request.
- Please follow the [pre-processing guidelines](https://github.com/eic/epic-prod/blob/main/docs/_documentation/input_preprocessing.md) when preparing your new input files for submission.
-3. Once your input files are ready, submit a [simulation request form](https://urldefense.proofpoint.com/v2/url?u=https-3A__docs.google.com_forms_d_e_1FAIpQLScDqiEaHayAcwBDGAWa4W6k-2D6yUFzS-2DXiWuhLolpy64mLk5FA_viewform&d=DwMFAg&c=CJqEzB1piLOyyvZjb8YUQw&r=1bclzxVlhTV419LkWWxwLTl3ztSqyuA_Q_Vnypx1RD4&m=q9b8IbAHm_MLsvy4XkI2Px2QKzFNqjpf0qc4nctB9ZHyf-uL5bZuiegs5-hwb-Ec&s=TFdsmJL2wPtUD-CCXVdkWIF5lxB1QYbz5MKGhB6nroA&e=).
+3. Once your input files are ready, submit a [simulation request form](https://docs.google.com/forms/d/e/1FAIpQLScDqiEaHayAcwBDGAWa4W6k-6yUFzS-XiWuhLolpy64mLk5FA/viewform).
- If your input is not pre-processed following the [pre-processing guidelines](https://github.com/eic/epic-prod/blob/main/docs/_documentation/input_preprocessing.md), it will not be simulated. Please review these carefully.
-# Simulation Files Organisation
+## Simulation Files Organisation
+
+Within a simulation campaign, there are three broad classes of files that are produced:
-Within a simulation campaign, there are three broad classes of files that are produce:
- EVGEN: The input hepmc3 datasets
- E.g. some files that have been supplied by a physics event generator
- FULL: The full GEANT4 output root files (usually only saved for a fraction of runs)
- - If running a simulation yourself, this would be your output from processing npsim
+ - If running a simulation yourself, this would be your output from processing [npsim](https://eic.github.io/tutorial-simulations-using-npsim-and-geant4/)
- RECO: The output root files from the reconstruction
- - And again, if running yourself, this would be your output from EICrecon (after you've used your awesome new reconstruction algorithm from the later tutorial of course)
+ - And again, if running yourself, this would be your output from EICrecon (after you've used your awesome new [reconstruction algorithm from the later tutorial](https://eic.github.io/tutorial-reconstruction-algorithms/) of course)
Most users and use cases will interact with RECO files, the output of the full simulation and reconstruction chain. We will explore some use cases and how to find the relevant files in each case.
-# How can I Browse the Simulation Campaign Output and Access Files?
+## How can I Browse the Simulation Campaign Output and Access Files?
To browse the campaign output and find the files we want, we can use [Rucio](https://rucio.cern.ch/). *Rucio* is an open source scientific data management system. It is utilised in other large physics experiments such as ATLAS.
-## Wait, I read I should use XrootD to find and access files?
+### Wait, I read I should use XrootD to find and access files?
-You may find reference to or instructions on using [XrootD]({{ page.root }}{% link _extras/xrootd.md %}) to browse and access files. These may still work and indeed, we will use some of these commands later in this tutorial. However, Rucio is now the preferred method for the cases we will examine. **The recommended workflow is now:**
+You may find reference to or instructions on using [XrootD](../learners/xrootd.md) to browse and access files. These may still work and indeed, we will use some of these commands later in this tutorial. However, Rucio is now the preferred method for the cases we will examine. **The recommended workflow is now:**
1. Find file location with Rucio
2. Stream or download with XrootD
-Why? This change isn't just to make everybody learn something new, it is also a consequence of the expansion of the volume of ePIC data now available. Previously (before 2026), all simulated data was stored on Jefferson Lab servers. However, data is now spread between multiple sites. This makes finding an accessing it using XrootD more complicated. Rucio can deal with this "issue" in a straightforward way.
+Why? This change isn't just to make everybody learn something new, it is also a consequence of the expansion of the volume of ePIC data now available. Previously (before 2026), all simulated data was stored on Jefferson Lab servers. However, data is now spread between multiple sites. This makes finding and accessing it using XrootD more complicated. Rucio can deal with this "issue" in a straightforward way.
+
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+You may also find reference to an S3 server. This is now deprecated and cannot be used.
+If you find such references or instructions to S3 server usage in tutorial material, please raise an issue on the GitHub page for this tutorial flagging that this should be removed.
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: keypoints
+
+- Simulation campaigns run on a regular (monthly) basis
+- Input requests **must** be formatted in a specific way and meet certain pre-requisites
+- Rucio is the primary way to browse and access simulated EIC/ePIC data
-> You may also find reference to an S3 server. This is now deprecated and cannot be used.
-> If you find such references or instructions to S3 server usage in tutorial material, please raise an issue on the GitHub page for this tutorial flagging that this should be removed.
-{: .callout}
+:::::::::::::::::::::::::::::::::::::::::::::
diff --git a/_episodes/02-rucio_usage.md b/episodes/02-rucio_usage.md
similarity index 70%
rename from _episodes/02-rucio_usage.md
rename to episodes/02-rucio_usage.md
index 724cac7..cbdbe2e 100644
--- a/_episodes/02-rucio_usage.md
+++ b/episodes/02-rucio_usage.md
@@ -2,20 +2,23 @@
title: "Rucio Usage"
teaching: 15
exercises: 15
-questions:
-- "How can I use Rucio?"
-objectives:
-- "Become familiar with aspects of Rucio"
-- "Use Rucio tags to find specific types of files"
-- "Learn how to download or stream files for further use"
-keypoints:
-- "Rucio works with datasets and Data Identifiers (DIDs)"
-- "ePIC DIDs may look or be formatted like a nested filepath, but they are flat"
-- "Tags can be used to quickly sort and find data of interest"
-- "Once you find the file location with Rucio, you can use xrootd to download or stream it too"
---
-# Getting Started
+::::::::::::::::::::::::::::::::::::::::::::: questions
+
+- How can I use Rucio?
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: objectives
+
+- Become familiar with aspects of Rucio
+- Use Rucio tags to find specific types of files
+- Learn how to download or stream files for further use
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+## Getting Started
We can access and run the Rucio client from within eic-shell. From wherever you have eic-shell:
@@ -25,7 +28,7 @@ rucio whoami
```
This should print out some information:
-```bash
+```output
email : eicprod@jlab.org
account : eicread
account_type : GROUP
@@ -40,13 +43,13 @@ rucio -h
To use Rucio further, we will need to briefly look at how Rucio organises data.
-# Datasets and DIDs
+## Datasets and DIDs
-Typically, we want to analyse data contained within specific files. Files can be grouped together into datasets which can themselves, be grouped into containers. All three refer to "data". As such, the term "data identifier` or **DID** is used in Rucio. A DID is just the name of a single file, dataset or container.
+Typically, we want to analyse data contained within specific files. Files can be grouped together into datasets which can themselves, be grouped into containers. All three refer to "data". As such, the term "data identifier" or **DID** is used in Rucio. A DID is just the name of a single file, dataset or container.
In Rucio, all DIDs follow a naming scheme which is composed of two strings - a **scope** and a **name**, formatted as:
-``scope:name``
+`scope:name`
For epic, the scope is always `epic`, meaning that __all__ of our DIDs look like:
@@ -62,11 +65,11 @@ The `name` here - `/RECO/26.02.0/epic_craterlake/EXCLUSIVE/DEMP/DEMPgen-1.2.4/10
- `RECO`
- This tells us that the DID contains reconstructed output file information
-- `26.02.0`
+- `26.02.0`
- This tells us that the `26.02.0` software release was used, the February 2026 release (version 0).
- `epic_craterlake`
- This tells us that the `epic_craterlake` detector configuration was used in the simulation
-- `EXCLSUIVE`
+- `EXCLUSIVE`
- The DID is for a dataset of exclusive physics events
- `DEMP`
- This is the specific exclusive process simulated in the dataset, **D**eeply **E**xclusive **M**eson **P**roduction, DEMP
@@ -80,13 +83,17 @@ The `name` here - `/RECO/26.02.0/epic_craterlake/EXCLUSIVE/DEMP/DEMPgen-1.2.4/10
- `pi+`
- Pi+ are generated in this output - this is specific to this DEMP reaction and signifies that it is Deeply Exclusive Pion Production
-> ## `Warning - Not a filepath!`
-> The `name` of our DID here looks a lot like a filepath, however it is a flat object and does **not** have any hierarchy as we will see in the next section.
-{: .caution}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+## Warning - Not a filepath!
+
+The `name` of our DID here looks a lot like a filepath, however it is a flat object and does **not** have any hierarchy as we will see in the next section.
+
+:::::::::::::::::::::::::::::::::::::::::::::
Other names may not necessarily contain all of the same information, but as a bare minimum, are likely to tell us something about the physics process simulated and beam conditions, as well as which software release was used. This is reflected in the metadata tags assigned as we will see later.
-# Finding DIDs
+## Finding DIDs
Now that we know what a DID looks like, how can we find the DID corresponding to the file or dataset that we're interested in?
@@ -110,10 +117,14 @@ rucio did list epic:/RECO/\*
We get an enormous number of DIDs returned! This is every reconstruction related DID available to access right now.
-> ## `Warning - Check the campaign date!`
-> If you encounter any issues when processing the DID listed earlier, it may be due to the software release version.
-> Remember that campaigns older than ~6 months will not be instantly accessible. Try switching to a more recent campaign version.
-{: .caution}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+## Warning - Check the campaign date!
+
+If you encounter any issues when processing the DID listed earlier, it may be due to the software release version.
+Remember that campaigns older than ~6 months will not be instantly accessible. Try switching to a more recent campaign version.
+
+:::::::::::::::::::::::::::::::::::::::::::::
Working backwards from the full DID we had earlier, we could combine in the software release, detector configuration, process and generator to narrow down the list of DIDs:
@@ -131,11 +142,15 @@ rucio did list epic:*RECO*26.02.0*DEMP*
But as above, this does require some knowledge of what our DID looks like to begin with.
-> ## `Pin for later:`
-> If we used `--short` as suggested to just get a list of DIDs, we could pipe this output to a file.
->
-> Each line would be the full DID for an item which we could potentially make use of.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+## Pin for later
+
+If we used `--short` as suggested to just get a list of DIDs, we could pipe this output to a file.
+
+Each line would be the full DID for an item which we could potentially make use of.
+
+:::::::::::::::::::::::::::::::::::::::::::::
As we can see, the DIDs we have in our list now are all datasets. We can check the contents of these datasets too. Let's pick one of our DIDs and examine the content. We can do this via:
@@ -161,34 +176,40 @@ We can check where a specific file in our dataset is stored too:
rucio replica list file --protocols root --pfns --rses isopenaccess epic:/RECO/26.02.0/epic_craterlake/EXCLUSIVE/DEMP/DEMPgen-1.2.4/10x250/q2_3_10/pi+/DEMPgen-1.2.4_10x250_pi+_q2_3_10_ab.0550.eicrecon.edm4eic.root
```
-> ## `list file comment:`
-> Despite the slightly misleading command above, we can actually just provide a dataset DID here too. If we do so, we will get the location of all files in the dataset in one command, e.g:
->
-> ```bash
-> rucio replica list file --protocols root --pfns --rses isopenaccess epic:/RECO/26.02.0/epic_craterlake/EXCLUSIVE/DEMP/DEMPgen-1.2.4/10x250/q2_3_10/pi+
-> ```
-> You could pipe this to a file for later usage. However, note that replicas may exist for a given file. Both would be printed by this command as is. You can check if multiple copies exist via:
-> ```bash
-> rucio rule list --did scope:name
-> ```
-> e.g.
-> ```bash
-> rucio rule list --did epic:/RECO/26.02.0/epic_craterlake/EXCLUSIVE/DEMP/DEMPgen-1.2.4/10x250/q2_3_10/pi+
-> ```
-> This will list where the DID is stored and how many copies exist.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+## list file comment
+
+Despite the slightly misleading command above, we can actually just provide a dataset DID here too. If we do so, we will get the location of all files in the dataset in one command, e.g:
+
+```bash
+rucio replica list file --protocols root --pfns --rses isopenaccess epic:/RECO/26.02.0/epic_craterlake/EXCLUSIVE/DEMP/DEMPgen-1.2.4/10x250/q2_3_10/pi+
+```
+You could pipe this to a file for later usage. However, note that replicas may exist for a given file. Both would be printed by this command as is. You can check if multiple copies exist via:
+```bash
+rucio rule list --did scope:name
+```
+e.g.
+```bash
+rucio rule list --did epic:/RECO/26.02.0/epic_craterlake/EXCLUSIVE/DEMP/DEMPgen-1.2.4/10x250/q2_3_10/pi+
+```
+This will list where the DID is stored and how many copies exist.
+
+:::::::::::::::::::::::::::::::::::::::::::::
The `root://dtn-eic.jlab.org` at the start of the output tells us that this particular file is stored on JLab servers. As mentioned in the outset, Rucio works across multiple sites easily, however, methods which we might use to stream files do not. **As such, being able to check where our files are stored is a useful feature.**
So, we can find DIDs, check what they are and what they contain. To get to this point though, we needed some pre-knowledge of what the DID looked like which isn't necessarily that helpful for finding something. However, a much easier approach to finding what we need is to use the metadata tags that are assigned all DIDs from March 2026 onwards.
-# Metadata Tags
+## Metadata Tags
+
+::::::::::::::::::::::::::::::::::::::::::::: callout
-> ## Thanks!
->
-> Automatically adding these metadata tags to datasets was enabled due to work by Sakib Rahman (BNL), Anil Panta (JLab) and ePIC Software & Computing. Thanks to their efforts, finding ePIC data using Rucio is more straightforward!
->
-{: .callout}
+## Thanks!
+
+Automatically adding these metadata tags to datasets was enabled due to work by Sakib Rahman (BNL), Anil Panta (JLab) and ePIC Software & Computing. Thanks to their efforts, finding ePIC data using Rucio is more straightforward!
+
+:::::::::::::::::::::::::::::::::::::::::::::
The following tags are available as of March 2026:
@@ -205,7 +226,7 @@ The following tags are available as of March 2026:
- Geometry config tag, e.g. `craterlake_18x275`, `craterlake_5x41_He3`
- **generator**
- MC event generator used to generate the simulated data
- - `pythia6`, `pythia8`, `beagle`, `djangoh`, `rapgap`, `dempgen`, `sartre`, `lager`, `estarlight`, `eic_sr_geant4`, `eic_esr_xsuite`, `sherpa`, `single_particle`, `epic`, `other`
+ - `pythia6`, `pythia8`, `beagle`, `djangoh`, `rapgap`, `dempgen`, `sartre`, `lager`, `estarlight`, `eic_sr_geant4`, `eic_esr_xsuite`, `sherpa`, `single_particle`, `epic`, `other`
- **requester\_pwg**
- Defines the physics working group (PWG) that the simulated data relates to, options are:
- `edt` (exclusive, diffractive and tagging), `inclusive`, `jets_hf`, `semi_inclusive`, `ew_bsm`, `other`
@@ -224,7 +245,7 @@ The following tags are available as of March 2026:
- `p`, `Au197`, `Cu63`, `He3`, `H2`, `Ru96`
- **q2\_min\_gev2**
- Minimum Q2 value (GeV^2) in the simulation file, entered as a number.
-- **q2\_max_gev2**
+- **q2\_max\_gev2**
- Maximum Q2 value (GeV^2) in the simulation file, entered as a number.
- **gun\_particle**
- Single particle type
@@ -245,11 +266,13 @@ The following tags are available as of March 2026:
- Type of distribution for particle gun
- `uniform`, `cos(theta)`, `eta`, `pseudorapidity`, `ffbar`
-> ## Reference Sheet
->
-> This information is available segmented out from this tutorial as a reference sheet in the extras section by following the [Rucio Metadata Tags]({{ page.root }}{% link _extras/metadata_tags.md %}) link.
->
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+## Reference Sheet
+
+This information is available segmented out from this tutorial as a reference sheet in the extras section by following the [Rucio Metadata Tags](../learners/metadata-tags.md) link.
+
+:::::::::::::::::::::::::::::::::::::::::::::
Most of the tags in this list are optional and may not be applied to all datasets. However, the following tags are **required** for all datasets:
@@ -283,11 +306,13 @@ rucio did list --filter 'electron_beam_energy_gev==10, ion_beam_energy_gev==250'
which will return only datasets with 10x250 collisions (10 GeV electrons on 250 GeV ions using the standard ePIC conventions). We can keep adding filters in this manner as we like to really narrow down the DIDs we return with our query.
-> ## Logical Expressions
->
-> Note that in our examples we use `==` with our filters, but other logical expressions can be used too. E.g. `>=`, `<=`, `>`, `<` and so on are all valid for tags expecting an integer/number value.
->
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+## Logical Expressions
+
+Note that in our examples we use `==` with our filters, but other logical expressions can be used too. E.g. `>=`, `<=`, `>`, `<` and so on are all valid for tags expecting an integer/number value.
+
+:::::::::::::::::::::::::::::::::::::::::::::
Note that we can also use wildcards in our tag searches. This could be helpful if we don't know if a particular dataset was run in a specific campaign for example. We could do:
@@ -297,17 +322,38 @@ rucio did list --filter 'software_release=26.*, electron_beam_energy_gev==10' 'e
to just get a list of all DIDs with 10 GeV beam electrons from 2026 software releases for example. **However, remember that tags have only been applied from March 2026 onwards.**
-> ## `Exercise:`
-> Using tags, find the DIDs of the **latest**:
-> - DEMP events in the Q2 range of 3 to 10 for 10 GeV electrons on 250 GeV protons
-> - Print the full DID and check the number of files in the dataset
->
-> **Hint** - Check the example name we looked at when introducing DIDs in a previous section.
-{: .challenge}
+::::::::::::::::::::::::::::::::::::::::::::: challenge
-# Using DIDs - Downloading or Processing Files
+## Exercise
-So far we've seen how we can find DIDs and check some basic info such as what type of data they point to and where that data is stored. We generally want to do a bit more than that though. Typically we want to find data to *use* it in some way. For our simulation data, this is usually to analyse it!
+Using tags, find the DIDs of the **latest**:
+
+- DEMP events in the Q2 range of 3 to 10 for 10 GeV electrons on 250 GeV protons
+- Print the full DID and check the number of files in the dataset
+
+**Hint** - Check the example name we looked at when introducing DIDs in a previous section.
+
+::::::::::::::: solution
+
+Combine the relevant tags in a single filtered `did list`, using the most recent `software_release`
+you find available, for example:
+
+```bash
+rucio did list --short --filter 'software_release=26.*, generator==dempgen, electron_beam_energy_gev==10, ion_beam_energy_gev==250, q2_min_gev2==3, q2_max_gev2==10' 'epic:*'
+```
+
+This should return the matching DEMP dataset DID (of the form
+`epic:/RECO/26.02.0/epic_craterlake/EXCLUSIVE/DEMP/DEMPgen-1.2.4/10x250/q2_3_10/pi+`). Feed that DID
+into `rucio did content list --short scope:name` and count the returned lines to get the number of
+files in the dataset.
+
+:::::::::::::::
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+## Using DIDs - Downloading or Processing Files
+
+So far we've seen how we can find DIDs and check some basic info such as what type of data they point to and where that data is stored. We generally want to do a bit more than that though. Typically we want to find data to *use* it in some way. For our simulation data, this is usually to analyse it!
We can download DIDs, containers, datasets or files, straightforwardly:
@@ -323,15 +369,19 @@ rucio download epic:/RECO/26.02.0/epic_craterlake/EXCLUSIVE/DEMP/DEMPgen-1.2.4/1
By default it will download to our current directory with its original name. In this case, that's unfortunate because as we noticed earlier, this looks a lot like a UNIX file path. As such, we now have a large number of nested directories to go through before we get to our file!
-> ## `Warning - Do you need the whole dataset?`
-> Think very carefully before downloading a DID. What is it? If it's a full dataset, do you **really** need all of the data?
->
-> Generally you will not need a local copy of a full dataset. It's generally best to only download a small subset of files to test and run.
->
-> We can *stream* files from a full dataset rather than downloading them as we'll see in a moment.
-{: .caution}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+## Warning - Do you need the whole dataset?
+
+Think very carefully before downloading a DID. What is it? If it's a full dataset, do you **really** need all of the data?
-It might actually be easier to use [XrootD]({{ page.root }}{% link _extras/xrootd.md %}) to grab our file as it's a bit more intuitive, we do need our location from earlier for this though:
+Generally you will not need a local copy of a full dataset. It's generally best to only download a small subset of files to test and run.
+
+We can *stream* files from a full dataset rather than downloading them as we'll see in a moment.
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+It might actually be easier to use [XrootD](../learners/xrootd.md) to grab our file as it's a bit more intuitive, we do need our location from earlier for this though:
```bash
xrdcp root://dtn-eic.jlab.org:1094//volatile/eic/EPIC//RECO/26.02.0/epic_craterlake/EXCLUSIVE/DEMP/DEMPgen-1.2.4/10x250/q2_3_10/pi+/DEMPgen-1.2.4_10x250_pi+_q2_3_10_ab.0550.eicrecon.edm4eic.root ./
@@ -363,7 +413,7 @@ file_path = "root://dtn-eic.jlab.org:1094//volatile/eic/EPIC//RECO/26.02.0/epic_
file = ROOT.TFile.Open(file_path, "READ")
```
-## Testing File Streaming
+### Testing File Streaming
We can quickly check the three methods above work.
@@ -388,7 +438,6 @@ import XRootD
file_path = "root://dtn-eic.jlab.org:1094//volatile/eic/EPIC//RECO/26.02.0/epic_craterlake/EXCLUSIVE/DEMP/DEMPgen-1.2.4/10x250/q2_3_10/pi+/DEMPgen-1.2.4_10x250_pi+_q2_3_10_ab.0550.eicrecon.edm4eic.root"
root_file = uproot.open(file_path)
print(root_file['events'].num_entries, "events in this tree.")
-
```
or directly using PyRoot:
@@ -400,4 +449,13 @@ file = ROOT.TFile.Open(file_path, "READ")
print((file.Get("events")).GetEntries(), "events in this tree.")
```
-All three approaches should yield the same result.
+All three approaches should yield the same result.
+
+::::::::::::::::::::::::::::::::::::::::::::: keypoints
+
+- Rucio works with datasets and Data Identifiers (DIDs)
+- ePIC DIDs may look or be formatted like a nested filepath, but they are flat
+- Tags can be used to quickly sort and find data of interest
+- Once you find the file location with Rucio, you can use xrootd to download or stream it too
+
+:::::::::::::::::::::::::::::::::::::::::::::
diff --git a/_episodes/03-use_cases.md b/episodes/03-use_cases.md
similarity index 53%
rename from _episodes/03-use_cases.md
rename to episodes/03-use_cases.md
index 4694bac..6882ac8 100644
--- a/_episodes/03-use_cases.md
+++ b/episodes/03-use_cases.md
@@ -2,20 +2,25 @@
title: "Use Cases"
teaching: 10
exercises: 40
-questions:
-- "How do users interact with EIC/ePIC data?"
-objectives:
-- "Explore different use cases for ePIC simulation data and how users work with EIC/ePIC data"
-- "Discover how simulation files can be utilised in further analysis"
-- "Know how to download files if needed (and when it might be needed)"
-keypoints:
-- "Files from datasets can be directly streamed in analysis scripts"
-- "Files (or whole datasets) can be downloaded locally, but this is usually *not* needed"
---
+::::::::::::::::::::::::::::::::::::::::::::: questions
+
+- How do users interact with EIC/ePIC data?
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+::::::::::::::::::::::::::::::::::::::::::::: objectives
+
+- Explore different use cases for ePIC simulation data and how users work with EIC/ePIC data
+- Discover how simulation files can be utilised in further analysis
+- Know how to download files if needed (and when it might be needed)
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
In this episode, we will explore a few common use cases and how users may want to interact with simulation campaign output in each case. Examples of carrying out some common tasks associated with each use case will be included.
-# Physics Analyser - Novice
+## Physics Analyser - Novice
This use case explores a user new to analysing ePIC data to try and look at a specific physics process. They will likely want to find and identify a specific physics process to pass through their analysis code. Their requirements are likely to include:
@@ -24,7 +29,7 @@ This use case explores a user new to analysing ePIC data to try and look at a sp
- The latest available files to test
- A specific collider (energy and ion species) configuration
-They may also want to only test a small subset of data to test and develop their analysis. **This use case is one example where downloading a small number of files locally may be beneficial**.
+They may also want to only test a small subset of data to test and develop their analysis. **This use case is one example where downloading a small number of files locally may be beneficial**.
To find files that meet their requirements they could utilise the following tags:
@@ -43,12 +48,16 @@ rucio did list --filter 'software_release==XXX, requester_pwg==YYY, electron_bea
Where we can substitute in our chosen values for each in place of `XXX`, `YYY`, `ZZ`, `iii` and `jjj`.
-> ## `Beam Energies:`
-> Whilst we can enter any number for the `electron_beam_energy_gev` and `ion_beam_energy_gev` values, there are only certain combinations actually in use.
-> `electron_beam_energy_gev` is typically 5, 9, 10 or 18 GeV
-> `ion_beam_energy_gev` is typically 41, 100, 130, 250 or 275 GeV for protons.
-> For other ion species, 110 and 166 may also be used.
-{: .callout}
+::::::::::::::::::::::::::::::::::::::::::::: callout
+
+## Beam Energies
+
+Whilst we can enter any number for the `electron_beam_energy_gev` and `ion_beam_energy_gev` values, there are only certain combinations actually in use.
+`electron_beam_energy_gev` is typically 5, 9, 10 or 18 GeV
+`ion_beam_energy_gev` is typically 41, 100, 130, 250 or 275 GeV for protons.
+For other ion species, 110 and 166 may also be used.
+
+:::::::::::::::::::::::::::::::::::::::::::::
Once we have identified a specific dataset of interest, we can look at the files within it using:
@@ -62,7 +71,7 @@ and we can get locations of the files within the dataset via:
rucio replica list file --protocols root --pfns --rses isopenaccess scope:name_of_file
rucio replica list file --protocols root --pfns --rses isopenaccess scope:name_of_Dataset
```
-as we saw in the last episode. We can get just the location of a specific file OR the location of all files within the dataest, depending upon which we specify. We could download this file locally using
+as we saw in the last episode. We can get just the location of a specific file OR the location of all files within the dataset, depending upon which we specify. We could download this file locally using
```bash
xrdcp FILEPATH ./
@@ -70,13 +79,33 @@ xrdcp FILEPATH ./
where `FILEPATH` is the path to one specific file from the output of one of the rucio commands above.
-> ## `Exercise:`
-> Using the suggested tags, find the **latest** available datasets for:
-> - Neutral current (NC) DIS events for 10 GeV electrons colliding with 130 GeV protons
-> - Download **one** file from this dataset of your choice
-{: .challenge}
+::::::::::::::::::::::::::::::::::::::::::::: challenge
+
+## Exercise
+
+Using the suggested tags, find the **latest** available datasets for:
+
+- Neutral current (NC) DIS events for 10 GeV electrons colliding with 130 GeV protons
+- Download **one** file from this dataset of your choice
-# Physics Analyser - Experienced
+::::::::::::::: solution
+
+Filter on the relevant tags (using the latest `software_release` available), for example:
+
+```bash
+rucio did list --short --filter 'software_release=26.*, requester_pwg==inclusive, electron_beam_energy_gev==10, ion_beam_energy_gev==130, ion_species==p, data_level==reconstruction' 'epic:*'
+```
+
+Pick a NC DIS dataset from the returned list, list its files with
+`rucio did content list --short scope:name`, get a file location with
+`rucio replica list file --protocols root --pfns --rses isopenaccess scope:name_of_file`, then copy
+a single file locally with `xrdcp FILEPATH ./`.
+
+:::::::::::::::
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+## Physics Analyser - Experienced
In this use case, we consider an experienced physics analyser that has a well developed analysis script that they want to run on a large number of files, possibly even a full dataset, for a specific physics process they're interested in. Their requirements are likely to include:
@@ -95,7 +124,7 @@ To find files that meet their requirements they could utilise the following tags
- generator
- data\_level
-They may also want to use the `q2_min_gev2` ad `q2_max_gev2` tags, along with the `ion_species` tags to narrow down to an even more specific subset of files. They may also want to analyse files with or without background enabled.
+They may also want to use the `q2_min_gev2` and `q2_max_gev2` tags, along with the `ion_species` tags to narrow down to an even more specific subset of files. They may also want to analyse files with or without background enabled.
As they want to process a large number of files, **it is unlikely (and not recommended) that they will want to download a large number of files to process them locally**. Instead, they may want to stream their files directly in their analysis script. They could do this via:
@@ -163,7 +192,7 @@ with open('FileList', 'r') as file:
print("Found file - ", file_path, "and appended to list for processing.")
except Exception as e:
print(f"Could not open file: {e}")
-
+
# Use the uproot iterate method to process our list of files - See https://uproot.readthedocs.io/en/stable/uproot.behaviors.TBranch.iterate.html
#for chunk in uproot.iterate({f: "events" for f in Files}, expressions=["MCParticles.PDG"]): # Open files in array f and process events tree with branches specified
# Process each chunk - Do something
@@ -172,19 +201,43 @@ with open('FileList', 'r') as file:
Note that we have restricted these examples to only print out the first five files in the list we created. We can comment out or change the lines as noted to process the full list (or adjust the cutoff value in the condition to process a different number).
-> ## `Exercise:`
-> Using the suggested tags, find the **latest** available dataset for:
-> - Deeply Virtual Compton Scattering (DVCS) events from the EpIC event generator for 10 GeV electrons colliding with 130 GeV protons *without* background included
-> 1. Stream **one** file from this dataset in a script, check the number of events in this file
-> 2. Print **all** of the files in this dataset to a text file
-> 3. Stream **five** of the files in this dataset in a script, check the total number of events contained in all five files.
->
-> *Hint* - See the example scripts in the last episode for how to get the number of events in a root file in a few different ways.
-{: .challenge}
+::::::::::::::::::::::::::::::::::::::::::::: challenge
+
+## Exercise
+
+Using the suggested tags, find the **latest** available dataset for:
+
+- Deeply Virtual Compton Scattering (DVCS) events from the EpIC event generator for 10 GeV electrons colliding with 130 GeV protons *without* background included
+1. Stream **one** file from this dataset in a script, check the number of events in this file
+2. Print **all** of the files in this dataset to a text file
+3. Stream **five** of the files in this dataset in a script, check the total number of events contained in all five files.
-# Detector Designer/Optimiser, Algorithm/Reconstruction Development
+*Hint* - See the example scripts in the last episode for how to get the number of events in a root file in a few different ways.
-In this use case, someone updating the design of a detector in DD4HEP, or adjusting a reconstruction algorithm for a detector, may not want full reconstructed data. Instead, they may want more raw, hit level information. They may also want a specific detector configuration for comparison. In terms of physics process, they may not be looking at an actual reaction at all, but a particle gun simulation. To summarise, they may want:
+::::::::::::::: solution
+
+Filter on the EpIC generator and the required beam energies, requiring no background mixing, for
+example:
+
+```bash
+rucio did list --short --filter 'software_release=26.*, generator==epic, electron_beam_energy_gev==10, ion_beam_energy_gev==130, is_background_mixed==False' 'epic:*'
+```
+
+1. Take one file location from `rucio replica list file ... scope:name` and stream it with a `Test.C`
+ or `Test.py` script as shown in the previous episode; print `tree->GetEntries()` (or
+ `num_entries` in uproot).
+2. Print the full file list with
+ `rucio replica list file --protocols root --pfns --rses isopenaccess scope:name_of_Dataset > FileList`.
+3. Use the `FileListProcess` C++/python loop from this episode, leaving the 5-file cutoff in place,
+ and sum the entries from each opened file.
+
+:::::::::::::::
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+## Detector Designer/Optimiser, Algorithm/Reconstruction Development
+
+In this use case, someone updating the design of a detector in [DD4hep](https://eic.github.io/tutorial-geometry-development-using-dd4hep/), or adjusting a [reconstruction algorithm](https://eic.github.io/tutorial-reconstruction-algorithms/) for a detector, may not want full reconstructed data. Instead, they may want more raw, hit level information. They may also want a specific detector configuration for comparison. In terms of physics process, they may not be looking at an actual reaction at all, but a particle gun simulation. To summarise, they may want:
- Simulated (FULL) as well as reconstructed output files (RECO)
- Particle gun studies with specific single particles
@@ -207,21 +260,48 @@ Some tags they might use to find their data include:
- gun\_phi\_max\_deg
- gun\_distribution
-> ## `Exercise:`
-> Using combinations of the suggested tags, find the **latest** available dataset(s) for:
-> - `K-` single particle gun simulations
-> - Determine the available momentum and angular ranges available for this/these dataset(s)
-> - Do non-reconstructed files exist for this/these dataset(s)?
-{: .challenge}
+::::::::::::::::::::::::::::::::::::::::::::: challenge
+
+## Exercise
+
+Using combinations of the suggested tags, find the **latest** available dataset(s) for:
+
+- `K-` single particle gun simulations
+- Determine the available momentum and angular ranges available for this/these dataset(s)
+- Do non-reconstructed files exist for this/these dataset(s)?
-# Conclusion and Comments
+::::::::::::::: solution
+
+Filter on the `kaon-` particle gun using the latest `software_release`, for example:
+
+```bash
+rucio did list --short --filter 'software_release=26.*, generator==single_particle, gun_particle==kaon-' 'epic:*'
+```
+
+Inspect the returned DID names (and their `gun_momentum_*`/`gun_theta_*` tags) to read off the
+available momentum and angular ranges. To check whether non-reconstructed files exist, repeat the
+query with `data_level==simulation` (FULL) instead of `data_level==reconstruction`, or look for
+matching DIDs whose name begins with `/FULL/` rather than `/RECO/`.
+
+:::::::::::::::
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+## Conclusion and Comments
That wraps up our introduction to using Rucio and some example use cases and scenarios.
-New tags may be added in the future. We're welcome to take on board any suggestions or changes as we roll out Rucio and it becomes more widely used. Get in touch via - [stephen.kay@york.ac.uk](stephen.kay@york.ac.uk)
+New tags may be added in the future. We're welcome to take on board any suggestions or changes as we roll out Rucio and it becomes more widely used. Get in touch via [stephen.kay@york.ac.uk](mailto:stephen.kay@york.ac.uk)
-or on Mattermost with suggestions, comments and feedback.
+or on the [ePIC software-tutorials Mattermost channel](https://chat.epic-eic.org/main/channels/software-tutorials) with suggestions, comments and feedback.
Remember to consider whether you need full datasets before downloading them and keep an eye on whether your files have multiple open access copies when making file lists.
Also, if you find any nice tricks or develop short scripts (maybe one which makes a file list for the latest version of a dataset based upon inputs?) then feel free to share them too!
+
+::::::::::::::::::::::::::::::::::::::::::::: keypoints
+
+- Files from datasets can be directly streamed in analysis scripts
+- Files (or whole datasets) can be downloaded locally, but this is usually *not* needed
+
+:::::::::::::::::::::::::::::::::::::::::::::
diff --git a/files/DIDlist_Parse.sh b/episodes/files/DIDlist_Parse.sh
similarity index 100%
rename from files/DIDlist_Parse.sh
rename to episodes/files/DIDlist_Parse.sh
diff --git a/fig/.gitkeep b/fig/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/files/.gitkeep b/files/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/index.md b/index.md
index a92e7d1..debaf1a 100644
--- a/index.md
+++ b/index.md
@@ -1,26 +1,27 @@
---
-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
+site: sandpaper::sandpaper_site
---
Welcome to the EIC/ePIC tutorial on file access. This tutorial will explore how to find and access EIC/ePIC simulation data using Rucio. Usage of metadata tags to quickly find data will be explored as well as some example use cases.
-> ## Prerequisites
-> Experience/knowledge of CERN ROOT and/or Python.
->
-> Experience/knowledge of working in UNIX environments and familiarty with running via the command line.
->
-> This tutorial follows other tutorial in the EIC series:
-> - [Setting Up Your Environment](https://eic.github.io/tutorial-setting-up-environment/)
->
-> Further information is included in other tutorials:
->
-> - [Analysis](https://eic.github.io/tutorial-analysis)
-> - [Geometry Development with DD4hep](https://eic.github.io/tutorial-geometry-development-using-dd4hep/)
-> - [Simulations Using DDsim and Geant4](https://eic.github.io/tutorial-simulations-using-npsim-and-geant4/)
-> - [Reconstruction Algorithms in JANA2](https://eic.github.io/tutorial-jana2)
-> - [Developing Benchmarks](https://eic.github.io/tutorial-developing-benchmarks/)
-{: .prereq}
-
-{% include links.md %}
+::::::::::::::::::::::::::::::::::::::::::::: prereq
+
+## Prerequisites
+
+Experience/knowledge of CERN ROOT and/or Python.
+
+Experience/knowledge of working in UNIX environments and familiarity with running via the command line.
+
+This tutorial follows other tutorials in the EIC series:
+
+- [Setting Up Your Environment](https://eic.github.io/tutorial-setting-up-environment/)
+
+Further information is included in other tutorials:
+
+- [Analysis](https://eic.github.io/tutorial-analysis)
+- [Geometry Development with DD4hep](https://eic.github.io/tutorial-geometry-development-using-dd4hep/)
+- [Simulations Using DDsim and Geant4](https://eic.github.io/tutorial-simulations-using-npsim-and-geant4/)
+- [Reconstruction Algorithms in JANA2](https://eic.github.io/tutorial-jana2)
+- [Developing Benchmarks](https://eic.github.io/tutorial-developing-benchmarks/)
+
+:::::::::::::::::::::::::::::::::::::::::::::
diff --git a/instructors/instructor-notes.md b/instructors/instructor-notes.md
new file mode 100644
index 0000000..e574694
--- /dev/null
+++ b/instructors/instructor-notes.md
@@ -0,0 +1,30 @@
+---
+title: "Instructor Notes"
+---
+
+This tutorial teaches learners how to find and access EIC/ePIC simulation campaign data with Rucio
+and xrootd. It assumes learners have already completed the
+[Setting Up Your EIC Environment](https://eic.github.io/tutorial-setting-up-environment/) tutorial
+and have a working `eic-shell`.
+
+## Before the session
+
+- Ask learners to have a working `eic-shell` ready in advance (see the [Setup](../learners/setup.md)
+ page). The Rucio client is run from inside `eic-shell`.
+- Confirm that learners can run `rucio whoami` from inside `eic-shell` before the live commands, so
+ that authentication issues are caught early.
+- Remind learners that campaigns older than ~6 months may not be directly accessible, so use a
+ recent `software_release`/campaign when demonstrating the `did list` commands live.
+
+## Timing
+
+The lesson is short on teaching (~35 minutes) but the exercises (particularly the use cases in the
+final episode) can absorb the rest of the time as learners explore the tag-based queries and stream
+or download files.
+
+## Common pitfalls
+
+- DIDs *look* like file paths but are flat; learners frequently expect a hierarchy that is not there.
+- Metadata tags are only applied from March 2026 onwards, so tag-based filtering will miss older
+ datasets.
+- Encourage streaming rather than downloading full datasets.
diff --git a/_extras/metadata_tags.md b/learners/metadata-tags.md
similarity index 96%
rename from _extras/metadata_tags.md
rename to learners/metadata-tags.md
index 32bccbd..defe21a 100644
--- a/_extras/metadata_tags.md
+++ b/learners/metadata-tags.md
@@ -19,7 +19,7 @@ The following tags are available as of March 2026:
- Geometry config tag, e.g. `craterlake_18x275`, `craterlake_5x41_He3`
- **generator**
- MC event generator used to generate the simulated data
- - `pythia6`, `pythia8`, `beagle`, `djangoh`, `rapgap`, `dempgen`, `sartre`, `lager`, `estarlight`, `eic_sr_geant4`, `eic_esr_xsuite`, `sherpa`, `single_particle`, `epic`, `other`
+ - `pythia6`, `pythia8`, `beagle`, `djangoh`, `rapgap`, `dempgen`, `sartre`, `lager`, `estarlight`, `eic_sr_geant4`, `eic_esr_xsuite`, `sherpa`, `single_particle`, `epic`, `other`
- **requester\_pwg**
- Defines the physics working group (PWG) that the simulated data relates to, options are:
- `edt` (exclusive, diffractive and tagging), `inclusive`, `jets_hf`, `semi_inclusive`, `ew_bsm`, `other`
@@ -38,7 +38,7 @@ The following tags are available as of March 2026:
- `p`, `Au197`, `Cu63`, `He3`, `H2`, `Ru96`
- **q2\_min\_gev2**
- Minimum Q2 value (GeV^2) in the simulation file, entered as a number.
-- **q2\_max_gev2**
+- **q2\_max\_gev2**
- Maximum Q2 value (GeV^2) in the simulation file, entered as a number.
- **gun\_particle**
- Single particle type
@@ -58,5 +58,3 @@ The following tags are available as of March 2026:
- **gun\_distribution**
- Type of distribution for particle gun
- `uniform`, `cos(theta)`, `eta`, `pseudorapidity`, `ffbar`
-
-{% include links.md %}
diff --git a/learners/reference.md b/learners/reference.md
new file mode 100644
index 0000000..764e9d5
--- /dev/null
+++ b/learners/reference.md
@@ -0,0 +1,20 @@
+---
+title: 'Reference'
+---
+
+## Glossary
+
+EIC
+: Electron-Ion Collider
+
+DID
+: Data Identifier - the `scope:name` used by Rucio to refer to a file, dataset or container
+
+RECO
+: Reconstructed output files (the output of the reconstruction, e.g. EICrecon)
+
+FULL
+: The full GEANT4 simulation output root files
+
+EVGEN
+: The input hepmc3 event generator datasets
diff --git a/setup.md b/learners/setup.md
similarity index 54%
rename from setup.md
rename to learners/setup.md
index 91af522..19a77bb 100644
--- a/setup.md
+++ b/learners/setup.md
@@ -1,8 +1,7 @@
---
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.
-This tutorial will go over how to browse and access simulation campaign files using rucio. We will also examine various use cases and how you might engage with the simulation output in each case. You will need a working eic-shell environment running for this tutorial.
+If you have not done so already, please follow the [setup instructions for Setting Up Your EIC Environment](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 %}
+This tutorial will go over how to browse and access simulation campaign files using rucio. We will also examine various use cases and how you might engage with the simulation output in each case. You will need a working eic-shell environment running for this tutorial.
diff --git a/_extras/xrootd.md b/learners/xrootd.md
similarity index 89%
rename from _extras/xrootd.md
rename to learners/xrootd.md
index 644dc9d..cad4ea0 100644
--- a/_extras/xrootd.md
+++ b/learners/xrootd.md
@@ -6,7 +6,7 @@ title: "xrootd"
In the tutorial, we used XrootD to copy files and to stream files. We could also use it to search for files as well if we wanted. However, the limitation is that typically when browsing we must specify a server. If our file isn't on this server, we won't see it, unlike in Rucio.
-# Directory Structure
+## Directory Structure
Unlike Rucio, if we use XrootD, we *do* have a file structure. Typically, file paths typically look like:
@@ -21,13 +21,17 @@ Unlike Rucio, if we use XrootD, we *do* have a file structure. Typically, file p
Some directories may include event generator versions after the physics process too. You may also find some examples where there are multiple sets of additional conditions.
-> ## `Warning - Non-JLab Servers`
-> Notice the `/volatile/` at the front of the path. This is specific to files on the JLab XrootD server.
->
-> Files stored elsewhere may just be under `/eic/EPIC/...`
-{: .caution}
+::::::::::::::::::::::::::::::::::::::::::::: callout
-# Browsing the Simulation Output with XrootD
+## Warning - Non-JLab Servers
+
+Notice the `/volatile/` at the front of the path. This is specific to files on the JLab XrootD server.
+
+Files stored elsewhere may just be under `/eic/EPIC/...`
+
+:::::::::::::::::::::::::::::::::::::::::::::
+
+## Browsing the Simulation Output with XrootD
We can browse the simulation output using XrootD from within the eic-shell. To browse the directory structure and exit, one can run the commands:
@@ -38,7 +42,7 @@ ls /volatile/eic/EPIC/RECO/26.02.0
exit
```
-`xrdfs` is the command to log in to a specific server, in this case `root://dtn-eic.jlab.org`.
+`xrdfs` is the command to log in to a specific server, in this case `root://dtn-eic.jlab.org`.
When calling ls, we should see everything in this subfolder. In this case, all files from the **February 2026** campaign.
@@ -58,7 +62,7 @@ In our earlier episode, we used this command to copy a file we found using Rucio
1. Find file location with Rucio
2. Stream or download with XrootD
-# Streaming Files
+## Streaming Files
It is also possible to open a file directly in ROOT if you have XrootD installed too. Note that the following command should be executed after opening root and `TFile::Open()` should be used:
@@ -83,5 +87,3 @@ import XRootD
file_path = "root://dtn-eic.jlab.org//volatile/eic/EPIC/RECO/path-to-file"
file = ROOT.TFile.Open(file_path, "READ")
```
-
-{% include links.md %}
diff --git a/links.md b/links.md
new file mode 100644
index 0000000..4c5cd2f
--- /dev/null
+++ b/links.md
@@ -0,0 +1,10 @@
+
+
+[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
new file mode 100644
index 0000000..ead9aae
--- /dev/null
+++ b/profiles/learner-profiles.md
@@ -0,0 +1,8 @@
+---
+title: 'Learner Profiles'
+---
+
+This lesson targets members of the ePIC collaboration — students, postdocs, and researchers — who
+need to find and access EIC simulation campaign data for their analysis or detector/reconstruction
+work. Learners are expected to have a working `eic-shell` environment and some familiarity with the
+UNIX command line and CERN ROOT and/or Python. No prior experience with Rucio or xrootd is assumed.
diff --git a/reference.md b/reference.md
deleted file mode 100644
index d6fe4ff..0000000
--- a/reference.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-layout: reference
----
-
-## Glossary
-
-EIC: Electron-Ion Collider
-
-{% include links.md %}