——————————————# -----------------------------------------------------------------------------
=encoding utf8
=head1 NAME
Quiq::Html::Tag - Generierung von HTML-Tags
=head1 BASE CLASS
L<Quiq::Hash>
=head1 DESCRIPTION
=head2 Zweck der Klasse
Die Klasse generiert HTML-Code. Sie implementiert zwei Methoden -
tag() und cat() - mit denen HTML-Code beliebiger Komplexität
erzeugt werden kann.
=head2 Methode new()
Ein Objekt der Klasse wird instantiiert durch
my $h = Quiq::Html::Tag->new;
Die Methoden dieses Objekts generieren (X)HTML.
=head2 Methode tag()
Die Methode tag() generiert einen Tag. Sie kennt
alle Element-Typen des W3C HTML/XHTML Standard und liefert bei Aufruf
eine formatierte Tag-Repräsentation des betreffenden Element-Typs
und seiner Attribute. Beispiel:
$str = $h->tag('img',src=>'URL');
ergibt
<img src="URL" alt="" />
Das Beispiel verdeutlicht, dass der Methode tag() die
Element-Typen "kennt". In diesem Fall "weiß" die Methode, dass
Elemente des Typs 'img' leer sind und diese ein Pflicht-Attribut
'alt' besitzen (wird es nicht gesetzt, erhält es den Wert "").
Die Methode tag() hat die Signatur:
$str = $h->tag($elem,@keyVal,$content);
Hierbei ist $elem der Element-Typ, @keyVal die Liste der
Attribut/Wert-Paare und Optionen (siehe unten) und $content
ist der Inhalt des Tag.
Da die Argumente der Methode die gleiche Abfolge haben wie die
Bestandteile eines Tag in HTML, können die Methodenaufrufe in Perl
analog geschachtelt werden wie die Tags in HTML. Beispiel:
$h->tag('a',href=>'URL1',
$h->tag('img',src=>'URL2'),
);
ergibt
<a href="URL1"><img src="URL2" alt="" /></a>
=head2 Methode cat()
Die Elemente eines HTML-Dokuments können syntaktisch natürlich
nicht nur geschachtelt auftreten, sie können auch
aufeinanderfolgen. Die Methode cat() fügt aufeinanderfolgende
HTML-Elemente zusammen.
Die Methode cat() hat folgende Signatur:
$str = $h->cat(@args);
Hierbei ist @args die Liste der HTML-Elemente, die zusammengefügt
werden sollen.
Beispiel:
$h->cat(
$h->tag('li','Orange'),
$h->tag('li','Zitrone'),
$h->tag('li','Ananas'),
);
ergibt
<li>Orange</li>
<li>Zitrone</li>
<li>Ananas</li>
Die Methode cat() wird von der Methode tag() intern gerufen, um
den Content des Tag zu konstruieren. Erstreckt sich der Content
über mehrere Argumente, statt nur einem Argument (dem letzten), muss der
Methode tag() das Ende der @keyVal Argumente angezeigt werden.
Dies geschieht durch Setzen des Arguments '-' vor die
Content-Argumente.
$h->tag('ul','-',
$h->tag('li','Orange'),
$h->tag('li','Zitrone'),
$h->tag('li','Ananas'),
);
ergibt
<ul>
<li>Orange</li>
<li>Zitrone</li>
<li>Ananas</li>
</ul>
Der Inhalt eines Tag kann nicht nur aus HTML-Elementen bestehen,
sondern auch einfachen Text enthalten oder aus einer Mischung
aus Text und Elementen bestehen. Beispiel:
$h->tag('p','-',
'Auf ',$h->tag('a',href=>'http://cpan.org','CPAN'),
' sind ',$h->tag('b','viele'),' Module!',
);
ergibt
<p>
Auf <a href="http://cpan.org">CPAN</a> sind <b>viele</b> Module!
</p>
=head2 Komplexere HTML-Strukturen
Mit der Methode tag() lassen sich beliebig komplexe HTML-Strukturen
generieren. Der folgende Code produziert ein einfaches, bis auf
die fehlende Dokumenttyp-Deklaration vollständiges HTML-Dokument.
$h->tag('html','-',
$h->tag('head',
$h->tag('title','Test'),
),
$h->tag('body',
$h->tag('p',q|
Hallo
Welt!
|),
),
);
liefert
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Test</title>
</head>
<body>
<p>
Hallo
Welt!
</p>
</body>
</html>
=head2 Formatierung
Wer die obigen Beispiele genau betrachtet, bemerkt, dass die Tags
nicht alle mit der gleichen Formatierung generiert werden.
Der a-Tag umschließt seinen Inhalt unmittelbar
<a ...>INHALT</a>
wohingegen der p-Tag seinen Inhalt um zwei Leerzeichen einrückt
und der Begin- und End-Tag auf einer eignenen Zeile stehen
<p>
INHALT
</p>
Dies sind zwei der Formatierungsvarianten, die die Methode
tag() kennt.
Für jeden HTML Element-Typ ist eine Default-Formatierung definiert,
welche bestimmt, wie die Tags des Element-Typs formatiert werden.
Jedes Format wird mit einem Buchstaben bezeichnet: e, E, i, m, M, p, v.
e=empty mit Zeilenumbruch
Der Tag hat keinen Inhalt und steht auf einer eigenen Zeile (d.h.
er wird mit \n abgeschlossen):
<br />\n
E=empty ohne Zeilenumbruch
Wie e, nur dass der Tag nicht auf einer eigenen Zeile steht (also
nicht mit \n abgeschlossen wird):
<img ... />
i=inline
Der Tag umschließt seinen Inhalt unmittelbar, es wird
kein \n angehängt:
<a ...>IRGENDEIN\n
INHALT</a>
m=multiline mit Einrückung
Der Inhalt wird um zwei Leerzeichen eingerückt, der Tag umschließt
diesen auf getrennten Zeilen. Das Konstrukt endet mit einem \n:
<p ...>\n
IRGENDEIN\n
INHALT\n
</p>\n
M=multiline ohne Einrückung
Wie m, nur dass der Inhalt nicht eingerückt wird:
<html ...>\n
HEAD\n
BODY\n
</html>\n
p=protected
Der Inhalt wird einzeilig gemacht, indem LF- und CR-Zeichen durch
die Entities und ersetzt werden und vom Tag
unmittelbar umschlossen werden.
<pre ...>IRGENDEIN INHALT</pre>\n
Diese Formatierung schützt den Tag-Inhalt davor, dass er
durch eine Einrückung äußerer Tags verändert wird. Sie wird
bei Tags angewendet, bei deren Inhalt Whitespace signifikant ist.
v=variable
Enthält der Inhalt keine Zeilenumbrüche, wird er einzeilig ausgelegt:
<title>INHALT</title>\n
Enthält der Inhalt Zeilenumbrüche, wird er mehrzeilig ausgelegt:
<title>
EIN
TITEL
</title>
=head2 Setzen von data-* Attributen
Die Methode tag() unterstützt data-* Attribute auf zwei Weisen:
=over 4
=item 1.
Einzelne Attribute:
$h->tag('form',
'data-x' => 'a',
'data-y' => 'b',
'data-z' => 'c',
...
);
=item 2.
Liste von Attributen:
$h->tag('form',
data => [
x => 'a',
y => 'b',
z => 'c',
],
...
);
=back
Das Resultat in beiden Fällen:
<form data-z="c" data-y="b" data-x="a" ...></form>
=head2 HTML Element-Typen
Die Methode tag() kennt alle Element-Typen des W3C-Standards. Die
folgende Liste gibt den Element-Typ, die Default-Formatierung und
die Default-Attribute an:
Element Format Default-Attribute
a i
abbr i
acronym i
address v
area e
b i
base e
bdo i
big i
blockquote m
body M
br e
button v
caption v
cite i
code i
col e
colgroup m
dd v
del i
dfn i
div m
dl m
dt v
em i
fieldset m
form M
frame e
frameset m
h1 v
h2 v
h3 v
h4 v
h5 v
h6 v
head m
hr e
html M xmlns=>'http://www.w3.org/1999/xhtml' (nur XHTML)
i i
iframe v
img E alt=>''
input e
ins i
kbd i
label v
legend v
li v
link e
map m
meta e
noframes m
noscript m
object m
ol m
optgroup m
option v
p m
param e
pre p
q i
samp i
script m type=>'text/javascript'
select v
small i
span i
strong i
style m type=>'text/css'
sub i
sup i
table M
tbody m
td v
textarea p
tfoot m
th v
thead m
title v
tr m
tt i
ul m
var i
Das Default-Format kann mit der Option -fmt=>$val bei Aufruf
der Methode tag() überschrieben werden.
=head2 Zeilenumbruch
Bei allen Formaten außer i (inline) endet der generierte Tag Code per
Default mit einem Zeilenumbruch. Die Zahl der angehängten Zeilenumbrüche
kann unabhängig davon mit der Option -nl=>$n gesetzt werden.
Bei -nl=>0 wird kein Zeilenumbruch angehängt.
=head2 HTML statt XHTML
Per Default wird XHTML-Code generiert. Um klassischen HTML-Code
zu generieren, wird die HTML-Version gesetzt:
Quiq::Html::Tag->setDefault(htmlVersion=>'html-4.01');
Ab diesem Aufruf instantiieren die nachfolgenden Konstruktor-Aufrufe
(sofern sie die htmlVersion nicht selbst setzen) Objekte zur Generierung
von klassischem HTML.
Der generierte Code sieht bei klassischem HTML folgendermaßen aus:
<a href="URL1"><img src="URL2" alt=""></a>
Man beachte, dass der img-Tag nun mit '>', nicht mit ' />'
abgeschlossen ist.
=head2 HTML mit Großschreibung
HTML-Code mit großgeschriebenen Element- und Attributnamen wird
nach folgender Setzung erzeugt:
Quiq::Html::Tag->setDefault(htmlVersion=>'html-4.01',uppercase=>1);
Der generierte Code sieht dann so aus:
<A HREF="URL1"><IMG SRC="URL2" ALT=""></A>
Ist XHTML ('xhtml-1.0') eingestellt, hat die Option uppercase=>1
keinen Einfluss.
=head1 ATTRIBUTES
=over 4
=item checkLevel => 0|1|2 (Default: 1)
Umfang der Element/Attribut-Prüfung.
=over 4
=item Z<>0
keine Prüfung
=item Z<>1
prüfe Element-Typ
=item Z<>2
prüfe Element-Typ und Attributnamen
=back
Wird ein Fehler festgestellt, wird eine Exception geworfen.
=item compact => $bool (Default: 0)
Generiere den HTML-Code einzeilig mit so wenig Whitespace wie möglich.
=item embedImages => $bool (Default: 0)
Füge Bilddaten direkt in HTML ein.
=item htmlVersion => 'html-4.01'|'html-5'|'xhtml-1.0' (Default: 'xhtml-1.0')
HTML-Version. Beginnt die Versionsangabe mit 'xhtml' wird das
HTML gemäß den Regeln für XHTML generiert, andernfalls für
klassisches HTML. Die Versionsnummer wird bei der Generierung
des DOCTYPE herangezogen.
=item indentation => $n (Default: undef)
Forciere eine Content-Einrückung um $n Leerzeichen. Bei indentation=>0
wird nicht eingerückt. Ist indentation nicht gesetzt, gilt die
Einrückung, die als Argument bei der Tag-Methode angegeben ist bzw.
die in der Methode tag() als Default vorgegeben ist.
=item uppercase => $bool (Default: 0)
Erzeuge Tag- und Attribut-Namen in Großschreibung. Diese Setzung
wird nur bei klassischem HTML - nicht bei XHTML - beachtet.
=back
=cut
# -----------------------------------------------------------------------------
package
Quiq::Html::Tag;
use
v5.10;
use
strict;
use
warnings;
use
utf8;
our
$VERSION
=
'1.225'
;
use
Quiq::Css;
use
Quiq::Template;
use
Quiq::String;
use
Quiq::JavaScript;
use
Scalar::Util ();
use
Quiq::Image;
use
Quiq::Path;
use
MIME::Base64 ();
# -----------------------------------------------------------------------------
# Attribute und Defaultwerte bei der Instantiierung. Werden die
# Werte anders gesetzt, gelten sie für die gesamte Applikation.
my
%Default
= (
checkLevel
=> 1,
# Umfang der Element- und Attribut-Prüfungen
compact
=> 0,
# Einzeilig, Whitespace komprimiert
embedImages
=> 0,
# Einbettung von Bildern
# htmlVersion => 'xhtml-1.0', # XHTML vs. HTML, Versionsnr. für DOCTYPE
htmlVersion
=>
'html-5'
,
# XHTML vs. HTML, Versionsnr. für DOCTYPE
indentation
=>
undef
,
# forcierte Einrückung
oneLineCss
=> 0,
# <style>-Content stets einzeilig
oneLineJs
=> 0,
# <script>-Content stets einzeilig
uppercase
=> 0,
# wandele Elem.- und Att.-Namen in Großschr.
);
Hash::Util::lock_keys(
%Default
);
# Liste der vom W3C definierten (X)HTML-DTD- und Frameset-DTD-Elemente
# und ihre Default-Formatierung
my
%Element
= (
a
=>
'i'
,
# Link, Anker
abbr
=>
'i'
,
# Text ist Abkürzung
acronym
=>
'i'
,
# Text ist Akronym
address
=>
'v'
,
# Text ist Adressangabe
area
=>
'e'
,
# Bereich in einer clientseitigen Image-Map
b
=>
'i'
,
# Fettschrift
base
=>
'e'
,
# Basis-Pfadname für alle relativen URLs
bdo
=>
'i'
,
# Text mit anderer Laufrichtung
big
=>
'i'
,
# Großschrift
blockquote
=>
'm'
,
# Zitatabschnitt
body
=>
'm'
,
# Rumpf HTML-Seite
br
=>
'e'
,
# Zeilenumbruch
button
=>
'v'
,
# Schaltfläche
caption
=>
'v'
,
# Beschriftung zu einer Tabelle
canvas
=>
'm'
,
# HTML 5, Fläche zum Zeichnen
cite
=>
'i'
,
# Text ist Hinweis auf Literaturstelle
code
=>
'i'
,
# Codebeispiel
col
=>
'e'
,
# Eigenschaften einer Tabellenspalte
colgroup
=>
'm'
,
# Definition einer Spaltengruppe
dd
=>
'v'
,
# Text Definitionsliste
del
=>
'i'
,
# gelöschter Text
details
=>
'm'
,
# Details, die aufklappbar sind
dfn
=>
'i'
,
# Text ist Definition
div
=>
'm'
,
# allgemeines Block-Element
dl
=>
'm'
,
# Definitionsliste
dt
=>
'v'
,
# Terminus Definitionsliste
em
=>
'i'
,
# hervorgehobener Text
fieldset
=>
'm'
,
# Gruppe von Feldern eines Formulars
form
=>
'm'
,
# Formular
frame
=>
'e'
,
# (Frameset-DTD) Frame eines Frameset
frameset
=>
'm'
,
# (Frameset-DTD) Frameset
h1
=>
'v'
,
# Überschrift
h2
=>
'v'
,
# Überschrift
h3
=>
'v'
,
# Überschrift
h4
=>
'v'
,
# Überschrift
h5
=>
'v'
,
# Überschrift
h6
=>
'v'
,
# Überschrift
head
=>
'm'
,
# Kopf HTML-Seite
hr
=>
'e'
,
# Trennbalken
html
=>
'M'
,
# HTML-Seite
i
=>
'i'
,
# kursiver Text
iframe
=>
'v'
,
# (Frameset-DTD) Inline-Frame
img
=>
'E'
,
# Bild
input
=>
'e'
,
# Formular-Eingabeelement
ins
=>
'i'
,
# eingefügter Text
kbd
=>
'i'
,
# Text stellt Benutzereingabe dar
label
=>
'v'
,
# Label zu Formular-Eingabeelement
legend
=>
'v'
,
# Beschriftung zu Fieldset
li
=>
'v'
,
# Listenelement
link
=>
'e'
,
# Definiert Beziehung zu anderem Dokument
map
=>
'm'
,
# Definition clientseitige Image-Map
meta
=>
'e'
,
# Information zum Dokument
noframes
=>
'm'
,
# (Frameset-DTD) Inhalt für nicht-framefähige Browser
noscript
=>
'm'
,
# Inhalt für nicht-skriptfähige Browser
object
=>
'm'
,
# einbettetes Objekt
ol
=>
'm'
,
# nummerierte Liste
optgroup
=>
'm'
,
# Gruppe von Optionen einer Selectliste
option
=>
'v'
,
# Option einer Selectliste
p
=>
'm'
,
# Absatz
param
=>
'e'
,
# Parameter eines Object
pre
=>
'p'
,
# Leerraum und Zeilenumbrüche erhalten
q =>
'i'
,
# Kurzzitat
samp
=>
'i'
,
# Text stellt Ausgabe eines Programms dar
script
=>
'c'
,
# Code, der vom Browser ausgeführt wird
select
=>
'v'
,
# Auswahlmenü
small
=>
'i'
,
# Kleinschrift
span
=>
'i'
,
# allgemeines Inline-Element
strong
=>
'i'
,
# stark hervorgehobener Text
style
=>
'v'
,
# Stylesheet-Regeln
sub
=>
'i'
,
# tiefgestellter Text
summary
=>
'm'
,
# Kurzbeschreibung zu Details (<details>)
sup
=>
'i'
,
# hochgestellter Text
table
=>
'M'
,
# Tabelle
tbody
=>
'm'
,
# Rumpf Tabelle
td
=>
'v'
,
# Tabellenzelle
textarea
=>
'p'
,
# Text-Eingabefeld
tfoot
=>
'm'
,
# Fuß Tabelle (identisch zu tbody)
th
=>
'v'
,
# Tabellenkopfzelle (identisch zu td)
thead
=>
'm'
,
# Kopf Tabelle (identisch zu tbody)
title
=>
'v'
,
# Titel HTML-Dokument
tr
=>
'm'
,
# Tabellenzeile
tt
=>
'i'
,
# Schreibmaschinenschrift
ul
=>
'm'
,
# Ungeordnete Liste
var
=>
'i'
,
# Text ist Variablenname
);
# Default-Optionen
my
%DefaultOptions
= (
p
=> [
-ignoreIfNull
=>1],
span
=> [
-ignoreIfNull
=>1],
);
# Default-Attribute von Elementen allgemein
my
%DefaultAttributes
= (
# form => [method=>'post',enctype=>'multipart/form-data'],
img
=> [
alt
=>
''
],
script
=> [
type
=>
'text/javascript'
],
style
=> [
type
=>
'text/css'
],
);
# Default-Attribute, nur XHTML
my
%DefaultAttributesXhtml
= (
);
# Liste aller Attribute und ihrer Domänen.
# FIXME: anhand DTD überprüfen
my
%Attribute
= (
'accept-charset'
=>
'charsets'
,
# form
'http-equiv'
=>
'cdata'
,
# meta
'xml:lang'
=>
'languageCode'
,
# I18N (XHTML)
'xml:space'
=>
'space'
,
# ENUM pre,script,style(XHTML)
abbr
=>
'text'
,
# Cell
accept
=>
'contentTypes'
,
# form,input
accesskey
=>
'character'
,
# Focus,legend
action
=>
'uri'
,
# form
align
=>
'cellAlign'
,
# ENUM CellAlign
alt
=>
'cdata'
,
# input(cdata);area,img(text)
archive
=>
'uriList'
,
# object
axis
=>
'cdata'
,
# Cell
border
=>
'pixels'
,
# table
cellpadding
=>
'length'
,
# table
cellspacing
=>
'length'
,
# table
char
=>
'character'
,
# CellAlign
charoff
=>
'length'
,
# CellAlign
charset
=>
'charset'
,
# a,link,script
checked
=>
'bool'
,
# input
cite
=>
'uri'
,
# blockquote,del,ins,q
class
=>
'class'
,
# Core
classid
=>
'uri'
,
# object
codebase
=>
'uri'
,
# object
codetype
=>
'contentType'
,
# object
cols
=>
'multiLengths'
,
# frameset(multiLengths),textarea(number)
colspan
=>
'number'
,
# Cell
content
=>
'cdata'
,
# meta
coords
=>
'coords'
,
# a, area
data
=>
'uri'
,
# object
datetime
=>
'datetime'
,
# del, ins
declare
=>
'bool'
,
# object
defer
=>
'bool'
,
# script
dir
=>
'dir'
,
# ENUM I18N
disabled
=>
'bool'
,
# button,input,optgroup,option,select,
# textarea
download
=>
'filename'
,
# a
enctype
=>
'contentType'
,
# form
frame
=>
'tframe'
,
# ENUM table
frameborder
=>
'frameBorder'
,
# ENUM frame, iframe
headers
=>
'idrefs'
,
# Cell
height
=>
'length'
,
# iframe,img,object
href
=>
'uri'
,
# a,area,base,link
hreflang
=>
'languageCode'
,
# a,link
id
=>
'id'
,
# Core
ismap
=>
'bool'
,
# img,input
label
=>
'text'
,
# optgroup,option
lang
=>
'languageCode'
,
# I18N
longdesc
=>
'uri'
,
# frame,iframe,img
marginheight
=>
'pixels'
,
# frame,iframe
marginwidth
=>
'pixels'
,
# frame,iframe
maxlength
=>
'number'
,
# input
media
=>
'mediaDesc'
,
# ENUM link,style
method
=>
'method'
,
# ENUM form
multiple
=>
'bool'
,
# select
name
=>
'nmtoken'
,
# button,input,meta,param,select,textarea,
# object;form,frame,map(HTML)
nohref
=>
'bool'
,
# area
noresize
=>
'bool'
,
# frame
onblur
=>
'script'
,
# Focus,select
onchange
=>
'script'
,
# input,select,textarea
onclick
=>
'script'
,
# Event
ondblclick
=>
'script'
,
# Event
onfocus
=>
'script'
,
# Focus,select
onkeydown
=>
'script'
,
# Event
onkeypress
=>
'script'
,
# Event
onkeyup
=>
'script'
,
# Event
onload
=>
'script'
,
# body,frameset
onmousedown
=>
'script'
,
# Event
onmousemove
=>
'script'
,
# Event
onmouseout
=>
'script'
,
# Event
onmouseover
=>
'script'
,
# Event
onmouseup
=>
'script'
,
# Event
onreset
=>
'script'
,
# form
onselect
=>
'script'
,
# input,textarea
onsubmit
=>
'script'
,
# form
onunload
=>
'script'
,
# body,frameset
profile
=>
'uri'
,
# head
readonly
=>
'bool'
,
# input,textarea
rel
=>
'linkTypes'
,
# ENUM a,link
rev
=>
'linkTypes'
,
# ENUM a,link
rows
=>
'multiLengths'
,
# frameset(multiLengths);textarea(number)
rowspan
=>
'number'
,
# Cell
rules
=>
'trules'
,
# ENUM table
scheme
=>
'cdata'
,
# meta
scope
=>
'scope'
,
# ENUM Cell
scoped
=>
'bool'
,
# style
scrolling
=>
'scrolling'
,
# ENUM frame,iframe
selected
=>
'bool'
,
# option
shape
=>
'shape'
,
# ENUM a,area
size
=>
'number'
,
# input,select
span
=>
'number'
,
# col,colgroup
src
=>
'uri'
,
# frame,iframe,img,input,script
standby
=>
'text'
,
# object
style
=>
'styleSheet'
,
# Core
summary
=>
'text'
,
# table
tabindex
=>
'number'
,
# Focus,object,select
target
=>
'frameTarget'
,
# a,area,base,form,link
title
=>
'text'
,
# Core,style
type
=>
'ContentType'
,
# button(ENUM);a,link,object,param,script,
# style;input(ENUM)
usemap
=>
'uri'
,
# img,input,object
valign
=>
'cellValign'
,
# ENUM CellAlign
value
=>
'cdata'
,
# button,input,option,param
valuetype
=>
'valueType'
,
# ENUM param
width
=>
'length'
,
# iframe,img,object,table(length);
# col,colgroup(multiLength)
xmlns
=>
'uri'
,
# html (XHTML)
);
# Attribut-Domänen (werden aktuell nicht geprüft)
my
%Domain
= (
bool
=> 1,
buttonType
=> [
qw/button reset submit/
],
cdata
=> 1,
cellAlign
=> [
qw/center char justify left right/
],
cellValign
=> [
qw/baseline bottom middle top/
],
character
=> 1,
charset
=> 1,
charsets
=> 1,
class
=> 1,
contentType
=> 1,
contentTypes
=> 1,
coords
=> 1,
datetime
=> 1,
dir
=> [
qw/ltr rtl/
],
frameBorder
=> [0,1],
frameTarget
=> 1,
id
=> 1,
idrefs
=> 1,
inputType
=> [
qw/checkbox file hidden password radio reset submit text/
],
languageCode
=> 1,
length
=> 1,
linkTypes
=> [
qw/stylesheet next prev copyright index glossary/
],
# unv.
mediaDesc
=> [
qw/all aural braille handheld print projection screen
tty tv/
],
method
=> [
qw/get post/
],
multiLength
=> 1,
multiLengths
=> 1,
nmtoken
=> 1,
number
=> 1,
pixels
=> 1,
scope
=> [
qw/col colgroup row rowgroup/
],
script
=> 1,
scrolling
=> [
qw/no yes auto/
],
shape
=> [
qw/circle default poly rect/
],
space
=> [
'preserve'
],
styleSheet
=> 1,
text
=> 1,
tframe
=> [
qw/above below border box hsides lhs rhs void vsides/
],
trules
=> [
qw/all cols groups none rows/
],
uri
=> 1,
uriList
=> 1,
valueType
=> [
qw/data object ref/
],
);
# # den Platzbdearf der Hashes ausgeben
#
# use Devel::Size;
# my @arr = (
# Default => \%Default,
# Element => \%Element,
# Attribute => \%Attribute,
# DefaultAttributes => \%DefaultAttributes,
# DefaultAttributesXhtml => \%DefaultAttributesXhtml,
# );
# my $bytes = 0;
# my $str = '';
# while (@arr) {
# my $name = shift @arr;
# my $ref = shift @arr;
# my $n = Devel::Size::total_size($ref);
# $bytes += $n;
# $str .= ', ' if $str;
# $str .= "%$name: $n";
# }
# warn "$str, Gesamt: $bytes Bytes\n";
# -----------------------------------------------------------------------------
=head1 METHODS
=head2 Klassenmethoden
=head3 new() - Konstruktor
=head4 Synopsis
$h = $class->new(@keyVal);
$h = $class->new($htmlVersion,@keyVal);
=head4 Description
Instantiiere ein Objekt zur Generierung von HTML-Code und liefere eine
Referenz auf dieses Objekt zurück.
=cut
# -----------------------------------------------------------------------------
sub
new {
my
$class
=
shift
;
my
$self
=
$class
->SUPER::new(
%Default
);
if
(
@_
%2) {
# Wenn ungerade Anzahl Argumente, interpretieren wir
# das erste Argument als HTML-Version.
unshift
@_
,
'htmlVersion'
;
}
$self
->set(
@_
);
return
$self
;
}
# -----------------------------------------------------------------------------
=head3 setDefault() - Setze Objekt-Defaults
=head4 Synopsis
$class->setDefault(@keyVal);
=head4 Arguments
=over 4
=item @keyVal
Liste der Schlüssel/Wert-Paare, die gesetzt werden sollen.
=back
=head4 Example
Definiere, dass applikationsweit jeglicher CSS- und JavaScript-Code
in HTML einzeilig dargestellt wird:
Quiq::Html::Tag->setDefault(
oneLineCss => 1,
oneLineJs => 1,
);
=cut
# -----------------------------------------------------------------------------
sub
setDefault {
my
$class
=
shift
;
# @_: @keyVal
while
(
@_
) {
my
$key
=
shift
;
if
(!
exists
$Default
{
$key
}) {
$class
->throw(
'HTML-00099: Unkown attribute'
,
Attribute
=>
$key
,
);
}
$Default
{
$key
} =
shift
;
}
return
;
}
# -----------------------------------------------------------------------------
=head2 Objektmethoden
=head3 tag() - Generiere HTML-Tag
=head4 Synopsis
$html = $h->tag($elem,@keyVal); # [1]
$html = $h->tag($elem,@keyVal,$content); # [2]
$html = $h->tag($elem,@keyVal,'-',@content); # [3]
=head4 Options
=over 4
=item - (Dash)
Ende der Attribut/Wert-Liste. Die restlichen Argumente werden
zum Inhalt konkateniert.
$h->tag('p',class=>'p1','-',
'Ein ',$h->tag('b','kurzer'),' Text.'
);
Man beachte die Kommata zwischen den Argumenten.
Ohne '-' wird allein das letzte Argument als Inhalt aufgefasst.
Besteht der Inhalt aus mehreren Teilen, müssen die Teile
dann konkateniert werden:
$h->tag('p',class => 'p1',
'Ein '.$h->tag('b','kurzer').' Text.'
);
=item -checkLevel => 0|1|2 (Default: 1)
Umfang der Element-Typ und Attribut-Prüfung.
=item -compact => $bool (Default: Wert des Attributs compact)
Generiere den HTML-Code einzeilig mit so wenig Whitespace wie möglich.
Soll sämtlicher HTML-Code einzeilig erzeugt werden,
genügt es, beim Konstruktor das Attribut compact zu setzen:
$h = Quiq::Html::Tag->new(...,compact=>1);
=item -contentInd => $n (Default: undef)
Reduziere die Content-Einrückung auf $n, wenn möglich. Im Falle
von undef wird an der Content-Einrückung nichts geändert. Diese
Option ist nützlich, wenn der Content eingerückter Code ist (Tags
<style> und <script>) und dieser auf bei der Seitengenerierung auf
eine geringere Einrücktiefe gebracht werden soll (z.B. von 4 auf 2).
=item -embedImage => $bool (Default: 0)
Bette im Falle eines <img>-Tag das Bild in den HTML-Code ein.
=item -endTagOnly => $bool (Default: 0)
Erzeuge nur den Content und den End-Tag. Diese Option ist nützlich,
wenn der HTML-Code inkrementell geschrieben wird und z.B. im Fehlerfall
nur der Rest mit schließenden Tags generiert werden soll.
my $html = $h->tag('html',
-endTagOnly => 1,
$h->tag('body',
-endTagOnly => 1,
'-',
$h->tag('h1','Fatal Error'),
$h->tag('pre',$msg),
)
);
$http->append($html);
=item -fmt => 'c'|'e'|'E'|'m'|'M'|'p'|'P'|'v'|'i' (Default: gemäß $elem)
Art der Content-Formatierung.
=over 4
=item 'e' (empty)
Element hat keinen Content (br, hr, ...)
<TAG ... />\n
=item 'E' (empty, kein NEWLINE)
Wie 'e', nur ohne NEWLINE am Ende (img, input, ...)
<TAG ... />
=item 'm' (multiline)
Content wird auf eigene Zeile(n) zwischen Begin- und End-Tag
gesetzt und um -ind=>$n Leerzeichen eingerückt. Ist der Content
leer, wird der End-Tag direkt hinter den Begin-Tag gesetzt.
(html, head, body, form, p, div, script, style, ...)
<TAG ...>
CONTENT
</TAG>\n
Ist der Content leer:
<TAG ...></TAG>
=item 'M' (multiline, ohne Einrückung)
Wie 'm', nur ohne Einrückung (html, ...)
<TAG ...>
CONTENT
</TAG>\n
=item 'c' (cdata)
Wie 'm', nur dass der Content in CDATA eingefasst wird:
<TAG ...>
// <![CDATA[
CONTENT
// ]]>
</TAG>
Der W3 HTML Validator bemängelt die Zeichen & und > dann nicht
im Content. (script)
=item 'p' (protect)
Der Content wird geschützt, indem dieser einzeilig gemacht
(LF und CR werden durch und ersetzt) und unmittelbar
zwischen Begin- und End-Tag gesetzt wird (pre, textarea, ...).
<TAG ...>CONTENT</TAG>\n
=item 'P' (protect, Einrückung entfernen)
Wie 'p', nur dass die Einrückung des Content entfernt wird.
=item 'v' (variable)
Ist der Content einzeilig, wird er unmittelbar zwischen Begin-
und End-Tag gesetzt:
<TAG ...>CONTENT</TAG>\n
Ist der Content mehrzeilig, wird er eingerückt:
<TAG ...>
CONTENT
</TAG>\n
(title, h1-h6, ...)
=item 'i' (inline)
Der Content wird belassen wie er ist. Dies ist der Default für Tags,
die in Fließtext eingesetzt werden. Ein NEWLINE wird nicht angehängt.
Text Text <TAG ...>Text Text
Text</TAG> Text Text
(a, b, span, ...)
=back
Bei Wert 0 generiere keinen End-Tag bei leerem Content, sondern
<TAG .../>
=item -ignoreIf => $bool (Default: 0)
Liefere Leerstring, wenn Bedingung $bool erfüllt ist.
=item -ignoreIfNull => $bool (Default: 0)
Liefere Leerstring, wenn $content null (Leerstring oder undef)
ist. Für verschiedene Tags ist C<< -ignoreIfNull=>1 >> der
Default. Siehe Hash C<%DefaultOptions>. MEMO: Dieser Hash ist
nicht vollständig erstellt und kann (soll) nach Bedarf ergänzt
werden, insbesondere hinsichtlich der Option C<-ignoreIfNull>.
Für zahlreiche weitere Tags dürfte dies ein sinnvoller Default
sein (aber nicht für alle).
=item -ignoreTagIf => $bool (Default: 0)
Liefere $content (ohne Tag), wenn $bool erfüllt ist.
=item -ind => $n (Default: 2)
Rücke $content um $n Leerzeichen ein.
=item -indPos => $n (Default: 0)
Rücke bis auf die erste Zeile den Tag um $n Leerzeichen ein
und entferne Zeilenumbrüche am Ende. Diese Option ist nützlich, wenn
der Tag für einen Platzhalter mit der Einrücktiefe $n
in bestehenden HTML-Code eingesetzt wird.
=item -indTag => $n (Default: 0)
Rücke den Tag um $n Leerzeichen ein.
=item -nl => $n (Default: 1)
Anzahl NEWLINEs am Ende.
-nl => 0 (kein NEWLINE):
<TAG>CONTENT</TAG>
-nl => 1 (ein NEWLINE):
<TAG>CONTENT</TAG>\n
-nl => 2 (zwei NEWLINEs):
<TAG>CONTENT</TAG>\n\n
usw.
=item -placeholders => \@keyVal (Default: undef)
Ersetze im generierten HTML-Code die angegebenen Platzhalter durch
ihre Werte.
=item -remComments => $bool (Default: 0)
Entferne Kommentare aus dem Content. Die Option berücksichtigt,
ob es sich um HTML-, CSS- oder JavaScript-Content handelt.
=item -remNl => $bool (Default: 0)
Entferne Leerzeilen aus dem Content.
=item -tag => $tag (Default: erstes Argument des Methodenaufrufs)
Mit dieser Option kann der Tag verändert werden. Anwendungsfall:
statt eigentlich beim Aufruf angegebenen td soll ein th-Tag
gesetzt werden. Beispiel:
$h->tag('td',
-tag => 'th',
...
);
Siehe: Quiq::Html::Table::Simple
=item -tagWrap => $n (Default: 0)
Brich eine Tag-Zeile um, wenn sie $n Zeichen überschreitet. Wenn 0,
findet kein Umbruch statt. Diese Option ist nützlich, wenn ein Tag lange
Attribute haben kann und dies stört (siehe <embed> in
R1::YouTube::Player).
=item -text => $bool (Default: 0)
Behandele den Content als Text, d.h. schütze &, < und >.
=back
=head4 Description
Generiere HTML-Tag $tag gemäß den Optionen und Attributen @keyVal
und dem Inhalt $content und liefere das Resultat zurück [2].
Der Inhalt kann auch fehlen [1] oder sich über mehrere Argumente
erstrecken [3].
B<Attribut style>
Als Wert des Attributs C<style> kann eine Array-Referenz mit
CSS-Regeln angegeben werden. Diese werden von der
Methode Quiq::Css->rules() aufgelöst.
B<Boolsche Attribute>
Boolsche Attribute werden in HTML ohne Wert und in XHTML mit sich
selbst als Wert generiert.
HTML
<TAG ... ATTRIBUTE ...>
XHTML
<TAG ... ATTRIBUTE="ATTRIBUTE" ...>
Boolsche Attribute sind:
checked
declare
defer
disabled
ismap
multiple
nohref
noresize
readonly
selected
B<Default-Attribute>
Für einige Elemente sind Default-Attribute vereinbart, die nicht
explizit gesetzt werden müssen.
Element Attribute
form method => 'post', enctype => 'multipart/form-data'
script type => 'text/javascript'
style type => 'text/css'
=cut
# -----------------------------------------------------------------------------
# Setze Formatiereigenschaften
# FIXME: Das Bündel *aller* Eigenschaften setzen, da eine Umschaltung
# zwischen beliebigen Status vorkommen kann.
my
$setFmtDefaults
=
sub
{
my
(
$fmt
,
$fmtR
,
$indR
,
$endTagR
,
$nlR
) =
@_
;
$$fmtR
=
$fmt
;
if
(
$fmt
eq
'i'
) {
$$nlR
= 0;
}
elsif
(
$fmt
eq
'e'
) {
$$endTagR
= 0;
$$nlR
= 1;
}
elsif
(
$fmt
eq
'E'
) {
$$fmtR
=
'e'
;
$$endTagR
= 0;
$$nlR
= 0;
}
elsif
(
$fmt
eq
'M'
) {
$$fmtR
=
'm'
;
$$indR
= 0;
}
elsif
(
$fmt
eq
'v'
) {
$$nlR
= 1;
}
return
;
};
sub
tag {
my
$self
=
shift
;
my
$tag
=
shift
;
# Defaults
my
(
$xhtml
,
$html5
);
my
$htmlVersion
=
$self
->{
'htmlVersion'
};
if
(
$htmlVersion
=~ /^html-5/) {
$html5
= 1;
}
elsif
(
$htmlVersion
=~ /^xhtml/) {
$xhtml
= 1;
}
my
$uppercase
=
$self
->{
'uppercase'
} && !(
$xhtml
||
$html5
);
my
$embedImage
=
$self
->{
'embedImages'
};
my
$checkLevel
=
$self
->{
'checkLevel'
};
my
$compact
=
$self
->{
'compact'
};
my
$oneLineJs
=
$self
->{
'oneLineJs'
};
my
$oneLineCss
=
$self
->{
'oneLineCss'
};
my
$contentInd
=
undef
;
my
$fmt
=
'm'
;
my
$nl
= 1;
my
$ind
= 2;
my
$indPos
= 0;
my
$indTag
= 0;
my
$endTag
= 1;
my
$endTagOnly
= 0;
my
$ignoreIfNull
= 0;
my
$ignoreTagIf
= 0;
my
$oneLine
= 0;
my
$tagWrap
= 0;
my
$remComments
= 0;
my
$remNl
= 0;
my
$text
= 0;
my
$placeholders
=
undef
;
# Tag-spezifische Defaults
if
(
$tag
eq
'style'
||
$tag
eq
'script'
) {
$remComments
= 1;
$remNl
= 1;
$contentInd
= 2;
}
# Element-Lookup -> elementspezifische Defaults
# Bei -checkLevel=>0 muss $tag nicht existieren
my
$e
=
$Element
{
$tag
};
if
(
$e
) {
$setFmtDefaults
->(
$e
,\
$fmt
,\
$ind
,\
$endTag
,\
$nl
);
}
# Default-Attribute ermitteln (z.B. type="text/css" bei style)
my
(
@defAttr
,
%seenAttr
);
if
(
my
$arr
=
$DefaultAttributes
{
$tag
}) {
push
@defAttr
,
@$arr
;
}
if
(
$xhtml
&& (
my
$arr
=
$DefaultAttributesXhtml
{
$tag
})) {
push
@defAttr
,
@$arr
;
}
# BUG: funktioniert nicht, da spezifizierte Defaultattribute dann
# mehrfach auftreten
# if (my $arr = $DefaultAttributes{$tag}) {
# unshift @_,@$arr;
# }
# if ($xhtml && (my $arr = $DefaultAttributesXhtml{$tag})) {
# unshift @_,@$arr;
# }
# Tagbezeichner wandeln
my
$tagStr
=
$uppercase
?
uc
$tag
:
$tag
;
# Tag generieren
my
$str
=
''
;
# Optionen und Attribute verarbeiten
if
(
my
$arr
=
$DefaultOptions
{
$tag
}) {
unshift
@_
,
@$arr
;
}
while
(
@_
) {
if
(
@_
== 1) {
# Letztes Argument ist Content. Dieser Test muss als erstes
# kommen, damit '-' als Content nicht unter den Tisch fällt.
last
;
}
elsif
(
defined
$_
[0] &&
$_
[0] eq
'-'
) {
# explizites Ende der Options- und Attributliste
shift
;
# '-' konsumieren
last
;
}
elsif
(
ref
$_
[0]) {
# Argument ist (Array-)Referenz
last
;
}
my
$key
=
shift
;
my
$val
=
shift
;
if
(
substr
(
$key
,0,1) eq
'-'
) {
# FIXME: Fallunterscheidungen durch Hash-Lookup ersetzen
if
(
$key
eq
'-nl'
) {
$nl
=
$val
;
}
elsif
(
$key
eq
'-fmt'
) {
$setFmtDefaults
->(
$val
,\
$fmt
,\
$ind
,\
$endTag
,\
$nl
);
}
elsif
(
$key
eq
'-ind'
) {
if
(
defined
$val
) {
$ind
=
$val
;
}
}
elsif
(
$key
eq
'-indTag'
) {
$indTag
=
$val
;
}
elsif
(
$key
eq
'-indPos'
) {
$indPos
=
$val
;
}
elsif
(
$key
eq
'-ignoreIf'
) {
# sofortiger Abbruch
return
''
if
$val
;
}
elsif
(
$key
eq
'-ignoreIfNull'
) {
$ignoreIfNull
=
$val
;
}
elsif
(
$key
eq
'-ignoreTagIf'
) {
$ignoreTagIf
=
$val
;
}
elsif
(
$key
eq
'-checkLevel'
) {
$checkLevel
=
$val
;
}
elsif
(
$key
eq
'-tagWrap'
) {
$tagWrap
=
$val
;
}
elsif
(
$key
eq
'-text'
) {
$text
=
$val
;
}
elsif
(
$key
eq
'-endTagOnly'
) {
$endTagOnly
=
$val
;
}
elsif
(
$key
eq
'-embedImage'
) {
$embedImage
=
$val
;
}
elsif
(
$key
eq
'-contentInd'
) {
$contentInd
=
$val
;
}
elsif
(
$key
eq
'-remComments'
) {
$remComments
=
$val
;
}
elsif
(
$key
eq
'-remNl'
) {
$remNl
=
$val
;
}
elsif
(
$key
eq
'-compact'
) {
$compact
=
$val
;
}
elsif
(
$key
eq
'-placeholders'
) {
$placeholders
=
$val
;
}
elsif
(
$key
eq
'-tag'
) {
if
(
defined
(
$val
) &&
$val
ne
''
) {
$tag
=
$val
;
$tagStr
=
$uppercase
?
uc
$tag
:
$tag
;
}
}
else
{
$self
->throw(
'HTML-00001: Unbekannte Option'
,
Option
=>
$key
,
Value
=>
$val
,
);
}
next
;
}
elsif
(
$key
eq
'data'
&& Scalar::Util::reftype(
$val
)) {
# data-* Attribute als Liste data=>\@keyVal
my
@keyVal
;
for
(
my
$i
=
@$val
-2;
$i
>= 0;
$i
-= 2) {
unshift
@_
,
'data-'
.
$val
->[
$i
],
$val
->[
$i
+1];
}
next
;
}
if
(
$endTagOnly
) {
$self
->throw(
'HTML-00004: Attribute bei Option -endTagOnly nicht erlaubt'
,
Tag
=>
$tag
,
Attribute
=>
$key
,
Value
=>
$val
,
);
}
# Attribut-Lookup. Attribut data-* ist generell erlaubt.
my
$dom
=
$Attribute
{
$key
} ||
''
;
if
(!
$dom
&&
$checkLevel
&&
$key
!~ /^data-/) {
$self
->throw(
'HTML-00003: Unbekanntes Attribut'
,
Tag
=>
$tag
,
Attribute
=>
$key
,
Value
=>
$val
,
);
}
# Attribut/Wert-Paar
if
(
$key
eq
'style'
&&
ref
(
$val
)) {
# liefert undef, wenn Array leer ist
$val
= Quiq::Css->properties(
@$val
);
}
if
(
defined
$val
) {
if
(
$checkLevel
>= 2) {
$self
->checkValue(
$dom
,
$val
);
}
if
(
$tag
eq
'img'
&&
$key
eq
'src'
) {
$val
=
$self
->imgSrcValue(
$val
,
$embedImage
);
}
else
{
$val
=~ s/"/
"
;/g;
# FIXME: auf best. Domänen einschränken
}
$key
=
uc
$key
if
$uppercase
;
if
(
$dom
eq
'bool'
) {
if
(
$val
) {
$str
.=
$xhtml
||
$html5
?
qq| $key="$key"|
:
" $key"
;
}
}
else
{
$str
.=
qq| $key="$val"|
;
}
# Gesetzte Attribute merken, falls wir Defaultattribute haben
if
(
@defAttr
) {
$seenAttr
{
$key
}++;
}
}
}
# Defaultattribute hinzufügen, die nicht gesetzt wurden
if
(
@defAttr
&& !
$endTagOnly
) {
for
(
my
$i
= 0;
$i
<
@defAttr
;
$i
+= 2) {
if
(!
$seenAttr
{
$defAttr
[
$i
]}) {
$str
.=
sprintf
' %s="%s"'
,
$defAttr
[
$i
],
$defAttr
[
$i
+1];
}
}
}
# Attribute für kompakte Darstellung einstellen
if
(
$compact
) {
if
(
lc
(
$fmt
) ne
'p'
) {
$fmt
=
'i'
;
}
$ind
= 0;
$nl
= 0;
$remComments
= 1;
}
if
(!
$endTagOnly
) {
$str
=
"<$tagStr$str"
;
}
# Tag umbrechen
if
(
$tagWrap
&&
length
(
$str
) >
$tagWrap
) {
$str
=
$self
->wrapTag(
$tagWrap
,
$str
);
}
# Element prüfen
if
(!
$e
&&
$checkLevel
) {
$self
->throw(
'HTML-00002: Unbekanntes Element'
,
Element
=>
$tag
);
}
# Content bestimmen
my
$content
=
$self
->cat(
@_
);
# Klauseln auswerten, die auf den Content Bezug nehmen
if
(
$ignoreIfNull
&& (!
defined
$content
||
$content
eq
''
)) {
return
''
;
}
if
(
$placeholders
) {
# Platzhalter ersetzen
my
$tpl
= Quiq::Template->new(
'text'
,\
$content
);
$tpl
->replace(
@$placeholders
);
$content
=
$tpl
->asStringNL;
}
if
(
$text
) {
# Content ist Text, wir schützen &, <, >
$content
=~ s/&/
&
;/g;
$content
=~ s/</
<
;/g;
$content
=~ s/>/
>
;/g;
}
if
(
$remNl
) {
$content
=~ s/^(\s*)\n//mg;
}
if
(
$remComments
) {
if
(
$tag
eq
'script'
) {
$content
= Quiq::String->removeComments(
$content
,
'//'
);
}
elsif
(
$tag
eq
'style'
) {
$content
= Quiq::String->removeComments(
$content
,
'/*'
,
'*/'
);
}
else
{
# HTML
$content
= Quiq::String->removeComments(
$content
,
'<!--'
,
'-->'
);
}
}
if
(
$compact
) {
$content
=~ s/^\s+//g;
$content
=~ s/\s+$//g;
if
(
$tag
eq
'script'
) {
$content
=~ s/\n\s*/ /g;
# Whitespace nur zwischen Zeilen kompr.
}
elsif
(
$tag
eq
'style'
) {
$content
=~ s/\s+/ /g;
}
else
{
# HTML
$content
=~ s/\s+/ /g;
}
}
if
(
$ignoreTagIf
) {
return
$content
;
}
# Content bearbeiten
if
(
$fmt
eq
'p'
||
$fmt
eq
'P'
) {
Quiq::String->removeIndentation(\
$content
)
if
$fmt
eq
'P'
;
$content
=~ s/\x0a/&
#10;/g;
$content
=~ s/\x0d/&
#13;/g;
}
elsif
(
$fmt
eq
's'
) {
# Inhalt einzeilig machen durch Whitespace-Manipulation
$self
->throw(
'Not implemented'
);
}
elsif
(
$fmt
eq
'e'
||
$fmt
eq
'E'
) {
if
(
length
$content
) {
$self
->throw(
'HTML-00003: Kein Content erwartet'
);
}
}
elsif
(
$fmt
eq
'v'
&&
$content
!~ /\n/ ||
$fmt
eq
'i'
) {
# nichts tun
}
elsif
(
$fmt
eq
'v'
||
$fmt
eq
'm'
||
$fmt
eq
'c'
) {
Quiq::String->removeIndentation(\
$content
);
if
(
$contentInd
) {
# Bringe Einrückung des Content auf Tiefe $contendInd
Quiq::String->reduceIndentation(
$contentInd
,\
$content
);
}
if
(
$fmt
eq
'c'
) {
if
(
$oneLineJs
) {
# mache JavaScript-Code einzeilig
$content
= Quiq::JavaScript->line(
$content
);
}
if
(
$content
=~
tr
/&<>//) {
# Script-Code in CDATA einfassen, wenn &, < oder > enthalten
$content
=
"// <![CDATA[\n$content\n// ]]>"
;
# Folgendes geht nicht!
# $content =~ s/&/&/g;
# $content =~ s/</</g;
}
}
elsif
(
$tag
eq
'style'
&&
$oneLineCss
) {
# mache CSS-Code einzeilig
$content
= Quiq::Css->oneLine(
$content
);
}
if
(
defined
(
my
$forceIndent
=
$self
->{
'indentation'
})) {
$ind
=
$forceIndent
;
}
if
(
$ind
) {
my
$space
=
' '
x
$ind
;
$content
=~ s/^(?!$)/
$space
/gm;
# Nicht-Leerzeilen einrücken
}
# MEMO: Test auf 'c', damit 'script' mit src nicht mehrzeilig wird
$content
=
$content
?
"\n$content\n"
:
''
;
# $fmt eq 'c'? '': "\n";
}
else
{
$self
->throw(
'HTML-00002: Unerlaubter Wert für Option -fmt'
,
Value
=>
$fmt
,
);
}
# End-Tag
if
(
$endTag
||
$content
) {
if
(
$endTagOnly
) {
$content
=~ s/^\n+//;
}
else
{
$str
.=
'>'
;
}
$str
.=
"$content</$tagStr>"
;
}
else
{
$str
.=
$xhtml
||
$html5
?
' />'
:
'>'
;
}
if
(
$indPos
) {
# Einrückung erzeugen
my
$space
=
' '
x
$indPos
;
$str
=~ s/^(?!$)/
$space
/gm;
# Nicht-Leerzeilen einrücken
# Einrückung erste Zeile entfernen
$str
=~ s/^
$space
//;
}
else
{
# NEWLINE
$str
.=
"\n"
x
$nl
;
}
if
(
$indTag
) {
# Einrückung erzeugen
my
$space
=
' '
x
$indTag
;
$str
=~ s/^(?!$)/
$space
/gm;
# Nicht-Leerzeilen einrücken
}
return
$str
;
}
# -----------------------------------------------------------------------------
=head3 wrapTag() - Umbreche langen Tag-String
=head4 Synopsis
$str = $this->wrapTag($maxWidth,$str);
=cut
# -----------------------------------------------------------------------------
sub
wrapTag {
my
(
$this
,
$max
,
$tag
) =
@_
;
my
$str
=
''
;
my
$tmp
=
''
;
for
my
$val
(
split
/ (?=\w+=
".*?"
)/,
$tag
) {
my
$l
=
length
$val
;
if
(
length
(
$tmp
)+
length
(
$val
) <=
$max
) {
if
(
$tmp
) {
$tmp
.=
' '
;
}
$tmp
.=
$val
;
}
else
{
if
(
$str
) {
$str
.=
"\n "
;
}
$str
.=
$tmp
;
$tmp
=
$val
;
}
}
if
(
$tmp
) {
$str
.=
"\n $tmp"
;
}
return
$str
;
}
# -----------------------------------------------------------------------------
=head3 cat() - Füge HTML-Fragmente zusammen
=head4 Synopsis
$html = $h->cat(@opt,@args);
=head4 Options
=over 4
=item -join => $str (Default: '')
Verwende $str als Trenner zwischen den Fragmenten. Diese Option
ist praktisch, wenn die Fragmente optional sind und die
auftretenden Elemente durch eine Zeichenkette voneinander getrennt
werden sollen. Beispiel: Eine Icon-Leiste, deren Zusammensetzung
nicht festgelegt ist und die nicht direkt aneinander stoßen
sollen, sondern durch Leerzeichen getrennt werden.
=item -placeholders => \@keyVal (Default: undef)
Ersetze im generierten HTML-Code die angegebenen Platzhalter durch
ihre Werte.
=back
=head4 Description
Füge die HTML-Fragmente @args zusammen und liefere den
resultierenden HTML-Code zurück.
@args ist eine Abfolge von Array-Referenzen und/oder Zeichenketten.
Der Aufruf
$h->cat(
['doctype'],
['comment',-nl=>2,'Copyright Lieschen Müller'],
['HTML',
['HEAD',
['TITLE','Meine Homepage'],
['STYLE',q|
.text { color: red; }
|],
],
['BODY',
['H1','Hallo Welt!'],
['P',class=>'text',q|
Ich heiße Lieschen Müller und dies
ist meine Homepage.
|],
],
]
);
ist äquivalent zu
$h->cat(
$h->doctype,
$h->comment(-nl=>2,'Copyright Lieschen Müller'),
$h->tag('html','-',
$h->tag('head','-',
$h->tag('title','Meine Homepage'),
$h->tag('style',q|
.text { color: red; }
|),
),
$h->tag('body','-',
$h->tag('h1','Hallo Welt!'),
$h->tag('p',class=>'text',q|
Ich heiße Lieschen Müller und dies
ist meine Homepage.
|),
),
),
);
Beide liefern
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
<!-- Copyright Lieschen Müller -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Meine Homepage</title>
<style type="text/css">
.text { color: red; }
</style>
</head>
<body>
<h1>Hallo Welt!</h1>
<p class="text">
Ich heiße Lieschen Müller und dies
ist meine Homepage.
</p>
</body>
</html>
=cut
# -----------------------------------------------------------------------------
sub
cat {
my
$self
=
shift
;
# Optionen
my
$join
=
''
;
my
$placeholders
=
undef
;
while
(
@_
) {
if
(!
defined
(
$_
[0]) ||
substr
(
$_
[0],0,1) ne
'-'
) {
# Ende der Optionen
last
;
}
elsif
(
$_
[0] eq
'-'
) {
# explizites Ende der Options-Liste
shift
;
# '-' konsumieren
last
;
}
elsif
(
$_
[0] eq
'-join'
) {
$join
=
splice
@_
,0,2;
next
;
}
elsif
(
$_
[0] eq
'-placeholders'
) {
$placeholders
=
splice
@_
,0,2;
next
;
}
#else {
# $self->throw(
# 'HTML-00002: Unbekannte Option',
# Option=>$_[0],
# );
#}
last
;
}
# Werte ermitteln und konkatenieren
my
@arr
;
for
my
$e
(
@_
) {
if
(
ref
$e
) {
# Argument ist (Array-)Referenz
if
(
$e
->[0] =~ /^[[:upper:]]/) {
# Namen beginnt mit Großbuchstaben -> Tag
push
@arr
,
$self
->tag(
lc
$e
->[0],
@$e
[1..
$#$e
]);
}
else
{
# Kleinbuchstabe -> Methode
my
$meth
=
$e
->[0];
push
@arr
,
$self
->
$meth
(
@$e
[1..
$#$e
]);
}
}
else
{
push
@arr
,
defined
(
$e
) &&
$e
ne
''
?
$e
: ();
}
}
my
$html
=
join
$join
,
@arr
;
# Platzhalter ersetzen
if
(
$placeholders
) {
my
$tpl
= Quiq::Template->new(
'text'
,\
$html
);
$tpl
->replace(
@$placeholders
);
$html
=
$tpl
->asStringNL;
}
return
$html
;
}
# -----------------------------------------------------------------------------
=head3 doctype() - <!DOCTYPE>-Tag
=head4 Synopsis
$str = $h->doctype(@opt);
=head4 Options
=over 4
=item -frameset => $bool (Default: 0)
Liefere Frameset-Doctype.
=item -nl => $n (Default: 2)
Ergänze Doctype um $n Zeilenumbrüche.
=back
=head4 Description
Liefere <!DOCTYPE>-Tag:
<!DOCTYPE ...>
Der Tag ergibt sich aus der eingestellten HTML-Variante.
=cut
# -----------------------------------------------------------------------------
sub
doctype {
my
$self
=
shift
;
my
$compact
=
$self
->{
'compact'
};
my
$frameset
= 0;
my
$nl
= 2;
while
(
@_
) {
my
$key
=
shift
;
if
(
$key
eq
'-compact'
) {
$compact
=
shift
;
}
elsif
(
$key
eq
'-frameset'
) {
$frameset
=
shift
;
}
elsif
(
$key
eq
'-nl'
) {
$nl
=
shift
;
}
else
{
$self
->throw(
'HTML-00001: Unbekannte Option'
,
Option
=>
$key
,
Value
=>
shift
,
);
}
}
if
(
$compact
) {
$nl
= 0;
}
my
$version
=
$self
->{
'htmlVersion'
};
my
$str
;
if
(
$version
eq
'xhtml-1.0'
&& !
$frameset
) {
$str
=
q|<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"|
;
if
(!
$compact
) {
$str
.=
"\n "
;
}
}
elsif
(
$version
eq
'xhtml-1.0'
&&
$frameset
) {
$str
=
q|<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"|
;
if
(!
$compact
) {
$str
.=
"\n "
;
}
}
elsif
(
$version
eq
'html-4.01'
&& !
$frameset
) {
$str
=
q|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01|
;
if
(!
$compact
) {
$str
.=
"\n "
;
}
}
elsif
(
$version
eq
'html-5'
) {
# && !$frameset) {
$str
=
q|<!DOCTYPE html>|
;
}
else
{
$self
->throw(
'HTML-00002: Unbekannte HTML-Version'
,
Version
=>
"'$version'"
,
);
}
$str
.=
"\n"
x
$nl
;
return
$str
;
}
# -----------------------------------------------------------------------------
=head3 comment() - Kommentar-Tag
=head4 Synopsis
$html = $h->comment(@keyVal,$content);
=head4 Options
Wie Methode tag()
=head4 Description
Liefere HTML Kommentar-Tag:
<!-- TEXT -->
bzw.
<!--
TEXT
-->
wenn TEXT mehrzeilig ist.
=cut
# -----------------------------------------------------------------------------
sub
comment {
my
$self
=
shift
;
return
''
if
!
defined
$_
[-1] ||
$_
[-1] eq
''
;
# kein Kommentar, wenn leer
my
$str
=
$self
->tag(
'COMMENT'
,
-checkLevel
=>0,
-fmt
=>
'v'
,
@_
);
$str
=~ s|^<COMMENT[^>]*>(\s?)|
'<!--'
.($1? $1:
' '
)|e;
$str
=~ s|(\s?)</COMMENT>|($1? $1:
' '
).
'-->'
|e;
return
$str
;
}
# -----------------------------------------------------------------------------
=head3 protect() - Schütze HTML Metazeichen
=head4 Synopsis
$html = $h->protect($text);
=head4 Description
Schütze alle Metazeichen in Text $text, so dass das Resultat
gefahrlos in den Content eines HTML-Tag eingesetzt werden kann.
=cut
# -----------------------------------------------------------------------------
sub
protect {
my
(
$self
,
$text
) =
@_
;
if
(
defined
$text
) {
$text
=~ s/&/
&
;/g;
$text
=~ s/</
<
;/g;
$text
=~ s/>/
>
;/g;
}
return
$text
;
}
# -----------------------------------------------------------------------------
=head3 optional() - Optional-Klammer: <!--optional-->...<!--/optional-->
=head4 Synopsis
$html = $h->optional(@keyVal,$content);
=head4 Description
Liefere optional-Klammer:
<!--optional-->...<!--/optional-->
bzw.
<!--optional-->
...
<!--/optional-->
wenn $content mehrzeilig ist.
=cut
# -----------------------------------------------------------------------------
sub
optional {
my
$self
=
shift
;
my
$content
=
pop
;
# @_: @keyVal
# Optionen
my
$nl
= 0;
while
(
@_
) {
my
$key
=
shift
;
my
$val
=
shift
;
if
(
$key
eq
'-nl'
) {
$nl
=
$val
;
}
else
{
$self
->throw(
'HTML--0001: Unbekannte Option'
,
Option
=>
$key
,
Value
=>
$val
,
);
}
}
# Generieren
my
$str
;
if
(!
defined
(
$content
) ||
$content
eq
''
) {
# kein Content
$str
=
''
;
}
elsif
(
$content
!~
tr
/\n//) {
# Content ist einzeilig
$str
=
"<!--optional-->$content<!--/optional-->"
;
}
else
{
# Content ist mehrzeilig
$str
=
"<!--optional-->\n$content<!--/optional-->\n"
;
}
if
(
$str
&&
$nl
) {
$str
.=
"\n"
x
$nl
;
}
return
$str
;
}
# -----------------------------------------------------------------------------
=head3 checkValue() - Prüfe Attributwert
=head4 Synopsis
$h->checkValue($dom,$val);
=head4 Description
Prüfe, ob der Attributwert $val für Domäne $dom korrekt ist.
Fällt die Prüfung negativ aus, löse eine Exception aus.
Die Methode liefert keinen Wert zurück.
=cut
# -----------------------------------------------------------------------------
sub
checkValue {
my
(
$self
,
$dom
,
$val
) =
@_
;
# FIXME: Wertprüfungen implementieren
return
;
}
# -----------------------------------------------------------------------------
=head3 imgSrcValue() - Liefere Wert für scr-Attribut eines Bildes
=head4 Synopsis
$val = $this->imgSrcValue($val,$embedImage);
=cut
# -----------------------------------------------------------------------------
sub
imgSrcValue {
my
(
$this
,
$val
,
$embedImage
) =
@_
;
if
(
ref
$val
) {
# Bilddaten in-memory
my
$type
= Quiq::Image->type(
$val
,
-enum
=>1);
$val
=
sprintf
'data:image/%s;base64,%s'
,
$type
,
MIME::Base64::encode_base64(
$$val
,
''
);
}
elsif
(
$embedImage
) {
if
(-f
$val
) {
# lokale Bilddatei
my
$type
= Quiq::Image->type(
$val
,
-enum
=>1);
$val
=
sprintf
'data:image/%s;base64,%s'
,
$type
,
MIME::Base64::encode_base64(
Quiq::Path->
read
(
$val
),
''
);
}
else
{
warn
'WARNING: <img> Embedding of URL-Image'
.
" currently not supported: $val\n"
;
}
}
return
$val
;
}
# -----------------------------------------------------------------------------
=head3 import() - Setze Konstruktor-Defaults der Klasse
=head4 Synopsis
$class->import(@keyVal);
=head4 Description
Setze die Konstruktor-Defaults @keyVal der Klasse. Die Setzung gilt
für alle folgenden Konstruktor-Aufrufe, bei denen die betreffenden Werte
beim Konstruktor-Aufruf nicht gesetzt werden.
Die Methode liefert keinen Wert zurück.
=head4 Example
=over 4
=item XHTML (Default):
use Quiq::Html::Tag;
=item HTML (Großschreibung) statt XHTML generieren:
use Quiq::Html::Tag(htmlVersion=>'html-4.01',uppercase=>1);
=back
=cut
# -----------------------------------------------------------------------------
sub
import
{
my
$class
=
shift
;
# @_: @keyVal
while
(
@_
) {
my
$key
=
shift
;
$Default
{
$key
} =
shift
;
}
return
;
}
# -----------------------------------------------------------------------------
=head1 VERSION
1.225
=head1 AUTHOR
Frank Seitz, L<http://fseitz.de/>
=head1 COPYRIGHT
Copyright (C) 2025 Frank Seitz
=head1 LICENSE
This code is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
# -----------------------------------------------------------------------------
1;
# eof