diff --git a/src/bootsupport/modules/fauxlink-0.1.2.tm b/src/bootsupport/modules/fauxlink-0.2.0.tm similarity index 84% rename from src/bootsupport/modules/fauxlink-0.1.2.tm rename to src/bootsupport/modules/fauxlink-0.2.0.tm index f66d8b15..76be6f0f 100644 --- a/src/bootsupport/modules/fauxlink-0.1.2.tm +++ b/src/bootsupport/modules/fauxlink-0.2.0.tm @@ -7,7 +7,7 @@ # (C) 2024 # # @@ Meta Begin -# Application fauxlink 0.1.2 +# Application fauxlink 0.2.0 # Meta platform tcl # Meta license MIT # @@ Meta End @@ -17,7 +17,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin fauxlink_module_fauxlink 0 0.1.2] +#[manpage_begin fauxlink_module_fauxlink 0 0.2.0] #[copyright "2024"] #[titledesc {faux link application shortcuts}] [comment {-- Name section and table of contents description --}] #[moddesc {.fauxlink .fxlnk}] [comment {-- Description at end of page heading --}] @@ -628,28 +628,126 @@ namespace eval fauxlink { } return $out } + #encode one #-delimited section (or one + delimited target segment) + #round-trip property: literal % is encoded as %25 so that pre-existing %XX / %UXXXX + #lookalike sequences in the input survive resolve's decoding untouched. + proc Segment_encode {str} { + variable encode_map + if {[tcl::string::first \x00 $str] >= 0} { + error "fauxlink::link_as NUL character cannot be represented in a fauxlink name" + } + set out [tcl::string::map {% %25} $str] + return [tcl::string::map $encode_map $out] + } namespace eval argdoc { variable PUNKARGS lappend PUNKARGS [list { @id -id ::fauxlink::link_as @cmd -name fauxlink::link_as\ -summary\ - "NOT YET IMPLEMENTED - encode a name and target as a fauxlink filename."\ + "Encode a name and target as a fauxlink filename (counterpart of resolve)."\ -help\ - "Reserved API - not yet implemented; currently raises an error. - Intended to perform the encoding counterpart of fauxlink::resolve: - produce the fauxlink filename for a given nominal name and target - path, applying the required %XX encodings and + path separators." - @values -min 2 -max 2 + "Return the fauxlink filename encoding the given nominal name and target + path, applying the required %XX encodings and + path separators, such + that fauxlink::resolve on the result returns the inputs unchanged + (resolve reports the decoded target tail as name when name is empty). + + target is a forward-slash separated path; each segment is encoded + separately (a literal + # % etc within a segment is escaped). A leading + // (e.g //server/share) is preserved via leading ++. + Literal % characters anywhere in the inputs are encoded as %25 so that + %XX or %UXXXX lookalike sequences in names/targets round-trip literally. + Unicode characters are left unencoded (modern filesystems accept utf-8). + + Only the filename is returned - no file is created; creating the + (normally zero-byte) link file is the caller's responsibility. + + Not representable (raises an error): NUL characters anywhere; a literal + forward slash within the name, a tag, a comment or a target segment + (fauxlink deliberately provides no %XX decoding for the slash - path + structure must be expressed via the + separators); an empty comment + (resolve drops empty comments, so it cannot round-trip)." + @leaders -min 2 -max 2 name -type string -help\ - "Nominal name for the link (may be empty to default to the target tail)" + "Nominal name for the link. May be empty: the encoded form then starts + with # and resolve reports the target tail as the effective name. + NOTE: some punkcheck-based punkshell tooling treats leading-# filenames + as hidden/aside - prefer a non-empty name in punkshell project trees." target -type string -help\ - "Target path the link should resolve to" + "Target path the link should resolve to (forward-slash separated)" + @opts + -tags -type list -default {} -help\ + "List of tags for the tagset section. A literal @ within a tag is + encoded as %40 (the tagset uses @ as its separator)." + -comments -type list -default {} -help\ + "List of non-empty comments (each becomes one #-delimited section)" + -extension -type string -default "fauxlink" -choices {fauxlink fxlnk} -help\ + "File extension for the link filename" }] } - proc link_as {name target} { - #todo - implement the encoder counterpart of resolve - error "fauxlink::link_as not yet implemented" + proc link_as {name target args} { + #manual args parsing - punk::args PUNKARGS definition above is documentation-only + #(keep behaviour synchronized with the definition) + set opts [dict create -tags {} -comments {} -extension fauxlink] + foreach {k v} $args { + switch -- $k { + -tags - -comments - -extension { + dict set opts $k $v + } + default { + error "fauxlink::link_as unknown option '$k'. Known options: -tags -comments -extension" + } + } + } + set extension [dict get $opts -extension] + if {$extension ni [list fauxlink fxlnk]} { + error "fauxlink::link_as -extension must be fauxlink or fxlnk" + } + + if {[tcl::string::first / $name] >= 0} { + error "fauxlink::link_as name cannot contain a forward slash (no %XX decoding exists for it - see fauxlink::resolve)" + } + set encname [Segment_encode $name] + + set enctarget_parts [list] + foreach segment [split $target /] { + lappend enctarget_parts [Segment_encode $segment] + } + set enctarget [join $enctarget_parts +] + + set enctags [list] + foreach tag [dict get $opts -tags] { + if {[tcl::string::first / $tag] >= 0} { + error "fauxlink::link_as tag cannot contain a forward slash" + } + #encode literal @ within a tag - @ is the tagset separator + lappend enctags [tcl::string::map {@ %40} [Segment_encode $tag]] + } + set enccomments [list] + foreach comment [dict get $opts -comments] { + if {$comment eq ""} { + error "fauxlink::link_as empty comment is not representable (resolve drops empty comments)" + } + if {[tcl::string::first / $comment] >= 0} { + error "fauxlink::link_as comment cannot contain a forward slash" + } + lappend enccomments [Segment_encode $comment] + } + + set parts [list $encname $enctarget] + if {[llength $enctags]} { + lappend parts "@[join $enctags @]" + if {![llength $enccomments]} { + #force a 4th (empty comment) section so the tagset is not read as a comment + lappend parts "" + } else { + lappend parts {*}$enccomments + } + } elseif {[llength $enccomments]} { + #explicit empty tagset section keeps comments unambiguous + lappend parts "" {*}$enccomments + } + return "[join $parts #].$extension" } #proc sample1 {p1 args} { @@ -717,7 +815,7 @@ namespace eval ::punk::args::register { package provide fauxlink [namespace eval fauxlink { variable pkg fauxlink variable version - set version 0.1.2 + set version 0.2.0 }] return diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/fauxlink-0.1.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/fauxlink-0.2.0.tm similarity index 84% rename from src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/fauxlink-0.1.2.tm rename to src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/fauxlink-0.2.0.tm index f66d8b15..76be6f0f 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/fauxlink-0.1.2.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/fauxlink-0.2.0.tm @@ -7,7 +7,7 @@ # (C) 2024 # # @@ Meta Begin -# Application fauxlink 0.1.2 +# Application fauxlink 0.2.0 # Meta platform tcl # Meta license MIT # @@ Meta End @@ -17,7 +17,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin fauxlink_module_fauxlink 0 0.1.2] +#[manpage_begin fauxlink_module_fauxlink 0 0.2.0] #[copyright "2024"] #[titledesc {faux link application shortcuts}] [comment {-- Name section and table of contents description --}] #[moddesc {.fauxlink .fxlnk}] [comment {-- Description at end of page heading --}] @@ -628,28 +628,126 @@ namespace eval fauxlink { } return $out } + #encode one #-delimited section (or one + delimited target segment) + #round-trip property: literal % is encoded as %25 so that pre-existing %XX / %UXXXX + #lookalike sequences in the input survive resolve's decoding untouched. + proc Segment_encode {str} { + variable encode_map + if {[tcl::string::first \x00 $str] >= 0} { + error "fauxlink::link_as NUL character cannot be represented in a fauxlink name" + } + set out [tcl::string::map {% %25} $str] + return [tcl::string::map $encode_map $out] + } namespace eval argdoc { variable PUNKARGS lappend PUNKARGS [list { @id -id ::fauxlink::link_as @cmd -name fauxlink::link_as\ -summary\ - "NOT YET IMPLEMENTED - encode a name and target as a fauxlink filename."\ + "Encode a name and target as a fauxlink filename (counterpart of resolve)."\ -help\ - "Reserved API - not yet implemented; currently raises an error. - Intended to perform the encoding counterpart of fauxlink::resolve: - produce the fauxlink filename for a given nominal name and target - path, applying the required %XX encodings and + path separators." - @values -min 2 -max 2 + "Return the fauxlink filename encoding the given nominal name and target + path, applying the required %XX encodings and + path separators, such + that fauxlink::resolve on the result returns the inputs unchanged + (resolve reports the decoded target tail as name when name is empty). + + target is a forward-slash separated path; each segment is encoded + separately (a literal + # % etc within a segment is escaped). A leading + // (e.g //server/share) is preserved via leading ++. + Literal % characters anywhere in the inputs are encoded as %25 so that + %XX or %UXXXX lookalike sequences in names/targets round-trip literally. + Unicode characters are left unencoded (modern filesystems accept utf-8). + + Only the filename is returned - no file is created; creating the + (normally zero-byte) link file is the caller's responsibility. + + Not representable (raises an error): NUL characters anywhere; a literal + forward slash within the name, a tag, a comment or a target segment + (fauxlink deliberately provides no %XX decoding for the slash - path + structure must be expressed via the + separators); an empty comment + (resolve drops empty comments, so it cannot round-trip)." + @leaders -min 2 -max 2 name -type string -help\ - "Nominal name for the link (may be empty to default to the target tail)" + "Nominal name for the link. May be empty: the encoded form then starts + with # and resolve reports the target tail as the effective name. + NOTE: some punkcheck-based punkshell tooling treats leading-# filenames + as hidden/aside - prefer a non-empty name in punkshell project trees." target -type string -help\ - "Target path the link should resolve to" + "Target path the link should resolve to (forward-slash separated)" + @opts + -tags -type list -default {} -help\ + "List of tags for the tagset section. A literal @ within a tag is + encoded as %40 (the tagset uses @ as its separator)." + -comments -type list -default {} -help\ + "List of non-empty comments (each becomes one #-delimited section)" + -extension -type string -default "fauxlink" -choices {fauxlink fxlnk} -help\ + "File extension for the link filename" }] } - proc link_as {name target} { - #todo - implement the encoder counterpart of resolve - error "fauxlink::link_as not yet implemented" + proc link_as {name target args} { + #manual args parsing - punk::args PUNKARGS definition above is documentation-only + #(keep behaviour synchronized with the definition) + set opts [dict create -tags {} -comments {} -extension fauxlink] + foreach {k v} $args { + switch -- $k { + -tags - -comments - -extension { + dict set opts $k $v + } + default { + error "fauxlink::link_as unknown option '$k'. Known options: -tags -comments -extension" + } + } + } + set extension [dict get $opts -extension] + if {$extension ni [list fauxlink fxlnk]} { + error "fauxlink::link_as -extension must be fauxlink or fxlnk" + } + + if {[tcl::string::first / $name] >= 0} { + error "fauxlink::link_as name cannot contain a forward slash (no %XX decoding exists for it - see fauxlink::resolve)" + } + set encname [Segment_encode $name] + + set enctarget_parts [list] + foreach segment [split $target /] { + lappend enctarget_parts [Segment_encode $segment] + } + set enctarget [join $enctarget_parts +] + + set enctags [list] + foreach tag [dict get $opts -tags] { + if {[tcl::string::first / $tag] >= 0} { + error "fauxlink::link_as tag cannot contain a forward slash" + } + #encode literal @ within a tag - @ is the tagset separator + lappend enctags [tcl::string::map {@ %40} [Segment_encode $tag]] + } + set enccomments [list] + foreach comment [dict get $opts -comments] { + if {$comment eq ""} { + error "fauxlink::link_as empty comment is not representable (resolve drops empty comments)" + } + if {[tcl::string::first / $comment] >= 0} { + error "fauxlink::link_as comment cannot contain a forward slash" + } + lappend enccomments [Segment_encode $comment] + } + + set parts [list $encname $enctarget] + if {[llength $enctags]} { + lappend parts "@[join $enctags @]" + if {![llength $enccomments]} { + #force a 4th (empty comment) section so the tagset is not read as a comment + lappend parts "" + } else { + lappend parts {*}$enccomments + } + } elseif {[llength $enccomments]} { + #explicit empty tagset section keeps comments unambiguous + lappend parts "" {*}$enccomments + } + return "[join $parts #].$extension" } #proc sample1 {p1 args} { @@ -717,7 +815,7 @@ namespace eval ::punk::args::register { package provide fauxlink [namespace eval fauxlink { variable pkg fauxlink variable version - set version 0.1.2 + set version 0.2.0 }] return diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/templates-0.1.3.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/templates-0.1.3.tm deleted file mode 100644 index c55d1cca..00000000 Binary files a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/templates-0.1.3.tm and /dev/null differ diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/fauxlink-0.1.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/fauxlink-0.2.0.tm similarity index 84% rename from src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/fauxlink-0.1.2.tm rename to src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/fauxlink-0.2.0.tm index f66d8b15..76be6f0f 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/fauxlink-0.1.2.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/fauxlink-0.2.0.tm @@ -7,7 +7,7 @@ # (C) 2024 # # @@ Meta Begin -# Application fauxlink 0.1.2 +# Application fauxlink 0.2.0 # Meta platform tcl # Meta license MIT # @@ Meta End @@ -17,7 +17,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin fauxlink_module_fauxlink 0 0.1.2] +#[manpage_begin fauxlink_module_fauxlink 0 0.2.0] #[copyright "2024"] #[titledesc {faux link application shortcuts}] [comment {-- Name section and table of contents description --}] #[moddesc {.fauxlink .fxlnk}] [comment {-- Description at end of page heading --}] @@ -628,28 +628,126 @@ namespace eval fauxlink { } return $out } + #encode one #-delimited section (or one + delimited target segment) + #round-trip property: literal % is encoded as %25 so that pre-existing %XX / %UXXXX + #lookalike sequences in the input survive resolve's decoding untouched. + proc Segment_encode {str} { + variable encode_map + if {[tcl::string::first \x00 $str] >= 0} { + error "fauxlink::link_as NUL character cannot be represented in a fauxlink name" + } + set out [tcl::string::map {% %25} $str] + return [tcl::string::map $encode_map $out] + } namespace eval argdoc { variable PUNKARGS lappend PUNKARGS [list { @id -id ::fauxlink::link_as @cmd -name fauxlink::link_as\ -summary\ - "NOT YET IMPLEMENTED - encode a name and target as a fauxlink filename."\ + "Encode a name and target as a fauxlink filename (counterpart of resolve)."\ -help\ - "Reserved API - not yet implemented; currently raises an error. - Intended to perform the encoding counterpart of fauxlink::resolve: - produce the fauxlink filename for a given nominal name and target - path, applying the required %XX encodings and + path separators." - @values -min 2 -max 2 + "Return the fauxlink filename encoding the given nominal name and target + path, applying the required %XX encodings and + path separators, such + that fauxlink::resolve on the result returns the inputs unchanged + (resolve reports the decoded target tail as name when name is empty). + + target is a forward-slash separated path; each segment is encoded + separately (a literal + # % etc within a segment is escaped). A leading + // (e.g //server/share) is preserved via leading ++. + Literal % characters anywhere in the inputs are encoded as %25 so that + %XX or %UXXXX lookalike sequences in names/targets round-trip literally. + Unicode characters are left unencoded (modern filesystems accept utf-8). + + Only the filename is returned - no file is created; creating the + (normally zero-byte) link file is the caller's responsibility. + + Not representable (raises an error): NUL characters anywhere; a literal + forward slash within the name, a tag, a comment or a target segment + (fauxlink deliberately provides no %XX decoding for the slash - path + structure must be expressed via the + separators); an empty comment + (resolve drops empty comments, so it cannot round-trip)." + @leaders -min 2 -max 2 name -type string -help\ - "Nominal name for the link (may be empty to default to the target tail)" + "Nominal name for the link. May be empty: the encoded form then starts + with # and resolve reports the target tail as the effective name. + NOTE: some punkcheck-based punkshell tooling treats leading-# filenames + as hidden/aside - prefer a non-empty name in punkshell project trees." target -type string -help\ - "Target path the link should resolve to" + "Target path the link should resolve to (forward-slash separated)" + @opts + -tags -type list -default {} -help\ + "List of tags for the tagset section. A literal @ within a tag is + encoded as %40 (the tagset uses @ as its separator)." + -comments -type list -default {} -help\ + "List of non-empty comments (each becomes one #-delimited section)" + -extension -type string -default "fauxlink" -choices {fauxlink fxlnk} -help\ + "File extension for the link filename" }] } - proc link_as {name target} { - #todo - implement the encoder counterpart of resolve - error "fauxlink::link_as not yet implemented" + proc link_as {name target args} { + #manual args parsing - punk::args PUNKARGS definition above is documentation-only + #(keep behaviour synchronized with the definition) + set opts [dict create -tags {} -comments {} -extension fauxlink] + foreach {k v} $args { + switch -- $k { + -tags - -comments - -extension { + dict set opts $k $v + } + default { + error "fauxlink::link_as unknown option '$k'. Known options: -tags -comments -extension" + } + } + } + set extension [dict get $opts -extension] + if {$extension ni [list fauxlink fxlnk]} { + error "fauxlink::link_as -extension must be fauxlink or fxlnk" + } + + if {[tcl::string::first / $name] >= 0} { + error "fauxlink::link_as name cannot contain a forward slash (no %XX decoding exists for it - see fauxlink::resolve)" + } + set encname [Segment_encode $name] + + set enctarget_parts [list] + foreach segment [split $target /] { + lappend enctarget_parts [Segment_encode $segment] + } + set enctarget [join $enctarget_parts +] + + set enctags [list] + foreach tag [dict get $opts -tags] { + if {[tcl::string::first / $tag] >= 0} { + error "fauxlink::link_as tag cannot contain a forward slash" + } + #encode literal @ within a tag - @ is the tagset separator + lappend enctags [tcl::string::map {@ %40} [Segment_encode $tag]] + } + set enccomments [list] + foreach comment [dict get $opts -comments] { + if {$comment eq ""} { + error "fauxlink::link_as empty comment is not representable (resolve drops empty comments)" + } + if {[tcl::string::first / $comment] >= 0} { + error "fauxlink::link_as comment cannot contain a forward slash" + } + lappend enccomments [Segment_encode $comment] + } + + set parts [list $encname $enctarget] + if {[llength $enctags]} { + lappend parts "@[join $enctags @]" + if {![llength $enccomments]} { + #force a 4th (empty comment) section so the tagset is not read as a comment + lappend parts "" + } else { + lappend parts {*}$enccomments + } + } elseif {[llength $enccomments]} { + #explicit empty tagset section keeps comments unambiguous + lappend parts "" {*}$enccomments + } + return "[join $parts #].$extension" } #proc sample1 {p1 args} { @@ -717,7 +815,7 @@ namespace eval ::punk::args::register { package provide fauxlink [namespace eval fauxlink { variable pkg fauxlink variable version - set version 0.1.2 + set version 0.2.0 }] return diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/templates-0.1.3.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/templates-0.1.3.tm deleted file mode 100644 index c55d1cca..00000000 Binary files a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/templates-0.1.3.tm and /dev/null differ