(* ::Package:: *) (*********************************************************************************************** This package defines a set of functions that facilitates creating printable notebooks for exams. Version 1.1.x by ~sh, 2025-12-30--2026-01-01 1.1.0: first release. 1.1.1: the "Stylesheet" option added. ************************************************************************************************) BeginPackage["ProblemSheet`"]; Unprotect["ProblemSheet`*"]; ClearAll["ProblemSheet`*"]; (**************** createExamSheet: ****************) createExamSheet::usage = "\ createExamSheet[\!\(\* StyleBox[\"title\", \"TI\"]\), \!\(\* StyleBox[\"subtitle\", \"TI\"]\), \!\(\* StyleBox[\"file\", \"TI\"]\), \!\(\* StyleBox[\"opts\", \"TI\"]\)] creates a new notebook containing a printable exam sheet with a header specified by the strings \!\(\* StyleBox[\"title\", \"TI\"]\) and \!\(\* StyleBox[\"subtitle\", \"TI\"]\), followed by a template body containing a list of numbered problem items, each with letter-numbered subitems, and saves it in \!\(\* StyleBox[\"file\", \"TI\"]\) that should be an absolute pathname, e.g. FileNameJoin[{NotebookDirectory[],\"Exam.nb\"}]. Excess or missing (sub)items can be deleted or copy-pasted as needed. The subtitle can be omitted by entering \"\" as the \!\(\* StyleBox[\"subtitle\", \"TI\"]\).\n\ Accepted \!\(\* StyleBox[\"opts\", \"TI\"]\) (default values are shown):\n\ \[Bullet]\[NonBreakingSpace]\"Items\"\[ThinSpace]\[Rule]\[ThinSpace]{6, 4}: a pair of nonnegative integers that specifies the number of problem items and the number of subitems for each item. Option values {1, 0}, {2, 0}, \[Ellipsis] create items with no subitems, {0, 0}, {0, 1}, {0, 2}, \[Ellipsis] give empty body. Specific for createExamSheet.\n\ \[Bullet]\[NonBreakingSpace]\"Stylesheet\"\[ThinSpace]\[Rule]\[ThinSpace]\"Default.nb\": specifies the style definitions notebook. Specific for createExamSheet.\n\ \[Bullet]\[NonBreakingSpace]Interactive\[ThinSpace]\[Rule]\[ThinSpace]False: a Boolean whether to save a notebook file interactively or not. Passed to NotebookSave."; Begin["`Private`"]; (**************** repeatedly used variables: ****************) $suitename = "ProblemSheet" (* the suite base name *); (**************** createExamSheet: ****************) createExamSheet::invtit = "`1`: a string is expected as \!\(\* StyleBox[\"title\", \"TI\"]\)."; createExamSheet::invsbt = "`1`: a string is expected as \!\(\* StyleBox[\"subtitle\", \"TI\"]\)."; createExamSheet::invfil = "`1`: a string is expected as \!\(\* StyleBox[\"file\", \"TI\"]\)."; createExamSheet::filexi = "`1`: file already exists; select another file name."; createExamSheet::invitm = "\"Items\"\[ThinSpace]\[Rule]\[ThinSpace]`1`: a pair of nonnegative integers is expected."; createExamSheet::invsty = "\"Stylesheet\"\[ThinSpace]\[Rule]\[ThinSpace]`1`: a string is expected."; createExamSheet::stynof = "\"Stylesheet\"\[ThinSpace]\[Rule]\[ThinSpace]`1`: style definitions notebook not found."; createExamSheet::invdbg = "\"Debug\"\[ThinSpace]\[Rule]\[ThinSpace]`1`: True or False is expected."; createExamSheet::inviac = "`1`: True or False is expected."; createExamSheet::badarg = "`2`[`1`] does not match the rule base."; Options[createExamSheet]={ "Items"->{6, 4}, "Stylesheet"->"Default.nb", "Debug"->False }; SyntaxInformation[createExamSheet]={"ArgumentsPattern"->{_,_,_,OptionsPattern[]}}; createExamSheet[ title_/;Or[ StringQ[title], Throw[Message[createExamSheet::invtit,title]/.Null->$Failed,"invtit",#1&] ], subtitle_/;Or[ StringQ[subtitle], Throw[Message[createExamSheet::invsbt,subtitle]/.Null->$Failed,"invsbt",#1&] ], file_/;Or[ StringQ[file], Throw[Message[createExamSheet::invfil,file]/.Null->$Failed,"invfil",#1&] ], opts:OptionsPattern[] ]:= Block[{itms,stydef,dbg,passopt,nsopt}, {itms,stydef,dbg}=OptionValue[createExamSheet, FilterRules[{opts},Options[createExamSheet]], Keys@Options[createExamSheet]]; passopt=FilterRules[{opts},Except[Options[createExamSheet]]]; nsopt=FilterRules[passopt, Options[NotebookSave]]; Or[And[VectorQ[itms,And[IntegerQ[#],NonNegative[#]]&],Length[itms]==2],Throw[Message[createExamSheet::invitm,itms]/.Null->$Failed,"invitm",#1&]]; Or[StringQ[stydef],Throw[Message[createExamSheet::invsty,stydef]/.Null->$Failed,"invsty",#1&]]; Or[Apply[Or,Flatten[Apply[List,Map[FileSystemMap[FileExistsQ,FileNameJoin[#],3,FileNameForms->stydef,MaxItems->1]&,{ {$UserBaseDirectory,"SystemFiles","FrontEnd","StyleSheets"}, {$BaseDirectory,"SystemFiles","FrontEnd","StyleSheets"}, {$InstallationDirectory,"SystemFiles","FrontEnd","StyleSheets"}, {$InstallationDirectory,"SystemFiles","Components","PresenterTools","FrontEnd","StyleSheets"} },Heads->False],Infinity,Heads->True]],Heads->True],Throw[Message[createExamSheet::stynof,stydef]/.Null->$Failed,"stynof",#1&]]; Or[BooleanQ[dbg],Throw[Message[createExamSheet::invdbg,dbg]/.Null->$Failed,"invdbg",#1&]]; Or[MatchQ[nsopt,{Rule[Interactive,True|False]}|{}],Throw[Message[createExamSheet::inviac,nsopt]/.Null->$Failed,"inviac",#1&]]; And[FileExistsQ[file], Message[createExamSheet::filexi,StringJoin["\[Ellipsis]",$PathnameSeparator,FileNameDrop[file,Ceiling[FileNameDepth[file]/2]]]];nsopt={Rule[Interactive,True]}]; If[dbg,Print["title = ",title,"\nsubtitle = ",subtitle,"\nfile = ",file,"\n-----\nitms = ",itms,"\nstydef = ",stydef,"\ndbg = ",dbg,"\n-----\npassopt = ",passopt,"\nnsopt = ",nsopt]; Return[] ]; With[{itemtext="Enter the problem description here.",subitemtext="Part of problem."}, {nb=CreateWindow[ DocumentNotebook[ Join[If[subtitle==="",{TextCell[title,"Title"]},{TextCell[title,"Title"],TextCell[subtitle,"Subtitle"]}], Flatten[ ConstantArray[{ TextCell[itemtext,"ItemNumbered", CellMargins->{{23,10},{6,6}},CellDingbatMargin->-17, CellFrame->{{True,0},{0,1}},CellFrameMargins->{{25,0},{0,6}}, CellFrameStyle->{{GrayLevel[1,0],GrayLevel[0]},{GrayLevel[0],GrayLevel[0]}} ], ConstantArray[ TextCell[subitemtext,"SubitemNumbered", CellDingbat->Cell[TextData[{"(",CounterBox["SubitemNumbered",CounterFunction->(Part[CharacterRange["a","z"],#]&)],")"}]], CellMargins->{{76.5,10},{4,4}} ], itms[[2]]] },itms[[1]]], 2] ] ], TrackCellChangeTimes->False, Visible->True, WindowSize->{Scaled[3/4],Scaled[1]}, WindowMargins->{{Automatic,0},{Automatic,0}}, PrintingCopies->1, PrintingStartingPageNumber->1, PrintingPageRange->{Automatic,Automatic}, PrintingStyleEnvironment->"Working", PageHeaders->{{None,None,None},{None,None,None}}, PageFooters->{{None,None,None},{None,Cell[TextData[{"\[LongDash]"," ",CounterBox["Page",CounterFunction:>Identity]," ","\[LongDash]"}],"Footer"],None}}, PageHeaderLines->{False,False}, PageFooterLines->{False,False}, PrintingOptions->{"FacingPages"->False,"FirstPageFace"->Right,"FirstPageFooter"->True,"FirstPageHeader"->False,"Magnification"->1.,"PaperOrientation"->"Portrait","PaperSize"->{790.8661417322835,1190.5511811023623`},"PrintingMargins"->{{28.8,43.199999999999996`},{36.,36.}}}, StyleDefinitions->stydef ]}, NotebookSave[nb,file,Sequence@@nsopt,Interactive->False]; NotebookClose[nb,Interactive->False]; ] ] createExamSheet[anything___]:=( Message[createExamSheet::badarg,Row[{anything},","],createExamSheet]; Return[$Failed] ) With[{syms = Apply[Join,Map[Names,{$suitename<>"`*"},Heads->False],Heads->False]}, SetAttributes[syms, {Protected, ReadProtected}] ]; End[]; EndPackage[];