ویکینبشته
fawikisource
https://fa.wikisource.org/wiki/%D8%B5%D9%81%D8%AD%D9%87%D9%94_%D8%A7%D8%B5%D9%84%DB%8C
MediaWiki 1.47.0-wmf.23
first-letter
مدیا
ویژه
بحث
کاربر
بحث کاربر
ویکینبشته
بحث ویکینبشته
پرونده
بحث پرونده
مدیاویکی
بحث مدیاویکی
الگو
بحث الگو
راهنما
بحث راهنما
رده
بحث رده
درگاه
بحث درگاه
پدیدآورنده
بحث پدیدآورنده
برگه
گفتگوی برگه
فهرست
گفتگوی فهرست
TimedText
TimedText talk
پودمان
بحث پودمان
Event
Event talk
الگو:بدون منبع
10
22527
304040
303997
2026-10-12T14:05:27Z
Hanooz
17889
304040
wikitext
text/x-wiki
{{ambox
|type=content
|text='''سند منبع این متن مشخص نیست.'''<br />
{{smaller block|لطفاً برای کسب اطلاعات بیشتر و بررسی صحت منبع، [[{{TALKPAGENAME}}|صفحهٔ بحث]] را ببینید. منظور از «منبع»، مکانی است که کاربران دیگر بتوانند نسخهای از این اثر را در آن بیابند. ترجیحاً باید نسخهای اسکنشده از اصل اثر باشد که بتوان آن را در [[commons:Main page|ویکیانبار]] بارگذاری و در [[راهنما:نمونهخوانی|نمونهخوانی]] کرد. در غیر این صورت، بهتر است نشانی اینترنتی (URL) ارائه شود. اگر هیچیک از این موارد در دسترس نیست، لطفاً دلیل آن را در صفحهٔ بحث توضیح دهید.}}
}}
<includeonly>{{category handler
| all = [[رده:آثار بدون منبع|{{PAGENAME}}]]
| nocat = {{{nocat|}}}
}}</includeonly><noinclude>
{{documentation}}
</noinclude>
bpwbyme0tpu1cwq7x9ro221gx4qvzoi
مدیاویکی:Gadgets-definition
8
26276
304045
299729
2026-10-12T17:12:04Z
NguoiDungKhongDinhDanh
30809
Remove duplicate definition (no functional changes) ([[:m:GS|global sysop]] action)
304045
wikitext
text/x-wiki
== browsing-gadgets ==
* popups|popups.js
== LanguageSupport ==
* BiDiEditing[default]|BiDiEditing.js
* ShortLink[default]|ShortLink.js
* GoogleTranslator|GoogleTranslator.js
== Interface ==
* ocr|ocr.js
* editzero[dependencies=mediawiki.util]|editzero.js
* lastdiff[dependencies=mediawiki.util]|lastdiff.js
* purgetab[dependencies=mediawiki.util]|purgetab.js
<!-- * contribsrange[default]|contribsrange.js -->
<!-- * signit[default]|signit.js -->
<!-- * diffswitchdir[default]|diffswitchdir.js -->
* defaultsummaries|defaultsummaries.js
== Extra-Editbuttons ==
* addon-buttonsformove|addon-buttonsformove.js
* CustomizedEditTool|CustomizedEditTool.js
* matchsplit|matchsplit.js
* AHK[dependencies=mediawiki.util]|AHK.js
* intropreload|intropreload.js
* Extra-Editbuttons[default|rights=move|dependencies=mediawiki.util]|Extra-Editbuttons.js|Extra-Editbuttons-persiantools.js|Extra-Editbuttons-persianwikitools.js|Extra-Editbuttons-dictionary.js|Extra-Editbuttons-autoed.js|Extra-Editbuttons-botworks.js
* UserMessages[dependencies=jquery.ui,mediawiki.util,ext.gadget.morebits|type=general]|UserMessages.js|UserMessages.css
* dropdown-menus[dependencies=mediawiki.api,mediawiki.util,mediawiki.Title,user.options|peers=dropdown-menus-pagestyles]|dropdown-menus.js
* dropdown-menus-pagestyles[hidden]|dropdown-menus-pagestyles.css
* Edittools[default|type=general]|Edittools.js|Edittools.css
* BotTools[dependencies=mediawiki.util]|BotTools.js
== VisualEditor ==
* VeSuperTool|VeSuperTool.js|Extra-Editbuttons-persiantools.js|Extra-Editbuttons-persianwikitools.js|Extra-Editbuttons-dictionary.js
* VeExtendedBar[dependencies=ext.visualEditor.desktopArticleTarget.init]|VeExtendedBar.js
* VeDirectionMarkTool[hidden|dependencies=ext.visualEditor.core|type=general]|VeDirectionMarkTool.js|VeDirectionMarkTool.css
== Categories ==
* HotCat|HotCat.js
== admin-gadgets ==
* CleanDeleteReasons|CleanDeleteReasons.js
== othermod ==
* InterwikiTranslate|InterwikiTranslate.js
* GreenRedirect[dependencies=mediawiki.util]|GreenRedirect.js
* ImageAnnotator|ImageAnnotator.js
* yekewarning|yekewarning.js
onrnj0695924vk3z4dr8gkl4piblkym
برگه:The Blind Owl.pdf/۵۸
104
46698
304046
166960
2026-10-12T18:03:55Z
Kaur.gurmel
39954
/* همسنجیشده */
304046
proofread-page
text/x-wiki
<noinclude><pagequality level="4" user="Kaur.gurmel" />{{سصم|۵۸||}}</noinclude>و روشن همین الان هم شک دارم– نمیدانم اگر انگشتهایم را به هاون سنگی گوشه حیاطمان بزنم و از او بپرسم آیا ثابت و محکم هستی در صورت جواب مثبت باید حرف او را باور بکنم یا نه.–
«آیا من یک موجود مجزا و مشخص هستم؟ نمیدانم– ولی حالا که در آینه نگاه کردم خودم را نشناختم، نه، آن «من» سابق مرده است، تجزیه شده ولی هیچ سد و مانعی بین ما وجود ندارد– باید حکایت خودم را نقل بکنم ولی نمیدانم باید از کجا شروع کرد– سرتاسر زندگی قصه و حکایت است. باید خوشه انگور را بفشارم و شیره آنرا قاشق قاشق در گلوی خشک این سایه پیر بریزم.
«آیا از کجا باید شروع کرد؟ چون همه فکرهائی که عجالتاً در کلهام میجوشد، مال همین الان است، ساعت و دقیقه و تاریخ ندارد– یک اتفاق دیروز ممکن است برای من کهنهتر و بی تاثیرتر از یک اتفاق هزار سال پیش باشد.
«شاید از آنجائیکه همه روابط من با دنیای زندهها بریده شده یادگارهای گذشته جلوم نقش میبندد– گذشته، آینده، ساعت، روز، ماه و<noinclude></noinclude>
0w0l07pwps87vcc8cqljp8bm0kwknth
برگه:The Blind Owl.pdf/۵۹
104
46699
304060
124159
2026-10-12T23:06:20Z
Kaur.gurmel
39954
/* همسنجیشده */
304060
proofread-page
text/x-wiki
<noinclude><pagequality level="4" user="Kaur.gurmel" />{{سصم|۵۹||}}</noinclude>سال همه برایم یکسان است. مراحل مختلف بچگی و پیری برای من جز حرفهای پوچ چیزدیگری نیست، فقط برای مردمان معمولی، برای رجالهها– رجاله با تشدید– همین لغت را میجستم، برای رجالهها که زندگی آنها موسم و حد معینی دارد، مثل فصلهای سال و در منطقه معتدل زندگی واقع شده است صدق میکند. ولی زندگی من همهاش یک فصل و یک حالت داشته– مثل اینست که در یک منطقه سردسیر و در تاریکی جاودانی گذشته است در صورتیکه میان تنم همیشه یک شعله میسوزد و مرا مثل شمع آب میکند.
«میان چهاردیواری که اطاق مرا تشکیل میدهد و حصاری که دور زندگی و افکار من کشیده، زندگی من مثل شمع خرده خرده آب میشود، نه، اشتباه میکنم– مثل یک کنده هیزم تر است که گوشه دیگدان افتاده و به آتش هیزمهای دیگر برشته و ذغال شده، ولی نه سوخته است و نه تر و تازه مانده فقط از دود و دم دیگران خفه شده.– اطاقم مثل همه اطاقها با خشت و آجر روی خرابه هزاران خانههای قدیمی ساخته شده، بدنه سفید کرده و یک حاشیه کتیبه دارد– درست شبیه مقبره است– کمترین حالات و جزئیات اطاقم کافی است که ساعتهای<noinclude></noinclude>
0c19225ycozdhhru6g3vhvk44pok965
304062
304060
2026-10-13T11:46:56Z
Kaur.gurmel
39954
304062
proofread-page
text/x-wiki
<noinclude><pagequality level="4" user="Kaur.gurmel" />{{سصم|۵۹||}}</noinclude>سال همه برایم یکسان است. مراحل مختلف بچگی و پیری برای من جز حرفهای پوچ چیزدیگری نیست، فقط برای مردمان معمولی، برای رجالهها– رجاله با تشدید– همین لغت را میجستم، برای رجالهها که زندگی آنها موسم و حد معینی دارد، مثل فصلهای سال و در منطقه معتدل زندگی واقع شده است صدق میکند. ولی زندگی من همهاش یک فصل و یک حالت داشته– مثل اینست که در یک منطقه سردسیر و در تاریکی جاودانی گذشته است در صورتیکه میان تنم همیشه یک شعله میسوزد و مرا مثل شمع آب میکند.
«میان چهاردیواری که اطاق مرا تشکیل میدهد و حصاری که دور زندگی و افکار من کشیده، زندگی من مثل شمع خرده خرده آب میشود، نه، اشتباه میکنم– مثل یک کنده هیزم تر است که گوشه دیگران افتاده و به آتش هیزمهای دیگر برشته و ذغال شده، ولی نه سوخته است و نه تر و تازه مانده فقط از دود و دم دیگران خفه شده.– اطاقم مثل همه اطاقها با خشت و آجر روی خرابه هزاران خانههای قدیمی ساخته شده، بدنه سفید کرده و یک حاشیه کتیبه دارد– درست شبیه مقبره است– کمترین حالات و جزئیات اطاقم کافی است که ساعتهای<noinclude></noinclude>
c2ar28i7b5vgummrm548uui1bmkh2np
الگو:صفحههای بیسند
10
65931
304041
199113
2026-10-12T14:14:34Z
Hanooz
17889
304041
wikitext
text/x-wiki
{{ambox
|type=content
|text=این صفحه حاوی متنی است که با روش ویکینبشته از روی یک سند اسکنشده پیادهسازی نشده است و احتمالاً برای ویکینبشته مناسب نیست. متن جاری به شکل فعلی مستند نیست و امکان عدم تطابق با متن اصلی در آن زیاد است. {{سخ}}
لطفاً سند اسکنشده مربوط به این متن را روی ویکیانبار بارگذاری کرده و به پیادهسازی و نمونهخوانی متن بر اساس آنچه در [[راهنما:راهنمای کوتاه|صفحه راهنما]] آمده بپردازید.}}
}}
<includeonly>{{category handler
| all = [[رده:متنهای بدون نسخهٔ اسکنشده|{{PAGENAME}}]]
| nocat = {{{nocat|}}}
}}</includeonly><noinclude>
{{documentation}}
</noinclude>
ci26bulk7pvxxwgt3tf0jy8nvwdwmqh
304042
304041
2026-10-12T14:14:52Z
Hanooz
17889
304042
wikitext
text/x-wiki
{{ambox
|type=content
|text=این صفحه حاوی متنی است که با روش ویکینبشته از روی یک سند اسکنشده پیادهسازی نشده است و احتمالاً برای ویکینبشته مناسب نیست. متن جاری به شکل فعلی مستند نیست و امکان عدم تطابق با متن اصلی در آن زیاد است. {{سخ}}
لطفاً سند اسکنشده مربوط به این متن را روی ویکیانبار بارگذاری کرده و به پیادهسازی و نمونهخوانی متن بر اساس آنچه در [[راهنما:راهنمای کوتاه|صفحه راهنما]] آمده بپردازید.
}}
<includeonly>{{category handler
| all = [[رده:متنهای بدون نسخهٔ اسکنشده|{{PAGENAME}}]]
| nocat = {{{nocat|}}}
}}</includeonly><noinclude>
{{documentation}}
</noinclude>
ilctk91o8le8gsa106w006k8hch4aql
الگو:شعر۲/styles.css
10
97053
304039
304036
2026-10-12T13:44:48Z
Hanooz
17889
304039
sanitized-css
text/css
/*
* الگو:شعر/styles.css — TemplateStyles برای «پودمان:شعر»
*
* table-layout: auto ⇒ عرض ستون مصراع از متنِ ردیف سایزر میآید.
* ردیف سایزر بلندترین مصراعِ شعر را در هر دو ستون مصراع میگذارد، پس
* هر دو ستون هماندازه و برابر با بلندترین مصراعاند.
*/
/* ═══ جدول ═══ */
.poem-table {
border-collapse: collapse;
border-spacing: 0;
table-layout: auto; /* ← کلید کار */
text-align: center;
}
.poem-table td {
padding: 0.1em 0;
vertical-align: middle;
}
/* ═══ ردیف سایزر: نامرئی، ولی در تعیین عرض شرکت میکند ═══
* visibility: hidden ⇒ متن رندر نمیشود ولی برای اندازهگیری عرض
* همچنان شمرده میشود. height: 0 و line-height: 0 ⇒ ارتفاع صفر.
*/
.poem-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
overflow: hidden;
white-space: nowrap;
}
.poem-sz {
visibility: hidden;
white-space: nowrap;
}
/* ═══ عرض ستونها ═══
* .poem-mh عرضی ندارد ⇒ از محتوای سایزر (بلندترین مصراع) میگیرد.
* .poem-g عرض ثابت ۲em ⇒ همان فاصلهٔ نسخهٔ اصلی.
* .poem-s حداقل ۲em ⇒ جا برای شماره.
*/
.poem-g { width: 2em; min-width: 2em; }
.poem-s { width: 2em; min-width: 2em; }
/* ═══ سلولهای خالی ساختاری ═══ */
.poem-empty {
padding: 0 !important;
width: auto !important;
min-width: 0;
border: 0;
line-height: 0;
font-size: 0;
}
/* ═══ مصراع ═══ */
.poem-m {
padding-inline: 0.4em;
white-space: nowrap; /* مصراع در یک خط؛ هماندازه با سایزر */
}
.poem-m .beyt {
display: inline;
}
/* ═══ شمارهٔ ابیات ═══ */
.poem-num {
font-size: 85%;
white-space: nowrap;
vertical-align: middle;
}
.poem-num-r { text-align: left; padding-left: 0.5em; }
.poem-num-l { text-align: right; padding-right: 0.5em; }
/* ═══ مصراع راست/چپ ═══ */
.poem-m-right { text-align: right; text-align-last: auto; }
.poem-m-left { text-align: left; text-align-last: auto; }
/* ═══ فاصلهگذارها ═══ */
.poem-indent,
.poem-gapcell { padding: 0; }
/* ═══ نستعلیق ═══ */
.poem-table.nastaliq,
.poem-table.nastaliq td,
.poem-table.nastaliq .beyt {
font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu',
'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif;
}
.poem-table.nastaliq { font-size: 125%; line-height: 2.1; }
/* ═══ چندصفحهای ═══ */
.poem-skip { display: none; }
.poem-row.poem-final-marker { display: none !important; }
.poem-partial-end td.b { border-bottom: 1px dashed; }
.poem-partial-start td.b { border-top: 1px dashed; }
/* ═══ صفحهٔ متوسط ═══ */
@media (max-width: 45em) {
.poem-g { width: 1.2em; min-width: 1.2em; }
.poem-s { width: 1.6em; min-width: 1.6em; }
.poem-m { padding-inline: 0.3em; }
.poem-num-r { padding-left: 0.3em; }
.poem-num-l { padding-right: 0.3em; }
}
/* ═══ صفحهٔ باریک: هر مصراع یک خط ═══ */
@media (max-width: 32em) {
.poem-fixed { width: 100% !important; max-width: none !important; }
.poem-table { display: block; width: 100% !important; table-layout: auto; }
.poem-table tbody { display: block; }
.poem-sizer { display: none !important; }
.poem-row {
display: block;
padding: 0.25em 0 0.5em;
margin-bottom: 0.6em;
border-bottom: 1px dotted #c8c8c8;
}
.poem-row.poem-stagger-1 {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 0;
}
.poem-row.poem-stagger-2 {
border-bottom: 1px dotted #c8c8c8;
margin-bottom: 0.6em;
}
.poem-row:last-child { border-bottom: none; margin-bottom: 0; }
.poem-row > td {
display: block;
width: auto !important;
padding: 0 !important;
text-align: center !important;
text-align-last: auto !important;
}
.poem-empty,
.poem-num-c,
.poem-g,
.poem-gapcell { display: none !important; }
.poem-num-r,
.poem-num-l {
display: block;
font-size: 70%;
line-height: 1.15;
color: #888;
padding: 0 0.3em !important;
}
.poem-num-r { text-align: right !important; }
.poem-num-l { text-align: left !important; }
.poem-m {
padding: 0.15em 0.3em !important;
text-align: center !important;
text-align-last: auto !important;
white-space: normal;
}
}
c3hjw0qicx3007crzbtnm9l3926y8fb
304043
304039
2026-10-12T15:04:55Z
Hanooz
17889
304043
sanitized-css
text/css
/*
* Template:شعر۲/styles.css
* Shared seven-column layout for Module:شعر
*/
/* ═══ Table ═══ */
.poem-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
text-align: center;
}
.poem-table td {
padding: 0.1em 0;
vertical-align: middle;
}
/* ═══ Seven-column grid ═══
* 5% | 21% | 21% | 6% | 21% | 21% | 5%
* Each hemistich spans two columns: 42%.
*/
.poem-s { width: 5%; }
.poem-mh { width: 21%; }
.poem-g { width: 6%; }
/* ═══ Structural empty cells ═══ */
.poem-empty {
padding: 0 !important;
width: auto !important;
min-width: 0;
border: 0;
line-height: 0;
font-size: 0;
}
/* ═══ Hemistichs ═══ */
.poem-m {
padding-inline: 0.4em;
}
.poem-m .beyt {
display: block;
width: 100%;
min-width: 0;
overflow-wrap: break-word;
hyphens: none;
text-align: inherit;
text-align-last: inherit;
}
.poem-m .beyt sup.reference {
white-space: nowrap;
}
/* ═══ Verse numbering ═══ */
.poem-num {
font-size: 85%;
white-space: nowrap;
vertical-align: middle;
}
.poem-num-r {
text-align: left;
padding-left: 0.5em;
}
.poem-num-l {
text-align: right;
padding-right: 0.5em;
}
/* ═══ Left/right hemistichs ═══ */
.poem-m-right {
text-align: right;
text-align-last: auto;
}
.poem-m-left {
text-align: left;
text-align-last: auto;
}
/* ═══ Spacers ═══ */
.poem-indent,
.poem-gapcell {
padding: 0;
}
/* ═══ Sizing row ═══ */
.poem-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
font-size: 0;
}
/* ═══ Nastaliq ═══ */
.poem-table.nastaliq,
.poem-table.nastaliq td,
.poem-table.nastaliq .beyt {
font-family:
'IranNastaliq',
'Nastaliq Navees',
'Noto Nastaliq Urdu',
'Jameel Noori Nastaleeq',
'Nafees Nastaleeq',
'Pak Nastaleeq',
serif;
}
.poem-table.nastaliq {
font-size: 125%;
line-height: 2.1;
}
/* ═══ Multi-page continuation ═══ */
.poem-skip {
display: none;
}
.poem-row.poem-final-marker {
display: none !important;
}
.poem-partial-end td.b {
border-bottom: 1px dashed;
}
.poem-partial-start td.b {
border-top: 1px dashed;
}
/* ═══ Medium screens ═══ */
@media (max-width: 45em) {
.poem-m {
padding-inline: 0.3em;
}
.poem-num-r {
padding-left: 0.3em;
}
.poem-num-l {
padding-right: 0.3em;
}
}
/* ═══ Narrow screens ═══ */
@media (max-width: 32em) {
.poem,
.poem-fixed {
width: 100% !important;
max-width: none !important;
}
.poem-table {
display: block;
width: 100% !important;
table-layout: fixed;
}
.poem-table tbody {
display: block;
}
.poem-sizer {
display: none !important;
}
.poem-row {
display: block;
padding: 0.25em 0 0.5em;
margin-bottom: 0.6em;
border-bottom: 1px dotted #c8c8c8;
}
.poem-row.poem-stagger-1 {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 0;
}
.poem-row.poem-stagger-2 {
border-bottom: 1px dotted #c8c8c8;
margin-bottom: 0.6em;
}
.poem-row:last-child {
border-bottom: none;
margin-bottom: 0;
}
.poem-row > td {
display: block;
width: auto !important;
padding: 0 !important;
text-align: center !important;
text-align-last: auto !important;
}
.poem-empty,
.poem-num-c,
.poem-g,
.poem-gapcell {
display: none !important;
}
.poem-num-r,
.poem-num-l {
display: block;
font-size: 70%;
line-height: 1.15;
color: #888;
padding: 0 0.3em !important;
}
.poem-num-r {
text-align: right !important;
}
.poem-num-l {
text-align: left !important;
}
.poem-m {
padding: 0.15em 0.3em !important;
text-align: center !important;
text-align-last: auto !important;
}
.poem-m .beyt {
width: 100%;
white-space: normal;
}
}
e01utxchevuc7mxs6l6cuyvizzjlan6
304048
304043
2026-10-12T18:56:39Z
Hanooz
17889
304048
sanitized-css
text/css
/*
* الگو:شعر۲/styles.css — TemplateStyles برای «پودمان:شعر»
*
* چیدمان ثابت: عرض ستونها با درصد تعیین میشود. جدول از table-layout:
* fixed استفاده میکند و ردیف سایزرِ خالی در آغاز، عرضها را تثبیت میکند.
* همهٔ مصراعها در همهٔ ابیات و انواع، ۴۲٪ جدولاند.
*/
/* ═══ جدول ═══ */
.poem-table {
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;
text-align: center;
}
.poem-table td {
padding: 0.1em 0;
vertical-align: middle;
}
/* ═══ عرض ستونها (روی ردیف سایزر) ═══
* ۵ + ۲۱ + ۲۱ + ۶ + ۲۱ + ۲۱ + ۵ = ۱۰۰
*/
.poem-s { width: 5%; }
.poem-mh { width: 21%; }
.poem-g { width: 6%; }
/* ═══ ردیف سایزر: نامرئی ═══ */
.poem-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
font-size: 0;
}
/* ═══ سلولهای خالی ساختاری (بیت۱/نردبانی) ═══ */
.poem-empty {
padding: 0 !important;
border: 0;
line-height: 0;
font-size: 0;
}
/* ═══ مصراع ═══
* ترازبندی از style اینلاین (ALIGN_STYLE در پودمان) میآید.
* اگر ندهد، از .poem-table به ارث میرسد (وسط).
* ارجاعها inline میمانند و به آخرین واژه میچسبند.
*/
.poem-m {
padding-inline: 0.4em;
}
.poem-m .beyt {
overflow-wrap: break-word;
hyphens: none;
}
.poem-m .beyt sup.reference {
white-space: nowrap;
}
/* ═══ شمارهٔ ابیات ═══ */
.poem-num {
font-size: 85%;
white-space: nowrap;
vertical-align: middle;
}
.poem-num-r { text-align: left; padding-left: 0.5em; }
.poem-num-l { text-align: right; padding-right: 0.5em; }
/* ═══ مصراع راست/چپ (شعر نو) ═══ */
.poem-m-right { text-align: right; text-align-last: auto; }
.poem-m-left { text-align: left; text-align-last: auto; }
/* ═══ فاصلهگذارها ═══ */
.poem-indent,
.poem-gapcell { padding: 0; }
/* ═══ نستعلیق ═══ */
.poem-table.nastaliq,
.poem-table.nastaliq td,
.poem-table.nastaliq .beyt {
font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu',
'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif;
}
.poem-table.nastaliq { font-size: 125%; line-height: 2.1; }
/* ═══ چندصفحهای ═══ */
.poem-skip { display: none; }
.poem-row.poem-final-marker { display: none !important; }
.poem-partial-end td.b { border-bottom: 1px dashed; }
.poem-partial-start td.b { border-top: 1px dashed; }
/* ═══ صفحهٔ متوسط ═══ */
@media (max-width: 45em) {
.poem-s { width: 4%; }
.poem-mh { width: 22%; }
.poem-g { width: 4%; }
.poem-m { padding-inline: 0.3em; }
.poem-num-r { padding-left: 0.3em; }
.poem-num-l { padding-right: 0.3em; }
}
/* ═══ صفحهٔ باریک: هر مصراع یک خط ═══ */
@media (max-width: 32em) {
.poem-fixed { width: 100% !important; max-width: none !important; }
.poem-table {
display: block;
width: 100% !important;
table-layout: auto;
}
.poem-table tbody { display: block; }
.poem-sizer { display: none !important; }
.poem-row {
display: block;
padding: 0.25em 0 0.5em;
margin-bottom: 0.6em;
border-bottom: 1px dotted #c8c8c8;
}
/* بیت۱ دو ردیف است: خطچین فقط پس از ردیف دوم */
.poem-row.poem-stagger-1 {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 0;
}
.poem-row.poem-stagger-2 {
border-bottom: 1px dotted #c8c8c8;
margin-bottom: 0.6em;
}
.poem-row:last-child { border-bottom: none; margin-bottom: 0; }
.poem-row > td {
display: block;
width: auto !important;
padding: 0 !important;
text-align: center !important;
text-align-last: auto !important;
}
.poem-empty,
.poem-num-c,
.poem-g,
.poem-gapcell { display: none !important; }
.poem-num-r,
.poem-num-l {
display: block;
font-size: 70%;
line-height: 1.15;
color: #888;
padding: 0 0.3em !important;
}
.poem-num-r { text-align: right !important; }
.poem-num-l { text-align: left !important; }
.poem-m {
padding: 0.15em 0.3em !important;
text-align: center !important;
text-align-last: auto !important;
}
.poem-m .beyt { white-space: normal; }
}
o6c2d1inu0i6tfgqfkf1cdiww26fvkh
304051
304048
2026-10-12T19:22:35Z
Hanooz
17889
304051
sanitized-css
text/css
/*
* الگو:شعر۲/styles.css — TemplateStyles برای «پودمان:شعر»
* عرض ستونها بهصورت inline در پودمان تعیین شده؛ این فایل فقط ظاهر
* (فاصله، قلم، واکنشگرا) را مدیریت میکند.
*/
/* ═══ جدول ═══ */
.poem-table {
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;
text-align: center;
}
.poem-table td {
padding: 0.1em 0;
vertical-align: middle;
}
/* ═══ عرض ستونها (پشتیبان؛ در پودمان هم inline هست) ═══ */
.poem-s { width: 5%; }
.poem-mh { width: 21%; }
.poem-g { width: 6%; }
/* ═══ سایزر ═══ */
.poem-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
font-size: 0;
}
/* ═══ سلولهای خالی ساختاری ═══ */
.poem-empty {
padding: 0 !important;
border: 0;
line-height: 0;
font-size: 0;
}
/* ═══ مصراع ═══ */
.poem-m {
padding-inline: 0.4em;
}
.poem-m .beyt {
overflow-wrap: break-word;
hyphens: none;
}
.poem-m .beyt sup.reference {
white-space: nowrap;
}
/* ═══ شمارهها ═══ */
.poem-num {
font-size: 85%;
white-space: nowrap;
vertical-align: middle;
}
.poem-num-r { text-align: left; padding-left: 0.5em; }
.poem-num-l { text-align: right; padding-right: 0.5em; }
/* ═══ مصراع راست/چپ ═══ */
.poem-m-right { text-align: right; text-align-last: auto; }
.poem-m-left { text-align: left; text-align-last: auto; }
/* ═══ فاصلهگذارها ═══ */
.poem-indent,
.poem-gapcell { padding: 0; }
/* ═══ نستعلیق ═══ */
.poem-table.nastaliq,
.poem-table.nastaliq td,
.poem-table.nastaliq .beyt {
font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu',
'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif;
}
.poem-table.nastaliq { font-size: 125%; line-height: 2.1; }
/* ═══ چندصفحهای ═══ */
.poem-skip { display: none; }
.poem-row.poem-final-marker { display: none !important; }
.poem-partial-end td.b { border-bottom: 1px dashed; }
.poem-partial-start td.b { border-top: 1px dashed; }
/* ═══ صفحهٔ متوسط ═══ */
@media (max-width: 45em) {
.poem-m { padding-inline: 0.3em; }
.poem-num-r { padding-left: 0.3em; }
.poem-num-l { padding-right: 0.3em; }
}
/* ═══ صفحهٔ باریک ═══ */
@media (max-width: 32em) {
.poem-fixed { width: 100% !important; max-width: none !important; }
.poem-table {
display: block;
width: 100% !important;
table-layout: auto;
}
.poem-table tbody { display: block; }
.poem-sizer { display: none !important; }
.poem-row {
display: block;
padding: 0.25em 0 0.5em;
margin-bottom: 0.6em;
border-bottom: 1px dotted #c8c8c8;
}
.poem-row.poem-stagger-1 {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 0;
}
.poem-row.poem-stagger-2 {
border-bottom: 1px dotted #c8c8c8;
margin-bottom: 0.6em;
}
.poem-row:last-child { border-bottom: none; margin-bottom: 0; }
.poem-row > td {
display: block;
width: auto !important;
padding: 0 !important;
text-align: center !important;
text-align-last: auto !important;
}
.poem-empty,
.poem-num-c,
.poem-g,
.poem-gapcell { display: none !important; }
.poem-num-r,
.poem-num-l {
display: block;
font-size: 70%;
line-height: 1.15;
color: #888;
padding: 0 0.3em !important;
}
.poem-num-r { text-align: right !important; }
.poem-num-l { text-align: left !important; }
.poem-m {
padding: 0.15em 0.3em !important;
text-align: center !important;
text-align-last: auto !important;
}
.poem-m .beyt { white-space: normal; }
}
gz1erx2bef8p81wgkxf44euewj5igb4
304055
304051
2026-10-12T19:59:02Z
Hanooz
17889
304055
sanitized-css
text/css
/*
* الگو:شعر/styles.css — TemplateStyles برای «پودمان:شعر»
*/
/* ═══ جدول ═══ */
.poem-table {
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;
text-align: center;
}
.poem-table td {
padding: 0.1em 0;
vertical-align: middle;
}
/* ═══ عرض ستونها ═══
* پیشفرض = با شماره:
* ۵٪ | ۲۱٪×۲ | ۶٪ | ۲۱٪×۲ | ۵٪ ⇒ مصراع ۴۲٪
* اگر هیچ سلول شمارهای در جدول محتوا نداشته باشد (نه خودکار، نه دستی)،
* فضا به مصراعها برمیگردد:
* ۲٪ | ۲۲.۵٪×۲ | ۶٪ | ۲۲.۵٪×۲ | ۲٪ ⇒ مصراع ۴۵٪
* سلولهای شماره همیشه یا خالیاند یا شماره دارند؛ پس :empty کار میکند.
*/
/* پشتیبان. عرضهای واقعی inline روی هر سلول نوشته میشوند. */
.poem-s { width: 5%; }
.poem-mh { width: 21%; }
.poem-g { width: 6%; }
/* ═══ سایزر ═══ */
.poem-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
font-size: 0;
}
/* ═══ سلولهای خالی ساختاری ═══ */
.poem-empty {
padding: 0 !important;
border: 0;
line-height: 0;
font-size: 0;
}
/* ═══ مصراع ═══ */
.poem-m {
padding-inline: 0.4em;
}
.poem-m .beyt {
overflow-wrap: break-word;
hyphens: none;
}
.poem-m .beyt sup.reference {
white-space: nowrap;
}
/* ═══ شمارهها ═══ */
.poem-num {
font-size: 85%;
white-space: nowrap;
vertical-align: middle;
}
.poem-num-r { text-align: left; padding-left: 0.5em; }
.poem-num-l { text-align: right; padding-right: 0.5em; }
/* ═══ مصراع راست/چپ (شعر نو) ═══ */
.poem-m-right { text-align: right; text-align-last: auto; }
.poem-m-left { text-align: left; text-align-last: auto; }
/* ═══ فاصلهگذارها ═══ */
.poem-indent,
.poem-gapcell { padding: 0; }
/* ═══ نستعلیق ═══ */
.poem-table.nastaliq,
.poem-table.nastaliq td,
.poem-table.nastaliq .beyt {
font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu',
'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif;
}
.poem-table.nastaliq { font-size: 125%; line-height: 2.1; }
/* ═══ چندصفحهای ═══ */
.poem-skip { display: none; }
.poem-row.poem-final-marker { display: none !important; }
.poem-partial-end td.b { border-bottom: 1px dashed; }
.poem-partial-start td.b { border-top: 1px dashed; }
/* ═══ صفحهٔ متوسط ═══ */
@media (max-width: 45em) {
.poem-m { padding-inline: 0.3em; }
.poem-num-r { padding-left: 0.3em; }
.poem-num-l { padding-right: 0.3em; }
}
/* ═══ صفحهٔ باریک ═══ */
@media (max-width: 32em) {
.poem-fixed { width: 100% !important; max-width: none !important; }
.poem-table {
display: block;
width: 100% !important;
table-layout: auto;
}
.poem-table tbody { display: block; }
.poem-sizer { display: none !important; }
.poem-row {
display: block;
padding: 0.25em 0 0.5em;
margin-bottom: 0.6em;
border-bottom: 1px dotted #c8c8c8;
}
.poem-row.poem-stagger-1 {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 0;
}
.poem-row.poem-stagger-2 {
border-bottom: 1px dotted #c8c8c8;
margin-bottom: 0.6em;
}
.poem-row:last-child { border-bottom: none; margin-bottom: 0; }
.poem-row > td {
display: block;
width: auto !important;
padding: 0 !important;
text-align: center !important;
text-align-last: auto !important;
}
.poem-empty,
.poem-num-c,
.poem-g,
.poem-gapcell { display: none !important; }
.poem-num-r,
.poem-num-l {
display: block;
font-size: 70%;
line-height: 1.15;
color: #888;
padding: 0 0.3em !important;
}
.poem-num-r { text-align: right !important; }
.poem-num-l { text-align: left !important; }
.poem-m {
padding: 0.15em 0.3em !important;
text-align: center !important;
text-align-last: auto !important;
}
.poem-m .beyt { white-space: normal; }
}
tqqtcca1uvbz72bx9qpn2sn9sr31dar
304056
304055
2026-10-12T20:01:15Z
Hanooz
17889
نسخهٔ [[Special:Diff/304055|304055]] از [[Special:Contributions/Hanooz|Hanooz]] ([[User talk:Hanooz|بحث]]) خنثی شد
304056
sanitized-css
text/css
/*
* الگو:شعر۲/styles.css — TemplateStyles برای «پودمان:شعر»
* عرض ستونها بهصورت inline در پودمان تعیین شده؛ این فایل فقط ظاهر
* (فاصله، قلم، واکنشگرا) را مدیریت میکند.
*/
/* ═══ جدول ═══ */
.poem-table {
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;
text-align: center;
}
.poem-table td {
padding: 0.1em 0;
vertical-align: middle;
}
/* ═══ عرض ستونها (پشتیبان؛ در پودمان هم inline هست) ═══ */
.poem-s { width: 5%; }
.poem-mh { width: 21%; }
.poem-g { width: 6%; }
/* ═══ سایزر ═══ */
.poem-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
font-size: 0;
}
/* ═══ سلولهای خالی ساختاری ═══ */
.poem-empty {
padding: 0 !important;
border: 0;
line-height: 0;
font-size: 0;
}
/* ═══ مصراع ═══ */
.poem-m {
padding-inline: 0.4em;
}
.poem-m .beyt {
overflow-wrap: break-word;
hyphens: none;
}
.poem-m .beyt sup.reference {
white-space: nowrap;
}
/* ═══ شمارهها ═══ */
.poem-num {
font-size: 85%;
white-space: nowrap;
vertical-align: middle;
}
.poem-num-r { text-align: left; padding-left: 0.5em; }
.poem-num-l { text-align: right; padding-right: 0.5em; }
/* ═══ مصراع راست/چپ ═══ */
.poem-m-right { text-align: right; text-align-last: auto; }
.poem-m-left { text-align: left; text-align-last: auto; }
/* ═══ فاصلهگذارها ═══ */
.poem-indent,
.poem-gapcell { padding: 0; }
/* ═══ نستعلیق ═══ */
.poem-table.nastaliq,
.poem-table.nastaliq td,
.poem-table.nastaliq .beyt {
font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu',
'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif;
}
.poem-table.nastaliq { font-size: 125%; line-height: 2.1; }
/* ═══ چندصفحهای ═══ */
.poem-skip { display: none; }
.poem-row.poem-final-marker { display: none !important; }
.poem-partial-end td.b { border-bottom: 1px dashed; }
.poem-partial-start td.b { border-top: 1px dashed; }
/* ═══ صفحهٔ متوسط ═══ */
@media (max-width: 45em) {
.poem-m { padding-inline: 0.3em; }
.poem-num-r { padding-left: 0.3em; }
.poem-num-l { padding-right: 0.3em; }
}
/* ═══ صفحهٔ باریک ═══ */
@media (max-width: 32em) {
.poem-fixed { width: 100% !important; max-width: none !important; }
.poem-table {
display: block;
width: 100% !important;
table-layout: auto;
}
.poem-table tbody { display: block; }
.poem-sizer { display: none !important; }
.poem-row {
display: block;
padding: 0.25em 0 0.5em;
margin-bottom: 0.6em;
border-bottom: 1px dotted #c8c8c8;
}
.poem-row.poem-stagger-1 {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 0;
}
.poem-row.poem-stagger-2 {
border-bottom: 1px dotted #c8c8c8;
margin-bottom: 0.6em;
}
.poem-row:last-child { border-bottom: none; margin-bottom: 0; }
.poem-row > td {
display: block;
width: auto !important;
padding: 0 !important;
text-align: center !important;
text-align-last: auto !important;
}
.poem-empty,
.poem-num-c,
.poem-g,
.poem-gapcell { display: none !important; }
.poem-num-r,
.poem-num-l {
display: block;
font-size: 70%;
line-height: 1.15;
color: #888;
padding: 0 0.3em !important;
}
.poem-num-r { text-align: right !important; }
.poem-num-l { text-align: left !important; }
.poem-m {
padding: 0.15em 0.3em !important;
text-align: center !important;
text-align-last: auto !important;
}
.poem-m .beyt { white-space: normal; }
}
gz1erx2bef8p81wgkxf44euewj5igb4
304059
304056
2026-10-12T22:47:30Z
Hanooz
17889
304059
sanitized-css
text/css
/*
* الگو:شعر/styles.css — شیوهنامهٔ TemplateStyles برای «پودمان:شعر»
* محتوای این صفحه باید از نوع «Sanitized CSS» باشد؛ پیشوند .mw-parser-output
* را خود افزونه اضافه میکند.
*/
/* ─── فاصلهها (همه یکجا) ──────────────────────────────────────────────────
* فاصلهٔ دو مصراع در هر بیت = ستون میانی (۴em) + حاشیهٔ درونی دو مصراع (۲×۰٫۹em)
* فاصلهٔ شماره تا مصراع = حاشیهٔ شماره (۱٫۴em) + حاشیهٔ درونی مصراع (۰٫۹em)
* (.poem-num-c و .poem-num کوچکتر (۸۵٪) نوشته میشوند، پس عددهایشان ×۰٫۸۵ است.)
*/
.poem-m {
padding-left: 0.9em;
padding-right: 0.9em;
}
.poem-num-c,
.poem-g {
width: 4.7em;
}
.poem-s {
width: 3.4em;
}
/* ─── نستعلیق ───────────────────────────────────────────────────────────────
* قلم و اندازه از خودِ الگوی «نستعلیق» میآید: پودمان {{نستعلیق|…}} را صدا میزند و
* کلاس/قلمی را که آن الگو (و «الگو:نستعلیق/styles.css») میگذارد روی شعر مینشاند؛ پس هر
* تغییر در آن صفحهها خودکار به شعرها میرسد. اینجا فقط میگوییم سلولها و مصراعها از
* شعر ارث ببرند تا قاعدههای سایت برای td/.b قلم را عوض نکنند.
*/
.poem-table.nastaliq td,
.poem-table.nastaliq .beyt {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
/* مصراعها وسطچین (پایه) */
.poem-row {
text-align: center;
}
/* ─── ترازبندی مصراعها (کلاسهایی که پودمان میگذارد) ─────────────────────────
* start/end بسته به جهت متناند (در فارسی: start = راست، end = چپ).
* «میانه» (پیشفرض بیت): مصراع ۱ end، مصراع ۲ start ← هر دو به محور میانی میچسبند.
* «لبه» (پیشفرض بیت۱ و نردبانی): مصراع ۱ start، مصراع ۲ end.
*/
.poem-a-start {
text-align: start;
}
.poem-a-end {
text-align: end;
}
.poem-a-right {
text-align: right;
}
.poem-a-left {
text-align: left;
}
/* نمادِ میان دو مصراع (جداکننده=…) */
.poem-sep {
color: #b3362b;
font-size: 90%;
}
/* شمارهٔ ابیات: کوچکتر و بدون شکستن خط */
.poem-num {
font-size: 85%;
white-space: nowrap;
}
.poem-num-r {
text-align: left;
padding-left: 1.4em;
}
.poem-num-l {
text-align: right;
padding-right: 1.4em;
}
/* مصراع تنها با تراز راست یا چپ (شعر نو) */
.poem-m-right {
text-align: right;
}
.poem-m-left {
text-align: left;
}
/* سلولهای فاصلهگذار در بیتهای پلکانی و ردیف فاصله */
.poem-indent,
.poem-gapcell {
padding: 0;
}
/* ─── چیدمان ثابت (پیشفرض) ──────────────────────────────────────────────────
* ستونها را ردیف نامرئی .poem-sizer تعیین میکند (.poem-s کنار، .poem-g میان)؛
* عرض و حداکثر عرض روی جعبهٔ .poem-fixed مینشیند، پس همهٔ جدولهای یک شعر
* هماندازهاند. (اینجا font-size صفر نمیشود: عرضها با em نوشته شدهاند.)
*/
.poem-fixed .poem-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
}
.poem-fixed .b {
overflow-wrap: break-word;
}
/* شعر چندصفحهای: ردیف نامرئی که جای «ادامه» مینشیند (خط خالی پدید نیاید) */
.poem-skip {
display: none;
}
/* پیشنمایش در فضای نام «صفحه»: ردیفی که از صفحهٔ قبل میآید یا به صفحهٔ بعد میرود */
.poem-partial-end td.b {
border-bottom: 1px dashed;
}
.poem-partial-start td.b {
border-top: 1px dashed;
}
/* ─── صفحهٔ باریک (تلفن همراه): هر بیت زیر هم، مانند گنجور ───────────────────────
* شماره بالا، دو مصراع وسطچین زیر هم، و خطچینِ جداکننده میان ابیات. نماد حذف میشود.
* با تلفن=دوستونه (یا ترازبندی=پهن) کلاس .poem-stack نیست و جدول دوستونه میماند.
*/
@media (max-width: 36em) {
.poem-stack .poem-table,
.poem-stack .poem-table tbody,
.poem-stack .poem-row,
.poem-stack .poem-row td {
display: block;
}
.poem-stack .poem-sizer {
display: none;
}
.poem-stack .poem-row td {
width: auto;
text-align: center;
}
.poem-stack .poem-bayt {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.4em 0;
border-bottom: 1px dashed rgba(128, 128, 128, 0.55);
}
.poem-stack .poem-bayt .poem-num-r,
.poem-stack .poem-bayt .poem-num-l {
order: -1;
}
.poem-stack .poem-sep {
display: none;
}
}
/* صفحهٔ باریک با جدولِ دوستونه: فاصلهها کمتر تا جا برای متن بماند */
@media (max-width: 30em) {
.poem-m {
padding-left: 0.4em;
padding-right: 0.4em;
}
.poem-num-r {
padding-left: 0.5em;
}
.poem-num-l {
padding-right: 0.5em;
}
.poem-num-c,
.poem-g {
width: 2.4em;
}
.poem-s {
width: 2em;
}
}
2zezziqlkc4yco6q3guhaid8jtb9z49
پودمان:شعر
828
97054
304038
304037
2026-10-12T13:44:45Z
Hanooz
17889
304038
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
عرض ستونها با محتوا تعیین میشود (table-layout: auto):
بلندترین مصراعِ شعر — هر جا که باشد — عرض هر دو ستون مصراع را مشخص
میکند، پس همهٔ مصراعهای شعر هماندازهاند.
ردیف سایزرِ نامرئی، متنِ بلندترین مصراع را در هر دو ستون مصراع
تکرار میکند؛ مرورگر با table-layout: auto این متن را برای تعیین عرض
میشمارد ولی بهخاطر visibility: hidden نمایش نمیدهد.
ارجاعهای پایانی (<ref>) پیش از سنجش بلندی حذف میشوند.
]==]
local p = {}
local ZWNJ = '\226\128\140'
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = 'none',
justDefault = { bayt = 'center', stagger = 'center' },
nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', "
.. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif",
mergeRows = false,
cats = {
unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر',
incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص',
error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر',
},
}
local N = {
right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' },
left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' },
class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' },
indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' },
height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' },
dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' },
numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' },
start = { 'شروع', 'start' }, side = { 'سمت', 'side' },
digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' },
unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' },
nastaliq = { 'نستعلیق', 'nastaliq' },
cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' },
continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' },
stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' },
maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' },
font = { 'قلم', 'font' },
just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' },
}
local KNOWN = {}
for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end
local ALIGN = {
['راست'] = 'right', right = 'right', r = 'right',
['چپ'] = 'left', left = 'left', l = 'left',
['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center',
}
local SIDE = { right = 'r', left = 'l', center = 'c' }
local KIND = {
['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt',
['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger',
['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder',
['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra',
}
local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa',
['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin',
['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' }
local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true,
yes=true, y=true, auto=true, ['true']=true, ['1']=true }
local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true }
local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch',
rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' }
local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev',
['بعد']='next', ['پسین']='next', next='next',
['هر دو']='both', ['هردو']='both', both='both' }
local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='',
['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ }
local WHICH = { ['هر دو']='both', ['هردو']='both', both='both',
['اول']='first', first='first', ['دوم']='second', second='second' }
local STANZA = { ['جدید']=true, new=true }
local JUST = {
['وسط']='center', ['مرکز']='center', center='center',
['راست']='right', right='right', ['چپ']='left', left='left',
['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify',
['لبه']='edge', edge='edge',
}
local ALIGN_STYLE = {
right = 'text-align:right;text-align-last:auto',
left = 'text-align:left;text-align-last:auto',
center = 'text-align:center;text-align-last:auto',
justify = 'text-align:justify;text-align-last:justify;'
.. '-moz-text-align-last:justify;-webkit-text-align-last:justify',
}
local KIND_KEYS = {}
for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end
table.sort(KIND_KEYS, function(a, b) return #a > #b end)
local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1,
lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 }
local state = { ctx = nil, open = false, carry = nil, hemistichs = {} }
local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end
local SKIP = '<tr class="poem-skip"></tr>'
local CLOSE_HTML = '</table></div>'
local FINAL_MARKER =
'<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>'
local function esc(s) return (s:gsub('&','&'):gsub('"','"'):gsub('<','<'):gsub('>','>')) end
local function pick(args, names)
for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end
return nil
end
local function present(args, names)
for _, n in ipairs(names) do if args[n] ~= nil then return true end end
return false
end
local function bool(v, d)
if v == nil then return d end
v = v:lower()
if YES[v] then return true end
if NO[v] then return false end
return d
end
local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end
local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end
local function getArgs(frame)
local a = {}
local function merge(src)
if not src then return end
for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end
end
merge(frame.args)
local parent = frame:getParent()
if parent then merge(parent.args) end
return a
end
local function hasUnknown(args, pos)
for k, v in pairs(args) do
if v ~= '' then
if type(k) == 'number' then
if k < 1 or k > pos or k % 1 ~= 0 then return true end
elseif not KNOWN[k] then return true end
end
end
return false
end
local function nsId() return mw.title.getCurrentTitle().namespace end
local function inPageNs() return nsId() == cfg.pageNs end
local function tracked()
local ns = nsId()
return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828)
end
local function categories(flags)
if not flags or next(flags) == nil or not tracked() then return '' end
local out = {}
for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do
if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end
end
return table.concat(out)
end
local function langDir(lang)
local base, rest = lang:lower():match('^(%a+)(.*)$')
if not base then return nil end
if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end
if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end
return 'ltr'
end
local function stylesTag(frame)
local ok, tag = pcall(function()
return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } }
end)
return ok and tag or ''
end
local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' }
local function convertDigits(s, mode)
if s == '' or mode == 'keep' then return s end
if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end
if mode == 'latin' then
s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end)
s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end)
return s
end
s = s:gsub('%d', function(d) return FA[tonumber(d)] end)
s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end)
return s
end
local function numberArg(args, names, digits)
local v = pick(args, names); if not v then return '' end
return convertDigits(v, digits)
end
local function intArg(v, lo, hi)
local n = tonumber((convertDigits(v, 'latin')))
if not n or n ~= n or n < lo or n > hi then return nil end
return math.floor(n)
end
local function splitLength(v)
if not v or v == '' then return nil end
v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.')
local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$')
if not num then return nil end
num = tonumber(num)
if not num or num > 10000 then return nil end
unit = UNITS[unit:lower()]
if not unit then return nil end
return num, unit
end
local function parseLength(v)
local n, u = splitLength(v); if not n then return nil end
return string.format('%g', n) .. u
end
---------------------------------------------------------------------------
-- ارجاعها
---------------------------------------------------------------------------
local function stripRefs(s)
if not s or s == '' then return '' end
local prev
repeat
prev = s
s = s:gsub('<ref[^>]*%s*/>', '')
s = s:gsub('<ref[^>]*>.-</ref>', '')
s = s:gsub('\127[^\127]*\127', '')
until s == prev
return s
end
-- متنِ فقط-دیدنی برای سنجش و سایزر
local function plainText(s)
if not s or s == '' then return '' end
s = stripRefs(s)
s = s:gsub('<[^>]*>', '')
s = s:gsub('%[%[[^%]|]*|', ''):gsub('%[%[', ''):gsub('%]%]', '')
s = s:gsub("'''''", ''):gsub("'''", ''):gsub("''", '')
return mw.text.trim(s)
end
-- طول متن دیدنی
local function charLen(s)
s = plainText(s)
s = s:gsub('\226\128\140', '')
return mw.ustring.len(s)
end
-- بلندترین مصراع در state.hemistichs (بر اساس طول متن دیدنی)
local function findLongest()
local longest, longestLen = '', 0
for _, h in ipairs(state.hemistichs) do
local t = plainText(h)
local n = mw.ustring.len(t)
if n > longestLen then longest, longestLen = t, n end
end
return longest
end
-- ثبت مصراع برای سایزر
local function register(kind, d)
if not state.open then return end
if kind == 'bayt' or kind == 'stagger' then
if d.m1 and d.m1 ~= '' then state.hemistichs[#state.hemistichs + 1] = d.m1 end
if d.m2 and d.m2 ~= '' then state.hemistichs[#state.hemistichs + 1] = d.m2 end
elseif kind == 'mesra' then
if d.text and d.text ~= '' and not d.blank then
state.hemistichs[#state.hemistichs + 1] = d.text
end
end
end
local function newCtx(args, kind, flags)
local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') }
local d = pick(args, N.digits)
if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end
local s = pick(args, N.start)
if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end
local sd = pick(args, N.side)
if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end
local m = pick(args, N.numbering)
if m then
local n = intArg(m, 0, 1e6)
if n then if n >= 1 then ctx.every = n end
else
local b = bool(m, nil)
if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end
end
end
return ctx
end
local function nextNumber(ctx)
if not ctx.every then return nil end
local n = ctx.start + ctx.count
ctx.count = ctx.count + 1
if n % ctx.every ~= 0 then return nil end
return convertDigits(string.format('%d', n), ctx.digits)
end
local function applyAuto(d, ctx, counted, noCenter)
if not counted then return end
local n = nextNumber(ctx)
if not n then return end
if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end
local side = ctx.side
if noCenter and side == 'c' then side = 'l' end
d[side] = n
end
local function joinStyle(...)
local parts = {}
for i = 1, select('#', ...) do
local v = select(i, ...)
if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end
end
return table.concat(parts, ';')
end
local function alignFor(j, which)
if not j or j == 'center' then return nil end
if j == 'edge' then
return (which == 1) and 'text-align:start;text-align-last:auto'
or 'text-align:end;text-align-last:auto'
end
return ALIGN_STYLE[j]
end
local function td(class, colspan, style, content)
local s = '<td'
if class and class ~= '' then s = s .. ' class="' .. class .. '"' end
if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end
if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end
return s .. '>' .. (content or '') .. '</td>'
end
local function hemiCell(text)
return '<span class="beyt">' .. (text or '') .. '</span>'
end
local function rowOpen(d, kind, noId)
local cls = 'poem-row ' .. kind
if d.class then cls = cls .. ' ' .. d.class end
local id = ''
if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end
return '<tr class="' .. esc(cls) .. '"' .. id .. '>'
end
local function rowBayt(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.bayt
return rowOpen(d, 'poem-bayt')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r)
.. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1))
.. td('poem-num poem-num-c', 1, d.style, d.c)
.. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2))
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l)
.. '</tr>'
end
local function rowStagger(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.stagger
local s1 = joinStyle(alignFor(j, 1), d.style)
local s2 = joinStyle(alignFor(j, 2), d.style)
local ind = d.indent
if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then
s1 = joinStyle('padding-inline-end:' .. ind, s1)
s2 = joinStyle('padding-inline-start:' .. ind, s2)
end
return rowOpen(d, 'poem-stagger poem-stagger-1')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r)
.. td('b poem-m poem-m1', 2, s1, hemiCell(m1))
.. td('poem-num poem-num-c poem-empty', 1, nil, '')
.. td('poem-empty', 2, nil, '')
.. td('poem-num poem-num-l poem-empty', 1, nil, '')
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num poem-num-r poem-empty', 1, nil, '')
.. td('poem-empty', 2, nil, '')
.. td('poem-num poem-num-c poem-empty', 1, nil, '')
.. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2))
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l)
.. '</tr>'
end
local function rowMesra(d)
local align = d.align or 'center'
local content = d.text
if d.blank and content == '' then content = cfg.nbsp end
local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil
local style = joinStyle(inline, d.style)
local cellClass = 'b poem-m'
local cs = { 2, 3, 2 }
if align ~= 'center' then
cs = { 1, 5, 1 }
cellClass = cellClass .. ' poem-m-' .. align
if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end
end
local cell
if d.blank and content == cfg.nbsp then cell = content
else cell = hemiCell(content) end
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r)
.. td(cellClass, cs[2], style, cell)
.. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l)
.. '</tr>'
end
local function rowGap(d)
local style = d.height and ('height:' .. d.height) or nil
local content = d.height and '' or cfg.nbsp
return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">'
.. td('poem-gapcell', 7, style, content) .. '</tr>'
end
local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra }
-- ردیف سایزر: متن بلندترین مصراع در هر دو ستون مصراع.
-- visibility: hidden ⇒ برای اندازهگیری عرض شمرده میشود، دیده نمیشود.
local function sizerRow()
local longest = findLongest()
if longest == '' then return '' end
local cell = '<span class="poem-sz">' .. esc(longest) .. '</span>'
return '<tr class="poem-sizer" aria-hidden="true">'
.. '<td class="poem-s"></td>'
.. '<td colspan="2" class="poem-mh">' .. cell .. '</td>'
.. '<td class="poem-g"></td>'
.. '<td colspan="2" class="poem-mh">' .. cell .. '</td>'
.. '<td class="poem-s"></td>'
.. '</tr>'
end
local function openHtml(o)
local cls = 'poem' .. (o.fixed and ' poem-fixed' or '')
if o.class then cls = cls .. ' ' .. o.class end
local div = '<div class="' .. esc(cls) .. '"'
if o.lang then div = div .. ' lang="' .. o.lang .. '"' end
if o.dir then div = div .. ' dir="' .. o.dir .. '"' end
local font = o.font and ('font-family:' .. o.font) or nil
local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '')
if o.fixed then
local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth,
'margin:0 auto', font, o.style)
return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls
.. '" role="presentation" style="table-layout:auto">'
end
local style = 'margin: 0 auto;'
if font then style = style .. font .. ';' end
if o.style then style = style .. ' ' .. o.style end
return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">'
end
---------------------------------------------------------------------------
-- چندصفحهای
---------------------------------------------------------------------------
local function joinText(a, b, sep)
if a == '' then return b end
if b == '' then return a end
return a .. sep .. b
end
local function mergeRow(head, tail, sep)
for _, f in ipairs({ 'm1', 'm2', 'text' }) do
if head[f] ~= nil or tail[f] ~= nil then
head[f] = joinText(head[f] or '', tail[f] or '', sep)
end
end
for _, f in ipairs({ 'r', 'c', 'l' }) do
if tail[f] and tail[f] ~= '' then head[f] = tail[f] end
end
head.style = head.style or tail.style
head.class = head.class or tail.class
head.id = head.id or tail.id
return head
end
local function flushHtml()
local c = state.carry
if not c then return '' end
state.carry = nil
return RENDER[c.kind](c.d)
end
local function plain(html)
if not inPageNs() then
local pre = flushHtml()
if pre ~= '' then return pre .. '\n' .. html end
end
return html
end
local function emit(kind, d, opt, flags)
opt = opt or {}
register(kind, d)
if inPageNs() then
if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end
if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end
return RENDER[kind](d)
end
if not cfg.mergeRows then return RENDER[kind](d) end
local out = ''
local carry = state.carry
state.carry = nil
if carry then
if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ')
else
out = RENDER[carry.kind](carry.d)
if opt.continued then flags.unknown = true end
end
end
if opt.unfinished then
state.carry = { kind = kind, d = d }
return out ~= '' and out or SKIP
end
local html = RENDER[kind](d)
if out ~= '' then return out .. '\n' .. html end
return html
end
local function rowOpts(args, flags)
local opt = {}
local u = pick(args, N.unfinished)
if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end
local c = pick(args, N.continued)
if c then
local w = WHICH[c:lower()]
if w then opt.continued, opt.which = true, w
else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end
end
local j = pick(args, N.joiner)
if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end
return opt
end
local function parseOpen(args, flags)
local o = {}
local isText = present(args, N.text)
for i = 1, 2 do
local v = args[i]
if v and v ~= '' then
local lv = v:lower()
if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true
elseif v == 'ادامه' or lv == 'continue' then
if isText then flags.unknown = true else o.cont = 'prev' end
else flags.unknown = true end
end
end
if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k then
if not isText and k == 'next' then flags.unknown = true; k = nil end
else
local b = bool(c, nil)
if b == nil then flags.unknown = true
elseif b then if isText then flags.unknown = true else k = 'prev' end end
end
if k then o.cont = k end
end
o.style = pick(args, N.style); o.class = pick(args, N.class)
local lang = pick(args, N.lang)
if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end
local dir = pick(args, N.dir)
if dir then
dir = dir:lower()
if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end
end
if o.lang and not o.dir then o.dir = langDir(o.lang) end
o.kind = 'bayt'
local k = pick(args, N.kind)
if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end
local a = pick(args, N.align)
if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end
local g = pick(args, N.gap)
if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end
local ind = pick(args, N.indent)
if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end
o.unitNum, o.unitUnit = splitLength(pick(args, N.unit))
if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end
local st = pick(args, N.stanza)
if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end
local jv = pick(args, N.just)
if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end
if o.nastaliq then o.font = cfg.nastaliqFonts end
local f = pick(args, N.font)
if f then
if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts
elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f
else flags.unknown = true end
end
local w = pick(args, N.width)
if w then
local lw = w:lower()
if lw == 'خودکار' or lw == 'auto' then o.fixed = false
else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end
end
local mx = pick(args, N.maxwidth)
if mx then
local lm = mx:lower()
if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none'
else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end
end
if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end
if o.fixed then
local explicit = (o.width ~= nil)
o.width = o.width or cfg.defaultWidth
o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth)
end
return o
end
local function parseClose(args, flags)
local cont = false
local v = args[1]
if v and v ~= '' then
if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end
end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k == 'next' or k == 'both' then cont = true
elseif k == 'prev' then flags.unknown = true
else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end
end
return cont
end
local function parseLine(raw, freeVerse)
local t = {}
local s = raw
if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end
if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3)
elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end
s = mw.text.trim(s)
local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$')
if pre then t.pre = pre; s = rest end
local body, post = s:match('^(.-)%s*>>>%s*(.+)$')
if body then t.post = post; s = body end
t.text = s
return t
end
local function parseDirective(line)
local rest = mw.text.trim(line:sub(2))
local lrest = rest:lower()
for _, key in ipairs(KIND_KEYS) do
if lrest:sub(1, #key) == key then
local after = rest:sub(#key + 1)
if after == '' or after:match('^%s') then
local dd, bad = { kind = KIND[key] }, false
for k, v in after:gmatch('(%S+)=(%S+)') do
local kk, lv = k:lower(), v:lower()
if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end
elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end
elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end
else bad = true end
end
return dd, bad
end
end
end
return nil, true
end
local function renderLines(text, o, ctx, ropt, flags)
local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just }
local items, pending, pendingCur, needGap = {}, nil, nil, false
local function info(c)
local twoH = (c.kind ~= 'mesra')
local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger')
return twoH, rowKind, c.indent
end
local function numbers(d, t)
if not t then return end
if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end
if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end
end
local function makeBayt(t1, t2, c)
local _, _, indent = info(c)
local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '',
r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just }
numbers(d, t1); numbers(d, t2)
return d
end
local function makeMesra(t, c, free)
local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed }
d.align = t.align or c.align or (free and 'right' or 'center')
if free and (t.level or 0) > 0 and d.align ~= 'center' then
d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit
end
numbers(d, t)
return d
end
local function push(kind, d, counted)
items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 }
needGap = false
end
local function flushPending()
if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end
end
local src = text:gsub('\r\n?', '\n')
for raw in (src .. '\n'):gmatch('([^\n]*)\n') do
local line = mw.text.trim(raw)
if line == '' then
flushPending()
if #items > 0 then needGap = true end
elseif line:sub(1, 1) == '@' then
local dd, bad = parseDirective(line)
flushPending()
if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end
if bad then flags.unknown = true end
else
local twoH, rowKind = info(cur)
local isFirst = (#items == 0 and not pending)
local t = parseLine(line, not twoH)
if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra)
else
local handled = false
if ropt.continued and isFirst then
local mode = ropt.which or 'auto'
if mode == 'auto' then
local c = state.carry
mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both'
end
if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true
elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end
end
if not handled then
if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil
else pending, pendingCur = t, cur end
end
end
end
end
if pending then
local _, rowKind = info(pendingCur)
if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true)
else push('mesra', makeMesra(pending, pendingCur, false), false) end
pending = nil
end
for i, it in ipairs(items) do
if it.counted and not (i == 1 and ropt.continued) then
applyAuto(it.d, ctx, true, it.kind ~= 'bayt')
end
end
local out = {}
for i, it in ipairs(items) do
if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end
local ro = {}
if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end
if i == #items and ropt.unfinished then ro.unfinished = true end
local h = emit(it.kind, it.d, ro, flags)
if h ~= SKIP and h ~= '' then out[#out + 1] = h end
end
return table.concat(out, '\n')
end
local function open(frame)
local args, flags = getArgs(frame), {}
local o = parseOpen(args, flags)
local isText = present(args, N.text)
local page = inPageNs()
if hasUnknown(args, 2) then flags.unknown = true end
local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both')
local ctx
if joins and state.ctx then ctx = state.ctx
else
ctx = newCtx(args, o.kind, flags)
ctx.fixed, ctx.just = o.fixed, o.just
end
local parts = {}
if not joins then
parts[#parts + 1] = stylesTag(frame) .. openHtml(o)
state.carry = nil
state.hemistichs = {}
end
state.open, state.ctx = true, ctx
if isText then
local ropt = rowOpts(args, flags)
if o.newStanza and joins and not ropt.continued then
parts[#parts + 1] = plain(rowGap({ height = o.gap }))
end
local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags)
if rows ~= '' then parts[#parts + 1] = rows end
local stay = (not page) and (o.cont == 'next' or o.cont == 'both')
if not stay then
local tail = flushHtml()
if tail ~= '' then parts[#parts + 1] = tail end
local sz = sizerRow()
if sz ~= '' then parts[#parts + 1] = sz end
parts[#parts + 1] = CLOSE_HTML
state.open, state.ctx, state.hemistichs = false, nil, {}
else
parts[#parts + 1] = FINAL_MARKER
end
end
if #parts == 0 then parts[1] = SKIP end
return table.concat(parts, '\n') .. categories(flags)
end
local function close(frame)
local args, flags = getArgs(frame), {}
local cont = parseClose(args, flags)
if hasUnknown(args, 1) then flags.unknown = true end
local html = flushHtml()
if html ~= '' then html = html .. '\n' end
if cont and not inPageNs() then
return html .. FINAL_MARKER .. categories(flags)
end
state.open, state.ctx = false, nil
local sz = sizerRow()
state.hemistichs = {}
return html .. sz .. CLOSE_HTML .. categories(flags)
end
local function commonRow(args, ctx, flags)
local d = {
r = numberArg(args, N.right, ctx.digits),
c = numberArg(args, N.center, ctx.digits),
l = numberArg(args, N.left, ctx.digits),
style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id),
fixed = ctx.fixed, just = ctx.just,
}
local jv = pick(args, N.just)
if jv then
d.just = JUST[jv:lower()]
if not d.just then flags.unknown = true; d.just = ctx.just end
end
return d
end
local function bayt(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, false)
local html = emit('bayt', d, opt, flags)
return html .. categories(flags)
end
local function staggerFn(defaultIndent)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
local raw = pick(args, N.indent)
if raw then
d.indent = parseLength(raw)
if not d.indent then flags.unknown = true end
d.indent = d.indent or defaultIndent
else
d.indent = defaultIndent
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, true)
local html = emit('stagger', d, opt, flags)
return html .. categories(flags)
end
end
local function mesra(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.text = clean(args[1])
local a = pick(args, N.align)
if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end
local raw = pick(args, N.indent)
d.indent = parseLength(raw)
if raw and not d.indent then flags.unknown = true end
if hasUnknown(args, 1) then flags.unknown = true end
applyAuto(d, ctx, ctx.mesra and not opt.continued, true)
local html = emit('mesra', d, opt, flags)
return html .. categories(flags)
end
local function emptyFn(kind)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local d = commonRow(args, ctx, flags)
local raw = pick(args, N.height)
d.height = parseLength(raw)
if raw and not d.height then flags.unknown = true end
if hasUnknown(args, 0) then flags.unknown = true end
local html
if d.height then html = rowGap(d)
elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d)
else d.text, d.blank = '', true; html = rowMesra(d) end
return plain(html) .. categories(flags)
end
end
local function safe(fn)
return function(frame)
local ok, res = pcall(fn, frame)
if ok then return res end
return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>'
.. categories({ error = true })
end
end
p.open = safe(open)
p.close = safe(close)
p.bayt = safe(bayt)
p.stagger = safe(staggerFn(nil))
p.ladder = safe(staggerFn(nil))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
pqdveyht8v0d34reosfiy99s2w8p3n8
304044
304038
2026-10-12T15:05:36Z
Hanooz
17889
304044
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
عرض ستونها با محتوا تعیین میشود (table-layout: auto):
بلندترین مصراعِ شعر — هر جا که باشد — عرض هر دو ستون مصراع را مشخص
میکند، پس همهٔ مصراعهای شعر هماندازهاند.
ردیف سایزرِ نامرئی، متنِ بلندترین مصراع را در هر دو ستون مصراع
تکرار میکند؛ مرورگر با table-layout: auto این متن را برای تعیین عرض
میشمارد ولی بهخاطر visibility: hidden نمایش نمیدهد.
ارجاعهای پایانی (<ref>) پیش از سنجش بلندی حذف میشوند.
]==]
local p = {}
local ZWNJ = '\226\128\140'
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = 'none',
justDefault = { bayt = 'center', stagger = 'center' },
nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', "
.. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif",
mergeRows = false,
cats = {
unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر',
incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص',
error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر',
},
}
local N = {
right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' },
left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' },
class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' },
indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' },
height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' },
dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' },
numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' },
start = { 'شروع', 'start' }, side = { 'سمت', 'side' },
digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' },
unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' },
nastaliq = { 'نستعلیق', 'nastaliq' },
cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' },
continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' },
stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' },
maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' },
font = { 'قلم', 'font' },
just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' },
}
local KNOWN = {}
for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end
local ALIGN = {
['راست'] = 'right', right = 'right', r = 'right',
['چپ'] = 'left', left = 'left', l = 'left',
['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center',
}
local SIDE = { right = 'r', left = 'l', center = 'c' }
local KIND = {
['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt',
['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger',
['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder',
['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra',
}
local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa',
['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin',
['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' }
local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true,
yes=true, y=true, auto=true, ['true']=true, ['1']=true }
local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true }
local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch',
rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' }
local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev',
['بعد']='next', ['پسین']='next', next='next',
['هر دو']='both', ['هردو']='both', both='both' }
local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='',
['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ }
local WHICH = { ['هر دو']='both', ['هردو']='both', both='both',
['اول']='first', first='first', ['دوم']='second', second='second' }
local STANZA = { ['جدید']=true, new=true }
local JUST = {
['وسط']='center', ['مرکز']='center', center='center',
['راست']='right', right='right', ['چپ']='left', left='left',
['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify',
['لبه']='edge', edge='edge',
}
local ALIGN_STYLE = {
right = 'text-align:right;text-align-last:auto',
left = 'text-align:left;text-align-last:auto',
center = 'text-align:center;text-align-last:auto',
justify = 'text-align:justify;text-align-last:justify;'
.. '-moz-text-align-last:justify;-webkit-text-align-last:justify',
}
local KIND_KEYS = {}
for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end
table.sort(KIND_KEYS, function(a, b) return #a > #b end)
local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1,
lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 }
local state = { ctx = nil, open = false, carry = nil, hemistichs = {} }
local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end
local SKIP = '<tr class="poem-skip"></tr>'
local CLOSE_HTML = '</table></div>'
local FINAL_MARKER =
'<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>'
local function esc(s) return (s:gsub('&','&'):gsub('"','"'):gsub('<','<'):gsub('>','>')) end
local function pick(args, names)
for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end
return nil
end
local function present(args, names)
for _, n in ipairs(names) do if args[n] ~= nil then return true end end
return false
end
local function bool(v, d)
if v == nil then return d end
v = v:lower()
if YES[v] then return true end
if NO[v] then return false end
return d
end
local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end
local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end
local function getArgs(frame)
local a = {}
local function merge(src)
if not src then return end
for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end
end
merge(frame.args)
local parent = frame:getParent()
if parent then merge(parent.args) end
return a
end
local function hasUnknown(args, pos)
for k, v in pairs(args) do
if v ~= '' then
if type(k) == 'number' then
if k < 1 or k > pos or k % 1 ~= 0 then return true end
elseif not KNOWN[k] then return true end
end
end
return false
end
local function nsId() return mw.title.getCurrentTitle().namespace end
local function inPageNs() return nsId() == cfg.pageNs end
local function tracked()
local ns = nsId()
return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828)
end
local function categories(flags)
if not flags or next(flags) == nil or not tracked() then return '' end
local out = {}
for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do
if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end
end
return table.concat(out)
end
local function langDir(lang)
local base, rest = lang:lower():match('^(%a+)(.*)$')
if not base then return nil end
if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end
if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end
return 'ltr'
end
local function stylesTag(frame)
local ok, tag = pcall(function()
return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } }
end)
return ok and tag or ''
end
local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' }
local function convertDigits(s, mode)
if s == '' or mode == 'keep' then return s end
if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end
if mode == 'latin' then
s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end)
s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end)
return s
end
s = s:gsub('%d', function(d) return FA[tonumber(d)] end)
s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end)
return s
end
local function numberArg(args, names, digits)
local v = pick(args, names); if not v then return '' end
return convertDigits(v, digits)
end
local function intArg(v, lo, hi)
local n = tonumber((convertDigits(v, 'latin')))
if not n or n ~= n or n < lo or n > hi then return nil end
return math.floor(n)
end
local function splitLength(v)
if not v or v == '' then return nil end
v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.')
local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$')
if not num then return nil end
num = tonumber(num)
if not num or num > 10000 then return nil end
unit = UNITS[unit:lower()]
if not unit then return nil end
return num, unit
end
local function parseLength(v)
local n, u = splitLength(v); if not n then return nil end
return string.format('%g', n) .. u
end
---------------------------------------------------------------------------
-- ارجاعها
---------------------------------------------------------------------------
local function stripRefs(s)
if not s or s == '' then return '' end
local prev
repeat
prev = s
s = s:gsub('<ref[^>]*%s*/>', '')
s = s:gsub('<ref[^>]*>.-</ref>', '')
s = s:gsub('\127[^\127]*\127', '')
until s == prev
return s
end
-- متنِ فقط-دیدنی برای سنجش و سایزر
local function plainText(s)
if not s or s == '' then return '' end
s = stripRefs(s)
s = s:gsub('<[^>]*>', '')
s = s:gsub('%[%[[^%]|]*|', ''):gsub('%[%[', ''):gsub('%]%]', '')
s = s:gsub("'''''", ''):gsub("'''", ''):gsub("''", '')
return mw.text.trim(s)
end
-- طول متن دیدنی
local function charLen(s)
s = plainText(s)
s = s:gsub('\226\128\140', '')
return mw.ustring.len(s)
end
-- بلندترین مصراع در state.hemistichs (بر اساس طول متن دیدنی)
local function findLongest()
local longest, longestLen = '', 0
for _, h in ipairs(state.hemistichs) do
local t = plainText(h)
local n = mw.ustring.len(t)
if n > longestLen then longest, longestLen = t, n end
end
return longest
end
-- ثبت مصراع برای سایزر
local function register(kind, d)
if not state.open then return end
if kind == 'bayt' or kind == 'stagger' then
if d.m1 and d.m1 ~= '' then state.hemistichs[#state.hemistichs + 1] = d.m1 end
if d.m2 and d.m2 ~= '' then state.hemistichs[#state.hemistichs + 1] = d.m2 end
elseif kind == 'mesra' then
if d.text and d.text ~= '' and not d.blank then
state.hemistichs[#state.hemistichs + 1] = d.text
end
end
end
local function newCtx(args, kind, flags)
local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') }
local d = pick(args, N.digits)
if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end
local s = pick(args, N.start)
if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end
local sd = pick(args, N.side)
if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end
local m = pick(args, N.numbering)
if m then
local n = intArg(m, 0, 1e6)
if n then if n >= 1 then ctx.every = n end
else
local b = bool(m, nil)
if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end
end
end
return ctx
end
local function nextNumber(ctx)
if not ctx.every then return nil end
local n = ctx.start + ctx.count
ctx.count = ctx.count + 1
if n % ctx.every ~= 0 then return nil end
return convertDigits(string.format('%d', n), ctx.digits)
end
local function applyAuto(d, ctx, counted, noCenter)
if not counted then return end
local n = nextNumber(ctx)
if not n then return end
if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end
local side = ctx.side
if noCenter and side == 'c' then side = 'l' end
d[side] = n
end
local function joinStyle(...)
local parts = {}
for i = 1, select('#', ...) do
local v = select(i, ...)
if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end
end
return table.concat(parts, ';')
end
local function alignFor(j, which)
if not j or j == 'center' then return nil end
if j == 'edge' then
return (which == 1) and 'text-align:start;text-align-last:auto'
or 'text-align:end;text-align-last:auto'
end
return ALIGN_STYLE[j]
end
local function td(class, colspan, style, content)
local s = '<td'
if class and class ~= '' then s = s .. ' class="' .. class .. '"' end
if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end
if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end
return s .. '>' .. (content or '') .. '</td>'
end
local function hemiCell(text)
return '<span class="beyt">' .. (text or '') .. '</span>'
end
local function rowOpen(d, kind, noId)
local cls = 'poem-row ' .. kind
if d.class then cls = cls .. ' ' .. d.class end
local id = ''
if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end
return '<tr class="' .. esc(cls) .. '"' .. id .. '>'
end
local function rowBayt(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.bayt
return rowOpen(d, 'poem-bayt')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r)
.. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1))
.. td('poem-num poem-num-c', 1, d.style, d.c)
.. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2))
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l)
.. '</tr>'
end
local function rowStagger(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.stagger
local s1 = joinStyle(alignFor(j, 1), d.style)
local s2 = joinStyle(alignFor(j, 2), d.style)
local ind = d.indent
if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then
s1 = joinStyle('padding-inline-end:' .. ind, s1)
s2 = joinStyle('padding-inline-start:' .. ind, s2)
end
return rowOpen(d, 'poem-stagger poem-stagger-1')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r)
.. td('b poem-m poem-m1', 2, s1, hemiCell(m1))
.. td('poem-num poem-num-c poem-empty', 1, nil, '')
.. td('poem-empty', 2, nil, '')
.. td('poem-num poem-num-l poem-empty', 1, nil, '')
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num poem-num-r poem-empty', 1, nil, '')
.. td('poem-empty', 2, nil, '')
.. td('poem-num poem-num-c poem-empty', 1, nil, '')
.. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2))
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l)
.. '</tr>'
end
local function rowMesra(d)
local align = d.align or 'center'
local content = d.text
if d.blank and content == '' then content = cfg.nbsp end
local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil
local style = joinStyle(inline, d.style)
local cellClass = 'b poem-m'
local cs = { 2, 3, 2 }
if align ~= 'center' then
cs = { 1, 5, 1 }
cellClass = cellClass .. ' poem-m-' .. align
if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end
end
local cell
if d.blank and content == cfg.nbsp then cell = content
else cell = hemiCell(content) end
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r)
.. td(cellClass, cs[2], style, cell)
.. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l)
.. '</tr>'
end
local function rowGap(d)
local style = d.height and ('height:' .. d.height) or nil
local content = d.height and '' or cfg.nbsp
return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">'
.. td('poem-gapcell', 7, style, content) .. '</tr>'
end
local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra }
-- ردیف سایزر: متن بلندترین مصراع در هر دو ستون مصراع.
-- visibility: hidden ⇒ برای اندازهگیری عرض شمرده میشود، دیده نمیشود.
local function sizerRow()
local longest = findLongest()
if longest == '' then return '' end
local cell = '<span class="poem-sz">' .. esc(longest) .. '</span>'
return '<tr class="poem-sizer" aria-hidden="true">'
.. '<td class="poem-s"></td>'
.. '<td colspan="2" class="poem-mh">' .. cell .. '</td>'
.. '<td class="poem-g"></td>'
.. '<td colspan="2" class="poem-mh">' .. cell .. '</td>'
.. '<td class="poem-s"></td>'
.. '</tr>'
end
local function openHtml(o)
local cls = 'poem' .. (o.fixed and ' poem-fixed' or '')
if o.class then cls = cls .. ' ' .. o.class end
local div = '<div class="' .. esc(cls) .. '"'
if o.lang then div = div .. ' lang="' .. o.lang .. '"' end
if o.dir then div = div .. ' dir="' .. o.dir .. '"' end
local font = o.font and ('font-family:' .. o.font) or nil
local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '')
-- Use the same width constraints in every namespace and mode.
local width = o.width or cfg.defaultWidth
local maxWidth = o.maxWidth or cfg.maxWidth
local wrap = joinStyle(
'width:' .. width,
'max-width:' .. maxWidth,
'margin:0 auto',
font,
o.style
)
-- Always use a fixed seven-column grid and its sizing row.
return div .. ' style="' .. esc(wrap) .. '">'
.. '<table class="' .. tcls
.. '" role="presentation" style="width:100%;table-layout:fixed">'
.. sizerRow()
end
---------------------------------------------------------------------------
-- چندصفحهای
---------------------------------------------------------------------------
local function joinText(a, b, sep)
if a == '' then return b end
if b == '' then return a end
return a .. sep .. b
end
local function mergeRow(head, tail, sep)
for _, f in ipairs({ 'm1', 'm2', 'text' }) do
if head[f] ~= nil or tail[f] ~= nil then
head[f] = joinText(head[f] or '', tail[f] or '', sep)
end
end
for _, f in ipairs({ 'r', 'c', 'l' }) do
if tail[f] and tail[f] ~= '' then head[f] = tail[f] end
end
head.style = head.style or tail.style
head.class = head.class or tail.class
head.id = head.id or tail.id
return head
end
local function flushHtml()
local c = state.carry
if not c then return '' end
state.carry = nil
return RENDER[c.kind](c.d)
end
local function plain(html)
if not inPageNs() then
local pre = flushHtml()
if pre ~= '' then return pre .. '\n' .. html end
end
return html
end
local function emit(kind, d, opt, flags)
opt = opt or {}
register(kind, d)
if inPageNs() then
if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end
if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end
return RENDER[kind](d)
end
if not cfg.mergeRows then return RENDER[kind](d) end
local out = ''
local carry = state.carry
state.carry = nil
if carry then
if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ')
else
out = RENDER[carry.kind](carry.d)
if opt.continued then flags.unknown = true end
end
end
if opt.unfinished then
state.carry = { kind = kind, d = d }
return out ~= '' and out or SKIP
end
local html = RENDER[kind](d)
if out ~= '' then return out .. '\n' .. html end
return html
end
local function rowOpts(args, flags)
local opt = {}
local u = pick(args, N.unfinished)
if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end
local c = pick(args, N.continued)
if c then
local w = WHICH[c:lower()]
if w then opt.continued, opt.which = true, w
else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end
end
local j = pick(args, N.joiner)
if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end
return opt
end
local function parseOpen(args, flags)
local o = {}
local isText = present(args, N.text)
for i = 1, 2 do
local v = args[i]
if v and v ~= '' then
local lv = v:lower()
if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true
elseif v == 'ادامه' or lv == 'continue' then
if isText then flags.unknown = true else o.cont = 'prev' end
else flags.unknown = true end
end
end
if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k then
if not isText and k == 'next' then flags.unknown = true; k = nil end
else
local b = bool(c, nil)
if b == nil then flags.unknown = true
elseif b then if isText then flags.unknown = true else k = 'prev' end end
end
if k then o.cont = k end
end
o.style = pick(args, N.style); o.class = pick(args, N.class)
local lang = pick(args, N.lang)
if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end
local dir = pick(args, N.dir)
if dir then
dir = dir:lower()
if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end
end
if o.lang and not o.dir then o.dir = langDir(o.lang) end
o.kind = 'bayt'
local k = pick(args, N.kind)
if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end
local a = pick(args, N.align)
if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end
local g = pick(args, N.gap)
if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end
local ind = pick(args, N.indent)
if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end
o.unitNum, o.unitUnit = splitLength(pick(args, N.unit))
if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end
local st = pick(args, N.stanza)
if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end
local jv = pick(args, N.just)
if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end
if o.nastaliq then o.font = cfg.nastaliqFonts end
local f = pick(args, N.font)
if f then
if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts
elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f
else flags.unknown = true end
end
local w = pick(args, N.width)
if w then
local lw = w:lower()
if lw == 'خودکار' or lw == 'auto' then o.fixed = false
else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end
end
local mx = pick(args, N.maxwidth)
if mx then
local lm = mx:lower()
if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none'
else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end
end
if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end
if o.fixed then
local explicit = (o.width ~= nil)
o.width = o.width or cfg.defaultWidth
o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth)
end
return o
end
local function parseClose(args, flags)
local cont = false
local v = args[1]
if v and v ~= '' then
if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end
end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k == 'next' or k == 'both' then cont = true
elseif k == 'prev' then flags.unknown = true
else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end
end
return cont
end
local function parseLine(raw, freeVerse)
local t = {}
local s = raw
if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end
if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3)
elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end
s = mw.text.trim(s)
local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$')
if pre then t.pre = pre; s = rest end
local body, post = s:match('^(.-)%s*>>>%s*(.+)$')
if body then t.post = post; s = body end
t.text = s
return t
end
local function parseDirective(line)
local rest = mw.text.trim(line:sub(2))
local lrest = rest:lower()
for _, key in ipairs(KIND_KEYS) do
if lrest:sub(1, #key) == key then
local after = rest:sub(#key + 1)
if after == '' or after:match('^%s') then
local dd, bad = { kind = KIND[key] }, false
for k, v in after:gmatch('(%S+)=(%S+)') do
local kk, lv = k:lower(), v:lower()
if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end
elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end
elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end
else bad = true end
end
return dd, bad
end
end
end
return nil, true
end
local function renderLines(text, o, ctx, ropt, flags)
local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just }
local items, pending, pendingCur, needGap = {}, nil, nil, false
local function info(c)
local twoH = (c.kind ~= 'mesra')
local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger')
return twoH, rowKind, c.indent
end
local function numbers(d, t)
if not t then return end
if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end
if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end
end
local function makeBayt(t1, t2, c)
local _, _, indent = info(c)
local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '',
r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just }
numbers(d, t1); numbers(d, t2)
return d
end
local function makeMesra(t, c, free)
local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed }
d.align = t.align or c.align or (free and 'right' or 'center')
if free and (t.level or 0) > 0 and d.align ~= 'center' then
d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit
end
numbers(d, t)
return d
end
local function push(kind, d, counted)
items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 }
needGap = false
end
local function flushPending()
if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end
end
local src = text:gsub('\r\n?', '\n')
for raw in (src .. '\n'):gmatch('([^\n]*)\n') do
local line = mw.text.trim(raw)
if line == '' then
flushPending()
if #items > 0 then needGap = true end
elseif line:sub(1, 1) == '@' then
local dd, bad = parseDirective(line)
flushPending()
if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end
if bad then flags.unknown = true end
else
local twoH, rowKind = info(cur)
local isFirst = (#items == 0 and not pending)
local t = parseLine(line, not twoH)
if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra)
else
local handled = false
if ropt.continued and isFirst then
local mode = ropt.which or 'auto'
if mode == 'auto' then
local c = state.carry
mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both'
end
if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true
elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end
end
if not handled then
if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil
else pending, pendingCur = t, cur end
end
end
end
end
if pending then
local _, rowKind = info(pendingCur)
if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true)
else push('mesra', makeMesra(pending, pendingCur, false), false) end
pending = nil
end
for i, it in ipairs(items) do
if it.counted and not (i == 1 and ropt.continued) then
applyAuto(it.d, ctx, true, it.kind ~= 'bayt')
end
end
local out = {}
for i, it in ipairs(items) do
if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end
local ro = {}
if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end
if i == #items and ropt.unfinished then ro.unfinished = true end
local h = emit(it.kind, it.d, ro, flags)
if h ~= SKIP and h ~= '' then out[#out + 1] = h end
end
return table.concat(out, '\n')
end
local function open(frame)
local args, flags = getArgs(frame), {}
local o = parseOpen(args, flags)
local isText = present(args, N.text)
local page = inPageNs()
if hasUnknown(args, 2) then flags.unknown = true end
local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both')
local ctx
if joins and state.ctx then ctx = state.ctx
else
ctx = newCtx(args, o.kind, flags)
ctx.fixed, ctx.just = o.fixed, o.just
end
local parts = {}
if not joins then
parts[#parts + 1] = stylesTag(frame) .. openHtml(o)
state.carry = nil
state.hemistichs = {}
end
state.open, state.ctx = true, ctx
if isText then
local ropt = rowOpts(args, flags)
if o.newStanza and joins and not ropt.continued then
parts[#parts + 1] = plain(rowGap({ height = o.gap }))
end
local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags)
if rows ~= '' then parts[#parts + 1] = rows end
local stay = (not page) and (o.cont == 'next' or o.cont == 'both')
if not stay then
local tail = flushHtml()
if tail ~= '' then parts[#parts + 1] = tail end
local sz = sizerRow()
if sz ~= '' then parts[#parts + 1] = sz end
parts[#parts + 1] = CLOSE_HTML
state.open, state.ctx, state.hemistichs = false, nil, {}
else
parts[#parts + 1] = FINAL_MARKER
end
end
if #parts == 0 then parts[1] = SKIP end
return table.concat(parts, '\n') .. categories(flags)
end
local function close(frame)
local args, flags = getArgs(frame), {}
local cont = parseClose(args, flags)
if hasUnknown(args, 1) then flags.unknown = true end
local html = flushHtml()
if html ~= '' then html = html .. '\n' end
if cont and not inPageNs() then
return html .. FINAL_MARKER .. categories(flags)
end
state.open, state.ctx = false, nil
local sz = sizerRow()
state.hemistichs = {}
return html .. sz .. CLOSE_HTML .. categories(flags)
end
local function commonRow(args, ctx, flags)
local d = {
r = numberArg(args, N.right, ctx.digits),
c = numberArg(args, N.center, ctx.digits),
l = numberArg(args, N.left, ctx.digits),
style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id),
fixed = ctx.fixed, just = ctx.just,
}
local jv = pick(args, N.just)
if jv then
d.just = JUST[jv:lower()]
if not d.just then flags.unknown = true; d.just = ctx.just end
end
return d
end
local function bayt(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, false)
local html = emit('bayt', d, opt, flags)
return html .. categories(flags)
end
local function staggerFn(defaultIndent)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
local raw = pick(args, N.indent)
if raw then
d.indent = parseLength(raw)
if not d.indent then flags.unknown = true end
d.indent = d.indent or defaultIndent
else
d.indent = defaultIndent
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, true)
local html = emit('stagger', d, opt, flags)
return html .. categories(flags)
end
end
local function mesra(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.text = clean(args[1])
local a = pick(args, N.align)
if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end
local raw = pick(args, N.indent)
d.indent = parseLength(raw)
if raw and not d.indent then flags.unknown = true end
if hasUnknown(args, 1) then flags.unknown = true end
applyAuto(d, ctx, ctx.mesra and not opt.continued, true)
local html = emit('mesra', d, opt, flags)
return html .. categories(flags)
end
local function emptyFn(kind)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local d = commonRow(args, ctx, flags)
local raw = pick(args, N.height)
d.height = parseLength(raw)
if raw and not d.height then flags.unknown = true end
if hasUnknown(args, 0) then flags.unknown = true end
local html
if d.height then html = rowGap(d)
elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d)
else d.text, d.blank = '', true; html = rowMesra(d) end
return plain(html) .. categories(flags)
end
end
local function safe(fn)
return function(frame)
local ok, res = pcall(fn, frame)
if ok then return res end
return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>'
.. categories({ error = true })
end
end
p.open = safe(open)
p.close = safe(close)
p.bayt = safe(bayt)
p.stagger = safe(staggerFn(nil))
p.ladder = safe(staggerFn(nil))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
ea4feyi4uc69givu77ar7iq2ole7r6z
304047
304044
2026-10-12T18:56:37Z
Hanooz
17889
304047
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
▸ شبکهٔ ثابت ۷ستونی با درصدهای قطعی:
۵٪ ش | ۲۱٪ | ۲۱٪ | ۶٪ شو | ۲۱٪ | ۲۱٪ | ۵٪ شچ
هر مصراع (colspan=2) دقیقاً ۴۲٪ جدول است؛ در همهٔ ابیات و همهٔ
انواع (بیت، بیت۱، نردبانی) هماندازه.
▸ ردیف سایزرِ خالی در آغاز جدول، عرض ستونها را تثبیت میکند
(table-layout: fixed).
▸ ترازبندی پیشفرض «وسط». ترازبندی=پهن یا لبه اختیاری.
▸ ارجاعهای <ref> در متن inline میمانند (به آخرین واژه میچسبند).
▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت.
]==]
local p = {}
local ZWNJ = '\226\128\140'
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '48em',
justDefault = { bayt = 'center', stagger = 'center' },
nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', "
.. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif",
mergeRows = false,
cats = {
unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر',
incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص',
error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر',
},
}
local N = {
right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' },
left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' },
class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' },
indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' },
height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' },
dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' },
numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' },
start = { 'شروع', 'start' }, side = { 'سمت', 'side' },
digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' },
unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' },
nastaliq = { 'نستعلیق', 'nastaliq' },
cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' },
continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' },
stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' },
maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' },
font = { 'قلم', 'font' },
just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' },
}
local KNOWN = {}
for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end
local ALIGN = {
['راست'] = 'right', right = 'right', r = 'right',
['چپ'] = 'left', left = 'left', l = 'left',
['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center',
}
local SIDE = { right = 'r', left = 'l', center = 'c' }
local KIND = {
['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt',
['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger',
['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder',
['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra',
}
local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa',
['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin',
['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' }
local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true,
yes=true, y=true, auto=true, ['true']=true, ['1']=true }
local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true }
local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch',
rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' }
local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev',
['بعد']='next', ['پسین']='next', next='next',
['هر دو']='both', ['هردو']='both', both='both' }
local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='',
['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ }
local WHICH = { ['هر دو']='both', ['هردو']='both', both='both',
['اول']='first', first='first', ['دوم']='second', second='second' }
local STANZA = { ['جدید']=true, new=true }
local JUST = {
['وسط']='center', ['مرکز']='center', center='center',
['راست']='right', right='right', ['چپ']='left', left='left',
['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify',
['لبه']='edge', edge='edge',
}
local ALIGN_STYLE = {
right = 'text-align:right;text-align-last:auto',
left = 'text-align:left;text-align-last:auto',
center = 'text-align:center;text-align-last:auto',
justify = 'text-align:justify;text-align-last:justify;'
.. '-moz-text-align-last:justify;-webkit-text-align-last:justify',
}
local KIND_KEYS = {}
for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end
table.sort(KIND_KEYS, function(a, b) return #a > #b end)
local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1,
lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 }
local state = { ctx = nil, open = false, carry = nil }
local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end
local SKIP = '<tr class="poem-skip"></tr>'
local CLOSE_HTML = '</table></div>'
local FINAL_MARKER =
'<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>'
local function esc(s) return (s:gsub('&','&'):gsub('"','"'):gsub('<','<'):gsub('>','>')) end
local function pick(args, names)
for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end
return nil
end
local function present(args, names)
for _, n in ipairs(names) do if args[n] ~= nil then return true end end
return false
end
local function bool(v, d)
if v == nil then return d end
v = v:lower()
if YES[v] then return true end
if NO[v] then return false end
return d
end
local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end
local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end
local function getArgs(frame)
local a = {}
local function merge(src)
if not src then return end
for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end
end
merge(frame.args)
local parent = frame:getParent()
if parent then merge(parent.args) end
return a
end
local function hasUnknown(args, pos)
for k, v in pairs(args) do
if v ~= '' then
if type(k) == 'number' then
if k < 1 or k > pos or k % 1 ~= 0 then return true end
elseif not KNOWN[k] then return true end
end
end
return false
end
local function nsId() return mw.title.getCurrentTitle().namespace end
local function inPageNs() return nsId() == cfg.pageNs end
local function tracked()
local ns = nsId()
return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828)
end
local function categories(flags)
if not flags or next(flags) == nil or not tracked() then return '' end
local out = {}
for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do
if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end
end
return table.concat(out)
end
local function langDir(lang)
local base, rest = lang:lower():match('^(%a+)(.*)$')
if not base then return nil end
if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end
if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end
return 'ltr'
end
local function stylesTag(frame)
local ok, tag = pcall(function()
return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } }
end)
return ok and tag or ''
end
local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' }
local function convertDigits(s, mode)
if s == '' or mode == 'keep' then return s end
if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end
if mode == 'latin' then
s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end)
s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end)
return s
end
s = s:gsub('%d', function(d) return FA[tonumber(d)] end)
s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end)
return s
end
local function numberArg(args, names, digits)
local v = pick(args, names); if not v then return '' end
return convertDigits(v, digits)
end
local function intArg(v, lo, hi)
local n = tonumber((convertDigits(v, 'latin')))
if not n or n ~= n or n < lo or n > hi then return nil end
return math.floor(n)
end
local function splitLength(v)
if not v or v == '' then return nil end
v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.')
local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$')
if not num then return nil end
num = tonumber(num)
if not num or num > 10000 then return nil end
unit = UNITS[unit:lower()]
if not unit then return nil end
return num, unit
end
local function parseLength(v)
local n, u = splitLength(v); if not n then return nil end
return string.format('%g', n) .. u
end
local function newCtx(args, kind, flags)
local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') }
local d = pick(args, N.digits)
if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end
local s = pick(args, N.start)
if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end
local sd = pick(args, N.side)
if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end
local m = pick(args, N.numbering)
if m then
local n = intArg(m, 0, 1e6)
if n then if n >= 1 then ctx.every = n end
else
local b = bool(m, nil)
if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end
end
end
return ctx
end
local function nextNumber(ctx)
if not ctx.every then return nil end
local n = ctx.start + ctx.count
ctx.count = ctx.count + 1
if n % ctx.every ~= 0 then return nil end
return convertDigits(string.format('%d', n), ctx.digits)
end
local function applyAuto(d, ctx, counted, noCenter)
if not counted then return end
local n = nextNumber(ctx)
if not n then return end
if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end
local side = ctx.side
if noCenter and side == 'c' then side = 'l' end
d[side] = n
end
local function joinStyle(...)
local parts = {}
for i = 1, select('#', ...) do
local v = select(i, ...)
if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end
end
return table.concat(parts, ';')
end
local function alignFor(j, which)
if not j or j == 'center' then return nil end
if j == 'edge' then
return (which == 1) and 'text-align:start;text-align-last:auto'
or 'text-align:end;text-align-last:auto'
end
return ALIGN_STYLE[j]
end
local function td(class, colspan, style, content)
local s = '<td'
if class and class ~= '' then s = s .. ' class="' .. class .. '"' end
if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end
if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end
return s .. '>' .. (content or '') .. '</td>'
end
local function hemiCell(text)
return '<span class="beyt">' .. (text or '') .. '</span>'
end
local function rowOpen(d, kind, noId)
local cls = 'poem-row ' .. kind
if d.class then cls = cls .. ' ' .. d.class end
local id = ''
if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end
return '<tr class="' .. esc(cls) .. '"' .. id .. '>'
end
local function rowBayt(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.bayt
return rowOpen(d, 'poem-bayt')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r)
.. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1))
.. td('poem-num poem-num-c', 1, d.style, d.c)
.. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2))
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l)
.. '</tr>'
end
local function rowStagger(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.stagger
local s1 = joinStyle(alignFor(j, 1), d.style)
local s2 = joinStyle(alignFor(j, 2), d.style)
local ind = d.indent
if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then
s1 = joinStyle('padding-inline-end:' .. ind, s1)
s2 = joinStyle('padding-inline-start:' .. ind, s2)
end
return rowOpen(d, 'poem-stagger poem-stagger-1')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r)
.. td('b poem-m poem-m1', 2, s1, hemiCell(m1))
.. td('poem-num poem-num-c poem-empty', 1, nil, '')
.. td('poem-empty', 2, nil, '')
.. td('poem-num poem-num-l poem-empty', 1, nil, '')
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num poem-num-r poem-empty', 1, nil, '')
.. td('poem-empty', 2, nil, '')
.. td('poem-num poem-num-c poem-empty', 1, nil, '')
.. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2))
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l)
.. '</tr>'
end
local function rowMesra(d)
local align = d.align or 'center'
local content = d.text
if d.blank and content == '' then content = cfg.nbsp end
local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil
local style = joinStyle(inline, d.style)
local cellClass = 'b poem-m'
local cs = { 2, 3, 2 }
if align ~= 'center' then
cs = { 1, 5, 1 }
cellClass = cellClass .. ' poem-m-' .. align
if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end
end
local cell
if d.blank and content == cfg.nbsp then cell = content
else cell = hemiCell(content) end
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r)
.. td(cellClass, cs[2], style, cell)
.. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l)
.. '</tr>'
end
local function rowGap(d)
local style = d.height and ('height:' .. d.height) or nil
local content = d.height and '' or cfg.nbsp
return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">'
.. td('poem-gapcell', 7, style, content) .. '</tr>'
end
local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra }
-- سایزرِ خالی: ۷ ستون با عرضهای ثابت درصدی.
-- با table-layout: fixed، این ردیف عرض ستونها را برای همهٔ ردیفهای
-- بعدی تثبیت میکند.
local function sizerRow()
return '<tr class="poem-sizer" aria-hidden="true">'
.. '<td class="poem-s"></td>'
.. '<td class="poem-mh"></td><td class="poem-mh"></td>'
.. '<td class="poem-g"></td>'
.. '<td class="poem-mh"></td><td class="poem-mh"></td>'
.. '<td class="poem-s"></td>'
.. '</tr>'
end
local function openHtml(o)
local cls = 'poem' .. (o.fixed and ' poem-fixed' or '')
if o.class then cls = cls .. ' ' .. o.class end
local div = '<div class="' .. esc(cls) .. '"'
if o.lang then div = div .. ' lang="' .. o.lang .. '"' end
if o.dir then div = div .. ' dir="' .. o.dir .. '"' end
local font = o.font and ('font-family:' .. o.font) or nil
local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '')
if o.fixed then
local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth,
'margin:0 auto', font, o.style)
return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls
.. '" role="presentation" style="width:100%;table-layout:fixed">'
.. sizerRow()
end
local style = 'margin: 0 auto;'
if font then style = style .. font .. ';' end
if o.style then style = style .. ' ' .. o.style end
return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">'
end
---------------------------------------------------------------------------
-- چندصفحهای
---------------------------------------------------------------------------
local function joinText(a, b, sep)
if a == '' then return b end
if b == '' then return a end
return a .. sep .. b
end
local function mergeRow(head, tail, sep)
for _, f in ipairs({ 'm1', 'm2', 'text' }) do
if head[f] ~= nil or tail[f] ~= nil then
head[f] = joinText(head[f] or '', tail[f] or '', sep)
end
end
for _, f in ipairs({ 'r', 'c', 'l' }) do
if tail[f] and tail[f] ~= '' then head[f] = tail[f] end
end
head.style = head.style or tail.style
head.class = head.class or tail.class
head.id = head.id or tail.id
return head
end
local function flushHtml()
local c = state.carry
if not c then return '' end
state.carry = nil
return RENDER[c.kind](c.d)
end
local function plain(html)
if not inPageNs() then
local pre = flushHtml()
if pre ~= '' then return pre .. '\n' .. html end
end
return html
end
local function emit(kind, d, opt, flags)
opt = opt or {}
if inPageNs() then
if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end
if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end
return RENDER[kind](d)
end
if not cfg.mergeRows then return RENDER[kind](d) end
local out = ''
local carry = state.carry
state.carry = nil
if carry then
if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ')
else
out = RENDER[carry.kind](carry.d)
if opt.continued then flags.unknown = true end
end
end
if opt.unfinished then
state.carry = { kind = kind, d = d }
return out ~= '' and out or SKIP
end
local html = RENDER[kind](d)
if out ~= '' then return out .. '\n' .. html end
return html
end
local function rowOpts(args, flags)
local opt = {}
local u = pick(args, N.unfinished)
if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end
local c = pick(args, N.continued)
if c then
local w = WHICH[c:lower()]
if w then opt.continued, opt.which = true, w
else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end
end
local j = pick(args, N.joiner)
if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end
return opt
end
local function parseOpen(args, flags)
local o = {}
local isText = present(args, N.text)
for i = 1, 2 do
local v = args[i]
if v and v ~= '' then
local lv = v:lower()
if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true
elseif v == 'ادامه' or lv == 'continue' then
if isText then flags.unknown = true else o.cont = 'prev' end
else flags.unknown = true end
end
end
if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k then
if not isText and k == 'next' then flags.unknown = true; k = nil end
else
local b = bool(c, nil)
if b == nil then flags.unknown = true
elseif b then if isText then flags.unknown = true else k = 'prev' end end
end
if k then o.cont = k end
end
o.style = pick(args, N.style); o.class = pick(args, N.class)
local lang = pick(args, N.lang)
if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end
local dir = pick(args, N.dir)
if dir then
dir = dir:lower()
if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end
end
if o.lang and not o.dir then o.dir = langDir(o.lang) end
o.kind = 'bayt'
local k = pick(args, N.kind)
if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end
local a = pick(args, N.align)
if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end
local g = pick(args, N.gap)
if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end
local ind = pick(args, N.indent)
if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end
o.unitNum, o.unitUnit = splitLength(pick(args, N.unit))
if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end
local st = pick(args, N.stanza)
if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end
local jv = pick(args, N.just)
if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end
if o.nastaliq then o.font = cfg.nastaliqFonts end
local f = pick(args, N.font)
if f then
if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts
elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f
else flags.unknown = true end
end
local w = pick(args, N.width)
if w then
local lw = w:lower()
if lw == 'خودکار' or lw == 'auto' then o.fixed = false
else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end
end
local mx = pick(args, N.maxwidth)
if mx then
local lm = mx:lower()
if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none'
else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end
end
if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end
if o.fixed then
local explicit = (o.width ~= nil)
o.width = o.width or cfg.defaultWidth
o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth)
end
return o
end
local function parseClose(args, flags)
local cont = false
local v = args[1]
if v and v ~= '' then
if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end
end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k == 'next' or k == 'both' then cont = true
elseif k == 'prev' then flags.unknown = true
else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end
end
return cont
end
local function parseLine(raw, freeVerse)
local t = {}
local s = raw
if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end
if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3)
elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end
s = mw.text.trim(s)
local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$')
if pre then t.pre = pre; s = rest end
local body, post = s:match('^(.-)%s*>>>%s*(.+)$')
if body then t.post = post; s = body end
t.text = s
return t
end
local function parseDirective(line)
local rest = mw.text.trim(line:sub(2))
local lrest = rest:lower()
for _, key in ipairs(KIND_KEYS) do
if lrest:sub(1, #key) == key then
local after = rest:sub(#key + 1)
if after == '' or after:match('^%s') then
local dd, bad = { kind = KIND[key] }, false
for k, v in after:gmatch('(%S+)=(%S+)') do
local kk, lv = k:lower(), v:lower()
if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end
elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end
elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end
else bad = true end
end
return dd, bad
end
end
end
return nil, true
end
local function renderLines(text, o, ctx, ropt, flags)
local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just }
local items, pending, pendingCur, needGap = {}, nil, nil, false
local function info(c)
local twoH = (c.kind ~= 'mesra')
local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger')
return twoH, rowKind, c.indent
end
local function numbers(d, t)
if not t then return end
if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end
if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end
end
local function makeBayt(t1, t2, c)
local _, _, indent = info(c)
local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '',
r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just }
numbers(d, t1); numbers(d, t2)
return d
end
local function makeMesra(t, c, free)
local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed }
d.align = t.align or c.align or (free and 'right' or 'center')
if free and (t.level or 0) > 0 and d.align ~= 'center' then
d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit
end
numbers(d, t)
return d
end
local function push(kind, d, counted)
items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 }
needGap = false
end
local function flushPending()
if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end
end
local src = text:gsub('\r\n?', '\n')
for raw in (src .. '\n'):gmatch('([^\n]*)\n') do
local line = mw.text.trim(raw)
if line == '' then
flushPending()
if #items > 0 then needGap = true end
elseif line:sub(1, 1) == '@' then
local dd, bad = parseDirective(line)
flushPending()
if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end
if bad then flags.unknown = true end
else
local twoH, rowKind = info(cur)
local isFirst = (#items == 0 and not pending)
local t = parseLine(line, not twoH)
if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra)
else
local handled = false
if ropt.continued and isFirst then
local mode = ropt.which or 'auto'
if mode == 'auto' then
local c = state.carry
mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both'
end
if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true
elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end
end
if not handled then
if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil
else pending, pendingCur = t, cur end
end
end
end
end
if pending then
local _, rowKind = info(pendingCur)
if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true)
else push('mesra', makeMesra(pending, pendingCur, false), false) end
pending = nil
end
for i, it in ipairs(items) do
if it.counted and not (i == 1 and ropt.continued) then
applyAuto(it.d, ctx, true, it.kind ~= 'bayt')
end
end
local out = {}
for i, it in ipairs(items) do
if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end
local ro = {}
if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end
if i == #items and ropt.unfinished then ro.unfinished = true end
local h = emit(it.kind, it.d, ro, flags)
if h ~= SKIP and h ~= '' then out[#out + 1] = h end
end
return table.concat(out, '\n')
end
local function open(frame)
local args, flags = getArgs(frame), {}
local o = parseOpen(args, flags)
local isText = present(args, N.text)
local page = inPageNs()
if hasUnknown(args, 2) then flags.unknown = true end
local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both')
local ctx
if joins and state.ctx then ctx = state.ctx
else
ctx = newCtx(args, o.kind, flags)
ctx.fixed, ctx.just = o.fixed, o.just
end
local parts = {}
if not joins then
parts[#parts + 1] = stylesTag(frame) .. openHtml(o)
state.carry = nil
end
state.open, state.ctx = true, ctx
if isText then
local ropt = rowOpts(args, flags)
if o.newStanza and joins and not ropt.continued then
parts[#parts + 1] = plain(rowGap({ height = o.gap }))
end
local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags)
if rows ~= '' then parts[#parts + 1] = rows end
local stay = (not page) and (o.cont == 'next' or o.cont == 'both')
if not stay then
local tail = flushHtml()
if tail ~= '' then parts[#parts + 1] = tail end
parts[#parts + 1] = CLOSE_HTML
state.open, state.ctx = false, nil
else
parts[#parts + 1] = FINAL_MARKER
end
end
if #parts == 0 then parts[1] = SKIP end
return table.concat(parts, '\n') .. categories(flags)
end
local function close(frame)
local args, flags = getArgs(frame), {}
local cont = parseClose(args, flags)
if hasUnknown(args, 1) then flags.unknown = true end
local html = flushHtml()
if html ~= '' then html = html .. '\n' end
if cont and not inPageNs() then
return html .. FINAL_MARKER .. categories(flags)
end
state.open, state.ctx = false, nil
return html .. CLOSE_HTML .. categories(flags)
end
local function commonRow(args, ctx, flags)
local d = {
r = numberArg(args, N.right, ctx.digits),
c = numberArg(args, N.center, ctx.digits),
l = numberArg(args, N.left, ctx.digits),
style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id),
fixed = ctx.fixed, just = ctx.just,
}
local jv = pick(args, N.just)
if jv then
d.just = JUST[jv:lower()]
if not d.just then flags.unknown = true; d.just = ctx.just end
end
return d
end
local function bayt(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, false)
local html = emit('bayt', d, opt, flags)
return html .. categories(flags)
end
local function staggerFn(defaultIndent)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
local raw = pick(args, N.indent)
if raw then
d.indent = parseLength(raw)
if not d.indent then flags.unknown = true end
d.indent = d.indent or defaultIndent
else
d.indent = defaultIndent
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, true)
local html = emit('stagger', d, opt, flags)
return html .. categories(flags)
end
end
local function mesra(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.text = clean(args[1])
local a = pick(args, N.align)
if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end
local raw = pick(args, N.indent)
d.indent = parseLength(raw)
if raw and not d.indent then flags.unknown = true end
if hasUnknown(args, 1) then flags.unknown = true end
applyAuto(d, ctx, ctx.mesra and not opt.continued, true)
local html = emit('mesra', d, opt, flags)
return html .. categories(flags)
end
local function emptyFn(kind)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local d = commonRow(args, ctx, flags)
local raw = pick(args, N.height)
d.height = parseLength(raw)
if raw and not d.height then flags.unknown = true end
if hasUnknown(args, 0) then flags.unknown = true end
local html
if d.height then html = rowGap(d)
elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d)
else d.text, d.blank = '', true; html = rowMesra(d) end
return plain(html) .. categories(flags)
end
end
local function safe(fn)
return function(frame)
local ok, res = pcall(fn, frame)
if ok then return res end
return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>'
.. categories({ error = true })
end
end
p.open = safe(open)
p.close = safe(close)
p.bayt = safe(bayt)
p.stagger = safe(staggerFn(nil))
p.ladder = safe(staggerFn(nil))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
9ja5iu2kjk4w3mhe1h96in6ql1l7bgx
304050
304047
2026-10-12T19:22:25Z
Hanooz
17889
304050
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
▸ عرض ستونها بهصورت inline روی هر سلول نوشته میشود، نه در CSS.
شبکهٔ ثابت ۷ستونی: ۵٪ | ۲۱٪ | ۲۱٪ | ۶٪ | ۲۱٪ | ۲۱٪ | ۵٪
هر مصراع (colspan=2) دقیقاً ۴۲٪ جدول است.
▸ بهاینترتیب حتی اگر شیوهنامه در فضای نام صفحه بارگذاری نشود یا
ProofreadPage تگ templatestyles را حذف کند، عرضها حفظ میشوند.
▸ ترازبندی پیشفرض «وسط». ترازبندی=پهن / لبه اختیاری.
▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت.
]==]
local p = {}
local ZWNJ = '\226\128\140'
-- عرضهای قطعی (درصد). مجموع = ۱۰۰.
local W_SIDE = '5%'
local W_MH = '21%'
local W_GAP = '6%'
local W_MISRA = '42%' -- 21 + 21
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '48em',
justDefault = { bayt = 'center', stagger = 'center' },
nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', "
.. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif",
mergeRows = false,
cats = {
unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر',
incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص',
error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر',
},
}
local N = {
right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' },
left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' },
class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' },
indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' },
height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' },
dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' },
numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' },
start = { 'شروع', 'start' }, side = { 'سمت', 'side' },
digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' },
unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' },
nastaliq = { 'نستعلیق', 'nastaliq' },
cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' },
continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' },
stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' },
maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' },
font = { 'قلم', 'font' },
just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' },
}
local KNOWN = {}
for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end
local ALIGN = {
['راست'] = 'right', right = 'right', r = 'right',
['چپ'] = 'left', left = 'left', l = 'left',
['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center',
}
local SIDE = { right = 'r', left = 'l', center = 'c' }
local KIND = {
['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt',
['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger',
['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder',
['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra',
}
local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa',
['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin',
['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' }
local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true,
yes=true, y=true, auto=true, ['true']=true, ['1']=true }
local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true }
local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch',
rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' }
local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev',
['بعد']='next', ['پسین']='next', next='next',
['هر دو']='both', ['هردو']='both', both='both' }
local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='',
['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ }
local WHICH = { ['هر دو']='both', ['هردو']='both', both='both',
['اول']='first', first='first', ['دوم']='second', second='second' }
local STANZA = { ['جدید']=true, new=true }
local JUST = {
['وسط']='center', ['مرکز']='center', center='center',
['راست']='right', right='right', ['چپ']='left', left='left',
['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify',
['لبه']='edge', edge='edge',
}
local ALIGN_STYLE = {
right = 'text-align:right;text-align-last:auto',
left = 'text-align:left;text-align-last:auto',
center = 'text-align:center;text-align-last:auto',
justify = 'text-align:justify;text-align-last:justify;'
.. '-moz-text-align-last:justify;-webkit-text-align-last:justify',
}
local KIND_KEYS = {}
for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end
table.sort(KIND_KEYS, function(a, b) return #a > #b end)
local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1,
lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 }
local state = { ctx = nil, open = false, carry = nil }
local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end
local SKIP = '<tr class="poem-skip"></tr>'
local CLOSE_HTML = '</table></div>'
local FINAL_MARKER =
'<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>'
local function esc(s) return (s:gsub('&','&'):gsub('"','"'):gsub('<','<'):gsub('>','>')) end
local function pick(args, names)
for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end
return nil
end
local function present(args, names)
for _, n in ipairs(names) do if args[n] ~= nil then return true end end
return false
end
local function bool(v, d)
if v == nil then return d end
v = v:lower()
if YES[v] then return true end
if NO[v] then return false end
return d
end
local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end
local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end
local function getArgs(frame)
local a = {}
local function merge(src)
if not src then return end
for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end
end
merge(frame.args)
local parent = frame:getParent()
if parent then merge(parent.args) end
return a
end
local function hasUnknown(args, pos)
for k, v in pairs(args) do
if v ~= '' then
if type(k) == 'number' then
if k < 1 or k > pos or k % 1 ~= 0 then return true end
elseif not KNOWN[k] then return true end
end
end
return false
end
local function nsId() return mw.title.getCurrentTitle().namespace end
local function inPageNs() return nsId() == cfg.pageNs end
local function tracked()
local ns = nsId()
return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828)
end
local function categories(flags)
if not flags or next(flags) == nil or not tracked() then return '' end
local out = {}
for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do
if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end
end
return table.concat(out)
end
local function langDir(lang)
local base, rest = lang:lower():match('^(%a+)(.*)$')
if not base then return nil end
if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end
if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end
return 'ltr'
end
local function stylesTag(frame)
local ok, tag = pcall(function()
return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } }
end)
return ok and tag or ''
end
local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' }
local function convertDigits(s, mode)
if s == '' or mode == 'keep' then return s end
if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end
if mode == 'latin' then
s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end)
s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end)
return s
end
s = s:gsub('%d', function(d) return FA[tonumber(d)] end)
s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end)
return s
end
local function numberArg(args, names, digits)
local v = pick(args, names); if not v then return '' end
return convertDigits(v, digits)
end
local function intArg(v, lo, hi)
local n = tonumber((convertDigits(v, 'latin')))
if not n or n ~= n or n < lo or n > hi then return nil end
return math.floor(n)
end
local function splitLength(v)
if not v or v == '' then return nil end
v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.')
local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$')
if not num then return nil end
num = tonumber(num)
if not num or num > 10000 then return nil end
unit = UNITS[unit:lower()]
if not unit then return nil end
return num, unit
end
local function parseLength(v)
local n, u = splitLength(v); if not n then return nil end
return string.format('%g', n) .. u
end
local function newCtx(args, kind, flags)
local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') }
local d = pick(args, N.digits)
if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end
local s = pick(args, N.start)
if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end
local sd = pick(args, N.side)
if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end
local m = pick(args, N.numbering)
if m then
local n = intArg(m, 0, 1e6)
if n then if n >= 1 then ctx.every = n end
else
local b = bool(m, nil)
if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end
end
end
return ctx
end
local function nextNumber(ctx)
if not ctx.every then return nil end
local n = ctx.start + ctx.count
ctx.count = ctx.count + 1
if n % ctx.every ~= 0 then return nil end
return convertDigits(string.format('%d', n), ctx.digits)
end
local function applyAuto(d, ctx, counted, noCenter)
if not counted then return end
local n = nextNumber(ctx)
if not n then return end
if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end
local side = ctx.side
if noCenter and side == 'c' then side = 'l' end
d[side] = n
end
local function joinStyle(...)
local parts = {}
for i = 1, select('#', ...) do
local v = select(i, ...)
if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end
end
return table.concat(parts, ';')
end
local function alignFor(j, which)
if not j or j == 'center' then return nil end
if j == 'edge' then
return (which == 1) and 'text-align:start;text-align-last:auto'
or 'text-align:end;text-align-last:auto'
end
return ALIGN_STYLE[j]
end
-- td با عرض قطعی: width همیشه در style اینلاین مینشیند.
local function td(class, colspan, style, content, width)
local s = '<td'
if class and class ~= '' then s = s .. ' class="' .. class .. '"' end
if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end
local full = style
if width then
if full and full ~= '' then
full = 'width:' .. width .. ';' .. full
else
full = 'width:' .. width
end
end
if full and full ~= '' then s = s .. ' style="' .. esc(full) .. '"' end
return s .. '>' .. (content or '') .. '</td>'
end
local function hemiCell(text)
return '<span class="beyt">' .. (text or '') .. '</span>'
end
local function rowOpen(d, kind, noId)
local cls = 'poem-row ' .. kind
if d.class then cls = cls .. ' ' .. d.class end
local id = ''
if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end
return '<tr class="' .. esc(cls) .. '"' .. id .. '>'
end
local function rowBayt(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.bayt
return rowOpen(d, 'poem-bayt')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE)
.. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1), W_MISRA)
.. td('poem-num poem-num-c', 1, d.style, d.c, W_GAP)
.. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2), W_MISRA)
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE)
.. '</tr>'
end
local function rowStagger(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.stagger
local s1 = joinStyle(alignFor(j, 1), d.style)
local s2 = joinStyle(alignFor(j, 2), d.style)
local ind = d.indent
if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then
s1 = joinStyle('padding-inline-end:' .. ind, s1)
s2 = joinStyle('padding-inline-start:' .. ind, s2)
end
-- ردیف ۱: side | misra1 | gap | empty | empty
-- ردیف ۲: empty | empty | gap | misra2 | side
return rowOpen(d, 'poem-stagger poem-stagger-1')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE)
.. td('b poem-m poem-m1', 2, s1, hemiCell(m1), W_MISRA)
.. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP)
.. td('poem-empty', 2, nil, '', W_MISRA)
.. td('poem-num poem-num-l poem-empty', 1, nil, '', W_SIDE)
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num poem-num-r poem-empty', 1, nil, '', W_SIDE)
.. td('poem-empty', 2, nil, '', W_MISRA)
.. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP)
.. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2), W_MISRA)
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE)
.. '</tr>'
end
local function rowMesra(d)
local align = d.align or 'center'
local content = d.text
if d.blank and content == '' then content = cfg.nbsp end
local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil
local style = joinStyle(inline, d.style)
local cellClass = 'b poem-m'
local cs = { 2, 3, 2 }
if align ~= 'center' then
cs = { 1, 5, 1 }
cellClass = cellClass .. ' poem-m-' .. align
if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end
end
local cell
if d.blank and content == cfg.nbsp then cell = content
else cell = hemiCell(content) end
local w1 = (cs[1] == 1) and W_SIDE or W_MISRA
local w2 = (cs[2] == 3) and W_MISRA or (W_SIDE .. '+') -- never used; simplified below
-- ساده: اگر cs = {2,3,2} باشد ⇒ side=42%? نه، side=5%+? مجموع = 7 ستون
-- cs={2,3,2}: 5+21 / 21+6+21 / 21+5 ⇒ 26% / 68% / 26%
-- cs={1,5,1}: 5 / 5+21+21+6+21+21 / 5 ⇒ 5% / 95% / 5%
local wLeft = (cs[1] == 1) and W_SIDE or '26%'
local wMid = (cs[2] == 3) and '48%' or '90%' -- approximation; browser resolves under fixed
local wRight = (cs[3] == 1) and W_SIDE or '26%'
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r, wLeft)
.. td(cellClass, cs[2], style, cell, wMid)
.. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l, wRight)
.. '</tr>'
end
local function rowGap(d)
local style = d.height and ('height:' .. d.height) or nil
local content = d.height and '' or cfg.nbsp
return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">'
.. td('poem-gapcell', 7, style, content, '100%') .. '</tr>'
end
local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra }
-- سایزر با عرضهای inline روی هر سلول.
local function sizerRow()
return '<tr class="poem-sizer" aria-hidden="true">'
.. td('poem-s', 1, nil, '', W_SIDE)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-g', 1, nil, '', W_GAP)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-s', 1, nil, '', W_SIDE)
.. '</tr>'
end
local function openHtml(o)
local cls = 'poem' .. (o.fixed and ' poem-fixed' or '')
if o.class then cls = cls .. ' ' .. o.class end
local div = '<div class="' .. esc(cls) .. '"'
if o.lang then div = div .. ' lang="' .. o.lang .. '"' end
if o.dir then div = div .. ' dir="' .. o.dir .. '"' end
local font = o.font and ('font-family:' .. o.font) or nil
local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '')
if o.fixed then
local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth,
'margin:0 auto', font, o.style)
return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls
.. '" role="presentation" style="width:100%;table-layout:fixed">'
.. sizerRow()
end
local style = 'margin: 0 auto;'
if font then style = style .. font .. ';' end
if o.style then style = style .. ' ' .. o.style end
return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">'
end
---------------------------------------------------------------------------
-- چندصفحهای
---------------------------------------------------------------------------
local function joinText(a, b, sep)
if a == '' then return b end
if b == '' then return a end
return a .. sep .. b
end
local function mergeRow(head, tail, sep)
for _, f in ipairs({ 'm1', 'm2', 'text' }) do
if head[f] ~= nil or tail[f] ~= nil then
head[f] = joinText(head[f] or '', tail[f] or '', sep)
end
end
for _, f in ipairs({ 'r', 'c', 'l' }) do
if tail[f] and tail[f] ~= '' then head[f] = tail[f] end
end
head.style = head.style or tail.style
head.class = head.class or tail.class
head.id = head.id or tail.id
return head
end
local function flushHtml()
local c = state.carry
if not c then return '' end
state.carry = nil
return RENDER[c.kind](c.d)
end
local function plain(html)
if not inPageNs() then
local pre = flushHtml()
if pre ~= '' then return pre .. '\n' .. html end
end
return html
end
local function emit(kind, d, opt, flags)
opt = opt or {}
if inPageNs() then
if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end
if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end
return RENDER[kind](d)
end
if not cfg.mergeRows then return RENDER[kind](d) end
local out = ''
local carry = state.carry
state.carry = nil
if carry then
if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ')
else
out = RENDER[carry.kind](carry.d)
if opt.continued then flags.unknown = true end
end
end
if opt.unfinished then
state.carry = { kind = kind, d = d }
return out ~= '' and out or SKIP
end
local html = RENDER[kind](d)
if out ~= '' then return out .. '\n' .. html end
return html
end
local function rowOpts(args, flags)
local opt = {}
local u = pick(args, N.unfinished)
if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end
local c = pick(args, N.continued)
if c then
local w = WHICH[c:lower()]
if w then opt.continued, opt.which = true, w
else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end
end
local j = pick(args, N.joiner)
if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end
return opt
end
local function parseOpen(args, flags)
local o = {}
local isText = present(args, N.text)
for i = 1, 2 do
local v = args[i]
if v and v ~= '' then
local lv = v:lower()
if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true
elseif v == 'ادامه' or lv == 'continue' then
if isText then flags.unknown = true else o.cont = 'prev' end
else flags.unknown = true end
end
end
if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k then
if not isText and k == 'next' then flags.unknown = true; k = nil end
else
local b = bool(c, nil)
if b == nil then flags.unknown = true
elseif b then if isText then flags.unknown = true else k = 'prev' end end
end
if k then o.cont = k end
end
o.style = pick(args, N.style); o.class = pick(args, N.class)
local lang = pick(args, N.lang)
if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end
local dir = pick(args, N.dir)
if dir then
dir = dir:lower()
if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end
end
if o.lang and not o.dir then o.dir = langDir(o.lang) end
o.kind = 'bayt'
local k = pick(args, N.kind)
if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end
local a = pick(args, N.align)
if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end
local g = pick(args, N.gap)
if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end
local ind = pick(args, N.indent)
if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end
o.unitNum, o.unitUnit = splitLength(pick(args, N.unit))
if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end
local st = pick(args, N.stanza)
if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end
local jv = pick(args, N.just)
if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end
if o.nastaliq then o.font = cfg.nastaliqFonts end
local f = pick(args, N.font)
if f then
if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts
elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f
else flags.unknown = true end
end
local w = pick(args, N.width)
if w then
local lw = w:lower()
if lw == 'خودکار' or lw == 'auto' then o.fixed = false
else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end
end
local mx = pick(args, N.maxwidth)
if mx then
local lm = mx:lower()
if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none'
else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end
end
if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end
if o.fixed then
local explicit = (o.width ~= nil)
o.width = o.width or cfg.defaultWidth
o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth)
end
return o
end
local function parseClose(args, flags)
local cont = false
local v = args[1]
if v and v ~= '' then
if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end
end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k == 'next' or k == 'both' then cont = true
elseif k == 'prev' then flags.unknown = true
else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end
end
return cont
end
local function parseLine(raw, freeVerse)
local t = {}
local s = raw
if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end
if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3)
elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end
s = mw.text.trim(s)
local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$')
if pre then t.pre = pre; s = rest end
local body, post = s:match('^(.-)%s*>>>%s*(.+)$')
if body then t.post = post; s = body end
t.text = s
return t
end
local function parseDirective(line)
local rest = mw.text.trim(line:sub(2))
local lrest = rest:lower()
for _, key in ipairs(KIND_KEYS) do
if lrest:sub(1, #key) == key then
local after = rest:sub(#key + 1)
if after == '' or after:match('^%s') then
local dd, bad = { kind = KIND[key] }, false
for k, v in after:gmatch('(%S+)=(%S+)') do
local kk, lv = k:lower(), v:lower()
if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end
elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end
elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end
else bad = true end
end
return dd, bad
end
end
end
return nil, true
end
local function renderLines(text, o, ctx, ropt, flags)
local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just }
local items, pending, pendingCur, needGap = {}, nil, nil, false
local function info(c)
local twoH = (c.kind ~= 'mesra')
local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger')
return twoH, rowKind, c.indent
end
local function numbers(d, t)
if not t then return end
if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end
if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end
end
local function makeBayt(t1, t2, c)
local _, _, indent = info(c)
local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '',
r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just }
numbers(d, t1); numbers(d, t2)
return d
end
local function makeMesra(t, c, free)
local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed }
d.align = t.align or c.align or (free and 'right' or 'center')
if free and (t.level or 0) > 0 and d.align ~= 'center' then
d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit
end
numbers(d, t)
return d
end
local function push(kind, d, counted)
items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 }
needGap = false
end
local function flushPending()
if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end
end
local src = text:gsub('\r\n?', '\n')
for raw in (src .. '\n'):gmatch('([^\n]*)\n') do
local line = mw.text.trim(raw)
if line == '' then
flushPending()
if #items > 0 then needGap = true end
elseif line:sub(1, 1) == '@' then
local dd, bad = parseDirective(line)
flushPending()
if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end
if bad then flags.unknown = true end
else
local twoH, rowKind = info(cur)
local isFirst = (#items == 0 and not pending)
local t = parseLine(line, not twoH)
if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra)
else
local handled = false
if ropt.continued and isFirst then
local mode = ropt.which or 'auto'
if mode == 'auto' then
local c = state.carry
mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both'
end
if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true
elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end
end
if not handled then
if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil
else pending, pendingCur = t, cur end
end
end
end
end
if pending then
local _, rowKind = info(pendingCur)
if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true)
else push('mesra', makeMesra(pending, pendingCur, false), false) end
pending = nil
end
for i, it in ipairs(items) do
if it.counted and not (i == 1 and ropt.continued) then
applyAuto(it.d, ctx, true, it.kind ~= 'bayt')
end
end
local out = {}
for i, it in ipairs(items) do
if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end
local ro = {}
if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end
if i == #items and ropt.unfinished then ro.unfinished = true end
local h = emit(it.kind, it.d, ro, flags)
if h ~= SKIP and h ~= '' then out[#out + 1] = h end
end
return table.concat(out, '\n')
end
local function open(frame)
local args, flags = getArgs(frame), {}
local o = parseOpen(args, flags)
local isText = present(args, N.text)
local page = inPageNs()
if hasUnknown(args, 2) then flags.unknown = true end
local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both')
local ctx
if joins and state.ctx then ctx = state.ctx
else
ctx = newCtx(args, o.kind, flags)
ctx.fixed, ctx.just = o.fixed, o.just
end
local parts = {}
if not joins then
parts[#parts + 1] = stylesTag(frame) .. openHtml(o)
state.carry = nil
end
state.open, state.ctx = true, ctx
if isText then
local ropt = rowOpts(args, flags)
if o.newStanza and joins and not ropt.continued then
parts[#parts + 1] = plain(rowGap({ height = o.gap }))
end
local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags)
if rows ~= '' then parts[#parts + 1] = rows end
local stay = (not page) and (o.cont == 'next' or o.cont == 'both')
if not stay then
local tail = flushHtml()
if tail ~= '' then parts[#parts + 1] = tail end
parts[#parts + 1] = CLOSE_HTML
state.open, state.ctx = false, nil
else
parts[#parts + 1] = FINAL_MARKER
end
end
if #parts == 0 then parts[1] = SKIP end
return table.concat(parts, '\n') .. categories(flags)
end
local function close(frame)
local args, flags = getArgs(frame), {}
local cont = parseClose(args, flags)
if hasUnknown(args, 1) then flags.unknown = true end
local html = flushHtml()
if html ~= '' then html = html .. '\n' end
if cont and not inPageNs() then
return html .. FINAL_MARKER .. categories(flags)
end
state.open, state.ctx = false, nil
return html .. CLOSE_HTML .. categories(flags)
end
local function commonRow(args, ctx, flags)
local d = {
r = numberArg(args, N.right, ctx.digits),
c = numberArg(args, N.center, ctx.digits),
l = numberArg(args, N.left, ctx.digits),
style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id),
fixed = ctx.fixed, just = ctx.just,
}
local jv = pick(args, N.just)
if jv then
d.just = JUST[jv:lower()]
if not d.just then flags.unknown = true; d.just = ctx.just end
end
return d
end
local function bayt(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, false)
local html = emit('bayt', d, opt, flags)
return html .. categories(flags)
end
local function staggerFn(defaultIndent)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
local raw = pick(args, N.indent)
if raw then
d.indent = parseLength(raw)
if not d.indent then flags.unknown = true end
d.indent = d.indent or defaultIndent
else
d.indent = defaultIndent
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, true)
local html = emit('stagger', d, opt, flags)
return html .. categories(flags)
end
end
local function mesra(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.text = clean(args[1])
local a = pick(args, N.align)
if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end
local raw = pick(args, N.indent)
d.indent = parseLength(raw)
if raw and not d.indent then flags.unknown = true end
if hasUnknown(args, 1) then flags.unknown = true end
applyAuto(d, ctx, ctx.mesra and not opt.continued, true)
local html = emit('mesra', d, opt, flags)
return html .. categories(flags)
end
local function emptyFn(kind)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local d = commonRow(args, ctx, flags)
local raw = pick(args, N.height)
d.height = parseLength(raw)
if raw and not d.height then flags.unknown = true end
if hasUnknown(args, 0) then flags.unknown = true end
local html
if d.height then html = rowGap(d)
elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d)
else d.text, d.blank = '', true; html = rowMesra(d) end
return plain(html) .. categories(flags)
end
end
local function safe(fn)
return function(frame)
local ok, res = pcall(fn, frame)
if ok then return res end
return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>'
.. categories({ error = true })
end
end
p.open = safe(open)
p.close = safe(close)
p.bayt = safe(bayt)
p.stagger = safe(staggerFn(nil))
p.ladder = safe(staggerFn(nil))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
ihe5rqn9bnxuu6wzjes4y19ov8umbwt
304053
304050
2026-10-12T19:29:46Z
Hanooz
17889
304053
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
▸ عرض ستونها بهصورت inline روی هر سلول نوشته میشود، نه در CSS.
شبکهٔ ثابت ۷ستونی: ۵٪ | ۲۱٪ | ۲۱٪ | ۶٪ | ۲۱٪ | ۲۱٪ | ۵٪
هر مصراع (colspan=2) دقیقاً ۴۲٪ جدول است.
▸ بهاینترتیب حتی اگر شیوهنامه در فضای نام صفحه بارگذاری نشود یا
ProofreadPage تگ templatestyles را حذف کند، عرضها حفظ میشوند.
▸ ترازبندی پیشفرض «وسط». ترازبندی=پهن / لبه اختیاری.
▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت.
]==]
local p = {}
local ZWNJ = '\226\128\140'
-- عرضهای قطعی (درصد). مجموع = ۱۰۰.
local W_SIDE = '5%'
local W_MH = '21%'
local W_GAP = '6%'
local W_MISRA = '42%' -- 21 + 21
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '48em',
justDefault = { bayt = 'center', stagger = 'center' },
nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', "
.. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif",
mergeRows = false,
cats = {
unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر',
incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص',
error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر',
},
}
local N = {
right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' },
left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' },
class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' },
indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' },
height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' },
dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' },
numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' },
start = { 'شروع', 'start' }, side = { 'سمت', 'side' },
digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' },
unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' },
nastaliq = { 'نستعلیق', 'nastaliq' },
cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' },
continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' },
stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' },
maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' },
font = { 'قلم', 'font' },
just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' },
}
local KNOWN = {}
for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end
local ALIGN = {
['راست'] = 'right', right = 'right', r = 'right',
['چپ'] = 'left', left = 'left', l = 'left',
['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center',
}
local SIDE = { right = 'r', left = 'l', center = 'c' }
local KIND = {
['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt',
['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger',
['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder',
['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra',
}
local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa',
['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin',
['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' }
local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true,
yes=true, y=true, auto=true, ['true']=true, ['1']=true }
local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true }
local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch',
rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' }
local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev',
['بعد']='next', ['پسین']='next', next='next',
['هر دو']='both', ['هردو']='both', both='both' }
local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='',
['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ }
local WHICH = { ['هر دو']='both', ['هردو']='both', both='both',
['اول']='first', first='first', ['دوم']='second', second='second' }
local STANZA = { ['جدید']=true, new=true }
local JUST = {
['وسط']='center', ['مرکز']='center', center='center',
['راست']='right', right='right', ['چپ']='left', left='left',
['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify',
['لبه']='edge', edge='edge',
}
local ALIGN_STYLE = {
right = 'text-align:right;text-align-last:auto',
left = 'text-align:left;text-align-last:auto',
center = 'text-align:center;text-align-last:auto',
justify = 'text-align:justify;text-align-last:justify;'
.. '-moz-text-align-last:justify;-webkit-text-align-last:justify',
}
local KIND_KEYS = {}
for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end
table.sort(KIND_KEYS, function(a, b) return #a > #b end)
local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1,
lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 }
local state = { ctx = nil, open = false, carry = nil }
local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end
local SKIP = '<tr class="poem-skip"></tr>'
local CLOSE_HTML = '</table></div>'
local FINAL_MARKER =
'<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>'
local function esc(s) return (s:gsub('&','&'):gsub('"','"'):gsub('<','<'):gsub('>','>')) end
local function pick(args, names)
for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end
return nil
end
local function present(args, names)
for _, n in ipairs(names) do if args[n] ~= nil then return true end end
return false
end
local function bool(v, d)
if v == nil then return d end
v = v:lower()
if YES[v] then return true end
if NO[v] then return false end
return d
end
local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end
local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end
local function getArgs(frame)
local a = {}
local function merge(src)
if not src then return end
for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end
end
merge(frame.args)
local parent = frame:getParent()
if parent then merge(parent.args) end
return a
end
local function hasUnknown(args, pos)
for k, v in pairs(args) do
if v ~= '' then
if type(k) == 'number' then
if k < 1 or k > pos or k % 1 ~= 0 then return true end
elseif not KNOWN[k] then return true end
end
end
return false
end
local function nsId() return mw.title.getCurrentTitle().namespace end
local function inPageNs() return nsId() == cfg.pageNs end
local function tracked()
local ns = nsId()
return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828)
end
local function categories(flags)
if not flags or next(flags) == nil or not tracked() then return '' end
local out = {}
for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do
if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end
end
return table.concat(out)
end
local function langDir(lang)
local base, rest = lang:lower():match('^(%a+)(.*)$')
if not base then return nil end
if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end
if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end
return 'ltr'
end
local function stylesTag(frame)
local ok, tag = pcall(function()
return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } }
end)
return ok and tag or ''
end
local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' }
local function convertDigits(s, mode)
if s == '' or mode == 'keep' then return s end
if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end
if mode == 'latin' then
s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end)
s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end)
return s
end
s = s:gsub('%d', function(d) return FA[tonumber(d)] end)
s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end)
return s
end
local function numberArg(args, names, digits)
local v = pick(args, names); if not v then return '' end
return convertDigits(v, digits)
end
local function intArg(v, lo, hi)
local n = tonumber((convertDigits(v, 'latin')))
if not n or n ~= n or n < lo or n > hi then return nil end
return math.floor(n)
end
local function splitLength(v)
if not v or v == '' then return nil end
v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.')
local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$')
if not num then return nil end
num = tonumber(num)
if not num or num > 10000 then return nil end
unit = UNITS[unit:lower()]
if not unit then return nil end
return num, unit
end
local function parseLength(v)
local n, u = splitLength(v); if not n then return nil end
return string.format('%g', n) .. u
end
local function newCtx(args, kind, flags)
local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') }
local d = pick(args, N.digits)
if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end
local s = pick(args, N.start)
if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end
local sd = pick(args, N.side)
if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end
local m = pick(args, N.numbering)
if m then
local n = intArg(m, 0, 1e6)
if n then if n >= 1 then ctx.every = n end
else
local b = bool(m, nil)
if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end
end
end
return ctx
end
local function nextNumber(ctx)
if not ctx.every then return nil end
local n = ctx.start + ctx.count
ctx.count = ctx.count + 1
if n % ctx.every ~= 0 then return nil end
return convertDigits(string.format('%d', n), ctx.digits)
end
local function applyAuto(d, ctx, counted, noCenter)
if not counted then return end
local n = nextNumber(ctx)
if not n then return end
if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end
local side = ctx.side
if noCenter and side == 'c' then side = 'l' end
d[side] = n
end
local function joinStyle(...)
local parts = {}
for i = 1, select('#', ...) do
local v = select(i, ...)
if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end
end
return table.concat(parts, ';')
end
local function alignFor(j, which)
if not j or j == 'center' then return nil end
if j == 'edge' then
return (which == 1) and 'text-align:start;text-align-last:auto'
or 'text-align:end;text-align-last:auto'
end
return ALIGN_STYLE[j]
end
-- td با عرض قطعی: width همیشه در style اینلاین مینشیند.
local function td(class, colspan, style, content, width)
local s = '<td'
if class and class ~= '' then s = s .. ' class="' .. class .. '"' end
if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end
local full = style
if width then
if full and full ~= '' then
full = 'width:' .. width .. ';' .. full
else
full = 'width:' .. width
end
end
if full and full ~= '' then s = s .. ' style="' .. esc(full) .. '"' end
return s .. '>' .. (content or '') .. '</td>'
end
local function hemiCell(text)
return '<span class="beyt">' .. (text or '') .. '</span>'
end
local function rowOpen(d, kind, noId)
local cls = 'poem-row ' .. kind
if d.class then cls = cls .. ' ' .. d.class end
local id = ''
if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end
return '<tr class="' .. esc(cls) .. '"' .. id .. '>'
end
local function rowBayt(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.bayt
return rowOpen(d, 'poem-bayt')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE)
.. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1), W_MISRA)
.. td('poem-num poem-num-c', 1, d.style, d.c, W_GAP)
.. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2), W_MISRA)
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE)
.. '</tr>'
end
local function rowStagger(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.stagger
local s1 = joinStyle(alignFor(j, 1), d.style)
local s2 = joinStyle(alignFor(j, 2), d.style)
local ind = d.indent
if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then
s1 = joinStyle('padding-inline-end:' .. ind, s1)
s2 = joinStyle('padding-inline-start:' .. ind, s2)
end
-- ردیف ۱: side | misra1 | gap | empty | empty
-- ردیف ۲: empty | empty | gap | misra2 | side
return rowOpen(d, 'poem-stagger poem-stagger-1')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE)
.. td('b poem-m poem-m1', 2, s1, hemiCell(m1), W_MISRA)
.. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP)
.. td('poem-empty', 2, nil, '', W_MISRA)
.. td('poem-num poem-num-l poem-empty', 1, nil, '', W_SIDE)
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num poem-num-r poem-empty', 1, nil, '', W_SIDE)
.. td('poem-empty', 2, nil, '', W_MISRA)
.. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP)
.. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2), W_MISRA)
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE)
.. '</tr>'
end
local function rowMesra(d)
local align = d.align or 'center'
local content = d.text
if d.blank and content == '' then content = cfg.nbsp end
local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil
local style = joinStyle(inline, d.style)
local cellClass = 'b poem-m'
local cs = { 2, 3, 2 }
if align ~= 'center' then
cs = { 1, 5, 1 }
cellClass = cellClass .. ' poem-m-' .. align
if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end
end
local cell
if d.blank and content == cfg.nbsp then cell = content
else cell = hemiCell(content) end
local w1 = (cs[1] == 1) and W_SIDE or W_MISRA
local w2 = (cs[2] == 3) and W_MISRA or (W_SIDE .. '+') -- never used; simplified below
-- ساده: اگر cs = {2,3,2} باشد ⇒ side=42%? نه، side=5%+? مجموع = 7 ستون
-- cs={2,3,2}: 5+21 / 21+6+21 / 21+5 ⇒ 26% / 68% / 26%
-- cs={1,5,1}: 5 / 5+21+21+6+21+21 / 5 ⇒ 5% / 95% / 5%
local wLeft = (cs[1] == 1) and W_SIDE or '26%'
local wMid = (cs[2] == 3) and '48%' or '90%' -- approximation; browser resolves under fixed
local wRight = (cs[3] == 1) and W_SIDE or '26%'
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r, wLeft)
.. td(cellClass, cs[2], style, cell, wMid)
.. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l, wRight)
.. '</tr>'
end
local function rowGap(d)
local style = d.height and ('height:' .. d.height) or nil
local content = d.height and '' or cfg.nbsp
return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">'
.. td('poem-gapcell', 7, style, content, '100%') .. '</tr>'
end
local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra }
-- سایزر با عرضهای inline روی هر سلول.
local function sizerRow()
return '<tr class="poem-sizer" aria-hidden="true">'
.. td('poem-s', 1, nil, '', W_SIDE)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-g', 1, nil, '', W_GAP)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-s', 1, nil, '', W_SIDE)
.. '</tr>'
end
local function openHtml(o)
local cls = 'poem' .. (o.fixed and ' poem-fixed' or '')
if o.class then cls = cls .. ' ' .. o.class end
local div = '<div class="' .. esc(cls) .. '"'
if o.lang then div = div .. ' lang="' .. o.lang .. '"' end
if o.dir then div = div .. ' dir="' .. o.dir .. '"' end
local font = o.font and ('font-family:' .. o.font) or nil
local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '')
if o.fixed then
-- عرض قطعی روی خود جدول، به em، نه درصد. جدول مستقل از والد
-- دارای عرض معیّن میشود و table-layout: fixed روی عدد ثابت کار میکند.
-- max-width: 100% برای صفحهٔ باریک نگه داشته میشود.
local absWidth = o.absWidth or '42em'
local tblStyle = 'width:' .. absWidth ..
';max-width:100%;table-layout:fixed;margin:0 auto;border-collapse:collapse'
if font then tblStyle = tblStyle .. ';' .. font end
if o.style then tblStyle = tblStyle .. ';' .. o.style end
local wrapperStyle = joinStyle(
'width:100%',
o.maxWidth and o.maxWidth ~= 'none' and ('max-width:' .. o.maxWidth) or nil,
'margin:0 auto',
font,
o.style
)
-- هر دو لایه: بیرونی برای مرکزکردن و سقف عرض، داخلی با عرض em
return div .. ' style="' .. esc(wrapperStyle) .. '">'
.. '<table class="' .. tcls .. '" role="presentation" style="'
.. esc(tblStyle) .. '">' .. sizerRow()
end
local style = 'margin: 0 auto;'
if font then style = style .. font .. ';' end
if o.style then style = style .. ' ' .. o.style end
return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">'
end
---------------------------------------------------------------------------
-- چندصفحهای
---------------------------------------------------------------------------
local function joinText(a, b, sep)
if a == '' then return b end
if b == '' then return a end
return a .. sep .. b
end
local function mergeRow(head, tail, sep)
for _, f in ipairs({ 'm1', 'm2', 'text' }) do
if head[f] ~= nil or tail[f] ~= nil then
head[f] = joinText(head[f] or '', tail[f] or '', sep)
end
end
for _, f in ipairs({ 'r', 'c', 'l' }) do
if tail[f] and tail[f] ~= '' then head[f] = tail[f] end
end
head.style = head.style or tail.style
head.class = head.class or tail.class
head.id = head.id or tail.id
return head
end
local function flushHtml()
local c = state.carry
if not c then return '' end
state.carry = nil
return RENDER[c.kind](c.d)
end
local function plain(html)
if not inPageNs() then
local pre = flushHtml()
if pre ~= '' then return pre .. '\n' .. html end
end
return html
end
local function emit(kind, d, opt, flags)
opt = opt or {}
if inPageNs() then
if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end
if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end
return RENDER[kind](d)
end
if not cfg.mergeRows then return RENDER[kind](d) end
local out = ''
local carry = state.carry
state.carry = nil
if carry then
if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ')
else
out = RENDER[carry.kind](carry.d)
if opt.continued then flags.unknown = true end
end
end
if opt.unfinished then
state.carry = { kind = kind, d = d }
return out ~= '' and out or SKIP
end
local html = RENDER[kind](d)
if out ~= '' then return out .. '\n' .. html end
return html
end
local function rowOpts(args, flags)
local opt = {}
local u = pick(args, N.unfinished)
if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end
local c = pick(args, N.continued)
if c then
local w = WHICH[c:lower()]
if w then opt.continued, opt.which = true, w
else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end
end
local j = pick(args, N.joiner)
if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end
return opt
end
local function parseOpen(args, flags)
local o = {}
local isText = present(args, N.text)
for i = 1, 2 do
local v = args[i]
if v and v ~= '' then
local lv = v:lower()
if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true
elseif v == 'ادامه' or lv == 'continue' then
if isText then flags.unknown = true else o.cont = 'prev' end
else flags.unknown = true end
end
end
if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k then
if not isText and k == 'next' then flags.unknown = true; k = nil end
else
local b = bool(c, nil)
if b == nil then flags.unknown = true
elseif b then if isText then flags.unknown = true else k = 'prev' end end
end
if k then o.cont = k end
end
o.style = pick(args, N.style); o.class = pick(args, N.class)
local lang = pick(args, N.lang)
if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end
local dir = pick(args, N.dir)
if dir then
dir = dir:lower()
if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end
end
if o.lang and not o.dir then o.dir = langDir(o.lang) end
o.kind = 'bayt'
local k = pick(args, N.kind)
if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end
local a = pick(args, N.align)
if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end
local g = pick(args, N.gap)
if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end
local ind = pick(args, N.indent)
if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end
o.unitNum, o.unitUnit = splitLength(pick(args, N.unit))
if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end
local st = pick(args, N.stanza)
if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end
local jv = pick(args, N.just)
if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end
if o.nastaliq then o.font = cfg.nastaliqFonts end
local f = pick(args, N.font)
if f then
if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts
elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f
else flags.unknown = true end
end
local w = pick(args, N.width)
if w then
local lw = w:lower()
if lw == 'خودکار' or lw == 'auto' then o.fixed = false
else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end
end
local mx = pick(args, N.maxwidth)
if mx then
local lm = mx:lower()
if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none'
else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end
end
if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end
if o.fixed then
local explicit = (o.width ~= nil)
o.width = o.width or cfg.defaultWidth
o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth)
end
return o
end
local function parseClose(args, flags)
local cont = false
local v = args[1]
if v and v ~= '' then
if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end
end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k == 'next' or k == 'both' then cont = true
elseif k == 'prev' then flags.unknown = true
else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end
end
return cont
end
local function parseLine(raw, freeVerse)
local t = {}
local s = raw
if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end
if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3)
elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end
s = mw.text.trim(s)
local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$')
if pre then t.pre = pre; s = rest end
local body, post = s:match('^(.-)%s*>>>%s*(.+)$')
if body then t.post = post; s = body end
t.text = s
return t
end
local function parseDirective(line)
local rest = mw.text.trim(line:sub(2))
local lrest = rest:lower()
for _, key in ipairs(KIND_KEYS) do
if lrest:sub(1, #key) == key then
local after = rest:sub(#key + 1)
if after == '' or after:match('^%s') then
local dd, bad = { kind = KIND[key] }, false
for k, v in after:gmatch('(%S+)=(%S+)') do
local kk, lv = k:lower(), v:lower()
if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end
elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end
elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end
else bad = true end
end
return dd, bad
end
end
end
return nil, true
end
local function renderLines(text, o, ctx, ropt, flags)
local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just }
local items, pending, pendingCur, needGap = {}, nil, nil, false
local function info(c)
local twoH = (c.kind ~= 'mesra')
local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger')
return twoH, rowKind, c.indent
end
local function numbers(d, t)
if not t then return end
if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end
if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end
end
local function makeBayt(t1, t2, c)
local _, _, indent = info(c)
local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '',
r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just }
numbers(d, t1); numbers(d, t2)
return d
end
local function makeMesra(t, c, free)
local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed }
d.align = t.align or c.align or (free and 'right' or 'center')
if free and (t.level or 0) > 0 and d.align ~= 'center' then
d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit
end
numbers(d, t)
return d
end
local function push(kind, d, counted)
items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 }
needGap = false
end
local function flushPending()
if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end
end
local src = text:gsub('\r\n?', '\n')
for raw in (src .. '\n'):gmatch('([^\n]*)\n') do
local line = mw.text.trim(raw)
if line == '' then
flushPending()
if #items > 0 then needGap = true end
elseif line:sub(1, 1) == '@' then
local dd, bad = parseDirective(line)
flushPending()
if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end
if bad then flags.unknown = true end
else
local twoH, rowKind = info(cur)
local isFirst = (#items == 0 and not pending)
local t = parseLine(line, not twoH)
if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra)
else
local handled = false
if ropt.continued and isFirst then
local mode = ropt.which or 'auto'
if mode == 'auto' then
local c = state.carry
mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both'
end
if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true
elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end
end
if not handled then
if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil
else pending, pendingCur = t, cur end
end
end
end
end
if pending then
local _, rowKind = info(pendingCur)
if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true)
else push('mesra', makeMesra(pending, pendingCur, false), false) end
pending = nil
end
for i, it in ipairs(items) do
if it.counted and not (i == 1 and ropt.continued) then
applyAuto(it.d, ctx, true, it.kind ~= 'bayt')
end
end
local out = {}
for i, it in ipairs(items) do
if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end
local ro = {}
if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end
if i == #items and ropt.unfinished then ro.unfinished = true end
local h = emit(it.kind, it.d, ro, flags)
if h ~= SKIP and h ~= '' then out[#out + 1] = h end
end
return table.concat(out, '\n')
end
local function open(frame)
local args, flags = getArgs(frame), {}
local o = parseOpen(args, flags)
local isText = present(args, N.text)
local page = inPageNs()
if hasUnknown(args, 2) then flags.unknown = true end
local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both')
local ctx
if joins and state.ctx then ctx = state.ctx
else
ctx = newCtx(args, o.kind, flags)
ctx.fixed, ctx.just = o.fixed, o.just
end
local parts = {}
if not joins then
parts[#parts + 1] = stylesTag(frame) .. openHtml(o)
state.carry = nil
end
state.open, state.ctx = true, ctx
if isText then
local ropt = rowOpts(args, flags)
if o.newStanza and joins and not ropt.continued then
parts[#parts + 1] = plain(rowGap({ height = o.gap }))
end
local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags)
if rows ~= '' then parts[#parts + 1] = rows end
local stay = (not page) and (o.cont == 'next' or o.cont == 'both')
if not stay then
local tail = flushHtml()
if tail ~= '' then parts[#parts + 1] = tail end
parts[#parts + 1] = CLOSE_HTML
state.open, state.ctx = false, nil
else
parts[#parts + 1] = FINAL_MARKER
end
end
if #parts == 0 then parts[1] = SKIP end
return table.concat(parts, '\n') .. categories(flags)
end
local function close(frame)
local args, flags = getArgs(frame), {}
local cont = parseClose(args, flags)
if hasUnknown(args, 1) then flags.unknown = true end
local html = flushHtml()
if html ~= '' then html = html .. '\n' end
if cont and not inPageNs() then
return html .. FINAL_MARKER .. categories(flags)
end
state.open, state.ctx = false, nil
return html .. CLOSE_HTML .. categories(flags)
end
local function commonRow(args, ctx, flags)
local d = {
r = numberArg(args, N.right, ctx.digits),
c = numberArg(args, N.center, ctx.digits),
l = numberArg(args, N.left, ctx.digits),
style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id),
fixed = ctx.fixed, just = ctx.just,
}
local jv = pick(args, N.just)
if jv then
d.just = JUST[jv:lower()]
if not d.just then flags.unknown = true; d.just = ctx.just end
end
return d
end
local function bayt(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, false)
local html = emit('bayt', d, opt, flags)
return html .. categories(flags)
end
local function staggerFn(defaultIndent)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
local raw = pick(args, N.indent)
if raw then
d.indent = parseLength(raw)
if not d.indent then flags.unknown = true end
d.indent = d.indent or defaultIndent
else
d.indent = defaultIndent
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, true)
local html = emit('stagger', d, opt, flags)
return html .. categories(flags)
end
end
local function mesra(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.text = clean(args[1])
local a = pick(args, N.align)
if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end
local raw = pick(args, N.indent)
d.indent = parseLength(raw)
if raw and not d.indent then flags.unknown = true end
if hasUnknown(args, 1) then flags.unknown = true end
applyAuto(d, ctx, ctx.mesra and not opt.continued, true)
local html = emit('mesra', d, opt, flags)
return html .. categories(flags)
end
local function emptyFn(kind)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local d = commonRow(args, ctx, flags)
local raw = pick(args, N.height)
d.height = parseLength(raw)
if raw and not d.height then flags.unknown = true end
if hasUnknown(args, 0) then flags.unknown = true end
local html
if d.height then html = rowGap(d)
elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d)
else d.text, d.blank = '', true; html = rowMesra(d) end
return plain(html) .. categories(flags)
end
end
local function safe(fn)
return function(frame)
local ok, res = pcall(fn, frame)
if ok then return res end
return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>'
.. categories({ error = true })
end
end
p.open = safe(open)
p.close = safe(close)
p.bayt = safe(bayt)
p.stagger = safe(staggerFn(nil))
p.ladder = safe(staggerFn(nil))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
p1mmb8g2rb5ijgk92jk7kd476crpa9f
304054
304053
2026-10-12T19:54:48Z
Hanooz
17889
304054
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
▸ جدول با عرض قطعی em؛ مصون از والدِ shrink-to-fit در فضای نام صفحه.
▸ عرض ستونها با CSS variable: پیشفرض «با شماره» = مصراع ۴۲٪.
اگر جدول هیچ شمارهای نداشته باشد، CSS بهکمک :has() متغیرها را
بازنویسی میکند و مصراع به ۴۵٪ میرسد. تمام CSS، بدون state.
▸ شمارههای خودکار با شمارهگذاری=…؛ شمارههای دستی با ش=، شو=، شچ=.
▸ ارجاعهای <ref> در متن inline میمانند.
▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت.
]==]
local p = {}
local ZWNJ = '\226\128\140'
-- عرضها با CSS variable. مقدار پیشفرض = «با شماره».
-- مقدار «بدون شماره» در styles.css تعیین میشود.
local W_SIDE = 'var(--poem-side, 5%)'
local W_MH = 'var(--poem-mh, 21%)'
local W_GAP = 'var(--poem-gap, 6%)'
local W_MISRA = 'var(--poem-misra, 42%)'
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '42em', -- عرض قطعی جدول
maxWidth = '48em',
justDefault = { bayt = 'center', stagger = 'center' },
nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', "
.. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif",
mergeRows = false,
cats = {
unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر',
incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص',
error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر',
},
}
local N = {
right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' },
left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' },
class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' },
indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' },
height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' },
dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' },
numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' },
start = { 'شروع', 'start' }, side = { 'سمت', 'side' },
digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' },
unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' },
nastaliq = { 'نستعلیق', 'nastaliq' },
cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' },
continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' },
stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' },
maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' },
font = { 'قلم', 'font' },
just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' },
}
local KNOWN = {}
for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end
local ALIGN = {
['راست'] = 'right', right = 'right', r = 'right',
['چپ'] = 'left', left = 'left', l = 'left',
['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center',
}
local SIDE = { right = 'r', left = 'l', center = 'c' }
local KIND = {
['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt',
['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger',
['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder',
['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra',
}
local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa',
['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin',
['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' }
local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true,
yes=true, y=true, auto=true, ['true']=true, ['1']=true }
local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true }
local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch',
rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' }
local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev',
['بعد']='next', ['پسین']='next', next='next',
['هر دو']='both', ['هردو']='both', both='both' }
local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='',
['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ }
local WHICH = { ['هر دو']='both', ['هردو']='both', both='both',
['اول']='first', first='first', ['دوم']='second', second='second' }
local STANZA = { ['جدید']=true, new=true }
local JUST = {
['وسط']='center', ['مرکز']='center', center='center',
['راست']='right', right='right', ['چپ']='left', left='left',
['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify',
['لبه']='edge', edge='edge',
}
local ALIGN_STYLE = {
right = 'text-align:right;text-align-last:auto',
left = 'text-align:left;text-align-last:auto',
center = 'text-align:center;text-align-last:auto',
justify = 'text-align:justify;text-align-last:justify;'
.. '-moz-text-align-last:justify;-webkit-text-align-last:justify',
}
local KIND_KEYS = {}
for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end
table.sort(KIND_KEYS, function(a, b) return #a > #b end)
local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1,
lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 }
local state = { ctx = nil, open = false, carry = nil }
local function defaultCtx() return { digits = 'fa', fixed = true } end
local SKIP = '<tr class="poem-skip"></tr>'
local CLOSE_HTML = '</table></div>'
local FINAL_MARKER =
'<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>'
local function esc(s) return (s:gsub('&','&'):gsub('"','"'):gsub('<','<'):gsub('>','>')) end
local function pick(args, names)
for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end
return nil
end
local function present(args, names)
for _, n in ipairs(names) do if args[n] ~= nil then return true end end
return false
end
local function bool(v, d)
if v == nil then return d end
v = v:lower()
if YES[v] then return true end
if NO[v] then return false end
return d
end
local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end
local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end
local function getArgs(frame)
local a = {}
local function merge(src)
if not src then return end
for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end
end
merge(frame.args)
local parent = frame:getParent()
if parent then merge(parent.args) end
return a
end
local function hasUnknown(args, pos)
for k, v in pairs(args) do
if v ~= '' then
if type(k) == 'number' then
if k < 1 or k > pos or k % 1 ~= 0 then return true end
elseif not KNOWN[k] then return true end
end
end
return false
end
local function nsId() return mw.title.getCurrentTitle().namespace end
local function inPageNs() return nsId() == cfg.pageNs end
local function tracked()
local ns = nsId()
return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828)
end
local function categories(flags)
if not flags or next(flags) == nil or not tracked() then return '' end
local out = {}
for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do
if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end
end
return table.concat(out)
end
local function langDir(lang)
local base, rest = lang:lower():match('^(%a+)(.*)$')
if not base then return nil end
if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end
if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end
return 'ltr'
end
local function stylesTag(frame)
local ok, tag = pcall(function()
return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } }
end)
return ok and tag or ''
end
local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' }
local function convertDigits(s, mode)
if s == '' or mode == 'keep' then return s end
if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end
if mode == 'latin' then
s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end)
s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end)
return s
end
s = s:gsub('%d', function(d) return FA[tonumber(d)] end)
s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end)
return s
end
local function numberArg(args, names, digits)
local v = pick(args, names); if not v then return '' end
return convertDigits(v, digits)
end
local function intArg(v, lo, hi)
local n = tonumber((convertDigits(v, 'latin')))
if not n or n ~= n or n < lo or n > hi then return nil end
return math.floor(n)
end
local function splitLength(v)
if not v or v == '' then return nil end
v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.')
local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$')
if not num then return nil end
num = tonumber(num)
if not num or num > 10000 then return nil end
unit = UNITS[unit:lower()]
if not unit then return nil end
return num, unit
end
local function parseLength(v)
local n, u = splitLength(v); if not n then return nil end
return string.format('%g', n) .. u
end
local function newCtx(args, kind, flags)
local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') }
local d = pick(args, N.digits)
if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end
local s = pick(args, N.start)
if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end
local sd = pick(args, N.side)
if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end
local m = pick(args, N.numbering)
if m then
local n = intArg(m, 0, 1e6)
if n then if n >= 1 then ctx.every = n end
else
local b = bool(m, nil)
if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end
end
end
return ctx
end
local function nextNumber(ctx)
if not ctx.every then return nil end
local n = ctx.start + ctx.count
ctx.count = ctx.count + 1
if n % ctx.every ~= 0 then return nil end
return convertDigits(string.format('%d', n), ctx.digits)
end
local function applyAuto(d, ctx, counted, noCenter)
if not counted then return end
local n = nextNumber(ctx)
if not n then return end
if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end
local side = ctx.side
if noCenter and side == 'c' then side = 'l' end
d[side] = n
end
local function joinStyle(...)
local parts = {}
for i = 1, select('#', ...) do
local v = select(i, ...)
if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end
end
return table.concat(parts, ';')
end
local function alignFor(j, which)
if not j or j == 'center' then return nil end
if j == 'edge' then
return (which == 1) and 'text-align:start;text-align-last:auto'
or 'text-align:end;text-align-last:auto'
end
return ALIGN_STYLE[j]
end
local function td(class, colspan, style, content, width)
local s = '<td'
if class and class ~= '' then s = s .. ' class="' .. class .. '"' end
if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end
local full = style
if width then
if full and full ~= '' then full = 'width:' .. width .. ';' .. full
else full = 'width:' .. width end
end
if full and full ~= '' then s = s .. ' style="' .. esc(full) .. '"' end
return s .. '>' .. (content or '') .. '</td>'
end
local function hemiCell(text)
return '<span class="beyt">' .. (text or '') .. '</span>'
end
local function rowOpen(d, kind, noId)
local cls = 'poem-row ' .. kind
if d.class then cls = cls .. ' ' .. d.class end
local id = ''
if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end
return '<tr class="' .. esc(cls) .. '"' .. id .. '>'
end
local function rowBayt(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.bayt
return rowOpen(d, 'poem-bayt')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE)
.. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1), W_MISRA)
.. td('poem-num poem-num-c', 1, d.style, d.c, W_GAP)
.. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2), W_MISRA)
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE)
.. '</tr>'
end
local function rowStagger(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.stagger
local s1 = joinStyle(alignFor(j, 1), d.style)
local s2 = joinStyle(alignFor(j, 2), d.style)
local ind = d.indent
if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then
s1 = joinStyle('padding-inline-end:' .. ind, s1)
s2 = joinStyle('padding-inline-start:' .. ind, s2)
end
return rowOpen(d, 'poem-stagger poem-stagger-1')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE)
.. td('b poem-m poem-m1', 2, s1, hemiCell(m1), W_MISRA)
.. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP)
.. td('poem-empty', 2, nil, '', W_MISRA)
.. td('poem-num poem-num-l poem-empty', 1, nil, '', W_SIDE)
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num poem-num-r poem-empty', 1, nil, '', W_SIDE)
.. td('poem-empty', 2, nil, '', W_MISRA)
.. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP)
.. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2), W_MISRA)
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE)
.. '</tr>'
end
local function rowMesra(d)
local align = d.align or 'center'
local content = d.text
if d.blank and content == '' then content = cfg.nbsp end
local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil
local style = joinStyle(inline, d.style)
local cellClass = 'b poem-m'
local cs = { 2, 3, 2 }
if align ~= 'center' then
cs = { 1, 5, 1 }
cellClass = cellClass .. ' poem-m-' .. align
if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end
end
local cell
if d.blank and content == cfg.nbsp then cell = content
else cell = hemiCell(content) end
-- عرضهای تقریبی روی colspan ها (در table-layout: fixed فقط ردیف
-- سایزر تعیینکننده است؛ اینها فقط جهت سازگاری بیشترند)
local wLeft = (cs[1] == 1) and W_SIDE or '26%'
local wMid = (cs[2] == 3) and '48%' or '90%'
local wRight = (cs[3] == 1) and W_SIDE or '26%'
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r, wLeft)
.. td(cellClass, cs[2], style, cell, wMid)
.. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l, wRight)
.. '</tr>'
end
local function rowGap(d)
local style = d.height and ('height:' .. d.height) or nil
local content = d.height and '' or cfg.nbsp
return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">'
.. td('poem-gapcell', 7, style, content, '100%') .. '</tr>'
end
local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra }
-- ردیف سایزر با عرضهای متغیر.
local function sizerRow()
return '<tr class="poem-sizer" aria-hidden="true">'
.. td('poem-s', 1, nil, '', W_SIDE)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-g', 1, nil, '', W_GAP)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-s', 1, nil, '', W_SIDE)
.. '</tr>'
end
local function openHtml(o)
local cls = 'poem' .. (o.fixed and ' poem-fixed' or '')
if o.class then cls = cls .. ' ' .. o.class end
local div = '<div class="' .. esc(cls) .. '"'
if o.lang then div = div .. ' lang="' .. o.lang .. '"' end
if o.dir then div = div .. ' dir="' .. o.dir .. '"' end
local font = o.font and ('font-family:' .. o.font) or nil
local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '')
if o.fixed then
-- عرض قطعی em روی خود جدول؛ مصون از والدِ shrink-to-fit.
local absWidth = o.width or cfg.defaultWidth
local tblStyle = 'width:' .. absWidth ..
';max-width:100%;table-layout:fixed;margin:0 auto;border-collapse:collapse'
if font then tblStyle = tblStyle .. ';' .. font end
if o.style then tblStyle = tblStyle .. ';' .. o.style end
local wrapperStyle = joinStyle(
'width:100%',
o.maxWidth and o.maxWidth ~= 'none' and ('max-width:' .. o.maxWidth) or nil,
'margin:0 auto'
)
return div .. ' style="' .. esc(wrapperStyle) .. '">'
.. '<table class="' .. tcls .. '" role="presentation" style="'
.. esc(tblStyle) .. '">' .. sizerRow()
end
local style = 'margin: 0 auto;'
if font then style = style .. font .. ';' end
if o.style then style = style .. ' ' .. o.style end
return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">'
end
---------------------------------------------------------------------------
-- چندصفحهای
---------------------------------------------------------------------------
local function joinText(a, b, sep)
if a == '' then return b end
if b == '' then return a end
return a .. sep .. b
end
local function mergeRow(head, tail, sep)
for _, f in ipairs({ 'm1', 'm2', 'text' }) do
if head[f] ~= nil or tail[f] ~= nil then
head[f] = joinText(head[f] or '', tail[f] or '', sep)
end
end
for _, f in ipairs({ 'r', 'c', 'l' }) do
if tail[f] and tail[f] ~= '' then head[f] = tail[f] end
end
head.style = head.style or tail.style
head.class = head.class or tail.class
head.id = head.id or tail.id
return head
end
local function flushHtml()
local c = state.carry
if not c then return '' end
state.carry = nil
return RENDER[c.kind](c.d)
end
local function plain(html)
if not inPageNs() then
local pre = flushHtml()
if pre ~= '' then return pre .. '\n' .. html end
end
return html
end
local function emit(kind, d, opt, flags)
opt = opt or {}
if inPageNs() then
if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end
if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end
return RENDER[kind](d)
end
if not cfg.mergeRows then return RENDER[kind](d) end
local out = ''
local carry = state.carry
state.carry = nil
if carry then
if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ')
else
out = RENDER[carry.kind](carry.d)
if opt.continued then flags.unknown = true end
end
end
if opt.unfinished then
state.carry = { kind = kind, d = d }
return out ~= '' and out or SKIP
end
local html = RENDER[kind](d)
if out ~= '' then return out .. '\n' .. html end
return html
end
local function rowOpts(args, flags)
local opt = {}
local u = pick(args, N.unfinished)
if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end
local c = pick(args, N.continued)
if c then
local w = WHICH[c:lower()]
if w then opt.continued, opt.which = true, w
else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end
end
local j = pick(args, N.joiner)
if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end
return opt
end
local function parseOpen(args, flags)
local o = {}
local isText = present(args, N.text)
for i = 1, 2 do
local v = args[i]
if v and v ~= '' then
local lv = v:lower()
if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true
elseif v == 'ادامه' or lv == 'continue' then
if isText then flags.unknown = true else o.cont = 'prev' end
else flags.unknown = true end
end
end
if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k then
if not isText and k == 'next' then flags.unknown = true; k = nil end
else
local b = bool(c, nil)
if b == nil then flags.unknown = true
elseif b then if isText then flags.unknown = true else k = 'prev' end end
end
if k then o.cont = k end
end
o.style = pick(args, N.style); o.class = pick(args, N.class)
local lang = pick(args, N.lang)
if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end
local dir = pick(args, N.dir)
if dir then
dir = dir:lower()
if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end
end
if o.lang and not o.dir then o.dir = langDir(o.lang) end
o.kind = 'bayt'
local k = pick(args, N.kind)
if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end
local a = pick(args, N.align)
if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end
local g = pick(args, N.gap)
if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end
local ind = pick(args, N.indent)
if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end
o.unitNum, o.unitUnit = splitLength(pick(args, N.unit))
if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end
local st = pick(args, N.stanza)
if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end
local jv = pick(args, N.just)
if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end
if o.nastaliq then o.font = cfg.nastaliqFonts end
local f = pick(args, N.font)
if f then
if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts
elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f
else flags.unknown = true end
end
local w = pick(args, N.width)
if w then
local lw = w:lower()
if lw == 'خودکار' or lw == 'auto' then o.fixed = false
else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end
end
local mx = pick(args, N.maxwidth)
if mx then
local lm = mx:lower()
if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none'
else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end
end
if o.fixed == nil then o.fixed = true end
if o.fixed then
o.width = o.width or cfg.defaultWidth
o.maxWidth = o.maxWidth or cfg.maxWidth
end
return o
end
local function parseClose(args, flags)
local cont = false
local v = args[1]
if v and v ~= '' then
if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end
end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k == 'next' or k == 'both' then cont = true
elseif k == 'prev' then flags.unknown = true
else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end
end
return cont
end
local function parseLine(raw, freeVerse)
local t = {}
local s = raw
if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end
if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3)
elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end
s = mw.text.trim(s)
local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$')
if pre then t.pre = pre; s = rest end
local body, post = s:match('^(.-)%s*>>>%s*(.+)$')
if body then t.post = post; s = body end
t.text = s
return t
end
local function parseDirective(line)
local rest = mw.text.trim(line:sub(2))
local lrest = rest:lower()
for _, key in ipairs(KIND_KEYS) do
if lrest:sub(1, #key) == key then
local after = rest:sub(#key + 1)
if after == '' or after:match('^%s') then
local dd, bad = { kind = KIND[key] }, false
for k, v in after:gmatch('(%S+)=(%S+)') do
local kk, lv = k:lower(), v:lower()
if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end
elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end
elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end
else bad = true end
end
return dd, bad
end
end
end
return nil, true
end
local function renderLines(text, o, ctx, ropt, flags)
local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just }
local items, pending, pendingCur, needGap = {}, nil, nil, false
local function info(c)
local twoH = (c.kind ~= 'mesra')
local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger')
return twoH, rowKind, c.indent
end
local function numbers(d, t)
if not t then return end
if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end
if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end
end
local function makeBayt(t1, t2, c)
local _, _, indent = info(c)
local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '',
r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just }
numbers(d, t1); numbers(d, t2)
return d
end
local function makeMesra(t, c, free)
local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed }
d.align = t.align or c.align or (free and 'right' or 'center')
if free and (t.level or 0) > 0 and d.align ~= 'center' then
d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit
end
numbers(d, t)
return d
end
local function push(kind, d, counted)
items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 }
needGap = false
end
local function flushPending()
if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end
end
local src = text:gsub('\r\n?', '\n')
for raw in (src .. '\n'):gmatch('([^\n]*)\n') do
local line = mw.text.trim(raw)
if line == '' then
flushPending()
if #items > 0 then needGap = true end
elseif line:sub(1, 1) == '@' then
local dd, bad = parseDirective(line)
flushPending()
if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end
if bad then flags.unknown = true end
else
local twoH, rowKind = info(cur)
local isFirst = (#items == 0 and not pending)
local t = parseLine(line, not twoH)
if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra)
else
local handled = false
if ropt.continued and isFirst then
local mode = ropt.which or 'auto'
if mode == 'auto' then
local c = state.carry
mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both'
end
if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true
elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end
end
if not handled then
if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil
else pending, pendingCur = t, cur end
end
end
end
end
if pending then
local _, rowKind = info(pendingCur)
if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true)
else push('mesra', makeMesra(pending, pendingCur, false), false) end
pending = nil
end
for i, it in ipairs(items) do
if it.counted and not (i == 1 and ropt.continued) then
applyAuto(it.d, ctx, true, it.kind ~= 'bayt')
end
end
local out = {}
for i, it in ipairs(items) do
if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end
local ro = {}
if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end
if i == #items and ropt.unfinished then ro.unfinished = true end
local h = emit(it.kind, it.d, ro, flags)
if h ~= SKIP and h ~= '' then out[#out + 1] = h end
end
return table.concat(out, '\n')
end
local function open(frame)
local args, flags = getArgs(frame), {}
local o = parseOpen(args, flags)
local isText = present(args, N.text)
local page = inPageNs()
if hasUnknown(args, 2) then flags.unknown = true end
local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both')
local ctx
if joins and state.ctx then ctx = state.ctx
else
ctx = newCtx(args, o.kind, flags)
ctx.fixed, ctx.just = o.fixed, o.just
end
local parts = {}
if not joins then
parts[#parts + 1] = stylesTag(frame) .. openHtml(o)
state.carry = nil
end
state.open, state.ctx = true, ctx
if isText then
local ropt = rowOpts(args, flags)
if o.newStanza and joins and not ropt.continued then
parts[#parts + 1] = plain(rowGap({ height = o.gap }))
end
local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags)
if rows ~= '' then parts[#parts + 1] = rows end
local stay = (not page) and (o.cont == 'next' or o.cont == 'both')
if not stay then
local tail = flushHtml()
if tail ~= '' then parts[#parts + 1] = tail end
parts[#parts + 1] = CLOSE_HTML
state.open, state.ctx = false, nil
else
parts[#parts + 1] = FINAL_MARKER
end
end
if #parts == 0 then parts[1] = SKIP end
return table.concat(parts, '\n') .. categories(flags)
end
local function close(frame)
local args, flags = getArgs(frame), {}
local cont = parseClose(args, flags)
if hasUnknown(args, 1) then flags.unknown = true end
local html = flushHtml()
if html ~= '' then html = html .. '\n' end
if cont and not inPageNs() then
return html .. FINAL_MARKER .. categories(flags)
end
state.open, state.ctx = false, nil
return html .. CLOSE_HTML .. categories(flags)
end
local function commonRow(args, ctx, flags)
local d = {
r = numberArg(args, N.right, ctx.digits),
c = numberArg(args, N.center, ctx.digits),
l = numberArg(args, N.left, ctx.digits),
style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id),
fixed = ctx.fixed, just = ctx.just,
}
local jv = pick(args, N.just)
if jv then
d.just = JUST[jv:lower()]
if not d.just then flags.unknown = true; d.just = ctx.just end
end
return d
end
local function bayt(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, false)
local html = emit('bayt', d, opt, flags)
return html .. categories(flags)
end
local function staggerFn(defaultIndent)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
local raw = pick(args, N.indent)
if raw then
d.indent = parseLength(raw)
if not d.indent then flags.unknown = true end
d.indent = d.indent or defaultIndent
else
d.indent = defaultIndent
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, true)
local html = emit('stagger', d, opt, flags)
return html .. categories(flags)
end
end
local function mesra(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.text = clean(args[1])
local a = pick(args, N.align)
if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end
local raw = pick(args, N.indent)
d.indent = parseLength(raw)
if raw and not d.indent then flags.unknown = true end
if hasUnknown(args, 1) then flags.unknown = true end
applyAuto(d, ctx, ctx.mesra and not opt.continued, true)
local html = emit('mesra', d, opt, flags)
return html .. categories(flags)
end
local function emptyFn(kind)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local d = commonRow(args, ctx, flags)
local raw = pick(args, N.height)
d.height = parseLength(raw)
if raw and not d.height then flags.unknown = true end
if hasUnknown(args, 0) then flags.unknown = true end
local html
if d.height then html = rowGap(d)
elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d)
else d.text, d.blank = '', true; html = rowMesra(d) end
return plain(html) .. categories(flags)
end
end
local function safe(fn)
return function(frame)
local ok, res = pcall(fn, frame)
if ok then return res end
return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>'
.. categories({ error = true })
end
end
p.open = safe(open)
p.close = safe(close)
p.bayt = safe(bayt)
p.stagger = safe(staggerFn(nil))
p.ladder = safe(staggerFn(nil))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
3ifycz6aje4lb6nd1wi8a9ggnzuslx1
304057
304054
2026-10-12T20:01:20Z
Hanooz
17889
نسخهٔ [[Special:Diff/304054|304054]] از [[Special:Contributions/Hanooz|Hanooz]] ([[User talk:Hanooz|بحث]]) خنثی شد
304057
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
▸ عرض ستونها بهصورت inline روی هر سلول نوشته میشود، نه در CSS.
شبکهٔ ثابت ۷ستونی: ۵٪ | ۲۱٪ | ۲۱٪ | ۶٪ | ۲۱٪ | ۲۱٪ | ۵٪
هر مصراع (colspan=2) دقیقاً ۴۲٪ جدول است.
▸ بهاینترتیب حتی اگر شیوهنامه در فضای نام صفحه بارگذاری نشود یا
ProofreadPage تگ templatestyles را حذف کند، عرضها حفظ میشوند.
▸ ترازبندی پیشفرض «وسط». ترازبندی=پهن / لبه اختیاری.
▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت.
]==]
local p = {}
local ZWNJ = '\226\128\140'
-- عرضهای قطعی (درصد). مجموع = ۱۰۰.
local W_SIDE = '5%'
local W_MH = '21%'
local W_GAP = '6%'
local W_MISRA = '42%' -- 21 + 21
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '48em',
justDefault = { bayt = 'center', stagger = 'center' },
nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', "
.. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif",
mergeRows = false,
cats = {
unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر',
incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص',
error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر',
},
}
local N = {
right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' },
left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' },
class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' },
indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' },
height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' },
dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' },
numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' },
start = { 'شروع', 'start' }, side = { 'سمت', 'side' },
digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' },
unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' },
nastaliq = { 'نستعلیق', 'nastaliq' },
cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' },
continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' },
stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' },
maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' },
font = { 'قلم', 'font' },
just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' },
}
local KNOWN = {}
for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end
local ALIGN = {
['راست'] = 'right', right = 'right', r = 'right',
['چپ'] = 'left', left = 'left', l = 'left',
['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center',
}
local SIDE = { right = 'r', left = 'l', center = 'c' }
local KIND = {
['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt',
['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger',
['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder',
['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra',
}
local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa',
['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin',
['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' }
local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true,
yes=true, y=true, auto=true, ['true']=true, ['1']=true }
local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true }
local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch',
rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' }
local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev',
['بعد']='next', ['پسین']='next', next='next',
['هر دو']='both', ['هردو']='both', both='both' }
local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='',
['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ }
local WHICH = { ['هر دو']='both', ['هردو']='both', both='both',
['اول']='first', first='first', ['دوم']='second', second='second' }
local STANZA = { ['جدید']=true, new=true }
local JUST = {
['وسط']='center', ['مرکز']='center', center='center',
['راست']='right', right='right', ['چپ']='left', left='left',
['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify',
['لبه']='edge', edge='edge',
}
local ALIGN_STYLE = {
right = 'text-align:right;text-align-last:auto',
left = 'text-align:left;text-align-last:auto',
center = 'text-align:center;text-align-last:auto',
justify = 'text-align:justify;text-align-last:justify;'
.. '-moz-text-align-last:justify;-webkit-text-align-last:justify',
}
local KIND_KEYS = {}
for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end
table.sort(KIND_KEYS, function(a, b) return #a > #b end)
local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1,
lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 }
local state = { ctx = nil, open = false, carry = nil }
local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end
local SKIP = '<tr class="poem-skip"></tr>'
local CLOSE_HTML = '</table></div>'
local FINAL_MARKER =
'<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>'
local function esc(s) return (s:gsub('&','&'):gsub('"','"'):gsub('<','<'):gsub('>','>')) end
local function pick(args, names)
for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end
return nil
end
local function present(args, names)
for _, n in ipairs(names) do if args[n] ~= nil then return true end end
return false
end
local function bool(v, d)
if v == nil then return d end
v = v:lower()
if YES[v] then return true end
if NO[v] then return false end
return d
end
local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end
local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end
local function getArgs(frame)
local a = {}
local function merge(src)
if not src then return end
for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end
end
merge(frame.args)
local parent = frame:getParent()
if parent then merge(parent.args) end
return a
end
local function hasUnknown(args, pos)
for k, v in pairs(args) do
if v ~= '' then
if type(k) == 'number' then
if k < 1 or k > pos or k % 1 ~= 0 then return true end
elseif not KNOWN[k] then return true end
end
end
return false
end
local function nsId() return mw.title.getCurrentTitle().namespace end
local function inPageNs() return nsId() == cfg.pageNs end
local function tracked()
local ns = nsId()
return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828)
end
local function categories(flags)
if not flags or next(flags) == nil or not tracked() then return '' end
local out = {}
for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do
if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end
end
return table.concat(out)
end
local function langDir(lang)
local base, rest = lang:lower():match('^(%a+)(.*)$')
if not base then return nil end
if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end
if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end
return 'ltr'
end
local function stylesTag(frame)
local ok, tag = pcall(function()
return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } }
end)
return ok and tag or ''
end
local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' }
local function convertDigits(s, mode)
if s == '' or mode == 'keep' then return s end
if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end
if mode == 'latin' then
s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end)
s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end)
return s
end
s = s:gsub('%d', function(d) return FA[tonumber(d)] end)
s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end)
return s
end
local function numberArg(args, names, digits)
local v = pick(args, names); if not v then return '' end
return convertDigits(v, digits)
end
local function intArg(v, lo, hi)
local n = tonumber((convertDigits(v, 'latin')))
if not n or n ~= n or n < lo or n > hi then return nil end
return math.floor(n)
end
local function splitLength(v)
if not v or v == '' then return nil end
v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.')
local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$')
if not num then return nil end
num = tonumber(num)
if not num or num > 10000 then return nil end
unit = UNITS[unit:lower()]
if not unit then return nil end
return num, unit
end
local function parseLength(v)
local n, u = splitLength(v); if not n then return nil end
return string.format('%g', n) .. u
end
local function newCtx(args, kind, flags)
local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') }
local d = pick(args, N.digits)
if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end
local s = pick(args, N.start)
if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end
local sd = pick(args, N.side)
if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end
local m = pick(args, N.numbering)
if m then
local n = intArg(m, 0, 1e6)
if n then if n >= 1 then ctx.every = n end
else
local b = bool(m, nil)
if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end
end
end
return ctx
end
local function nextNumber(ctx)
if not ctx.every then return nil end
local n = ctx.start + ctx.count
ctx.count = ctx.count + 1
if n % ctx.every ~= 0 then return nil end
return convertDigits(string.format('%d', n), ctx.digits)
end
local function applyAuto(d, ctx, counted, noCenter)
if not counted then return end
local n = nextNumber(ctx)
if not n then return end
if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end
local side = ctx.side
if noCenter and side == 'c' then side = 'l' end
d[side] = n
end
local function joinStyle(...)
local parts = {}
for i = 1, select('#', ...) do
local v = select(i, ...)
if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end
end
return table.concat(parts, ';')
end
local function alignFor(j, which)
if not j or j == 'center' then return nil end
if j == 'edge' then
return (which == 1) and 'text-align:start;text-align-last:auto'
or 'text-align:end;text-align-last:auto'
end
return ALIGN_STYLE[j]
end
-- td با عرض قطعی: width همیشه در style اینلاین مینشیند.
local function td(class, colspan, style, content, width)
local s = '<td'
if class and class ~= '' then s = s .. ' class="' .. class .. '"' end
if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end
local full = style
if width then
if full and full ~= '' then
full = 'width:' .. width .. ';' .. full
else
full = 'width:' .. width
end
end
if full and full ~= '' then s = s .. ' style="' .. esc(full) .. '"' end
return s .. '>' .. (content or '') .. '</td>'
end
local function hemiCell(text)
return '<span class="beyt">' .. (text or '') .. '</span>'
end
local function rowOpen(d, kind, noId)
local cls = 'poem-row ' .. kind
if d.class then cls = cls .. ' ' .. d.class end
local id = ''
if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end
return '<tr class="' .. esc(cls) .. '"' .. id .. '>'
end
local function rowBayt(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.bayt
return rowOpen(d, 'poem-bayt')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE)
.. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1), W_MISRA)
.. td('poem-num poem-num-c', 1, d.style, d.c, W_GAP)
.. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2), W_MISRA)
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE)
.. '</tr>'
end
local function rowStagger(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = d.just or cfg.justDefault.stagger
local s1 = joinStyle(alignFor(j, 1), d.style)
local s2 = joinStyle(alignFor(j, 2), d.style)
local ind = d.indent
if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then
s1 = joinStyle('padding-inline-end:' .. ind, s1)
s2 = joinStyle('padding-inline-start:' .. ind, s2)
end
-- ردیف ۱: side | misra1 | gap | empty | empty
-- ردیف ۲: empty | empty | gap | misra2 | side
return rowOpen(d, 'poem-stagger poem-stagger-1')
.. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE)
.. td('b poem-m poem-m1', 2, s1, hemiCell(m1), W_MISRA)
.. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP)
.. td('poem-empty', 2, nil, '', W_MISRA)
.. td('poem-num poem-num-l poem-empty', 1, nil, '', W_SIDE)
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num poem-num-r poem-empty', 1, nil, '', W_SIDE)
.. td('poem-empty', 2, nil, '', W_MISRA)
.. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP)
.. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2), W_MISRA)
.. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE)
.. '</tr>'
end
local function rowMesra(d)
local align = d.align or 'center'
local content = d.text
if d.blank and content == '' then content = cfg.nbsp end
local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil
local style = joinStyle(inline, d.style)
local cellClass = 'b poem-m'
local cs = { 2, 3, 2 }
if align ~= 'center' then
cs = { 1, 5, 1 }
cellClass = cellClass .. ' poem-m-' .. align
if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end
end
local cell
if d.blank and content == cfg.nbsp then cell = content
else cell = hemiCell(content) end
local w1 = (cs[1] == 1) and W_SIDE or W_MISRA
local w2 = (cs[2] == 3) and W_MISRA or (W_SIDE .. '+') -- never used; simplified below
-- ساده: اگر cs = {2,3,2} باشد ⇒ side=42%? نه، side=5%+? مجموع = 7 ستون
-- cs={2,3,2}: 5+21 / 21+6+21 / 21+5 ⇒ 26% / 68% / 26%
-- cs={1,5,1}: 5 / 5+21+21+6+21+21 / 5 ⇒ 5% / 95% / 5%
local wLeft = (cs[1] == 1) and W_SIDE or '26%'
local wMid = (cs[2] == 3) and '48%' or '90%' -- approximation; browser resolves under fixed
local wRight = (cs[3] == 1) and W_SIDE or '26%'
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r, wLeft)
.. td(cellClass, cs[2], style, cell, wMid)
.. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l, wRight)
.. '</tr>'
end
local function rowGap(d)
local style = d.height and ('height:' .. d.height) or nil
local content = d.height and '' or cfg.nbsp
return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">'
.. td('poem-gapcell', 7, style, content, '100%') .. '</tr>'
end
local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra }
-- سایزر با عرضهای inline روی هر سلول.
local function sizerRow()
return '<tr class="poem-sizer" aria-hidden="true">'
.. td('poem-s', 1, nil, '', W_SIDE)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-g', 1, nil, '', W_GAP)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-mh', 1, nil, '', W_MH)
.. td('poem-s', 1, nil, '', W_SIDE)
.. '</tr>'
end
local function openHtml(o)
local cls = 'poem' .. (o.fixed and ' poem-fixed' or '')
if o.class then cls = cls .. ' ' .. o.class end
local div = '<div class="' .. esc(cls) .. '"'
if o.lang then div = div .. ' lang="' .. o.lang .. '"' end
if o.dir then div = div .. ' dir="' .. o.dir .. '"' end
local font = o.font and ('font-family:' .. o.font) or nil
local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '')
if o.fixed then
-- عرض قطعی روی خود جدول، به em، نه درصد. جدول مستقل از والد
-- دارای عرض معیّن میشود و table-layout: fixed روی عدد ثابت کار میکند.
-- max-width: 100% برای صفحهٔ باریک نگه داشته میشود.
local absWidth = o.absWidth or '42em'
local tblStyle = 'width:' .. absWidth ..
';max-width:100%;table-layout:fixed;margin:0 auto;border-collapse:collapse'
if font then tblStyle = tblStyle .. ';' .. font end
if o.style then tblStyle = tblStyle .. ';' .. o.style end
local wrapperStyle = joinStyle(
'width:100%',
o.maxWidth and o.maxWidth ~= 'none' and ('max-width:' .. o.maxWidth) or nil,
'margin:0 auto',
font,
o.style
)
-- هر دو لایه: بیرونی برای مرکزکردن و سقف عرض، داخلی با عرض em
return div .. ' style="' .. esc(wrapperStyle) .. '">'
.. '<table class="' .. tcls .. '" role="presentation" style="'
.. esc(tblStyle) .. '">' .. sizerRow()
end
local style = 'margin: 0 auto;'
if font then style = style .. font .. ';' end
if o.style then style = style .. ' ' .. o.style end
return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">'
end
---------------------------------------------------------------------------
-- چندصفحهای
---------------------------------------------------------------------------
local function joinText(a, b, sep)
if a == '' then return b end
if b == '' then return a end
return a .. sep .. b
end
local function mergeRow(head, tail, sep)
for _, f in ipairs({ 'm1', 'm2', 'text' }) do
if head[f] ~= nil or tail[f] ~= nil then
head[f] = joinText(head[f] or '', tail[f] or '', sep)
end
end
for _, f in ipairs({ 'r', 'c', 'l' }) do
if tail[f] and tail[f] ~= '' then head[f] = tail[f] end
end
head.style = head.style or tail.style
head.class = head.class or tail.class
head.id = head.id or tail.id
return head
end
local function flushHtml()
local c = state.carry
if not c then return '' end
state.carry = nil
return RENDER[c.kind](c.d)
end
local function plain(html)
if not inPageNs() then
local pre = flushHtml()
if pre ~= '' then return pre .. '\n' .. html end
end
return html
end
local function emit(kind, d, opt, flags)
opt = opt or {}
if inPageNs() then
if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end
if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end
return RENDER[kind](d)
end
if not cfg.mergeRows then return RENDER[kind](d) end
local out = ''
local carry = state.carry
state.carry = nil
if carry then
if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ')
else
out = RENDER[carry.kind](carry.d)
if opt.continued then flags.unknown = true end
end
end
if opt.unfinished then
state.carry = { kind = kind, d = d }
return out ~= '' and out or SKIP
end
local html = RENDER[kind](d)
if out ~= '' then return out .. '\n' .. html end
return html
end
local function rowOpts(args, flags)
local opt = {}
local u = pick(args, N.unfinished)
if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end
local c = pick(args, N.continued)
if c then
local w = WHICH[c:lower()]
if w then opt.continued, opt.which = true, w
else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end
end
local j = pick(args, N.joiner)
if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end
return opt
end
local function parseOpen(args, flags)
local o = {}
local isText = present(args, N.text)
for i = 1, 2 do
local v = args[i]
if v and v ~= '' then
local lv = v:lower()
if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true
elseif v == 'ادامه' or lv == 'continue' then
if isText then flags.unknown = true else o.cont = 'prev' end
else flags.unknown = true end
end
end
if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k then
if not isText and k == 'next' then flags.unknown = true; k = nil end
else
local b = bool(c, nil)
if b == nil then flags.unknown = true
elseif b then if isText then flags.unknown = true else k = 'prev' end end
end
if k then o.cont = k end
end
o.style = pick(args, N.style); o.class = pick(args, N.class)
local lang = pick(args, N.lang)
if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end
local dir = pick(args, N.dir)
if dir then
dir = dir:lower()
if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end
end
if o.lang and not o.dir then o.dir = langDir(o.lang) end
o.kind = 'bayt'
local k = pick(args, N.kind)
if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end
local a = pick(args, N.align)
if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end
local g = pick(args, N.gap)
if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end
local ind = pick(args, N.indent)
if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end
o.unitNum, o.unitUnit = splitLength(pick(args, N.unit))
if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end
local st = pick(args, N.stanza)
if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end
local jv = pick(args, N.just)
if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end
if o.nastaliq then o.font = cfg.nastaliqFonts end
local f = pick(args, N.font)
if f then
if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts
elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f
else flags.unknown = true end
end
local w = pick(args, N.width)
if w then
local lw = w:lower()
if lw == 'خودکار' or lw == 'auto' then o.fixed = false
else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end
end
local mx = pick(args, N.maxwidth)
if mx then
local lm = mx:lower()
if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none'
else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end
end
if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end
if o.fixed then
local explicit = (o.width ~= nil)
o.width = o.width or cfg.defaultWidth
o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth)
end
return o
end
local function parseClose(args, flags)
local cont = false
local v = args[1]
if v and v ~= '' then
if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end
end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k == 'next' or k == 'both' then cont = true
elseif k == 'prev' then flags.unknown = true
else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end
end
return cont
end
local function parseLine(raw, freeVerse)
local t = {}
local s = raw
if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end
if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3)
elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end
s = mw.text.trim(s)
local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$')
if pre then t.pre = pre; s = rest end
local body, post = s:match('^(.-)%s*>>>%s*(.+)$')
if body then t.post = post; s = body end
t.text = s
return t
end
local function parseDirective(line)
local rest = mw.text.trim(line:sub(2))
local lrest = rest:lower()
for _, key in ipairs(KIND_KEYS) do
if lrest:sub(1, #key) == key then
local after = rest:sub(#key + 1)
if after == '' or after:match('^%s') then
local dd, bad = { kind = KIND[key] }, false
for k, v in after:gmatch('(%S+)=(%S+)') do
local kk, lv = k:lower(), v:lower()
if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end
elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end
elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end
else bad = true end
end
return dd, bad
end
end
end
return nil, true
end
local function renderLines(text, o, ctx, ropt, flags)
local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just }
local items, pending, pendingCur, needGap = {}, nil, nil, false
local function info(c)
local twoH = (c.kind ~= 'mesra')
local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger')
return twoH, rowKind, c.indent
end
local function numbers(d, t)
if not t then return end
if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end
if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end
end
local function makeBayt(t1, t2, c)
local _, _, indent = info(c)
local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '',
r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just }
numbers(d, t1); numbers(d, t2)
return d
end
local function makeMesra(t, c, free)
local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed }
d.align = t.align or c.align or (free and 'right' or 'center')
if free and (t.level or 0) > 0 and d.align ~= 'center' then
d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit
end
numbers(d, t)
return d
end
local function push(kind, d, counted)
items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 }
needGap = false
end
local function flushPending()
if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end
end
local src = text:gsub('\r\n?', '\n')
for raw in (src .. '\n'):gmatch('([^\n]*)\n') do
local line = mw.text.trim(raw)
if line == '' then
flushPending()
if #items > 0 then needGap = true end
elseif line:sub(1, 1) == '@' then
local dd, bad = parseDirective(line)
flushPending()
if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end
if bad then flags.unknown = true end
else
local twoH, rowKind = info(cur)
local isFirst = (#items == 0 and not pending)
local t = parseLine(line, not twoH)
if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra)
else
local handled = false
if ropt.continued and isFirst then
local mode = ropt.which or 'auto'
if mode == 'auto' then
local c = state.carry
mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both'
end
if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true
elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end
end
if not handled then
if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil
else pending, pendingCur = t, cur end
end
end
end
end
if pending then
local _, rowKind = info(pendingCur)
if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true)
else push('mesra', makeMesra(pending, pendingCur, false), false) end
pending = nil
end
for i, it in ipairs(items) do
if it.counted and not (i == 1 and ropt.continued) then
applyAuto(it.d, ctx, true, it.kind ~= 'bayt')
end
end
local out = {}
for i, it in ipairs(items) do
if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end
local ro = {}
if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end
if i == #items and ropt.unfinished then ro.unfinished = true end
local h = emit(it.kind, it.d, ro, flags)
if h ~= SKIP and h ~= '' then out[#out + 1] = h end
end
return table.concat(out, '\n')
end
local function open(frame)
local args, flags = getArgs(frame), {}
local o = parseOpen(args, flags)
local isText = present(args, N.text)
local page = inPageNs()
if hasUnknown(args, 2) then flags.unknown = true end
local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both')
local ctx
if joins and state.ctx then ctx = state.ctx
else
ctx = newCtx(args, o.kind, flags)
ctx.fixed, ctx.just = o.fixed, o.just
end
local parts = {}
if not joins then
parts[#parts + 1] = stylesTag(frame) .. openHtml(o)
state.carry = nil
end
state.open, state.ctx = true, ctx
if isText then
local ropt = rowOpts(args, flags)
if o.newStanza and joins and not ropt.continued then
parts[#parts + 1] = plain(rowGap({ height = o.gap }))
end
local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags)
if rows ~= '' then parts[#parts + 1] = rows end
local stay = (not page) and (o.cont == 'next' or o.cont == 'both')
if not stay then
local tail = flushHtml()
if tail ~= '' then parts[#parts + 1] = tail end
parts[#parts + 1] = CLOSE_HTML
state.open, state.ctx = false, nil
else
parts[#parts + 1] = FINAL_MARKER
end
end
if #parts == 0 then parts[1] = SKIP end
return table.concat(parts, '\n') .. categories(flags)
end
local function close(frame)
local args, flags = getArgs(frame), {}
local cont = parseClose(args, flags)
if hasUnknown(args, 1) then flags.unknown = true end
local html = flushHtml()
if html ~= '' then html = html .. '\n' end
if cont and not inPageNs() then
return html .. FINAL_MARKER .. categories(flags)
end
state.open, state.ctx = false, nil
return html .. CLOSE_HTML .. categories(flags)
end
local function commonRow(args, ctx, flags)
local d = {
r = numberArg(args, N.right, ctx.digits),
c = numberArg(args, N.center, ctx.digits),
l = numberArg(args, N.left, ctx.digits),
style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id),
fixed = ctx.fixed, just = ctx.just,
}
local jv = pick(args, N.just)
if jv then
d.just = JUST[jv:lower()]
if not d.just then flags.unknown = true; d.just = ctx.just end
end
return d
end
local function bayt(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, false)
local html = emit('bayt', d, opt, flags)
return html .. categories(flags)
end
local function staggerFn(defaultIndent)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
local raw = pick(args, N.indent)
if raw then
d.indent = parseLength(raw)
if not d.indent then flags.unknown = true end
d.indent = d.indent or defaultIndent
else
d.indent = defaultIndent
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, true)
local html = emit('stagger', d, opt, flags)
return html .. categories(flags)
end
end
local function mesra(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.text = clean(args[1])
local a = pick(args, N.align)
if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end
local raw = pick(args, N.indent)
d.indent = parseLength(raw)
if raw and not d.indent then flags.unknown = true end
if hasUnknown(args, 1) then flags.unknown = true end
applyAuto(d, ctx, ctx.mesra and not opt.continued, true)
local html = emit('mesra', d, opt, flags)
return html .. categories(flags)
end
local function emptyFn(kind)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local d = commonRow(args, ctx, flags)
local raw = pick(args, N.height)
d.height = parseLength(raw)
if raw and not d.height then flags.unknown = true end
if hasUnknown(args, 0) then flags.unknown = true end
local html
if d.height then html = rowGap(d)
elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d)
else d.text, d.blank = '', true; html = rowMesra(d) end
return plain(html) .. categories(flags)
end
end
local function safe(fn)
return function(frame)
local ok, res = pcall(fn, frame)
if ok then return res end
return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>'
.. categories({ error = true })
end
end
p.open = safe(open)
p.close = safe(close)
p.bayt = safe(bayt)
p.stagger = safe(staggerFn(nil))
p.ladder = safe(staggerFn(nil))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
p1mmb8g2rb5ijgk92jk7kd476crpa9f
304058
304057
2026-10-12T22:47:19Z
Hanooz
17889
304058
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
الگو نقطهٔ ورود در {{#invoke:شعر|…}}
{{شعر}} open آغاز شعر؛ با پارامتر «متن» کل شعر را میسازد
{{پایان شعر}} close
{{بیت}} bayt بیت دومصراعی
{{بیت۱}} stagger بیت پلکانی (ضربدری)
{{بیت نردبانی}} ladder بیت نردبانی (تورفتگی پیشفرض بیشتر)
{{مصراع}} mesra یک مصراع
{{بیت خالی}} emptyBayt
{{مصراع خالی}} emptyMesra
ساختار خروجی همان جدول ۷ستونی پیشین است (ظاهر شعرهای موجود و خروجی
کتاب الکترونیکی تغییری نمیکند). شیوهنامه: «الگو:شعر/styles.css».
چیدمان (تازه): جدول پیشفرض عرض ثابت و مستقل از محتوا دارد (عرض=، حداکثر عرض=)،
تا همهٔ ابیات شعر — حتی در جدولهای جدا (صفحههای جدا) — از یک نقطه آغاز شوند.
ترازبندی=وسط|راست|چپ|پهن|لبه برای هر نوع بیت؛ قلم=، نستعلیق خودبسنده (بدون Common.css).
در حالت «متن»، خطی که با @ آغاز شود نوع ردیفهای بعدی را عوض میکند: «@بیت نردبانی».
شعر چندصفحهای (ایدهٔ start/end در ppoem):
• در فضای نام «صفحه» هر صفحه همیشه خودبسنده و بسته نمایش داده میشود.
• در صفحهٔ اصلی (درج با <pages/>) پارامتر «ادامه» صفحهها را به هم میدوزد:
{{شعر|ادامه}} جدول تازه باز نمیکند و {{پایان شعر|ادامه}} نمیبندد.
• ردیفِ ناتمام (مصراعی که در صفحهٔ بعد ادامه دارد) نگه داشته میشود
(ناتمام=بله) و با نخستین ردیفِ صفحهٔ بعد (دنباله=بله) یکی میشود.
]==]
local p = {}
---------------------------------------------------------------------------
-- تنظیمات
---------------------------------------------------------------------------
local ZWNJ = '\226\128\140' -- نیمفاصله
local cfg = {
styles = 'Template:شعر۲/styles.css', -- صفحهٔ TemplateStyles
pageNs = 104, -- شمارهٔ فضای نام «صفحه» (ProofreadPage)
defaultIndent = '20%', -- تورفتگی پیشفرض {{بیت۱}}
ladderIndent = '45%', -- تورفتگی پیشفرض {{بیت نردبانی}}
mesraUnit = '1.5em', -- تورفتگی هر «:» در حالت متن با نوع=مصراع
nbsp = ' ',
-- چیدمان: 'auto' = جدولِ هماندازهٔ محتوا (رفتار قدیمی)؛ طول/درصد = عرض ثابت
defaultWidth = '100%',
maxWidth = '50em', -- سقف عرض (وقتی عرض داده نشده)
-- فاصلهٔ دو مصراع، ستون شمارهٔ کناری و فاصلهٔ شماره تا مصراع در الگو:شعر/styles.css است
-- ترازبندی پیشفرض هر نوع: inner | center | right | left | justify | edge
-- inner (میانه): مصراعها به محور میانی میچسبند — شیوهٔ گنجور و کتابهای کلاسیک
-- edge (لبه) : مصراع ۱ به راست، مصراع ۲ به چپ
justDefault = { bayt = 'inner', stagger = 'edge' },
-- نستعلیق: خودِ الگوی ویکی ({{نستعلیق|…}}) صدا زده میشود و کلاس/قلمی که میگذارد روی شعر
-- مینشیند؛ پس هر تغییر در «الگو:نستعلیق» یا «الگو:نستعلیق/styles.css» خودکار به شعرها میرسد.
nastaliqTemplate = 'نستعلیق',
-- آخرین چاره، فقط اگر آن الگو در دسترس نبود
nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif",
-- نمادِ میان دو مصراع: separator برای همهٔ شعرها ('' = بدون نماد، یا مثلاً '❖')؛
-- ornament نمادِ «جداکننده=بله» است
separator = '',
ornament = '❖',
-- ترازبندی «پهن»: اگر کوتاهترین مصراعِ بیت کمتر از این نسبتِ طولِ بلندترین باشد، آن بیت
-- کشیده نمیشود (وسطچین میماند) تا فاصلهٔ واژهها بیجهت باز نشود
justifyMinRatio = 0.7,
-- true: ردیف ناتمامِ آخر صفحه نگه داشته و با دنبالهاش ادغام میشود (نیازمند اجرای ترتیبی
-- الگوها در یک رندر). اگر متنی در مرز صفحه گم شد، false کنید: هر تکه فوراً و جدا نمایش داده میشود.
mergeRows = true,
cats = {
unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر',
incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص',
error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر',
},
}
-- نام پارامترها (فارسی + معادل انگلیسی)
local N = {
right = { 'ش', 'r', 'right' },
center = { 'شو', 'c', 'center' },
left = { 'شچ', 'l', 'left' },
style = { 'سبک', 'style' },
class = { 'کلاس', 'class' },
id = { 'شناسه', 'id' },
indent = { 'تورفتگی', 'indent' },
align = { 'تراز', 'align' },
height = { 'ارتفاع', 'height' },
lang = { 'زبان', 'lang' },
dir = { 'جهت', 'dir' },
text = { 'متن', 'text' },
kind = { 'نوع', 'type' },
numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' },
start = { 'شروع', 'start' },
side = { 'سمت', 'side' },
digits = { 'ارقام', 'digits' },
gap = { 'فاصله', 'gap' },
unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' },
nastaliq = { 'نستعلیق', 'nastaliq' },
cont = { 'ادامه', 'continue' }, -- شعر از/به صفحهٔ دیگر ادامه دارد
unfinished = { 'ناتمام', 'unfinished' }, -- این ردیف در صفحهٔ بعد ادامه دارد
continued = { 'دنباله', 'continued' }, -- این ردیف ادامهٔ ردیف ناتمامِ صفحهٔ قبل است
joiner = { 'اتصال', 'join' }, -- چسباندن دو تکه: فاصله | چسبیده | نیمفاصله
stanza = { 'بند', 'stanza' }, -- بند=جدید
width = { 'عرض', 'width' },
maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' },
sep = { 'جداکننده', 'separator' },
phone = { 'تلفن', 'phone' },
just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' },
}
local KNOWN = {}
for _, list in pairs(N) do
for _, name in ipairs(list) do
KNOWN[name] = true
end
end
local ALIGN = {
['راست'] = 'right', right = 'right', r = 'right',
['چپ'] = 'left', left = 'left', l = 'left',
['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center',
}
local SIDE = { right = 'r', left = 'l', center = 'c' }
local KIND = {
['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt',
['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger',
['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder',
['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra',
mesra = 'mesra', line = 'mesra',
}
local DIGITS = {
['فارسی'] = 'fa', fa = 'fa', persian = 'fa',
['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin',
['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep',
}
local YES = {
['بله'] = true, ['بلی'] = true, ['آری'] = true, ['خودکار'] = true,
yes = true, y = true, auto = true, ['true'] = true, ['1'] = true,
}
local NO = {
['نه'] = true, ['خیر'] = true, no = true, n = true, ['false'] = true, ['0'] = true,
}
local UNITS = {
[''] = 'px', ['%'] = '%', px = 'px', em = 'em', ex = 'ex', ch = 'ch', rem = 'rem',
vw = 'vw', vh = 'vh', pt = 'pt', cm = 'cm', mm = 'mm', ['in'] = 'in',
}
local CONT = {
['قبل'] = 'prev', ['پیشین'] = 'prev', prev = 'prev',
['بعد'] = 'next', ['پسین'] = 'next', next = 'next',
['هر دو'] = 'both', ['هردو'] = 'both', both = 'both',
}
local JOIN = {
['فاصله'] = ' ', space = ' ',
['چسبیده'] = '', ['بدون فاصله'] = '', tight = '',
['نیم' .. ZWNJ .. 'فاصله'] = ZWNJ, ['نیمفاصله'] = ZWNJ, ['نیم فاصله'] = ZWNJ, zwnj = ZWNJ,
}
local WHICH = {
['هر دو'] = 'both', ['هردو'] = 'both', both = 'both',
['اول'] = 'first', first = 'first', ['دوم'] = 'second', second = 'second',
}
local STANZA = { ['جدید'] = true, new = true }
local JUST = {
['وسط'] = 'center', ['مرکز'] = 'center', center = 'center',
['راست'] = 'right', right = 'right',
['چپ'] = 'left', left = 'left',
['پهن'] = 'justify', ['دوطرفه'] = 'justify', ['دو' .. ZWNJ .. 'طرفه'] = 'justify', justify = 'justify',
['لبه'] = 'edge', edge = 'edge',
['میانه'] = 'inner', ['درون'] = 'inner', inner = 'inner',
}
local ALIGN_CLASS = { right = 'poem-a-right', left = 'poem-a-left' }
local JUSTIFY_STYLE = 'text-align:justify;text-align-last:justify'
local PHONE = {
['ستونی'] = 'stack', stack = 'stack',
['دوستونه'] = 'two', ['دو ستونه'] = 'two', ['دو' .. ZWNJ .. 'ستونه'] = 'two', two = 'two',
}
local KIND_KEYS = {}
for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end
table.sort(KIND_KEYS, function(a, b) return #a > #b end)
local RTL_LANGS = {
ar = true, arz = true, azb = true, bgn = true, ckb = true, dv = true, fa = true, glk = true,
he = true, ks = true, lrc = true, mzn = true, pnb = true, prs = true, ps = true, sd = true,
ug = true, ur = true, yi = true,
}
-- وضعیتِ میان فراخوانیهای یک صفحه (هر تجزیه موتور Lua تازه دارد)
local state = {
ctx = nil, -- شمارهگذاری و ارقام شعرِ باز
open = false, -- آیا شعری باز است؟ (برای دوختن صفحهها در صفحهٔ اصلی)
carry = nil, -- ردیف ناتمام در انتظار دنباله: { kind = , d = }
}
local function defaultCtx()
return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto'), sep = cfg.separator }
end
-- ردیف نامرئی: جای خروجی «تهی» تا خط خالی (و در نتیجه <p><br>) پدید نیاید
local SKIP = '<tr class="poem-skip"></tr>'
local CLOSE_HTML = '</table></div>'
---------------------------------------------------------------------------
-- ابزارهای عمومی
---------------------------------------------------------------------------
local function esc(s)
s = s:gsub('"', '"'):gsub('<', '<'):gsub('>', '>')
return s
end
local function pick(args, names)
for _, name in ipairs(names) do
local v = args[name]
if v and v ~= '' then return v end
end
return nil
end
-- آیا یکی از نامها داده شده است (حتی با مقدار تهی)؟
local function present(args, names)
for _, name in ipairs(names) do
if args[name] ~= nil then return true end
end
return false
end
local function bool(v, default)
if v == nil then return default end
v = v:lower()
if YES[v] then return true end
if NO[v] then return false end
return default
end
local function addClass(a, b)
if not a or a == '' then return b end
return a .. ' ' .. b
end
-- مصراع: یکخطیکردن و حذف فاصلهٔ اضافه
local function clean(s)
if not s then return '' end
s = mw.text.trim(s)
s = s:gsub('%s*\n%s*', ' ')
return s
end
-- آرگومانهای الگوی فراخوانکننده (در صورت نبود، آرگومانهای #invoke)
local function getArgs(frame)
local args = {}
local function merge(src)
if not src then return end
for k, v in pairs(src) do
if type(v) == 'string' then args[k] = mw.text.trim(v) end
end
end
merge(frame.args)
local parent = frame:getParent()
if parent then merge(parent.args) end
return args
end
-- آیا پارامتر ناشناخته (یا مصراعِ اضافه) وجود دارد؟
-- اشتباه رایج: وجود «=» در مصراع که آن را به پارامتر نامدار تبدیل میکند.
local function hasUnknown(args, positional)
for k, v in pairs(args) do
if v ~= '' then
if type(k) == 'number' then
if k < 1 or k > positional or k % 1 ~= 0 then return true end
elseif not KNOWN[k] then
return true
end
end
end
return false
end
local function nsId()
return mw.title.getCurrentTitle().namespace
end
-- فضای نام «صفحه»: همیشه خودبسنده نمایش میدهیم
local function inPageNs()
return nsId() == cfg.pageNs
end
-- ردیابی فقط در اسمفضای اصلی و اسمفضاهای محتوایی سفارشی (مثل صفحه)
local function tracked()
local ns = nsId()
return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828)
end
local function categories(flags)
if not flags or next(flags) == nil or not tracked() then return '' end
local out = {}
for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do
if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end
end
return table.concat(out)
end
local function langDir(lang)
local base, rest = lang:lower():match('^(%a+)(.*)$')
if not base then return nil end
if rest:find('latn', 1, true) or rest:find('cyrl', 1, true) then return 'ltr' end
if RTL_LANGS[base] or rest:find('arab', 1, true) or rest:find('hebr', 1, true) then return 'rtl' end
return 'ltr'
end
---------------------------------------------------------------------------
-- ارقام
---------------------------------------------------------------------------
local FA = { [0] = '۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹' }
-- mode: 'fa' (پیشفرض) | 'latin' | 'keep'
-- اگر مقدار شبیه نشانهگذاری (HTML/ویکی) باشد دست نمیزنیم.
local function convertDigits(s, mode)
if s == '' or mode == 'keep' then return s end
-- \127 = نشانگر strip (مثل <ref>) که خودش رقم دارد و نباید خراب شود
if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end
if mode == 'latin' then
s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end)
s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end)
return s
end
s = s:gsub('%d', function(d) return FA[tonumber(d)] end)
s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end)
return s
end
local function numberArg(args, names, digits)
local v = pick(args, names)
if not v then return '' end
return convertDigits(v, digits)
end
-- عدد صحیح در بازهٔ [lo, hi] یا nil (از inf و nan و اعداد غولآسا جلوگیری میکند)
local function intArg(v, lo, hi)
local n = tonumber((convertDigits(v, 'latin')))
if not n or n ~= n or n < lo or n > hi then return nil end
return math.floor(n)
end
---------------------------------------------------------------------------
-- اندازههای CSS
---------------------------------------------------------------------------
local function splitLength(v)
if not v or v == '' then return nil end
v = convertDigits(v, 'latin')
v = v:gsub('٪', '%%'):gsub('٫', '.')
local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$')
if not num then return nil end
num = tonumber(num)
if not num or num > 10000 then return nil end
unit = UNITS[unit:lower()]
if not unit then return nil end
return num, unit
end
local function parseLength(v)
local num, unit = splitLength(v)
if not num then return nil end
return string.format('%g', num) .. unit
end
---------------------------------------------------------------------------
-- شمارهگذاری خودکار
---------------------------------------------------------------------------
local function newCtx(args, kind, flags)
local ctx = { digits = 'fa', count = 0, start = 1, side = 'l', mesra = (kind == 'mesra') }
local d = pick(args, N.digits)
if d then
ctx.digits = DIGITS[d:lower()]
if not ctx.digits then ctx.digits = 'fa'; flags.unknown = true end
end
local s = pick(args, N.start)
if s then
local n = intArg(s, -1e9, 1e9)
if n then ctx.start = n else flags.unknown = true end
end
local sd = pick(args, N.side)
if sd then
local a = ALIGN[sd:lower()]
if a then ctx.side = SIDE[a] else flags.unknown = true end
end
local m = pick(args, N.numbering)
if m then
local n = intArg(m, 0, 1e6)
if n then
if n >= 1 then ctx.every = n end -- ۰ یعنی خاموش
else
local b = bool(m, nil)
if b == true then ctx.every = 1
elseif b == nil then flags.unknown = true end
end
end
return ctx
end
-- شمارهٔ بعدی (یا nil اگر شمارهگذاری خاموش است یا نوبت نمایش نیست)
local function nextNumber(ctx)
if not ctx.every then return nil end
local n = ctx.start + ctx.count
ctx.count = ctx.count + 1
if n % ctx.every ~= 0 then return nil end
return convertDigits(string.format('%d', n), ctx.digits)
end
-- counted: آیا این ردیف در شمارهگذاری شمرده شود؟ noCenter: ردیف «وسط» ندارد
local function applyAuto(d, ctx, counted, noCenter)
if not counted then return end
local n = nextNumber(ctx)
if not n then return end
if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end -- شمارهٔ دستی مقدم است
local side = ctx.side
if noCenter and side == 'c' then side = 'l' end
d[side] = n
end
---------------------------------------------------------------------------
-- ساخت HTML
---------------------------------------------------------------------------
-- چند تکه استایل را با «;» به هم میچسباند
local function joinStyle(...)
local parts = {}
for i = 1, select('#', ...) do
local v = select(i, ...)
if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end
end
return table.concat(parts, ';')
end
-- کلاس ترازِ مصراعِ which (۱ یا ۲) برای ترازبندی j. وسط = پیشفرض شیوهنامه؛ پهن = استایل
-- درونخطی (JUSTIFY_STYLE). ترازها کلاساند تا در صفحهٔ باریک (تلفن) بتوان آنها را وسطچین کرد.
local function alignClass(j, which)
if not j or j == 'center' or j == 'justify' then return nil end
if j == 'edge' then return (which == 1) and 'poem-a-start' or 'poem-a-end' end
if j == 'inner' then return (which == 1) and 'poem-a-end' or 'poem-a-start' end
return ALIGN_CLASS[j]
end
local function withClass(base, extra)
if extra then return base .. ' ' .. extra end
return base
end
-- شمار تقریبی نویسههای دیدنیِ یک مصراع (بدون نشانهگذاری و نشانگرهای strip)
local function charLen(s)
s = s:gsub('\127[^\127]*\127', ''):gsub('<[^>]*>', ''):gsub('%[%[[^%]|]*|', ''):gsub('[%[%]]', '')
return #(s:gsub('[\128-\191]', ''))
end
-- «پهن» فقط وقتی که دو مصراع هماندازهاند؛ وگرنه کشش زیاد میشود ← وسطچین
local function effectiveJust(j, a, b)
if j == 'justify' then
local la, lb = charLen(a), charLen(b)
local hi = math.max(la, lb)
if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return 'center' end
end
return j
end
local function td(class, colspan, style, content)
local s = '<td class="' .. class .. '"'
if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end
if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end
return s .. '>' .. (content or '') .. '</td>'
end
local function hemi(text)
return '<span class="beyt">' .. text .. '</span>'
end
local function rowOpen(d, kind, noId)
local cls = 'poem-row ' .. kind
if d.class then cls = cls .. ' ' .. d.class end
local id = ''
if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end
return '<tr class="' .. esc(cls) .. '"' .. id .. '>'
end
-- {{بیت}}
local function rowBayt(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end -- ردیف خالی ارتفاع خط را حفظ کند
local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2)
local js = (j == 'justify') and JUSTIFY_STYLE or nil
local mid = d.c
if mid == '' and d.sep and d.sep ~= '' then
mid = '<span class="poem-sep">' .. d.sep .. '</span>' -- نمادِ میان دو مصراع (اگر شمارهٔ میانی نیست)
end
return rowOpen(d, 'poem-bayt')
.. td('poem-num poem-num-r', 1, nil, d.r)
.. td(withClass('b poem-m poem-m1', alignClass(j, 1)), 2, joinStyle(js, d.style), hemi(m1))
.. td('poem-num poem-num-c', 1, d.style, mid)
.. td(withClass('b poem-m poem-m2', alignClass(j, 2)), 2, joinStyle(js, d.style), hemi(d.m2))
.. td('poem-num poem-num-l', 1, nil, d.l)
.. '</tr>'
end
-- پهنای جعبهٔ هر خط در بیت پلکانی با چیدمان ثابت: ۱۰۰٪ منهای تورفتگی
local function boxWidth(indent)
local num, unit = indent:match('^([%d%.]+)(.*)$')
if unit == '%' then
return string.format('%g%%', math.max(100 - tonumber(num), 10))
end
return 'calc(100% - ' .. indent .. ')'
end
-- {{بیت۱}} و {{بیت نردبانی}}
local function rowStagger(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2)
local js = (j == 'justify') and JUSTIFY_STYLE or nil
local s1, s2 = joinStyle(js, d.style), joinStyle(js, d.style)
local c1, c2 = withClass('b poem-m poem-m1', alignClass(j, 1)), withClass('b poem-m poem-m2', alignClass(j, 2))
if d.fixed then
-- چیدمان ثابت: هر خط جعبهای با پهنای درصدی در سلولِ ۵ستونه؛ جعبهٔ اول به آغاز، دوم به پایان میچسبد
local bw = boxWidth(d.indent)
return rowOpen(d, 'poem-stagger poem-stagger-1')
.. td('poem-num poem-num-r', 1, nil, d.r)
.. td(c1, 5, s1,
'<div class="poem-st" style="width:' .. bw .. ';margin-inline-end:auto">' .. hemi(m1) .. '</div>')
.. td('poem-num', 1, nil, '')
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num', 1, nil, '')
.. td(c2, 5, s2,
'<div class="poem-st" style="width:' .. bw .. ';margin-inline-start:auto">' .. hemi(d.m2) .. '</div>')
.. td('poem-num poem-num-l', 1, nil, d.l)
.. '</tr>'
end
local sp = 'width:' .. d.indent
return rowOpen(d, 'poem-stagger poem-stagger-1')
.. td('poem-num poem-num-r', 1, nil, d.r)
.. td(c1, 3, s1, hemi(m1))
.. td('poem-indent', 2, sp, '')
.. td('poem-num', 1, nil, '')
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num', 1, nil, '')
.. td('poem-indent', 2, sp, '')
.. td(c2, 3, s2, hemi(d.m2))
.. td('poem-num poem-num-l', 1, nil, d.l)
.. '</tr>'
end
-- {{مصراع}}
local function rowMesra(d)
local align = d.align or 'center'
local content = d.text
if d.blank and content == '' then content = cfg.nbsp end
local style = d.style or ''
local cellClass = 'b poem-m'
local cs = { 2, 3, 2 }
if align ~= 'center' then
cs = { 1, 5, 1 }
cellClass = cellClass .. ' poem-m-' .. align
if d.indent then
style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style
end
end
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], nil, d.r)
.. td(cellClass, cs[2], style, content)
.. td('poem-num poem-num-l', cs[3], nil, d.l)
.. '</tr>'
end
-- ردیف فاصله با ارتفاع دلخواه (یا یک خط خالی)
local function rowGap(d)
local style = d.height and ('height:' .. d.height) or nil
local content = d.height and '' or cfg.nbsp
return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">'
.. td('poem-gapcell', 7, style, content) .. '</tr>'
end
local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra }
-- ردیف نامرئی نخست در چیدمان ثابت: پهنای ستونها را مستقل از محتوا تعیین میکند
local function sizerRow()
return '<tr class="poem-sizer">'
.. '<td class="poem-s"></td><td></td><td></td>'
.. '<td class="poem-g"></td><td></td><td></td>'
.. '<td class="poem-s"></td></tr>'
end
local function openHtml(o)
local look = o.look -- نمای نستعلیق (کلاس و قلمِ الگوی ویکی)
local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') .. (o.stack and ' poem-stack' or '')
if look and look.class then cls = cls .. ' ' .. look.class end
if o.class then cls = cls .. ' ' .. o.class end
local div = '<div class="' .. esc(cls) .. '"'
if o.lang then div = div .. ' lang="' .. o.lang .. '"' end
if o.dir then div = div .. ' dir="' .. o.dir .. '"' end
local font = look and look.style or nil
local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '')
if o.fixed then
-- عرض و قلم و سبک کاربر روی جعبهٔ بیرونی؛ جدول همیشه ۱۰۰٪ آن و با ستونهای ثابت
local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style)
return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls
.. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow()
end
-- چیدمان خودکار (قدیمی): همان بایتبهبایتِ پیشین وقتی قلم/سبک نیست
local style = 'margin: 0 auto;'
if font then style = style .. font .. ';' end
if o.style then style = style .. ' ' .. o.style end
return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">'
end
---------------------------------------------------------------------------
-- شعر چندصفحهای: نگهداشتن ردیف ناتمام و یکیکردن آن با دنبالهاش
---------------------------------------------------------------------------
local function joinText(a, b, sep)
if a == '' then return b end
if b == '' then return a end
return a .. sep .. b
end
-- tail را در head (ردیف ناتمام) ادغام میکند: متن مصراعها پشت هم، شمارهها از tail
local function mergeRow(head, tail, sep)
for _, f in ipairs({ 'm1', 'm2', 'text' }) do
if head[f] ~= nil or tail[f] ~= nil then
head[f] = joinText(head[f] or '', tail[f] or '', sep)
end
end
for _, f in ipairs({ 'r', 'c', 'l' }) do
if tail[f] and tail[f] ~= '' then head[f] = tail[f] end
end
head.style = head.style or tail.style
head.class = head.class or tail.class
head.id = head.id or tail.id
return head
end
-- ردیفِ نگهداشتهشده را (اگر باشد) بهصورت HTML بیرون میدهد
local function flushHtml()
local c = state.carry
if not c then return '' end
state.carry = nil
return RENDER[c.kind](c.d)
end
-- ردیفهای بدون منطق ادامه (فاصله، خالی): ردیف ناتمامِ جاافتاده را هم بیرون میریزد
local function plain(html)
if not inPageNs() then
local pre = flushHtml()
if pre ~= '' then return pre .. '\n' .. html end
end
return html
end
--[[
یک ردیف را خروجی میدهد.
فضای نام «صفحه»: همانجا و کامل (فقط کلاسهای ناتمام/دنباله برای نشاندادن).
جاهای دیگر (صفحهٔ اصلی، درج صفحهها): ردیف ناتمام نگه داشته میشود و با
نخستین ردیفِ دارای «دنباله» یکی میشود.
opt: unfinished, continued, joiner
]]
local function emit(kind, d, opt, flags)
opt = opt or {}
if inPageNs() then
if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end
if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end
return RENDER[kind](d)
end
if not cfg.mergeRows then return RENDER[kind](d) end -- بدون ادغام: هر تکه فوراً و جدا
local out = ''
local carry = state.carry
state.carry = nil
if carry then
if opt.continued and carry.kind == kind then
d = mergeRow(carry.d, d, opt.joiner or ' ')
else
out = RENDER[carry.kind](carry.d) -- دنبالهای نیامد: همانطور که هست
if opt.continued then flags.unknown = true end -- نوع ردیفها نمیخواند
end
end
if opt.unfinished then
state.carry = { kind = kind, d = d }
return out ~= '' and out or SKIP
end
local html = RENDER[kind](d)
if out ~= '' then return out .. '\n' .. html end
return html
end
---------------------------------------------------------------------------
-- خواندن پارامترها
---------------------------------------------------------------------------
-- ناتمام / دنباله / اتصال
local function rowOpts(args, flags)
local opt = {}
local u = pick(args, N.unfinished)
if u then
local b = bool(u, nil)
if b == nil then flags.unknown = true else opt.unfinished = b end
end
local c = pick(args, N.continued)
if c then
local w = WHICH[c:lower()]
if w then
opt.continued, opt.which = true, w
else
local b = bool(c, nil)
if b == nil then flags.unknown = true else opt.continued = b end
end
end
local j = pick(args, N.joiner)
if j then
opt.joiner = JOIN[j:lower()]
if opt.joiner == nil then flags.unknown = true end
end
return opt
end
-- جداکننده: نه | بله (نماد پیشفرض) | یک نماد کوتاه (مثل ✤)
local function parseSep(v, flags)
local lv = v:lower()
if lv == 'نه' or lv == 'خیر' or lv == 'بدون' or lv == 'ندارد' or lv == 'no' then return '' end
if lv == 'بله' or lv == 'خودکار' or lv == 'yes' or lv == 'auto' then return cfg.ornament end
if #v <= 24 and not v:find('[<>{}|%[%]"\n]') then return v end
flags.unknown = true
return cfg.separator
end
local function parseOpen(args, flags)
local o = {}
local isText = present(args, N.text)
-- پارامترهای بدون نام: نستعلیق، ادامه
for i = 1, 2 do
local v = args[i]
if v and v ~= '' then
local lv = v:lower()
if v == 'نستعلیق' or lv == 'nastaliq' then
o.nastaliq = true
elseif v == 'ادامه' or lv == 'continue' then
if isText then flags.unknown = true else o.cont = 'prev' end
else
flags.unknown = true
end
end
end
if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end
-- ادامه از/به صفحهٔ دیگر
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k then
if not isText and k == 'next' then flags.unknown = true; k = nil end -- «به بعد» را در {{پایان شعر}} بنویسید
else
local b = bool(c, nil)
if b == nil then flags.unknown = true
elseif b then
if isText then flags.unknown = true else k = 'prev' end
end
end
if k then o.cont = k end
end
o.style = pick(args, N.style)
o.class = pick(args, N.class)
local lang = pick(args, N.lang)
if lang then
if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end
end
local dir = pick(args, N.dir)
if dir then
dir = dir:lower()
if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end
end
if o.lang and not o.dir then o.dir = langDir(o.lang) end
o.kind = 'bayt'
local k = pick(args, N.kind)
if k then
o.kind = KIND[k:lower()]
if not o.kind then o.kind = 'bayt'; flags.unknown = true end
end
local a = pick(args, N.align)
if a then
o.align = ALIGN[a:lower()]
if not o.align then flags.unknown = true end
end
local g = pick(args, N.gap)
if g then
o.gap = parseLength(g)
if not o.gap then flags.unknown = true end
end
local ind = pick(args, N.indent)
if ind then
o.indent = parseLength(ind)
if not o.indent then flags.unknown = true end
end
o.unitNum, o.unitUnit = splitLength(pick(args, N.unit))
if not o.unitNum then
o.unitNum, o.unitUnit = splitLength(cfg.mesraUnit)
if pick(args, N.unit) then flags.unknown = true end
end
local st = pick(args, N.stanza)
if st then
if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end
end
-- ترازبندی (همهٔ ابیات)
local jv = pick(args, N.just)
if jv then
o.just = JUST[jv:lower()]
if not o.just then flags.unknown = true end
end
-- جداکننده (نماد میان دو مصراع)
local sv = pick(args, N.sep)
if sv then o.sep = parseSep(sv, flags) else o.sep = cfg.separator end
-- تلفن (صفحهٔ باریک): ستونی (پیشفرض: عدد، دو مصراع زیر هم) یا دوستونه
local stack = true
local ph = pick(args, N.phone)
if ph then
local m = PHONE[ph:lower()]
if m == 'two' then stack = false elseif not m then flags.unknown = true end
end
o.stack = stack and o.just ~= 'justify' -- شعر دوطرفه همیشه دوستونه میماند
-- چیدمان: عرض ثابت (پیشفرض) یا «خودکار» = هماندازهٔ محتوا
local w = pick(args, N.width)
if w then
local lw = w:lower()
if lw == 'خودکار' or lw == 'auto' then
o.fixed = false
else
o.width = parseLength(w)
if o.width then o.fixed = true else flags.unknown = true end
end
end
local mx = pick(args, N.maxwidth)
if mx then
local lm = mx:lower()
if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then
o.maxWidth = 'none'
else
o.maxWidth = parseLength(mx)
if not o.maxWidth then flags.unknown = true end
end
end
if o.fixed == nil then
-- «پهن» (دوطرفه): ستونها به اندازهٔ بلندترین مصراع میشوند تا کمترین کشش لازم باشد؛
-- با دادن «عرض» همان عرضِ ثابت اعمال میشود
o.fixed = (cfg.defaultWidth ~= 'auto') and (o.just ~= 'justify')
end
if o.fixed then
local explicit = (o.width ~= nil)
o.width = o.width or cfg.defaultWidth
o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth)
end
return o
end
-- ادامهٔ شعر در صفحهٔ بعد؟ {{پایان شعر|ادامه}} یا ادامه=بعد
local function parseClose(args, flags)
local cont = false
local v = args[1]
if v and v ~= '' then
if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end
end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k == 'next' or k == 'both' then
cont = true
elseif k == 'prev' then
flags.unknown = true -- «از قبل» را در {{شعر}} بنویسید
else
local b = bool(c, nil)
if b == nil then flags.unknown = true elseif b then cont = true end
end
end
return cont
end
---------------------------------------------------------------------------
-- حالت «متن»: ساخت کل شعر از خطهای ساده
---------------------------------------------------------------------------
--[[
نشانههای درونخطی (به الگوی ppoem):
>> متن مصراع راستچین <> متن مصراع وسطچین
۱۲ <<< متن شمارهٔ سمت راست (ش) متن >>> ۱۲ شمارهٔ سمت چپ (شچ)
:: متن (نوع=مصراع) تورفتگی
]]
local function parseLine(raw, freeVerse)
local t = {}
local s = raw
if freeVerse then
t.level = #s:match('^:*')
s = s:sub(t.level + 1)
end
if s:sub(1, 2) == '<>' then
t.align = 'center'
s = s:sub(3)
elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then
t.align = 'right'
s = s:sub(3)
end
s = mw.text.trim(s)
local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$')
if pre then
t.pre = pre
s = rest
end
local body, post = s:match('^(.-)%s*>>>%s*(.+)$')
if body then
t.post = post
s = body
end
t.text = s
return t
end
-- «@بیت نردبانی تورفتگی=30%» ← { kind = 'ladder', indent = '30%' }؛ پارامتر دوم: گزینهٔ نامعتبر داشت
local function parseDirective(line)
local rest = mw.text.trim(line:sub(2))
local lrest = rest:lower()
for _, key in ipairs(KIND_KEYS) do
if lrest:sub(1, #key) == key then
local after = rest:sub(#key + 1)
if after == '' or after:match('^%s') then
local dd, bad = { kind = KIND[key] }, false
for k, v in after:gmatch('(%S+)=(%S+)') do
local kk, lv = k:lower(), v:lower()
if kk == 'تورفتگی' or kk == 'indent' then
dd.indent = parseLength(v)
if not dd.indent then bad = true end
elseif kk == 'تراز' or kk == 'align' then
dd.align = ALIGN[lv]
if not dd.align then bad = true end
elseif kk == 'ترازبندی' or kk == 'justify' then
dd.just = JUST[lv]
if not dd.just then bad = true end
else
bad = true
end
end
return dd, bad
end
end
end
return nil, true
end
local function renderLines(text, o, ctx, ropt, flags)
-- وضعیت جاری: نوع ردیف و گزینههایش؛ خط «@…» آن را عوض میکند
local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just }
local items, pending, pendingCur, needGap = {}, nil, nil, false
local function info(c)
local twoH = (c.kind ~= 'mesra')
local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger')
local indent
if c.kind == 'stagger' then indent = c.indent or cfg.defaultIndent
elseif c.kind == 'ladder' then indent = c.indent or cfg.ladderIndent end
return twoH, rowKind, indent
end
local function numbers(d, t)
if not t then return end
if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end
if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end
end
local function makeBayt(t1, t2, c)
local _, _, indent = info(c)
local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '',
r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just, sep = ctx.sep }
numbers(d, t1)
numbers(d, t2)
return d
end
local function makeMesra(t, c, free)
local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed }
d.align = t.align or c.align or (free and 'right' or 'center')
if free and (t.level or 0) > 0 and d.align ~= 'center' then
d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit
end
numbers(d, t)
return d
end
local function push(kind, d, counted)
items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 }
needGap = false
end
local function flushPending()
if pending then
push('mesra', makeMesra(pending, pendingCur, false), false) -- مصراع تنها: وسطچین، بیشمارش
pending = nil
end
end
-- ۱) خط به خط
local src = text:gsub('\r\n?', '\n')
for raw in (src .. '\n'):gmatch('([^\n]*)\n') do
local line = mw.text.trim(raw)
if line == '' then
flushPending()
if #items > 0 then needGap = true end
elseif line:sub(1, 1) == '@' then
local dd, bad = parseDirective(line)
flushPending()
if dd then
cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just }
end
if bad then flags.unknown = true end
else
local twoH, rowKind = info(cur)
local isFirst = (#items == 0 and not pending)
local t = parseLine(line, not twoH)
if not twoH then
push('mesra', makeMesra(t, cur, true), ctx.mesra) -- مصراعهای نوع «مصراع» فقط در شعرِ نوع=مصراع شمرده میشوند
else
local handled = false
if ropt.continued and isFirst then
-- ادامهٔ ردیفِ ناتمام: کدام مصراع(ها) در این صفحه ادامه دارند؟
local mode = ropt.which or 'auto'
if mode == 'auto' then
local c = state.carry
mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both'
end
if mode == 'first' then
push(rowKind, makeBayt(t, nil, cur), true); handled = true
elseif mode == 'second' then
push(rowKind, makeBayt(nil, t, cur), true); handled = true
end
end
if not handled then
if pending then
push(rowKind, makeBayt(pending, t, pendingCur), true)
pending = nil
else
pending, pendingCur = t, cur
end
end
end
end
end
if pending then
local _, rowKind = info(pendingCur)
if ropt.unfinished then
push(rowKind, makeBayt(pending, nil, pendingCur), true) -- بیتِ ناتمام: مصراع دوم در صفحهٔ بعد
else
push('mesra', makeMesra(pending, pendingCur, false), false)
end
pending = nil
end
-- ۲) شمارهگذاری (ردیفِ ادامه را دوباره نمیشماریم)
for i, it in ipairs(items) do
if it.counted and not (i == 1 and ropt.continued) then
applyAuto(it.d, ctx, true, it.kind ~= 'bayt')
end
end
-- ۳) خروجی
local out = {}
for i, it in ipairs(items) do
if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end
local ro = {}
if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end
if i == #items and ropt.unfinished then ro.unfinished = true end
local h = emit(it.kind, it.d, ro, flags)
if h ~= SKIP and h ~= '' then out[#out + 1] = h end
end
return table.concat(out, '\n')
end
---------------------------------------------------------------------------
-- نقطههای ورود
---------------------------------------------------------------------------
-- نمای نستعلیق از روی خودِ الگوی ویکی: {{نستعلیق|x}} را بسط میدهد و از خروجیاش نشانگر
-- شیوهنامه، کلاس و (فقط) استایلهای قلم را برمیدارد. اگر الگو نبود، فهرست قلمِ cfg.
local FONT_PROPS = {
['font-family'] = true, ['font-size'] = true, ['font-style'] = true, ['font-weight'] = true,
['font-variant'] = true, ['font'] = true, ['line-height'] = true, ['letter-spacing'] = true,
['word-spacing'] = true, ['text-rendering'] = true, ['font-feature-settings'] = true,
}
local function nastaliqLook(frame)
local ok, res = pcall(function()
return frame:expandTemplate{ title = cfg.nastaliqTemplate, args = { 'x' } }
end)
if ok and type(res) == 'string' then
local prefix = mw.text.trim(res:match('^(.-)<%a') or '')
local attrs = res:match('<%a+([^>]*)>')
if attrs then
local cls = attrs:match('class%s*=%s*"([^"]*)"') or attrs:match("class%s*=%s*'([^']*)'")
local sty = attrs:match('style%s*=%s*"([^"]*)"') or attrs:match("style%s*=%s*'([^']*)'")
if cls then cls = mw.text.trim((cls:gsub('[^%w%s_%-\128-\255]', ''))) end
local keep = {}
for decl in (sty or ''):gmatch('[^;]+') do
local prop = mw.text.trim(decl:match('^([^:]+):') or ''):lower()
if FONT_PROPS[prop] then keep[#keep + 1] = mw.text.trim(decl) end
end
if (cls and cls ~= '') or #keep > 0 or prefix ~= '' then
return {
prefix = prefix,
class = (cls and cls ~= '') and cls or 'nastaliq',
style = (#keep > 0) and table.concat(keep, ';') or nil,
}
end
end
end
return { prefix = '', class = 'nastaliq', style = 'font-family:' .. cfg.nastaliqFonts }
end
local function open(frame)
local args, flags = getArgs(frame), {}
local o = parseOpen(args, flags)
local isText = present(args, N.text)
local page = inPageNs()
if hasUnknown(args, 2) then flags.unknown = true end
-- صفحهٔ اصلی: اگر شعری باز است و این صفحه ادامهٔ آن است، جدول تازه باز نمیکنیم
local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both')
local ctx
if joins and state.ctx then
ctx = state.ctx
else
ctx = newCtx(args, o.kind, flags)
ctx.fixed, ctx.just, ctx.sep = o.fixed, o.just, o.sep
end
local parts = {}
if not joins then
if o.nastaliq then o.look = nastaliqLook(frame) end
parts[#parts + 1] = frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } }
.. ((o.look and o.look.prefix) or '') .. openHtml(o)
state.carry = nil
end
state.open, state.ctx = true, ctx
if isText then
-- کل شعر در یک فراخوانی؛ نیازی به {{پایان شعر}} نیست
local ropt = rowOpts(args, flags)
if o.newStanza and joins and not ropt.continued then
parts[#parts + 1] = plain(rowGap({ height = o.gap }))
end
local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags)
if rows ~= '' then parts[#parts + 1] = rows end
local stay = (not page) and (o.cont == 'next' or o.cont == 'both')
if not stay then
local tail = flushHtml()
if tail ~= '' then parts[#parts + 1] = tail end
parts[#parts + 1] = CLOSE_HTML
state.open, state.ctx = false, nil
end
end
if #parts == 0 then parts[1] = SKIP end
return table.concat(parts, '\n') .. categories(flags)
end
local function close(frame)
local args, flags = getArgs(frame), {}
local cont = parseClose(args, flags)
if hasUnknown(args, 1) then flags.unknown = true end
if cont and not inPageNs() then
-- شعر در صفحهٔ بعد ادامه دارد: نبندیم (ردیف ناتمام هم نگه داشته میماند)
return SKIP .. categories(flags)
end
local html = flushHtml()
state.open, state.ctx = false, nil
if html ~= '' then html = html .. '\n' end
return html .. CLOSE_HTML .. categories(flags)
end
local function commonRow(args, ctx, flags)
local d = {
r = numberArg(args, N.right, ctx.digits),
c = numberArg(args, N.center, ctx.digits),
l = numberArg(args, N.left, ctx.digits),
style = pick(args, N.style),
class = pick(args, N.class),
id = pick(args, N.id),
fixed = ctx.fixed,
just = ctx.just,
sep = ctx.sep,
}
local sv = pick(args, N.sep)
if sv then d.sep = parseSep(sv, flags) end
local jv = pick(args, N.just)
if jv then
d.just = JUST[jv:lower()]
if not d.just then flags.unknown = true; d.just = ctx.just end
end
return d
end
local function bayt(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, false)
local html = emit('bayt', d, opt, flags)
return html .. categories(flags)
end
local function staggerFn(defaultIndent)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
local raw = pick(args, N.indent)
d.indent = parseLength(raw)
if raw and not d.indent then flags.unknown = true end
d.indent = d.indent or defaultIndent
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, true)
local html = emit('stagger', d, opt, flags)
return html .. categories(flags)
end
end
local function mesra(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.text = clean(args[1])
local a = pick(args, N.align)
if a then
d.align = ALIGN[a:lower()]
if not d.align then flags.unknown = true end
end
local raw = pick(args, N.indent)
d.indent = parseLength(raw)
if raw and not d.indent then flags.unknown = true end
if hasUnknown(args, 1) then flags.unknown = true end
applyAuto(d, ctx, ctx.mesra and not opt.continued, true)
local html = emit('mesra', d, opt, flags)
return html .. categories(flags)
end
local function emptyFn(kind)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local d = commonRow(args, ctx, flags)
local raw = pick(args, N.height)
d.height = parseLength(raw)
if raw and not d.height then flags.unknown = true end
if hasUnknown(args, 0) then flags.unknown = true end
local html
if d.height then
html = rowGap(d)
elseif kind == 'bayt' then
d.m1, d.m2 = '', ''
html = rowBayt(d)
else
d.text, d.blank = '', true
html = rowMesra(d)
end
return plain(html) .. categories(flags)
end
end
-- هر خطای پیشبینینشده بهجای شکستن صفحه، پیامی قرمز + رده میدهد
local function safe(fn)
return function(frame)
local ok, res = pcall(fn, frame)
if ok then return res end
return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>'
.. categories({ error = true })
end
end
p.open = safe(open)
p.close = safe(close)
p.bayt = safe(bayt)
p.stagger = safe(staggerFn(cfg.defaultIndent))
p.ladder = safe(staggerFn(cfg.ladderIndent))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg -- (برای آزمون و سفارشیسازی)
return p
9ikh7e97d9cpcetu2hzogkzw6dcfkrz
304061
304058
2026-10-13T03:40:15Z
Hanooz
17889
304061
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
الگو نقطهٔ ورود در {{#invoke:شعر|…}}
{{شعر}} open آغاز شعر؛ با پارامتر «متن» کل شعر را میسازد
{{پایان شعر}} close
{{بیت}} bayt بیت دومصراعی
{{بیت۱}} stagger بیت پلکانی (ضربدری)
{{بیت نردبانی}} ladder بیت نردبانی (تورفتگی پیشفرض بیشتر)
{{مصراع}} mesra یک مصراع
{{بیت خالی}} emptyBayt
{{مصراع خالی}} emptyMesra
ساختار خروجی همان جدول ۷ستونی پیشین است (ظاهر شعرهای موجود و خروجی
کتاب الکترونیکی تغییری نمیکند). شیوهنامه: «الگو:شعر/styles.css».
چیدمان (تازه): جدول پیشفرض عرض ثابت و مستقل از محتوا دارد (عرض=، حداکثر عرض=)،
تا همهٔ ابیات شعر — حتی در جدولهای جدا (صفحههای جدا) — از یک نقطه آغاز شوند.
ترازبندی=وسط|راست|چپ|پهن|لبه|میانه برای هر نوع بیت؛ قلم=، نستعلیق خودبسنده.
در حالت «متن»، خطی که با @ آغاز شود نوع ردیفهای بعدی را عوض میکند: «@بیت نردبانی».
▸ پیشفرض بیت: «پهن» (دوطرفه). بهاینترتیب در فضای نام «صفحه» هم که
هر صفحهٔ تنها باز میشود، ابیات کشیده میشوند و به «میانه»/«وسط» فرو نمیافتند.
▸ پیشفرض بیت پلکانی: «لبه» (مصراع ۱ راست، مصراع ۲ چپ). «پهن» برای
بیت پلکانی به «لبه» فرو میافتد چون کشیدهکردن واژهها بیجهت فاصله میسازد.
شعر چندصفحهای (ایدهٔ start/end در ppoem):
• در فضای نام «صفحه» هر صفحه همیشه خودبسنده و بسته نمایش داده میشود.
• در صفحهٔ اصلی (درج با <pages/>) پارامتر «ادامه» صفحهها را به هم میدوزد:
{{شعر|ادامه}} جدول تازه باز نمیکند و {{پایان شعر|ادامه}} نمیبندد.
• ردیفِ ناتمام (مصراعی که در صفحهٔ بعد ادامه دارد) نگه داشته میشود
(ناتمام=بله) و با نخستین ردیفِ صفحهٔ بعد (دنباله=بله) یکی میشود.
]==]
local p = {}
---------------------------------------------------------------------------
-- تنظیمات
---------------------------------------------------------------------------
local ZWNJ = '\226\128\140' -- نیمفاصله
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
defaultIndent = '20%',
ladderIndent = '45%',
mesraUnit = '1.5em',
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '50em',
-- ترازبندی پیشفرض هر نوع: inner | center | right | left | justify | edge
-- justify (پهن) : پیشفرض بیت — دوطرفه. در صفحهٔ تنها هم کشیده میشود.
-- edge (لبه) : پیشفرض بیت۱ و نردبانی — مصراع ۱ راست، مصراع ۲ چپ.
-- inner (میانه) : اختیاری — مصراعها به محور میانی میچسبند (گنجور).
justDefault = { bayt = 'justify', stagger = 'edge' },
nastaliqTemplate = 'نستعلیق',
nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif",
separator = '',
ornament = '❖',
justifyMinRatio = 0.7,
mergeRows = true,
cats = {
unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر',
incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص',
error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر',
},
}
local N = {
right = { 'ش', 'r', 'right' },
center = { 'شو', 'c', 'center' },
left = { 'شچ', 'l', 'left' },
style = { 'سبک', 'style' },
class = { 'کلاس', 'class' },
id = { 'شناسه', 'id' },
indent = { 'تورفتگی', 'indent' },
align = { 'تراز', 'align' },
height = { 'ارتفاع', 'height' },
lang = { 'زبان', 'lang' },
dir = { 'جهت', 'dir' },
text = { 'متن', 'text' },
kind = { 'نوع', 'type' },
numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' },
start = { 'شروع', 'start' },
side = { 'سمت', 'side' },
digits = { 'ارقام', 'digits' },
gap = { 'فاصله', 'gap' },
unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' },
nastaliq = { 'نستعلیق', 'nastaliq' },
cont = { 'ادامه', 'continue' },
unfinished = { 'ناتمام', 'unfinished' },
continued = { 'دنباله', 'continued' },
joiner = { 'اتصال', 'join' },
stanza = { 'بند', 'stanza' },
width = { 'عرض', 'width' },
maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' },
sep = { 'جداکننده', 'separator' },
phone = { 'تلفن', 'phone' },
just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' },
}
local KNOWN = {}
for _, list in pairs(N) do
for _, name in ipairs(list) do
KNOWN[name] = true
end
end
local ALIGN = {
['راست'] = 'right', right = 'right', r = 'right',
['چپ'] = 'left', left = 'left', l = 'left',
['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center',
}
local SIDE = { right = 'r', left = 'l', center = 'c' }
local KIND = {
['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt',
['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger',
['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder',
['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra',
mesra = 'mesra', line = 'mesra',
}
local DIGITS = {
['فارسی'] = 'fa', fa = 'fa', persian = 'fa',
['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin',
['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep',
}
local YES = {
['بله'] = true, ['بلی'] = true, ['آری'] = true, ['خودکار'] = true,
yes = true, y = true, auto = true, ['true'] = true, ['1'] = true,
}
local NO = {
['نه'] = true, ['خیر'] = true, no = true, n = true, ['false'] = true, ['0'] = true,
}
local UNITS = {
[''] = 'px', ['%'] = '%', px = 'px', em = 'em', ex = 'ex', ch = 'ch', rem = 'rem',
vw = 'vw', vh = 'vh', pt = 'pt', cm = 'cm', mm = 'mm', ['in'] = 'in',
}
local CONT = {
['قبل'] = 'prev', ['پیشین'] = 'prev', prev = 'prev',
['بعد'] = 'next', ['پسین'] = 'next', next = 'next',
['هر دو'] = 'both', ['هردو'] = 'both', both = 'both',
}
local JOIN = {
['فاصله'] = ' ', space = ' ',
['چسبیده'] = '', ['بدون فاصله'] = '', tight = '',
['نیم' .. ZWNJ .. 'فاصله'] = ZWNJ, ['نیمفاصله'] = ZWNJ, ['نیم فاصله'] = ZWNJ, zwnj = ZWNJ,
}
local WHICH = {
['هر دو'] = 'both', ['هردو'] = 'both', both = 'both',
['اول'] = 'first', first = 'first', ['دوم'] = 'second', second = 'second',
}
local STANZA = { ['جدید'] = true, new = true }
local JUST = {
['وسط'] = 'center', ['مرکز'] = 'center', center = 'center',
['راست'] = 'right', right = 'right',
['چپ'] = 'left', left = 'left',
['پهن'] = 'justify', ['دوطرفه'] = 'justify', ['دو' .. ZWNJ .. 'طرفه'] = 'justify', justify = 'justify',
['لبه'] = 'edge', edge = 'edge',
['میانه'] = 'inner', ['درون'] = 'inner', inner = 'inner',
}
local ALIGN_CLASS = { right = 'poem-a-right', left = 'poem-a-left' }
local JUSTIFY_STYLE = 'text-align:justify;text-align-last:justify'
local PHONE = {
['ستونی'] = 'stack', stack = 'stack',
['دوستونه'] = 'two', ['دو ستونه'] = 'two', ['دو' .. ZWNJ .. 'ستونه'] = 'two', two = 'two',
}
local KIND_KEYS = {}
for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end
table.sort(KIND_KEYS, function(a, b) return #a > #b end)
local RTL_LANGS = {
ar = true, arz = true, azb = true, bgn = true, ckb = true, dv = true, fa = true, glk = true,
he = true, ks = true, lrc = true, mzn = true, pnb = true, prs = true, ps = true, sd = true,
ug = true, ur = true, yi = true,
}
local state = {
ctx = nil,
open = false,
carry = nil,
}
local function defaultCtx()
return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto'), sep = cfg.separator }
end
local SKIP = '<tr class="poem-skip"></tr>'
local CLOSE_HTML = '</table></div>'
---------------------------------------------------------------------------
-- ابزارهای عمومی
---------------------------------------------------------------------------
local function esc(s)
s = s:gsub('"', '"'):gsub('<', '<'):gsub('>', '>')
return s
end
local function pick(args, names)
for _, name in ipairs(names) do
local v = args[name]
if v and v ~= '' then return v end
end
return nil
end
local function present(args, names)
for _, name in ipairs(names) do
if args[name] ~= nil then return true end
end
return false
end
local function bool(v, default)
if v == nil then return default end
v = v:lower()
if YES[v] then return true end
if NO[v] then return false end
return default
end
local function addClass(a, b)
if not a or a == '' then return b end
return a .. ' ' .. b
end
local function clean(s)
if not s then return '' end
s = mw.text.trim(s)
s = s:gsub('%s*\n%s*', ' ')
return s
end
local function getArgs(frame)
local args = {}
local function merge(src)
if not src then return end
for k, v in pairs(src) do
if type(v) == 'string' then args[k] = mw.text.trim(v) end
end
end
merge(frame.args)
local parent = frame:getParent()
if parent then merge(parent.args) end
return args
end
local function hasUnknown(args, positional)
for k, v in pairs(args) do
if v ~= '' then
if type(k) == 'number' then
if k < 1 or k > positional or k % 1 ~= 0 then return true end
elseif not KNOWN[k] then
return true
end
end
end
return false
end
local function nsId()
return mw.title.getCurrentTitle().namespace
end
local function inPageNs()
return nsId() == cfg.pageNs
end
local function tracked()
local ns = nsId()
return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828)
end
local function categories(flags)
if not flags or next(flags) == nil or not tracked() then return '' end
local out = {}
for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do
if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end
end
return table.concat(out)
end
local function langDir(lang)
local base, rest = lang:lower():match('^(%a+)(.*)$')
if not base then return nil end
if rest:find('latn', 1, true) or rest:find('cyrl', 1, true) then return 'ltr' end
if RTL_LANGS[base] or rest:find('arab', 1, true) or rest:find('hebr', 1, true) then return 'rtl' end
return 'ltr'
end
---------------------------------------------------------------------------
-- ارقام
---------------------------------------------------------------------------
local FA = { [0] = '۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹' }
local function convertDigits(s, mode)
if s == '' or mode == 'keep' then return s end
if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end
if mode == 'latin' then
s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end)
s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end)
return s
end
s = s:gsub('%d', function(d) return FA[tonumber(d)] end)
s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end)
return s
end
local function numberArg(args, names, digits)
local v = pick(args, names)
if not v then return '' end
return convertDigits(v, digits)
end
local function intArg(v, lo, hi)
local n = tonumber((convertDigits(v, 'latin')))
if not n or n ~= n or n < lo or n > hi then return nil end
return math.floor(n)
end
---------------------------------------------------------------------------
-- اندازههای CSS
---------------------------------------------------------------------------
local function splitLength(v)
if not v or v == '' then return nil end
v = convertDigits(v, 'latin')
v = v:gsub('٪', '%%'):gsub('٫', '.')
local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$')
if not num then return nil end
num = tonumber(num)
if not num or num > 10000 then return nil end
unit = UNITS[unit:lower()]
if not unit then return nil end
return num, unit
end
local function parseLength(v)
local num, unit = splitLength(v)
if not num then return nil end
return string.format('%g', num) .. unit
end
---------------------------------------------------------------------------
-- شمارهگذاری خودکار
---------------------------------------------------------------------------
local function newCtx(args, kind, flags)
local ctx = { digits = 'fa', count = 0, start = 1, side = 'l', mesra = (kind == 'mesra') }
local d = pick(args, N.digits)
if d then
ctx.digits = DIGITS[d:lower()]
if not ctx.digits then ctx.digits = 'fa'; flags.unknown = true end
end
local s = pick(args, N.start)
if s then
local n = intArg(s, -1e9, 1e9)
if n then ctx.start = n else flags.unknown = true end
end
local sd = pick(args, N.side)
if sd then
local a = ALIGN[sd:lower()]
if a then ctx.side = SIDE[a] else flags.unknown = true end
end
local m = pick(args, N.numbering)
if m then
local n = intArg(m, 0, 1e6)
if n then
if n >= 1 then ctx.every = n end
else
local b = bool(m, nil)
if b == true then ctx.every = 1
elseif b == nil then flags.unknown = true end
end
end
return ctx
end
local function nextNumber(ctx)
if not ctx.every then return nil end
local n = ctx.start + ctx.count
ctx.count = ctx.count + 1
if n % ctx.every ~= 0 then return nil end
return convertDigits(string.format('%d', n), ctx.digits)
end
local function applyAuto(d, ctx, counted, noCenter)
if not counted then return end
local n = nextNumber(ctx)
if not n then return end
if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end
local side = ctx.side
if noCenter and side == 'c' then side = 'l' end
d[side] = n
end
---------------------------------------------------------------------------
-- ساخت HTML
---------------------------------------------------------------------------
local function joinStyle(...)
local parts = {}
for i = 1, select('#', ...) do
local v = select(i, ...)
if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end
end
return table.concat(parts, ';')
end
local function alignClass(j, which)
if not j or j == 'center' or j == 'justify' then return nil end
if j == 'edge' then return (which == 1) and 'poem-a-start' or 'poem-a-end' end
if j == 'inner' then return (which == 1) and 'poem-a-end' or 'poem-a-start' end
return ALIGN_CLASS[j]
end
local function withClass(base, extra)
if extra then return base .. ' ' .. extra end
return base
end
local function charLen(s)
s = s:gsub('\127[^\127]*\127', ''):gsub('<[^>]*>', ''):gsub('%[%[[^%]|]*|', ''):gsub('[%[%]]', '')
return #(s:gsub('[\128-\191]', ''))
end
local function effectiveJust(j, a, b)
if j == 'justify' then
local la, lb = charLen(a), charLen(b)
local hi = math.max(la, lb)
if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return 'center' end
end
return j
end
local function td(class, colspan, style, content)
local s = '<td class="' .. class .. '"'
if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end
if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end
return s .. '>' .. (content or '') .. '</td>'
end
local function hemi(text)
return '<span class="beyt">' .. text .. '</span>'
end
local function rowOpen(d, kind, noId)
local cls = 'poem-row ' .. kind
if d.class then cls = cls .. ' ' .. d.class end
local id = ''
if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end
return '<tr class="' .. esc(cls) .. '"' .. id .. '>'
end
-- {{بیت}}
local function rowBayt(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2)
local js = (j == 'justify') and JUSTIFY_STYLE or nil
local mid = d.c
if mid == '' and d.sep and d.sep ~= '' then
mid = '<span class="poem-sep">' .. d.sep .. '</span>'
end
return rowOpen(d, 'poem-bayt')
.. td('poem-num poem-num-r', 1, nil, d.r)
.. td(withClass('b poem-m poem-m1', alignClass(j, 1)), 2, joinStyle(js, d.style), hemi(m1))
.. td('poem-num poem-num-c', 1, d.style, mid)
.. td(withClass('b poem-m poem-m2', alignClass(j, 2)), 2, joinStyle(js, d.style), hemi(d.m2))
.. td('poem-num poem-num-l', 1, nil, d.l)
.. '</tr>'
end
local function boxWidth(indent)
local num, unit = indent:match('^([%d%.]+)(.*)$')
if unit == '%' then
return string.format('%g%%', math.max(100 - tonumber(num), 10))
end
return 'calc(100% - ' .. indent .. ')'
end
-- {{بیت۱}} و {{بیت نردبانی}}
local function rowStagger(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
-- ترازبندی: پیشفرض stagger = edge. اگر کاربر «پهن» خواسته، آن را به
-- «لبه» فرو میافکنیم چون کشیدهکردن واژهها در بیت پلکانی فاصلههای
-- بیجهت میسازد.
local j = d.just or cfg.justDefault.stagger
if j == 'justify' then j = 'edge' end
j = effectiveJust(j, d.m1, d.m2)
local js = (j == 'justify') and JUSTIFY_STYLE or nil
local s1, s2 = joinStyle(js, d.style), joinStyle(js, d.style)
local c1 = withClass('b poem-m poem-m1', alignClass(j, 1))
local c2 = withClass('b poem-m poem-m2', alignClass(j, 2))
if d.fixed then
local bw = boxWidth(d.indent)
return rowOpen(d, 'poem-stagger poem-stagger-1')
.. td('poem-num poem-num-r', 1, nil, d.r)
.. td(c1, 5, s1,
'<div class="poem-st" style="width:' .. bw .. ';margin-inline-end:auto">' .. hemi(m1) .. '</div>')
.. td('poem-num', 1, nil, '')
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num', 1, nil, '')
.. td(c2, 5, s2,
'<div class="poem-st" style="width:' .. bw .. ';margin-inline-start:auto">' .. hemi(d.m2) .. '</div>')
.. td('poem-num poem-num-l', 1, nil, d.l)
.. '</tr>'
end
local sp = 'width:' .. d.indent
return rowOpen(d, 'poem-stagger poem-stagger-1')
.. td('poem-num poem-num-r', 1, nil, d.r)
.. td(c1, 3, s1, hemi(m1))
.. td('poem-indent', 2, sp, '')
.. td('poem-num', 1, nil, '')
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num', 1, nil, '')
.. td('poem-indent', 2, sp, '')
.. td(c2, 3, s2, hemi(d.m2))
.. td('poem-num poem-num-l', 1, nil, d.l)
.. '</tr>'
end
-- {{مصراع}}
local function rowMesra(d)
local align = d.align or 'center'
local content = d.text
if d.blank and content == '' then content = cfg.nbsp end
local style = d.style or ''
local cellClass = 'b poem-m'
local cs = { 2, 3, 2 }
if align ~= 'center' then
cs = { 1, 5, 1 }
cellClass = cellClass .. ' poem-m-' .. align
if d.indent then
style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style
end
end
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], nil, d.r)
.. td(cellClass, cs[2], style, content)
.. td('poem-num poem-num-l', cs[3], nil, d.l)
.. '</tr>'
end
local function rowGap(d)
local style = d.height and ('height:' .. d.height) or nil
local content = d.height and '' or cfg.nbsp
return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">'
.. td('poem-gapcell', 7, style, content) .. '</tr>'
end
local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra }
local function sizerRow()
return '<tr class="poem-sizer">'
.. '<td class="poem-s"></td><td></td><td></td>'
.. '<td class="poem-g"></td><td></td><td></td>'
.. '<td class="poem-s"></td></tr>'
end
local function openHtml(o)
local look = o.look
local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') .. (o.stack and ' poem-stack' or '')
if look and look.class then cls = cls .. ' ' .. look.class end
if o.class then cls = cls .. ' ' .. o.class end
local div = '<div class="' .. esc(cls) .. '"'
if o.lang then div = div .. ' lang="' .. o.lang .. '"' end
if o.dir then div = div .. ' dir="' .. o.dir .. '"' end
local font = look and look.style or nil
local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '')
if o.fixed then
local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style)
return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls
.. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow()
end
local style = 'margin: 0 auto;'
if font then style = style .. font .. ';' end
if o.style then style = style .. ' ' .. o.style end
return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">'
end
---------------------------------------------------------------------------
-- شعر چندصفحهای
---------------------------------------------------------------------------
local function joinText(a, b, sep)
if a == '' then return b end
if b == '' then return a end
return a .. sep .. b
end
local function mergeRow(head, tail, sep)
for _, f in ipairs({ 'm1', 'm2', 'text' }) do
if head[f] ~= nil or tail[f] ~= nil then
head[f] = joinText(head[f] or '', tail[f] or '', sep)
end
end
for _, f in ipairs({ 'r', 'c', 'l' }) do
if tail[f] and tail[f] ~= '' then head[f] = tail[f] end
end
head.style = head.style or tail.style
head.class = head.class or tail.class
head.id = head.id or tail.id
return head
end
local function flushHtml()
local c = state.carry
if not c then return '' end
state.carry = nil
return RENDER[c.kind](c.d)
end
local function plain(html)
if not inPageNs() then
local pre = flushHtml()
if pre ~= '' then return pre .. '\n' .. html end
end
return html
end
local function emit(kind, d, opt, flags)
opt = opt or {}
if inPageNs() then
if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end
if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end
return RENDER[kind](d)
end
if not cfg.mergeRows then return RENDER[kind](d) end
local out = ''
local carry = state.carry
state.carry = nil
if carry then
if opt.continued and carry.kind == kind then
d = mergeRow(carry.d, d, opt.joiner or ' ')
else
out = RENDER[carry.kind](carry.d)
if opt.continued then flags.unknown = true end
end
end
if opt.unfinished then
state.carry = { kind = kind, d = d }
return out ~= '' and out or SKIP
end
local html = RENDER[kind](d)
if out ~= '' then return out .. '\n' .. html end
return html
end
---------------------------------------------------------------------------
-- خواندن پارامترها
---------------------------------------------------------------------------
local function rowOpts(args, flags)
local opt = {}
local u = pick(args, N.unfinished)
if u then
local b = bool(u, nil)
if b == nil then flags.unknown = true else opt.unfinished = b end
end
local c = pick(args, N.continued)
if c then
local w = WHICH[c:lower()]
if w then
opt.continued, opt.which = true, w
else
local b = bool(c, nil)
if b == nil then flags.unknown = true else opt.continued = b end
end
end
local j = pick(args, N.joiner)
if j then
opt.joiner = JOIN[j:lower()]
if opt.joiner == nil then flags.unknown = true end
end
return opt
end
local function parseSep(v, flags)
local lv = v:lower()
if lv == 'نه' or lv == 'خیر' or lv == 'بدون' or lv == 'ندارد' or lv == 'no' then return '' end
if lv == 'بله' or lv == 'خودکار' or lv == 'yes' or lv == 'auto' then return cfg.ornament end
if #v <= 24 and not v:find('[<>{}|%[%]"\n]') then return v end
flags.unknown = true
return cfg.separator
end
local function parseOpen(args, flags)
local o = {}
local isText = present(args, N.text)
for i = 1, 2 do
local v = args[i]
if v and v ~= '' then
local lv = v:lower()
if v == 'نستعلیق' or lv == 'nastaliq' then
o.nastaliq = true
elseif v == 'ادامه' or lv == 'continue' then
if isText then flags.unknown = true else o.cont = 'prev' end
else
flags.unknown = true
end
end
end
if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k then
if not isText and k == 'next' then flags.unknown = true; k = nil end
else
local b = bool(c, nil)
if b == nil then flags.unknown = true
elseif b then
if isText then flags.unknown = true else k = 'prev' end
end
end
if k then o.cont = k end
end
o.style = pick(args, N.style)
o.class = pick(args, N.class)
local lang = pick(args, N.lang)
if lang then
if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end
end
local dir = pick(args, N.dir)
if dir then
dir = dir:lower()
if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end
end
if o.lang and not o.dir then o.dir = langDir(o.lang) end
o.kind = 'bayt'
local k = pick(args, N.kind)
if k then
o.kind = KIND[k:lower()]
if not o.kind then o.kind = 'bayt'; flags.unknown = true end
end
local a = pick(args, N.align)
if a then
o.align = ALIGN[a:lower()]
if not o.align then flags.unknown = true end
end
local g = pick(args, N.gap)
if g then
o.gap = parseLength(g)
if not o.gap then flags.unknown = true end
end
local ind = pick(args, N.indent)
if ind then
o.indent = parseLength(ind)
if not o.indent then flags.unknown = true end
end
o.unitNum, o.unitUnit = splitLength(pick(args, N.unit))
if not o.unitNum then
o.unitNum, o.unitUnit = splitLength(cfg.mesraUnit)
if pick(args, N.unit) then flags.unknown = true end
end
local st = pick(args, N.stanza)
if st then
if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end
end
local jv = pick(args, N.just)
if jv then
o.just = JUST[jv:lower()]
if not o.just then flags.unknown = true end
end
local sv = pick(args, N.sep)
if sv then o.sep = parseSep(sv, flags) else o.sep = cfg.separator end
local stack = true
local ph = pick(args, N.phone)
if ph then
local m = PHONE[ph:lower()]
if m == 'two' then stack = false elseif not m then flags.unknown = true end
end
o.stack = stack and o.just ~= 'justify'
local w = pick(args, N.width)
if w then
local lw = w:lower()
if lw == 'خودکار' or lw == 'auto' then
o.fixed = false
else
o.width = parseLength(w)
if o.width then o.fixed = true else flags.unknown = true end
end
end
local mx = pick(args, N.maxwidth)
if mx then
local lm = mx:lower()
if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then
o.maxWidth = 'none'
else
o.maxWidth = parseLength(mx)
if not o.maxWidth then flags.unknown = true end
end
end
if o.fixed == nil then
o.fixed = (cfg.defaultWidth ~= 'auto') and (o.just ~= 'justify')
end
if o.fixed then
local explicit = (o.width ~= nil)
o.width = o.width or cfg.defaultWidth
o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth)
end
return o
end
local function parseClose(args, flags)
local cont = false
local v = args[1]
if v and v ~= '' then
if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end
end
local c = pick(args, N.cont)
if c then
local k = CONT[c:lower()]
if k == 'next' or k == 'both' then
cont = true
elseif k == 'prev' then
flags.unknown = true
else
local b = bool(c, nil)
if b == nil then flags.unknown = true elseif b then cont = true end
end
end
return cont
end
---------------------------------------------------------------------------
-- حالت «متن»
---------------------------------------------------------------------------
local function parseLine(raw, freeVerse)
local t = {}
local s = raw
if freeVerse then
t.level = #s:match('^:*')
s = s:sub(t.level + 1)
end
if s:sub(1, 2) == '<>' then
t.align = 'center'
s = s:sub(3)
elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then
t.align = 'right'
s = s:sub(3)
end
s = mw.text.trim(s)
local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$')
if pre then
t.pre = pre
s = rest
end
local body, post = s:match('^(.-)%s*>>>%s*(.+)$')
if body then
t.post = post
s = body
end
t.text = s
return t
end
local function parseDirective(line)
local rest = mw.text.trim(line:sub(2))
local lrest = rest:lower()
for _, key in ipairs(KIND_KEYS) do
if lrest:sub(1, #key) == key then
local after = rest:sub(#key + 1)
if after == '' or after:match('^%s') then
local dd, bad = { kind = KIND[key] }, false
for k, v in after:gmatch('(%S+)=(%S+)') do
local kk, lv = k:lower(), v:lower()
if kk == 'تورفتگی' or kk == 'indent' then
dd.indent = parseLength(v)
if not dd.indent then bad = true end
elseif kk == 'تراز' or kk == 'align' then
dd.align = ALIGN[lv]
if not dd.align then bad = true end
elseif kk == 'ترازبندی' or kk == 'justify' then
dd.just = JUST[lv]
if not dd.just then bad = true end
else
bad = true
end
end
return dd, bad
end
end
end
return nil, true
end
local function renderLines(text, o, ctx, ropt, flags)
local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just }
local items, pending, pendingCur, needGap = {}, nil, nil, false
local function info(c)
local twoH = (c.kind ~= 'mesra')
local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger')
local indent
if c.kind == 'stagger' then indent = c.indent or cfg.defaultIndent
elseif c.kind == 'ladder' then indent = c.indent or cfg.ladderIndent end
return twoH, rowKind, indent
end
local function numbers(d, t)
if not t then return end
if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end
if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end
end
local function makeBayt(t1, t2, c)
local _, _, indent = info(c)
local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '',
r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just, sep = ctx.sep }
numbers(d, t1)
numbers(d, t2)
return d
end
local function makeMesra(t, c, free)
local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed }
d.align = t.align or c.align or (free and 'right' or 'center')
if free and (t.level or 0) > 0 and d.align ~= 'center' then
d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit
end
numbers(d, t)
return d
end
local function push(kind, d, counted)
items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 }
needGap = false
end
local function flushPending()
if pending then
push('mesra', makeMesra(pending, pendingCur, false), false)
pending = nil
end
end
local src = text:gsub('\r\n?', '\n')
for raw in (src .. '\n'):gmatch('([^\n]*)\n') do
local line = mw.text.trim(raw)
if line == '' then
flushPending()
if #items > 0 then needGap = true end
elseif line:sub(1, 1) == '@' then
local dd, bad = parseDirective(line)
flushPending()
if dd then
cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just }
end
if bad then flags.unknown = true end
else
local twoH, rowKind = info(cur)
local isFirst = (#items == 0 and not pending)
local t = parseLine(line, not twoH)
if not twoH then
push('mesra', makeMesra(t, cur, true), ctx.mesra)
else
local handled = false
if ropt.continued and isFirst then
local mode = ropt.which or 'auto'
if mode == 'auto' then
local c = state.carry
mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both'
end
if mode == 'first' then
push(rowKind, makeBayt(t, nil, cur), true); handled = true
elseif mode == 'second' then
push(rowKind, makeBayt(nil, t, cur), true); handled = true
end
end
if not handled then
if pending then
push(rowKind, makeBayt(pending, t, pendingCur), true)
pending = nil
else
pending, pendingCur = t, cur
end
end
end
end
end
if pending then
local _, rowKind = info(pendingCur)
if ropt.unfinished then
push(rowKind, makeBayt(pending, nil, pendingCur), true)
else
push('mesra', makeMesra(pending, pendingCur, false), false)
end
pending = nil
end
for i, it in ipairs(items) do
if it.counted and not (i == 1 and ropt.continued) then
applyAuto(it.d, ctx, true, it.kind ~= 'bayt')
end
end
local out = {}
for i, it in ipairs(items) do
if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end
local ro = {}
if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end
if i == #items and ropt.unfinished then ro.unfinished = true end
local h = emit(it.kind, it.d, ro, flags)
if h ~= SKIP and h ~= '' then out[#out + 1] = h end
end
return table.concat(out, '\n')
end
---------------------------------------------------------------------------
-- نقطههای ورود
---------------------------------------------------------------------------
local FONT_PROPS = {
['font-family'] = true, ['font-size'] = true, ['font-style'] = true, ['font-weight'] = true,
['font-variant'] = true, ['font'] = true, ['line-height'] = true, ['letter-spacing'] = true,
['word-spacing'] = true, ['text-rendering'] = true, ['font-feature-settings'] = true,
}
local function nastaliqLook(frame)
local ok, res = pcall(function()
return frame:expandTemplate{ title = cfg.nastaliqTemplate, args = { 'x' } }
end)
if ok and type(res) == 'string' then
local prefix = mw.text.trim(res:match('^(.-)<%a') or '')
local attrs = res:match('<%a+([^>]*)>')
if attrs then
local cls = attrs:match('class%s*=%s*"([^"]*)"') or attrs:match("class%s*=%s*'([^']*)'")
local sty = attrs:match('style%s*=%s*"([^"]*)"') or attrs:match("style%s*=%s*'([^']*)'")
if cls then cls = mw.text.trim((cls:gsub('[^%w%s_%-\128-\255]', ''))) end
local keep = {}
for decl in (sty or ''):gmatch('[^;]+') do
local prop = mw.text.trim(decl:match('^([^:]+):') or ''):lower()
if FONT_PROPS[prop] then keep[#keep + 1] = mw.text.trim(decl) end
end
if (cls and cls ~= '') or #keep > 0 or prefix ~= '' then
return {
prefix = prefix,
class = (cls and cls ~= '') and cls or 'nastaliq',
style = (#keep > 0) and table.concat(keep, ';') or nil,
}
end
end
end
return { prefix = '', class = 'nastaliq', style = 'font-family:' .. cfg.nastaliqFonts }
end
local function open(frame)
local args, flags = getArgs(frame), {}
local o = parseOpen(args, flags)
local isText = present(args, N.text)
local page = inPageNs()
if hasUnknown(args, 2) then flags.unknown = true end
local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both')
local ctx
if joins and state.ctx then
ctx = state.ctx
else
ctx = newCtx(args, o.kind, flags)
ctx.fixed, ctx.just, ctx.sep = o.fixed, o.just, o.sep
end
local parts = {}
if not joins then
if o.nastaliq then o.look = nastaliqLook(frame) end
parts[#parts + 1] = frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } }
.. ((o.look and o.look.prefix) or '') .. openHtml(o)
state.carry = nil
end
state.open, state.ctx = true, ctx
if isText then
local ropt = rowOpts(args, flags)
if o.newStanza and joins and not ropt.continued then
parts[#parts + 1] = plain(rowGap({ height = o.gap }))
end
local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags)
if rows ~= '' then parts[#parts + 1] = rows end
local stay = (not page) and (o.cont == 'next' or o.cont == 'both')
if not stay then
local tail = flushHtml()
if tail ~= '' then parts[#parts + 1] = tail end
parts[#parts + 1] = CLOSE_HTML
state.open, state.ctx = false, nil
end
end
if #parts == 0 then parts[1] = SKIP end
return table.concat(parts, '\n') .. categories(flags)
end
local function close(frame)
local args, flags = getArgs(frame), {}
local cont = parseClose(args, flags)
if hasUnknown(args, 1) then flags.unknown = true end
if cont and not inPageNs() then
return SKIP .. categories(flags)
end
local html = flushHtml()
state.open, state.ctx = false, nil
if html ~= '' then html = html .. '\n' end
return html .. CLOSE_HTML .. categories(flags)
end
local function commonRow(args, ctx, flags)
local d = {
r = numberArg(args, N.right, ctx.digits),
c = numberArg(args, N.center, ctx.digits),
l = numberArg(args, N.left, ctx.digits),
style = pick(args, N.style),
class = pick(args, N.class),
id = pick(args, N.id),
fixed = ctx.fixed,
just = ctx.just,
sep = ctx.sep,
}
local sv = pick(args, N.sep)
if sv then d.sep = parseSep(sv, flags) end
local jv = pick(args, N.just)
if jv then
d.just = JUST[jv:lower()]
if not d.just then flags.unknown = true; d.just = ctx.just end
end
return d
end
local function bayt(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, false)
local html = emit('bayt', d, opt, flags)
return html .. categories(flags)
end
local function staggerFn(defaultIndent)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.m1, d.m2 = clean(args[1]), clean(args[2])
if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then
flags.incomplete = true
end
local raw = pick(args, N.indent)
d.indent = parseLength(raw)
if raw and not d.indent then flags.unknown = true end
d.indent = d.indent or defaultIndent
if hasUnknown(args, 2) then flags.unknown = true end
applyAuto(d, ctx, not opt.continued, true)
local html = emit('stagger', d, opt, flags)
return html .. categories(flags)
end
end
local function mesra(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local opt = rowOpts(args, flags)
local d = commonRow(args, ctx, flags)
d.text = clean(args[1])
local a = pick(args, N.align)
if a then
d.align = ALIGN[a:lower()]
if not d.align then flags.unknown = true end
end
local raw = pick(args, N.indent)
d.indent = parseLength(raw)
if raw and not d.indent then flags.unknown = true end
if hasUnknown(args, 1) then flags.unknown = true end
applyAuto(d, ctx, ctx.mesra and not opt.continued, true)
local html = emit('mesra', d, opt, flags)
return html .. categories(flags)
end
local function emptyFn(kind)
return function(frame)
local args = getArgs(frame)
local ctx = state.ctx or defaultCtx()
local flags = {}
local d = commonRow(args, ctx, flags)
local raw = pick(args, N.height)
d.height = parseLength(raw)
if raw and not d.height then flags.unknown = true end
if hasUnknown(args, 0) then flags.unknown = true end
local html
if d.height then
html = rowGap(d)
elseif kind == 'bayt' then
d.m1, d.m2 = '', ''
html = rowBayt(d)
else
d.text, d.blank = '', true
html = rowMesra(d)
end
return plain(html) .. categories(flags)
end
end
local function safe(fn)
return function(frame)
local ok, res = pcall(fn, frame)
if ok then return res end
return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>'
.. categories({ error = true })
end
end
p.open = safe(open)
p.close = safe(close)
p.bayt = safe(bayt)
p.stagger = safe(staggerFn(cfg.defaultIndent))
p.ladder = safe(staggerFn(cfg.ladderIndent))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
hx4zveqnbl0cfc9939epo0hmr0cuj3g
برگه:بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۹
104
97061
304049
304012
2026-10-12T18:58:42Z
Hanooz
17889
تست
304049
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />{{سرصفحهمستمر||بدایع شیخ سعدی|۵}}</noinclude><section begin="p19-20" />
{{وسط|{{اندازه قلم|110%|'''۴'''}}}}
{{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}}
{{شعر۲|نوع=ضربدری|ادامه=بعد|ترازبندی=پهن|متن=
با جوانی سرخوش است این پیر بی تدبیر را
جهل باشد با جوانان پنجه کردن پیر را
من که با موئی بقوّت بر نیایم ایعجب
با یکی<ref>I(a); کسی, for یکی.</ref> افتاده ام کو بگسلد زنجیر را
چون کمان در بازو آرد سروقدّ سیمتن
آرزویم میکند<ref>L, Z; کشد, for کند.</ref> کآماج باشم تیر<ref>This line is omitted in R.</ref> را
میرود تا در کمند افتد بپای خویشتن
گر بر آن دست<ref>L, Z; تیر, for دست.</ref> و کمان چشم اوفتد نخجیر را<ref>In R the second hemistich runs as follows:—
<div dir="rtl">«چون تو چوگان میزنی جرمی نباشد تیر را».</div></ref>
کس ندید است آدمیزاد<ref>R; را, for زاد.</ref> از تو شیرین تر سخن
شکر از پستان مادر خوردهٔ یا شیر را
روز بازار جوانی پنج<ref>C, Y(a), Y(b); چند, for پنج.</ref> روزی بیش نیست
نقد را باش ای پسر کآفت بود تأخیر را
ایکه گفتی دیده از دیدار بت رویان<ref>I(a), R, L, C, Y(a), Y(b); مه, for بت.</ref> بدوز
هر چه گوئی چاره دانم کرد جز تقدیر را
}}<noinclude>{{خطکش}}
{{پانویس|چپچین=بله}}</noinclude>
pkzrbtbmhkdxkzv79hrdnwanxecajiu
304052
304049
2026-10-12T19:23:52Z
Hanooz
17889
نسخهٔ [[Special:Diff/304049|304049]] از [[Special:Contributions/Hanooz|Hanooz]] ([[User talk:Hanooz|بحث]]) خنثی شد
304052
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />{{سرصفحهمستمر||بدایع شیخ سعدی|۵}}</noinclude><section begin="p19-20" />
{{وسط|{{اندازه قلم|110%|'''۴'''}}}}
{{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}}
{{شعر۲|نوع=ضربدری|ادامه=بعد|متن=
با جوانی سرخوش است این پیر بی تدبیر را
جهل باشد با جوانان پنجه کردن پیر را
من که با موئی بقوّت بر نیایم ایعجب
با یکی<ref>I(a); کسی, for یکی.</ref> افتاده ام کو بگسلد زنجیر را
چون کمان در بازو آرد سروقدّ سیمتن
آرزویم میکند<ref>L, Z; کشد, for کند.</ref> کآماج باشم تیر<ref>This line is omitted in R.</ref> را
میرود تا در کمند افتد بپای خویشتن
گر بر آن دست<ref>L, Z; تیر, for دست.</ref> و کمان چشم اوفتد نخجیر را<ref>In R the second hemistich runs as follows:—
<div dir="rtl">«چون تو چوگان میزنی جرمی نباشد تیر را».</div></ref>
کس ندید است آدمیزاد<ref>R; را, for زاد.</ref> از تو شیرین تر سخن
شکر از پستان مادر خوردهٔ یا شیر را
روز بازار جوانی پنج<ref>C, Y(a), Y(b); چند, for پنج.</ref> روزی بیش نیست
نقد را باش ای پسر کآفت بود تأخیر را
ایکه گفتی دیده از دیدار بت رویان<ref>I(a), R, L, C, Y(a), Y(b); مه, for بت.</ref> بدوز
هر چه گوئی چاره دانم کرد جز تقدیر را
}}<noinclude>{{خطکش}}
{{پانویس|چپچین=بله}}</noinclude>
6d0tvt4f1u7t27mgi88ra3bbhmm9bm0