ویکینبشته
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
1356
304000
302592
2026-10-11T20:24:47Z
Hanooz
17889
304000
wikitext
text/x-wiki
<includeonly><span class="poem"><table class="{{#ifeq:{{{1|}}}|نستعلیق|nastaliq}}" style="margin: 0 auto; {{{سبک|}}}"><tr style="text-align:center;"><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr></includeonly><noinclude>
{{توضیحات}}
[[رده:الگوهای شعر]]
</noinclude>
3r4oxlvxht606tvlp2nkk2tak6yxduc
الگو:پایان شعر
10
1357
304007
302591
2026-10-11T20:24:59Z
Hanooz
17889
304007
wikitext
text/x-wiki
</table></span><noinclude>{{توضیحات|الگو:شعر/توضیحات}}[[Category:الگوهای شعر|پایان شعر]]{{pp-template}}</noinclude>
lfru2wp06wykhdkpvicqbuhm36jaoi7
الگو:بیت
10
2016
304006
302598
2026-10-11T20:24:57Z
Hanooz
17889
304006
wikitext
text/x-wiki
<includeonly><tr style="text-align:center;">
<td style="text-align:left;">{{کوچکتر۲|{{{ش|}}}}}<span style="display:inline-block; width:2em;"> </span></td>
<td colspan="2" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt">{{{1}}}</span></td>
<td style="width:2em;{{{سبک|}}}">{{کوچکتر۲|{{{شو|}}}}}</td>
<td colspan="2" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt">{{{2}}}</span></td>
<td style="text-align:right;"><span style="display:inline-block; width:2em;"> </span>{{کوچکتر۲|{{{شچ|}}}}}</td></tr></includeonly><noinclude>
{{توضیحات|الگو:شعر/توضیحات}}
---
[[رده:الگوهای شعر|ب]]
{{pp-template|small=yes}}</noinclude>
l8abae220rfnxu5khajsbdfr7qenv2t
الگو:شعر/توضیحات
10
21606
304009
303729
2026-10-11T20:27:11Z
Hanooz
17889
304009
wikitext
text/x-wiki
{{زیرصفحه توضیحات}}
این الگو برای نمایش اشعار به کار میرود.
=== کاربرد ===
قبل از بیتهای شعر باید از الگوی <nowiki>{{شعر}}</nowiki> استفاده کنید.برای هر بیت از شعر به صورت زیر از الگوی '''ب''' (مخفف ''بیت'') استفاده کنید:
<nowiki>{{ب|مصراع اول|مصراع دوم}}</nowiki>
در پایان شعر نیز باید از الگوی <nowiki>{{پایان شعر}}</nowiki> استفاده کنید.
این الگو برای نوشتن شعر در ویکینبشته به کار میآید.
=== نمونه ===
دستورات زیر:
<pre>
{{شعر}}
{{ب|دوش وقت سحر از غصه نجاتم دادند|واندر آن ظلمت شب آب حیاتم دادند}}
{{م|بیخود از شعشهٔ پرتو ذاتم گردند}}
{{م|باده از جام تجلی صفاتم دادند}}
{{پایان شعر}}
</pre>
=== نمونه نستعلیق ===
*نکته: در صورت استفاده از نستعلیق {{الگو|دارای نستعلیق}} را استفاده کنید تا اگر کاربر فونت نستعلیق را ندارد از آن را نصب و استفاده نماید.
{{دارای نستعلیق}}
دستورات زیر:
<pre>
{{شعر|نستعلیق}}
{{ب|دوش وقت سحر از غصه نجاتم دادند|واندر آن ظلمت شب آب حیاتم دادند}}
{{م|بیخود از شعشهٔ پرتو ذاتم گردند}}
{{م|باده از جام تجلی صفاتم دادند}}
{{پایان شعر}}
</pre>
چنین نتیجه میدهد:
{{شعر|نستعلیق}}
{{ب|دوش وقت سحر از غصه نجاتم دادند|واندر آن ظلمت شب آب حیاتم دادند}}
{{م|بیخود از شعشهٔ پرتو ذاتم گردند}}
{{م|باده از جام تجلی صفاتم دادند}}
{{پایان شعر}}
=== نمایش ابیات به شکل نامتقارن ===
* به منظور نمایش ابیات به صورت ضربدری و نامتقارن از الگوی {{الگو|ب۱}} بجای {{الگو|ب}} استفاده کنید:
<pre>
{{شعر}}
{{ب۱|آن همه شوکت و ناموس شهان آخر کار|چند سطری است که بر صفحهٔ دفتر گذرد}}
{{ب۱|عاقبت در دو سه خط جمع شود از بد و نیک|آنچه یک عمر به دارا و سکندر گذرد}}
{{پایان شعر}}
</pre>
نتیجه:
{{شعر}}
{{ب۱|آن همه شوکت و ناموس شهان آخر کار|چند سطری است که بر صفحهٔ دفتر گذرد}}
{{ب۱|عاقبت در دو سه خط جمع شود از بد و نیک|آنچه یک عمر به دارا و سکندر گذرد}}
{{پایان شعر}}
<pre>
{{شعر}}
{{ب۱|ش=۲۵۵|شچ=۲۵۶|تورفتگی=5%|یوسف گمگشته بازآید بکنعان غم مخور|کلبهٔ احزان شود روزی گلستان غم مخور}}
{{ب۱|تورفتگی=5%|ای دل غمدیده حالت به شود دل بد مکن|وین سر شوریده بازآید بسامان غم مخور}}
{{پایان شعر}}
</pre>
نتیجه:
{{شعر}}
{{ب۱|ش=۲۵۵|شچ=۲۵۶|تورفتگی=5%|یوسف گمگشته بازآید بکنعان غم مخور|کلبهٔ احزان شود روزی گلستان غم مخور}}
{{ب۱|تورفتگی=5%|ای دل غمدیده حالت به شود دل بد مکن|وین سر شوریده بازآید بسامان غم مخور}}
{{پایان شعر}}
<pre>
{{شعر}}
{{ب|ش=۲۰۱|شو=۲۰۲|شچ=۲۰۳|ابر و باد و مه و خورشید و فلک در کارند|تا تو نانی بکف آریّ و بغفلت نخوری}}
{{ب۱|ش=۲۰۴|شچ=۲۰۵|همه از بهر تو سر گشته و فرمانبردار|شرط انصاف نباشد که تو فرمان نبری}}
{{پایان شعر}}
</pre>
نتیجه:
{{شعر}}
{{ب|ش=۲۰۱|شو=۲۰۲|شچ=۲۰۳|ابر و باد و مه و خورشید و فلک در کارند|تا تو نانی بکف آریّ و بغفلت نخوری}}
{{ب۱|ش=۲۰۴|شچ=۲۰۵|همه از بهر تو سر گشته و فرمانبردار|شرط انصاف نباشد که تو فرمان نبری}}
{{پایان شعر}}
=== ایجاد فاصله بین ابیات و مصراعها ===
هنگامی که بخواهید میان ابیات یا مصراعهای یک قطعه شعر یک خط فاصله ایجاد کنید، میتوانید از الگوهای ذیل استفاده کنید:
* {{الگو|بیت خالی}}
* {{الگو|مصراع خالی}}
=== نکات استفاده ===
* برای نوشتن به شکل نستعلیق با این الگو آن را به صورت <nowiki>{{شعر|نستعلیق}}</nowiki> به کار بگیرید.
* اگر میخواهید در سمت راست یک بیت شمارهٔ آن را درج کنید، از پارامتر «ش» که مخفف «شماره» است استفاده کنید. همچنین برای قرار دادن شماره در وسط یک بیت از پارامتر «شو» و برای گنجاندن شماره در سمت چپ بیت از پارامتر «شچ» استفاده کنید. مثال:
<pre>
{{شعر}}
{{ب|ش=۱۰۰|نیست آواز کسی آنجا که اوست|کی رسد جان کسی آنجا که اوست}}
{{ب|شو=۱۰۱|صد هزاران طور از جان بر ترست|هر چه خواهم گفت او زان برتر است}}
{{ب|شچ=۱۰۲|عقل در سودای او حیران بماند|جان ز عجز انگشت در دندان بماند}}
{{م|ش=۱۰۳|ای مرغ سحر عشق ز پروانه بیاموز}}
{{م|شچ=۱۰۴|کان سوخته را جان شد و آواز نیامد}}
{{پایان شعر}}
</pre>
{{شعر}}
{{ب|ش=۱۰۰|نیست آواز کسی آنجا که اوست|کی رسد جان کسی آنجا که اوست}}
{{ب|شو=۱۰۱|صد هزاران طور از جان بر ترست|هر چه خواهم گفت او زان برتر است}}
{{ب|شچ=۱۰۲|عقل در سودای او حیران بماند|جان ز عجز انگشت در دندان بماند}}
{{م|ش=۱۰۳|ای مرغ سحر عشق ز پروانه بیاموز}}
{{م|شچ=۱۰۴|کان سوخته را جان شد و آواز نیامد}}
{{پایان شعر}}
* اگر میخواهید که برای شعر سبک اضافهای به کار بگیرید از <code>|سبک</code> استفاده کنید.
*:برای رنگ پشتزمینه دلخواه <nowiki>{{شعر|سبک=background: blue}}</nowiki>
*:رنگ دلخواه برای متن <nowiki>{{شعر|سبک=color: white}}</nowiki>
*:برای قرار نگرفتن در وسط <nowiki>{{شعر|سبک=margin: 0}}</nowiki>
نمونهای از ترکیب هر سه:{{سخ}}
<pre>
{{شعر|سبک=background: blue; margin: 0; color: white}}
{{ب|دوش وقت سحر از غصه نجاتم دادند|واندر آن ظلمت شب آب حیاتم دادند}}
{{پایان شعر}}
</pre>
{{شعر|سبک=background: blue; margin: 0; color: white}}
{{ب|دوش وقت سحر از غصه نجاتم دادند|واندر آن ظلمت شب آب حیاتم دادند}}
{{پایان شعر}}
=== الگوهای مرتبط ===
* [[الگو:مصراع]] برای نوشتن تک مصراعها
* [[الگو:بیت]] برای نوشتن بیتها
* [[الگو:پایان شعر]] برای تمام کردن الگو
4kugucwr4rxc2la9pr3r75uk58oqup1
الگو:بدون منبع
10
22527
303997
303000
2026-10-11T18:31:02Z
Hanooz
17889
ترجمه هوش مصنوعی
303997
wikitext
text/x-wiki
{{ambox
|type=content
|text='''منبع این متن مشخص نیست.'''<br />
{{smaller block|لطفاً برای کسب اطلاعات بیشتر و بررسی صحت منبع، [[{{TALKPAGENAME}}|صفحهٔ بحث این صفحه]] را ببینید. منظور از «منبع»، مکانی است که کاربران دیگر بتوانند نسخهای از این اثر را در آن بیابند. ترجیحاً باید نسخهای اسکنشده از اصل اثر باشد که بتوان آن را در [[commons:صفحهٔ اصلی|ویکیانبار]] بارگذاری و در [[راهنما:نمونهخوانی|نمونهخوانی]] کرد. در غیر این صورت، بهتر است نشانی اینترنتی (URL) ارائه شود. اگر هیچیک از این موارد در دسترس نیست، لطفاً دلیل آن را در صفحهٔ بحث توضیح دهید.}}
}}
<includeonly>{{category handler
| all = [[رده:آثار بدون منبع|{{PAGENAME}}]]
| nocat = {{{nocat|}}}
}}</includeonly><noinclude>
{{documentation}}
</noinclude>
0c1krax2pt35pyoh40yywond28f7zyp
الگو:مصراع
10
31044
304003
302595
2026-10-11T20:24:52Z
Hanooz
17889
304003
wikitext
text/x-wiki
<includeonly><tr style="text-align:center;">
<td colspan="2"style="text-align:left;">{{کوچکتر۲|{{{ش|}}}}}<span style="display:inline-block; width:2em;"> </span></td>
<td colspan="3" class="b" style="{{{سبک|}}}">{{{1}}}</td>
<td colspan="2"style="text-align:right;"><span style="display:inline-block; width:2em;"> </span>{{کوچکتر۲|{{{شچ|}}}}}</td>
</tr>
</includeonly><noinclude>
{{توضیحات|الگو:شعر/توضیحات}}
[[رده:الگوهای شعر|ب]]
{{pp-template}}</noinclude>
tw874jsmv4pyr4bgtzdz6cui9n635j8
کاربر:Hanooz/صفحه تمرین
2
38907
303982
303005
2026-10-11T17:00:18Z
Hanooz
17889
جایگزینی صفحه با '1 {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}} 2 {{وزن بدایع سعدی|مفاعیلن|مفاعیلن|فعولن}} 3 {{#invoke:وزن بدایع سعدی|main|مفاعیلن|مفاعیلن|فعولن}}'
303982
wikitext
text/x-wiki
1
{{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}}
2
{{وزن بدایع سعدی|مفاعیلن|مفاعیلن|فعولن}}
3
{{#invoke:وزن بدایع سعدی|main|مفاعیلن|مفاعیلن|فعولن}}
tnyoijg2xc081mak26xmhwjxsd5hlfl
303985
303982
2026-10-11T17:31:25Z
Hanooz
17889
303985
wikitext
text/x-wiki
{{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}}
5ufglv7g9s2mf74ojf9xhkmm09ebvuf
الگو:بیت۱
10
39723
304005
302597
2026-10-11T20:24:56Z
Hanooz
17889
304005
wikitext
text/x-wiki
<includeonly>
<tr style="text-align:center;">
<td style="text-align:left;">{{کوچکتر۲|{{{ش|}}}}}<span style="display:inline-block; width:2em;"> </span></td>
<td colspan="3" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt">{{{1}}}</span></td>
<td colspan="2" width="{{{تورفتگی|20%}}}"></td>
<td></td>
</tr>
<tr style="text-align:center;">
<td></td>
<td colspan="2" width="{{{تورفتگی|20%}}}"></td>
<td colspan="3" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt">{{{2}}}</span></td>
<td style="text-align:right;"><span style="display:inline-block; width:2em;"> </span>{{کوچکتر۲|{{{شچ|}}}}}</td>
</tr>
</includeonly><noinclude>
{{توضیحات|الگو:شعر/توضیحات}}
[[رده:الگوهای شعر|ب]]
{{pp-template|small=yes}}</noinclude>
4vfs56h0cdhpfu4hwg83nfkzunvnjkc
الگو:بیت نردبانی
10
41687
304004
302596
2026-10-11T20:24:54Z
Hanooz
17889
304004
wikitext
text/x-wiki
<includeonly>
<tr style="text-align:center;">
<td width="5%">{{کوچکتر۲|{{{ش|}}}}}<span style="display:inline-block; width:2em;"> </span></td>
<td width="50%" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt">{{{1}}}</span></td>
<td width="45%"></td>
</tr>
<tr style="text-align:center;">
<td width="45%"></td>
<td width="50%" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt">{{{2}}}</span></td>
<td width="5%"><span style="display:inline-block; width:2em;"> </span>{{کوچکتر۲|{{{شچ|}}}}}</td>
</tr>
</includeonly><noinclude>
این الگو شبیه به الگوی بیت است با این تفاوت که ابیات را به شکل نردبانی نشان میدهد. البته فعلاً آزمایشی است.
{{توضیحات|الگو:شعر/توضیحات}}
---
[[رده:الگوهای شعر|ب]]
{{pp-template|small=yes}}</noinclude>
0e5x3lksz0lzmj39fohg5nc3wvk35dy
برگه:The Blind Owl.pdf/۵۶
104
46696
303964
124156
2026-10-11T15:33:04Z
Charan Gill
6444
/* همسنجیشده */
303964
proofread-page
text/x-wiki
<noinclude><pagequality level="4" user="Charan Gill" />{{سصم|۵۶||}}</noinclude>است روی کاغذ بیاورم– چون باین وسیله بهتر میتوانم افکار خودم را مرتب و منظم بکنم– آیا مقصودم نوشتن وصیت نامه است؟ هرگز. چون نه مال دارم که دیوان بخورد و نه دین دارم که شیطان ببرد، وانگهی چه چیزی روی زمین میتواند برایم کوچکترین ارزش را داشته باشد– آنچه که زندگی بوده است از دست دادهام، گذاشتم و خواستم از دستم برود و بعد از آنکه من رفتم، بدرک، میخواهد کسی کاغذ پارههای مرا بخواند، میخواهد هفتاد سال سیاه هم نخواند – من فقط برای این احتیاج نوشتن که عجالتاً برایم ضروری شده است مینویسم– من محتاجم، بیش از پیش محتاجم که افکار خودم را به موجود خیالی خودم، بسایه خودم ارتباط بدهم– این سایه شومی که جلو روشنائی پیهسوز روی دیوار خم شده و مثل اینست آنچه که مینویسم بدقت میخواند و میبلعد– این سایه حتماً بهتر از من میفهمد! فقط با سایه خودم خوب میتوانم حرف بزنم، اوست که مرا وادار بحرف زدن میکند، فقط او میتواند مرا بشناسد، او حتماً میفهمد ... میخواهم عصاره، نه، شراب تلخ زندگی خودم را چکه چکه در گلوی خشک سایهام چکانیده باو بگویم: «این زندگی منست !»
«هر کس دیروز مرا دیده، جوان شکسته و ناخوشی<noinclude></noinclude>
kg9h7q6x0yc5k1ydnbgc0desujloa1x
برگه:The Blind Owl.pdf/۵۷
104
46697
303966
166959
2026-10-11T15:42:20Z
Charan Gill
6444
/* همسنجیشده */
303966
proofread-page
text/x-wiki
<noinclude><pagequality level="4" user="Charan Gill" />{{سصم|۵۷||}}</noinclude>دیده است ولی امروز پیرمرد قوزی میبیند که موهای سفید، چشمهای واسوخته و لب شکری دارد. من میترسم از پنجره اطاقم به بیرون نگاه بکنم، در آینه بخودم نگاه بکنم، چون همه جا سایههای مضاعف خودم را میبینم– اما برای اینکه بتوانم زندگی خودم را برای سایه خمیدهام شرح بدهم باید یک حکایت نقل بکنم– اوه، چقدر حکایتهائی راجع به ایام طفولیت، راجع به عشق، جماع، عروسی و مرگ وجود دارد و هیچکدام حقیقت ندارند– من از قصهها و عبارت پردازی خسته شدهام.–
«من سعی خواهم کرد که این خوشه را بفشارم ولی آیا در آن کمترین اثر حقیقت وجود خواهد داشت یا نه– این را دیگر نمیدانم– من نمیدانم کجا هستم و این تکه آسمان بالای سرم، یا این چند وجب زمینی که رویش نشستهام مال نیشابور یا بلخ و یا بنارس است– در هر صورت من بهیچ چیز اطمینان ندارم.
«من از بس چیزهای متناقض دیده و حرفهای جور بجور شنیدهام و از بسکه دید چشمهایم روی سطح اشیاء مختلف ساییده شده– این قشر نازک و سختی که روح پشت آن پنهان است، حالا هیچ چیز را باور نمیکنم– به ثقل و ثبوت اشیاء، به حقایق آشکار<noinclude></noinclude>
ezfyijkhenqm8regfdwdoeptfr9h6q5
الگو:بیت خالی
10
69657
304002
302594
2026-10-11T20:24:51Z
Hanooz
17889
304002
wikitext
text/x-wiki
<includeonly><tr style="text-align:center;">
<td style="text-align:left;">{{کوچکتر۲|{{{ش|}}}}}<span style="display:inline-block; width:2em;"> </span></td>
<td colspan="2" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt"></span></td>
<td style="width:2em;{{{سبک|}}}">{{کوچکتر۲|{{{شو|}}}}}</td>
<td colspan="2" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt"></span></td>
<td style="text-align:right;"><span style="display:inline-block; width:2em;"> </span>{{کوچکتر۲|{{{شچ|}}}}}</td></tr></includeonly><noinclude>
{{توضیحات|الگو:شعر/توضیحات}}
---
[[رده:الگوهای شعر|بیت خالی]]
{{pp-template|small=yes}}</noinclude>
8o2oqs3dxergtvn977g3dcxuh74anlh
الگو:مصراع خالی
10
69658
304001
302593
2026-10-11T20:24:49Z
Hanooz
17889
304001
wikitext
text/x-wiki
<includeonly><tr style="text-align:center;">
<td colspan="2"style="text-align:left;">{{کوچکتر۲|{{{ش|}}}}}<span style="display:inline-block; width:2em;"> </span></td>
<td colspan="3" class="b" style="{{{سبک|}}}"></td>
<td colspan="2"style="text-align:right;"><span style="display:inline-block; width:2em;"> </span>{{کوچکتر۲|{{{شچ|}}}}}</td>
</tr>
</includeonly><noinclude>
{{توضیحات|الگو:شعر/توضیحات}}
[[رده:الگوهای شعر|ب]]
{{pp-template}}</noinclude>
et864forbvml6qugtbdyxhixv71y7vq
پودمان:Header/attribution data
828
79642
303979
299649
2026-10-11T16:47:56Z
Hanooz
17889
303979
Scribunto
text/plain
-- Persian (fa.wikisource) localization of the English contributor-types
-- config module. Field names (noun, prefix, param_name, special_cases,
-- language_prefix, index) are unchanged, since another module reads this
-- table by these keys - only the user-facing strings were translated.
-- param_name values were kept in Latin script so existing-style template
-- calls (e.g. |author=, |translator=) keep working.
require('strict')
local function anon(text)
text = text or 'ناشناس'
local cat = ''
if mw.title.getCurrentTitle():inNamespaces(0, 114) then
cat = '[[رده:' .. 'آثار نویسندگان ناشناس' .. ']]'
end
return '[[درگاه:متون ناشناس|' .. text .. ']]' .. cat
end
local function anon_nocat(text)
return '[[درگاه:متون ناشناس|' .. (text or 'ناشناس') .. ']]'
end
local contrib_types = {
{noun = 'نویسنده', prefix = 'اثر', param_name = 'author'},
{noun = 'منسوب به', prefix = 'منسوب به', param_name = 'attributed-to', special_cases = {}},
{noun = 'اقتباسکننده', prefix = 'اقتباس از', param_name = 'adapter'},
{noun = 'خلاصهکننده', prefix = 'خلاصه از', param_name = 'abridger'},
{noun = 'تصویرگر', prefix = 'تصویرگری از', param_name = 'illustrator'},
{noun = 'کارگردان', prefix = 'کارگردانی از', param_name = 'director'},
{noun = 'ترانهسرا', prefix = 'ترانه از', param_name = 'lyricist'},
{noun = 'لیبرتونویس', prefix = 'لیبرتو از', param_name = 'librettist'},
{
noun = 'کتاب از',
prefix = 'کتاب از',
param_name = 'book-by',
special_cases = {
['?'] = 'نویسنده نامعلوم',
['unknown'] = 'نویسنده نامعلوم',
['not mentioned'] = 'نویسنده نامعلوم',
['anon'] = anon,
['anonymous'] = anon,
['various'] = 'نویسندههای مختلف'
}
},
{noun = 'آهنگساز', prefix = 'آهنگسازی از', param_name = 'composer'},
{noun = 'تنظیمکننده', prefix = 'تنظیم از', param_name = 'arranger'},
{
noun = 'مترجم',
prefix = 'ترجمهٔ',
param_name = 'translator',
language_prefix = function(language_name)
return 'ترجمهشده از ' .. language_name .. ' توسط'
end,
special_cases = {
['?'] = 'مترجم نامعلوم',
['unknown'] = 'مترجم نامعلوم',
['not mentioned'] = 'مترجم نامعلوم',
['anon'] = anon,
['anonymous'] = anon,
['various'] = 'مترجمهای مختلف',
['wikisource'] = '[[:en:Wikisource:Translations|ویکینبشتهٔ انگلیسی]]'
}
},
{
noun = 'ویراستار',
prefix = 'تصحیح',
param_name = 'editor',
special_cases = {
['?'] = 'ویراستار نامعلوم',
['unknown'] = 'ویراستار نامعلوم',
['not mentioned'] = 'ویراستار نامعلوم',
['anon'] = anon_nocat,
['anonymous'] = anon_nocat,
['various'] = 'ویراستارهای مختلف'
}
},
{noun = 'گردآورنده', prefix = 'گردآوری از', param_name = 'compiler'}
}
for i = 1, #contrib_types do
local noun = contrib_types[i]['noun']
contrib_types[i]['param_name'] = contrib_types[i]['param_name'] or string.gsub(noun, ' ', '-')
contrib_types[i]['special_cases'] = contrib_types[i]['special_cases'] or {
['?'] = noun .. ' نامعلوم',
['unknown'] = noun .. ' نامعلوم',
['not mentioned'] = noun .. ' نامعلوم',
['anon'] = anon,
['anonymous'] = anon,
['various'] = noun .. 'های مختلف'
}
contrib_types[i]['index'] = i
end
return contrib_types
rug0tqmyijybaut2q89nm8jhch92sup
الگو:ایضاً
10
83517
303967
303155
2026-10-11T15:44:40Z
Hanooz
17889
303967
wikitext
text/x-wiki
<includeonly><templatestyles src="ایضاً/styles.css"/><!--
--><span class="__ditto"><span class="__ditto_hidden">{{{1}}}</span><span class="__ditto_text __ditto_text_align_{{#switch:{{{align|{{{3|}}}}}}|l|left=left|r|right=right|j|justify=justify|c|center|#default=center}} __ditto_text_overflow_{{#switch:{{{overflow|}}}|h|hidden=hidden|v|visible|#default=visible}}">{{#switch: {{{tooltip|}}}
| on = {{tooltip|{{{2|„}}}|{{{1}}}}}
| #default = {{{2|„}}}
}}</span></span></includeonly><noinclude>{{documentation}}</noinclude>
ph5hharpzzh70tol8gkcmfv312opfn0
الگو:ریاضی
10
86603
303971
303176
2026-10-11T15:49:23Z
Hanooz
17889
303971
wikitext
text/x-wiki
<templatestyles src="ریاضی/styles.css"/><span class="wst-wikimath-texhtml {{#if:{{{big|}}}{{{size|}}}|wst-wikimath-texhtml-big}}" dir="ltr" {{#if:{{{size|}}}|style="font-size: {{{size}}};"}}>{{{1}}}</span><noinclude>
{{Documentation}}
</noinclude>
l26ad2xo0wytr015prau2yovcbm02kl
الگو:تکفاصله
10
89847
303970
303139
2026-10-11T15:48:41Z
Hanooz
17889
303970
wikitext
text/x-wiki
<onlyinclude><templatestyles src="تکفاصله/styles.css" /><span class="wst-mono">{{{1}}}</span></onlyinclude><noinclude>{{documentation}}</noinclude>
msnfazsbifkrqf5a6ophi6fzi1kt8r9
پودمان:Translations
828
94826
303969
297741
2026-10-11T15:48:24Z
Hanooz
17889
303969
Scribunto
text/plain
require('strict')
local p = {}
local getArgs = require('Module:Arguments').getArgs
local header = require('Module:Header')._header
local lang = require('Module:Lang')._lang
local TableTools = require('Module:TableTools')
local function _translations(args)
local current_frame = mw.getCurrentFrame()
local current_title = mw.title.getCurrentTitle()
args.translated = args.translated or current_title.text
local headerArgs = TableTools.shallowClone(args)
headerArgs.pretitle = 'ترجمههای فارسی '
headerArgs.section = '' -- header distinguishes nil and blank args in some cases
headerArgs.noyearcat = true
headerArgs.noyear = args.year == nil
headerArgs.disambiguation = nil
headerArgs['allow-explicit-formatting'] = args.translated ~= nil
headerArgs.header_class = 'ws-dynlayout-disable'
headerArgs['language-required'] = true
headerArgs.original = nil
headerArgs.translated = nil
local original = (args.original and args.language and lang({language = args.language, text = args.original})) or args.original
local translated = tostring(mw.html.create('i'):wikitext(args.translated))
if args.original and args.original ~= args.translated then
headerArgs.title = original .. ' (' .. translated .. ')'
else
headerArgs.title = original or translated
end
local subNote = mw.html.create('div')
:addClass('wst-sub-note')
:tag('span')
:attr('id', 'nofooter')
:wikitext('ترجمههای فارسی ' .. tostring(mw.html.create('b'):wikitext(original or args.translated)) .. ' عبارتند از:')
return table.concat({
current_frame:extensionTag('templatestyles', '', {src = 'Template:ترجمهها/styles.css'}),
header(headerArgs),
current_frame:preprocess('__DISAMBIG__'),
tostring(subNote),
(current_title:inNamespaces(0, 114) and (args.category or '[[Category:' .. 'Translations pages' .. ']]')) or ''
})
end
function p.translations(frame)
return _translations(getArgs(frame))
end
return p
e28lmqeydhxpnkf1zcp6wusgjnljz92
الگو:شعر۲/styles.css
10
97053
303999
303749
2026-10-11T20:16:25Z
Hanooz
17889
303999
sanitized-css
text/css
/*
* الگو:شعر/styles.css — شیوهنامهٔ TemplateStyles برای «پودمان:شعر»
* این صفحه باید از نوع «Sanitized CSS» باشد؛ افزونه پیشوند .mw-parser-output
* را خودش اضافه میکند.
*/
/* ─── جدول: پایهٔ چیدمان ──────────────────────────────────────────────────
* border-collapse: collapse حیاتی است؛ بدون آن، جدولهای ۷ستونی با
* table-layout: fixed عرض ستونها را درست جمع نمیکنند و ترازبندی
* ناهمگون میشود.
*/
.poem-table {
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;
text-align: center;
}
.poem-table td {
padding: 0.15em 0;
vertical-align: middle;
}
/* ─── فاصلهها ───────────────────────────────────────────────────────────
* فاصلهٔ دو مصراع در هر بیت = ستون میانی (۴.۷em) + حاشیهٔ درونی دو مصراع (۲×۰.۹em)
* فاصلهٔ شماره تا مصراع = حاشیهٔ شماره (۱.۴em) + حاشیهٔ درونی مصراع (۰.۹em)
*/
.poem-m {
padding-inline: 0.9em;
}
.poem-num-c,
.poem-g {
width: 4.7em;
}
.poem-s {
width: 3.4em;
}
/* ─── نستعلیق ────────────────────────────────────────────────────────────
* خودبسنده است و به Common.css وابسته نیست. فهرست قلم باید با
* cfg.nastaliqFonts در پودمان یکی باشد.
*/
.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;
}
/* ─── سلول مصراع: قاعدهٔ اصلی ترازبندی ───────────────────────────────────
* .beyt یک بلوک تمامعرض است تا text-align-last: justify قابلاعتماد کار کند.
* (در بعضی مرورگرها ترازبندی روی <td> وقتی تنها فرزندش یک <span> inline
* است، اعمال نمیشود.) ترتیب اولویت استایلها:
* ۱) style اینلاین روی <td> (از ماژول: تراز + سبک کاربر)
* ۲) .poem-m در همین فایل (پایه)
* ۳) .poem-m .beyt روی بلوک داخلی
*/
.poem-m .beyt {
display: block;
width: 100%;
text-align: inherit;
text-align-last: inherit;
word-spacing: normal;
word-break: keep-all;
overflow-wrap: break-word;
hyphens: none;
}
/* پایه: پهن. اگر ماژول تراز دیگری بدهد، آن اینلاین برنده است. */
.poem-m {
text-align: justify;
text-align-last: justify;
text-align-last: justify;
text-align-last: justify;
}
/* مصراع تنها با تراز راست/چپ (شعر نو): ترازبندی نمیشود */
.poem-m-right {
text-align: right;
text-align-last: auto;
}
.poem-m-left {
text-align: left;
text-align-last: auto;
}
/* ─── شمارهٔ ابیات ─────────────────────────────────────────────────────── */
.poem-num {
font-size: 85%;
white-space: nowrap;
vertical-align: middle;
}
.poem-num-r {
text-align: left;
padding-left: 1.4em;
}
.poem-num-l {
text-align: right;
padding-right: 1.4em;
}
/* ─── سلولهای فاصلهگذار و ردیف فاصله ─────────────────────────────────── */
.poem-indent,
.poem-gapcell {
padding: 0;
}
/* ─── چیدمان ثابت ────────────────────────────────────────────────────────
* ستونها را ردیف نامرئی .poem-sizer تعیین میکند. عرض و حداکثر عرض روی
* جعبهٔ .poem-fixed مینشیند، پس همهٔ جدولهای یک شعر هماندازهاند.
*/
.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;
}
/* ─── صفحهٔ باریک (تلفن همراه) ─────────────────────────────────────────── */
@media (max-width: 30em) {
.poem-m {
padding-inline: 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;
}
}
l4npi65yls0eqobcxdzwrnmwzhm7yiq
304008
303999
2026-10-11T20:26:34Z
Hanooz
17889
Hanooz صفحهٔ [[الگو:شعر/styles.css]] را بدون برجایگذاشتن تغییرمسیر به [[الگو:شعر۲/styles.css]] منتقل کرد
303999
sanitized-css
text/css
/*
* الگو:شعر/styles.css — شیوهنامهٔ TemplateStyles برای «پودمان:شعر»
* این صفحه باید از نوع «Sanitized CSS» باشد؛ افزونه پیشوند .mw-parser-output
* را خودش اضافه میکند.
*/
/* ─── جدول: پایهٔ چیدمان ──────────────────────────────────────────────────
* border-collapse: collapse حیاتی است؛ بدون آن، جدولهای ۷ستونی با
* table-layout: fixed عرض ستونها را درست جمع نمیکنند و ترازبندی
* ناهمگون میشود.
*/
.poem-table {
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;
text-align: center;
}
.poem-table td {
padding: 0.15em 0;
vertical-align: middle;
}
/* ─── فاصلهها ───────────────────────────────────────────────────────────
* فاصلهٔ دو مصراع در هر بیت = ستون میانی (۴.۷em) + حاشیهٔ درونی دو مصراع (۲×۰.۹em)
* فاصلهٔ شماره تا مصراع = حاشیهٔ شماره (۱.۴em) + حاشیهٔ درونی مصراع (۰.۹em)
*/
.poem-m {
padding-inline: 0.9em;
}
.poem-num-c,
.poem-g {
width: 4.7em;
}
.poem-s {
width: 3.4em;
}
/* ─── نستعلیق ────────────────────────────────────────────────────────────
* خودبسنده است و به Common.css وابسته نیست. فهرست قلم باید با
* cfg.nastaliqFonts در پودمان یکی باشد.
*/
.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;
}
/* ─── سلول مصراع: قاعدهٔ اصلی ترازبندی ───────────────────────────────────
* .beyt یک بلوک تمامعرض است تا text-align-last: justify قابلاعتماد کار کند.
* (در بعضی مرورگرها ترازبندی روی <td> وقتی تنها فرزندش یک <span> inline
* است، اعمال نمیشود.) ترتیب اولویت استایلها:
* ۱) style اینلاین روی <td> (از ماژول: تراز + سبک کاربر)
* ۲) .poem-m در همین فایل (پایه)
* ۳) .poem-m .beyt روی بلوک داخلی
*/
.poem-m .beyt {
display: block;
width: 100%;
text-align: inherit;
text-align-last: inherit;
word-spacing: normal;
word-break: keep-all;
overflow-wrap: break-word;
hyphens: none;
}
/* پایه: پهن. اگر ماژول تراز دیگری بدهد، آن اینلاین برنده است. */
.poem-m {
text-align: justify;
text-align-last: justify;
text-align-last: justify;
text-align-last: justify;
}
/* مصراع تنها با تراز راست/چپ (شعر نو): ترازبندی نمیشود */
.poem-m-right {
text-align: right;
text-align-last: auto;
}
.poem-m-left {
text-align: left;
text-align-last: auto;
}
/* ─── شمارهٔ ابیات ─────────────────────────────────────────────────────── */
.poem-num {
font-size: 85%;
white-space: nowrap;
vertical-align: middle;
}
.poem-num-r {
text-align: left;
padding-left: 1.4em;
}
.poem-num-l {
text-align: right;
padding-right: 1.4em;
}
/* ─── سلولهای فاصلهگذار و ردیف فاصله ─────────────────────────────────── */
.poem-indent,
.poem-gapcell {
padding: 0;
}
/* ─── چیدمان ثابت ────────────────────────────────────────────────────────
* ستونها را ردیف نامرئی .poem-sizer تعیین میکند. عرض و حداکثر عرض روی
* جعبهٔ .poem-fixed مینشیند، پس همهٔ جدولهای یک شعر هماندازهاند.
*/
.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;
}
/* ─── صفحهٔ باریک (تلفن همراه) ─────────────────────────────────────────── */
@media (max-width: 30em) {
.poem-m {
padding-inline: 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;
}
}
l4npi65yls0eqobcxdzwrnmwzhm7yiq
304020
304008
2026-10-11T21:19:18Z
Hanooz
17889
304020
sanitized-css
text/css
/*
* الگو:شعر/styles.css — شیوهنامهٔ TemplateStyles برای «پودمان:شعر»
* این صفحه باید از نوع «Sanitized CSS» باشد؛ افزونه پیشوند .mw-parser-output
* را خودش اضافه میکند.
*
* چیدمان واکنشگرا:
* • > 45em : دو ستون، فاصلهٔ ۲em بین دو مصراع (مانند الگوهای اصلی)
* • 30-45em: دو ستون، فاصلهٔ کمتر
* • < 30em : هر مصراع یک خط، با فاصله بین بیتها
*/
/* ═══ پایهٔ جدول ═══ */
.poem-table {
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed; /* ستونها مستقل از محتوا، ثابت */
text-align: center; /* معادل tr style="text-align:center" در الگوهای اصلی */
}
.poem-table td {
padding: 0.1em 0;
vertical-align: middle;
}
/* ═══ فاصلهٔ بین دو مصراع (مطابق الگوهای اصلی: ۲em) ═══
* .poem-g روی ردیفِ سایزر (سلول ۴) و .poem-num-c روی خودِ سلولِ میانی
* اثر میگذارند. هر دو باید یک اندازه باشند تا چیدمان ثابت درست کار کند.
*/
.poem-g,
.poem-num-c {
width: 2em;
}
/* سلول شمارهٔ کناری: جا برای شماره + فاصلهٔ ۲em تا مصراع */
.poem-s {
width: 3.5em;
}
/* حاشیهٔ درونی مصراع: با فاصلهٔ ۲em میانی، فاصلهٔ دیداری ≈ ۳em */
.poem-m {
padding-inline: 0.5em;
}
/* ═══ نستعلیق (خودبسنده، بدون وابستگی به Common.css) ═══ */
.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-m .beyt {
overflow-wrap: break-word;
hyphens: none;
}
/* ═══ شمارهٔ ابیات ═══
* ش در RTL لبهٔ چپِ سلول = لبهٔ راستِ مصراع ⇒ با padding-left فاصله میگیرد
* شچ در RTL لبهٔ راستِ سلول = لبهٔ چپِ مصراع ⇒ با padding-right فاصله میگیرد
*/
.poem-num {
font-size: 85%;
white-space: nowrap;
vertical-align: middle;
}
.poem-num-r {
text-align: left;
padding-left: 2em;
}
.poem-num-l {
text-align: right;
padding-right: 2em;
}
/* ═══ مصراع تنها با تراز راست/چپ (شعر نو) ═══ */
.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-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;
}
/* ═══════════════════════════════════════════════════════════════════════
* صفحهٔ متوسط (۳۰em تا ۴۵em):
* فاصلهٔ میانی کمتر، فاصلهٔ کنارهها کمتر، اما مصراعها همچنان کنار هم.
* ═══════════════════════════════════════════════════════════════════════ */
@media (max-width: 45em) {
.poem-g,
.poem-num-c {
width: 1.2em;
}
.poem-s {
width: 2.2em;
}
.poem-m {
padding-inline: 0.35em;
}
.poem-num-r {
padding-left: 0.6em;
}
.poem-num-l {
padding-right: 0.6em;
}
}
/* ═══════════════════════════════════════════════════════════════════════
* صفحهٔ باریک (زیر ۳۰em):
* هر مصراع یک خط کامل؛ پس از هر بیت (دو مصراع) فاصله و خطچینِ کمرنگ
* تا پایانِ بیت دیده شود. شمارهها کوچک و بیآزار.
* ═══════════════════════════════════════════════════════════════════════ */
@media (max-width: 30em) {
/* جعبهٔ بیرونی عرض کامل صفحه را بگیرد */
.poem-fixed {
width: 100% !important;
max-width: none !important;
}
/* جدول از حالت جدول خارج شود تا سلولها روی هم بیفتند */
.poem-table {
display: block;
width: 100% !important;
table-layout: auto;
border-collapse: separate;
border-spacing: 0;
}
.poem-sizer {
display: none !important;
}
/* هر بیت یک بلوک مستقل با فاصله و جداکنندهٔ پایین */
.poem-row {
display: block;
padding: 0.3em 0 0.6em;
margin-bottom: 0.7em;
border-bottom: 1px dotted #c8c8c8;
}
.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-num-c {
display: none !important;
}
/* شمارههای کناری: کوچک، بالای بیت، چسبیده به لبهها */
.poem-num-r,
.poem-num-l {
display: block;
width: auto;
font-size: 72%;
line-height: 1.15;
color: #888;
padding: 0 0.4em !important;
}
.poem-num-r {
text-align: right !important;
}
.poem-num-l {
text-align: left !important;
}
/* هر مصراع روی خط خودش */
.poem-m {
padding: 0.2em 0.4em !important;
text-align: center !important;
}
/* بیتهای پلکانی/نردبانی: تورفتگی حذف، هر دو مصراع وسطچین */
.poem-indent,
.poem-st {
display: none !important;
}
.poem-stagger .poem-m {
padding: 0.2em 0.4em !important;
}
}
6s7j2457rh0iulvhhm21h9rhdag6pdh
304021
304020
2026-10-11T21:33:37Z
Hanooz
17889
304021
sanitized-css
text/css
/*
* الگو:شعر/styles.css — شیوهنامهٔ TemplateStyles برای «پودمان:شعر»
* این صفحه باید از نوع «Sanitized CSS» باشد.
*
* چیدمان واکنشگرا:
* > 40em : دو ستون، فاصلهٔ میانی نسبیِ ۵٪ (روی جدول ۴۰em ≈ ۲em)
* 32-40em : دو ستون، همان نسبت، فاصلهٔ کمکی کمتر
* < 32em : هر مصراع یک خط، با خطچین بین بیتها
*/
/* ═══ پایهٔ جدول ═══ */
.poem-table {
border-collapse: separate;
border-spacing: 0;
table-layout: fixed; /* عرض ستونها مستقل از محتوا */
text-align: center;
}
.poem-table td {
padding: 0.1em 0;
vertical-align: middle;
}
/* ═══ عرض ستونها (روی ردیف سایزر) ═══
* جمع = 6 + 20.75 + 20.75 + 5 + 20.75 + 20.75 + 6 = 100٪
* دو مصراع هر یک ۴۱.۵٪ جدول: هماندازه در همهٔ ابیات.
*/
.poem-s { width: 6%; } /* ستون ش و شچ */
.poem-mh { width: 20.75%; } /* هر نیم از یک مصراع */
.poem-g { width: 5%; } /* فاصلهٔ میانی */
/* حاشیهٔ داخلی مصراع (فاصلهٔ دیداری تا شماره و تا میانی) */
.poem-m {
padding-inline: 0.4em;
}
/* ═══ نستعلیق ═══ */
.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-m .beyt {
overflow-wrap: break-word;
hyphens: none;
}
/* ═══ شمارهٔ ابیات ═══ */
.poem-num {
font-size: 85%;
white-space: nowrap;
vertical-align: middle;
}
.poem-num-r { text-align: left; padding-left: 0.4em; }
.poem-num-l { text-align: right; padding-right: 0.4em; }
/* ═══ مصراع راست/چپ (شعر نو) ═══ */
.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-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; }
/* ═══ صفحهٔ متوسط (۳۲em تا ۴۰em): فاصلهٔ میانی کمی کمتر ═══ */
@media (max-width: 40em) {
.poem-s { width: 5%; }
.poem-g { width: 4%; }
.poem-mh { width: 21.5%; } /* 5 + 21.5×2 + 4 + 21.5×2 + 5 = 100 */
.poem-m { padding-inline: 0.3em; }
}
/* ═══ صفحهٔ باریک (زیر ۳۲em): هر مصراع یک خط ═══
* جدول به بلوک تبدیل میشود؛ میانی حذف؛ شمارهها کوچک بالای بیت.
* بین هر دو مصراع (پایان بیت) فاصله و خطچین کمرنگ.
*/
@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: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-num-c { display: none !important; }
.poem-num-r,
.poem-num-l {
display: block;
width: auto;
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-stagger .poem-m { padding-inline: 0.3em !important; }
}
6onuxykz705vey4z8lmnn22tejtcm2w
304025
304021
2026-10-11T21:50:02Z
Hanooz
17889
304025
sanitized-css
text/css
/*
* الگو:شعر/styles.css — شیوهنامهٔ TemplateStyles برای «پودمان:شعر»
* این صفحه باید از نوع «Sanitized CSS» باشد.
*/
/* ═══ پایهٔ جدول ═══ */
.poem-table {
border-collapse: collapse;
border-spacing: 0;
table-layout: auto; /* برای آنکه ردیف سایزرِ پایین هم در عرض ستونها اثر بگذارد */
text-align: center;
}
.poem-table td {
padding: 0.1em 0;
vertical-align: middle;
}
/* ═══ ردیف سایزر: نامرئی ولی اثرگذار در عرض ستونها ═══ */
.poem-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
overflow: hidden;
}
.poem-sz {
display: block;
height: 0;
overflow: hidden;
white-space: nowrap;
line-height: 0;
visibility: hidden;
}
/* ═══ عرض ثابت ستونهای کناری و میانی ═══ */
.poem-num-c,
.poem-g {
width: 2em;
min-width: 2em;
}
.poem-s {
width: 3.5em;
min-width: 3.5em;
}
.poem-m {
padding-inline: 0.5em;
}
.poem-m .beyt {
white-space: nowrap;
overflow-wrap: normal;
hyphens: none;
}
.poem-m-empty {
padding: 0;
}
/* ═══ شمارهها ═══ */
.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-partial-end td.b { border-bottom: 1px dashed; }
.poem-partial-start td.b { border-top: 1px dashed; }
/* ═══ صفحهٔ متوسط: فاصله کمتر ولی همچنان دوستونی ═══ */
@media (max-width: 45em) {
.poem-num-c,
.poem-g { width: 1.2em; min-width: 1.2em; }
.poem-s { width: 2.2em; min-width: 2.2em; }
.poem-m { padding-inline: 0.3em; }
.poem-num-r { padding-left: 0.4em; }
.poem-num-l { padding-right: 0.4em; }
}
/* ═══ صفحهٔ باریک: هر مصراع یک خط، با خطچین بین بیتها ═══ */
@media (max-width: 32em) {
.poem-fixed { width: 100% !important; max-width: none !important; }
.poem-table { display: block; width: 100% !important; }
.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: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-num-c { display: none !important; }
.poem-num-r, .poem-num-l {
display: block;
width: auto;
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; }
.poem-m-empty { display: none !important; }
}
jvt9svjxiz2eu63oywi30gap9p1dg2r
304026
304025
2026-10-11T22:06:47Z
Hanooz
17889
304026
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;
}
/* ═══ عرض ستونها (روی ردیف سایزر) ═══
* ۵ + ۲۱ + ۲۱ + ۶ + ۲۱ + ۲۱ + ۵ = ۱۰۰
* هر مصراع (colspan=2) = ۴۲٪ جدول؛ فاصلهٔ میانی = ۶٪
*/
.poem-s { width: 5%; }
.poem-mh { width: 21%; }
.poem-g { width: 6%; }
/* ═══ سلولهای پُرنشده (فقط ساختاریاند) ═══ */
.poem-empty { padding: 0 !important; }
/* ═══ حاشیهٔ درونی مصراع ═══ */
.poem-m { padding-inline: 0.4em; }
.poem-m .beyt {
overflow-wrap: break-word;
hyphens: none;
}
/* ═══ شمارهٔ ابیات ═══ */
.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-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
font-size: 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-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%; } /* 4 + 22×4 + 4 + 4 = 100 */
.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; }
}
pks8299tghobqvbjpc8js96ns95jc6x
304031
304026
2026-10-11T22:22:11Z
Hanooz
17889
304031
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;
}
/* ═══ عرض ستونها (روی ردیف سایزر) ═══
* ۵ + ۲۱ + ۲۱ + ۶ + ۲۱ + ۲۱ + ۵ = ۱۰۰
* هر مصراع (colspan=2) = ۴۲٪ جدول؛ فاصلهٔ میانی = ۶٪
*/
.poem-s { width: 5%; }
.poem-mh { width: 21%; }
.poem-g { width: 6%; }
/* ═══ سلولهای پُرنشده (فقط ساختاریاند) ═══ */
.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; }
.poem-m .beyt {
overflow-wrap: break-word;
hyphens: none;
}
/* ═══ شمارهٔ ابیات ═══ */
.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-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
font-size: 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-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; }
}
1b63sj2ruz4qrtsdp81o1ijl3ltvr2c
304033
304031
2026-10-11T22:33:49Z
Hanooz
17889
304033
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;
}
/* ═══ عرض ستونها ═══ */
.poem-s { width: 5%; }
.poem-mh { width: 21%; }
.poem-g { width: 6%; }
/* ═══ سلولهای پُرنشده ═══ */
.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; }
/* ═══ محتوای مصراع ═══
* ساختار: <span class="beyt">
* <span class="beyt-text">متن کشیدهشده</span>
* <span class="beyt-refs"><sup>[۱]</sup>...</span>
* </span>
* .beyt-text در جریان ترازبندی شرکت میکند؛ .beyt-refs در انتهای
* مصراع چسبیده به آخرین واژه میماند و از همکشیدگی جدا میشود.
*/
.poem-m .beyt {
overflow-wrap: break-word;
hyphens: none;
}
.poem-m .beyt-refs {
white-space: nowrap;
margin-inline-start: 0.15em;
}
.poem-m .beyt-refs sup {
line-height: 1;
}
/* ═══ شمارهٔ ابیات ═══ */
.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-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
font-size: 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; }
.poem-m .beyt-refs { display: inline; }
}
rork2qvtyk6v1sm59rgn4r71vtkt8ut
304034
304033
2026-10-11T22:44:39Z
Hanooz
17889
304034
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;
}
/* ═══ عرض ستونها ═══ */
.poem-s { width: 5%; }
.poem-mh { width: 21%; }
.poem-g { width: 6%; }
/* ═══ سلولهای پُرنشده ═══ */
.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; }
/* ═══ محتوای مصراع (flex: متن + ارجاعها) ═══
* .beyt یک flex row است: .beyt-text با flex:1 کل فضای آزاد را میگیرد
* (و درونش ترازبندی اعمال میشود)، .beyt-refs با flex:0 فقط اندازهٔ خودش
* را میگیرد و در انتهای مصراع مینشیند. بهاینترتیب حضور ارجاع، متن را
* کوتاه نمیکند.
*/
.poem-m .beyt {
display: flex;
align-items: baseline;
width: 100%;
gap: 0;
}
.poem-m .beyt-text {
flex: 1 1 0;
min-width: 0;
overflow-wrap: break-word;
hyphens: none;
text-align: inherit;
text-align-last: inherit;
}
.poem-m .beyt-refs {
flex: 0 0 auto;
white-space: nowrap;
margin-inline-start: 0.15em;
overflow-wrap: normal;
}
.poem-m .beyt-refs sup {
line-height: 1;
}
/* ═══ شمارهٔ ابیات ═══ */
.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-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
font-size: 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 { display: block; }
.poem-m .beyt-text { display: inline; }
.poem-m .beyt-refs { display: inline; margin-inline-start: 0.15em; }
}
jje7fociftyqv5zvoqhizfxc7oz5bda
304036
304034
2026-10-12T03:59:27Z
Hanooz
17889
304036
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;
}
/* ═══ عرض ستونها ═══ */
.poem-s { width: 5%; }
.poem-mh { width: 21%; }
.poem-g { width: 6%; }
/* ═══ سلولهای خالی ساختاری ═══ */
.poem-empty {
padding: 0 !important;
width: auto !important;
min-width: 0;
border: 0;
line-height: 0;
font-size: 0;
}
/* ═══ مصراع ═══
* ترازبندی از style اینلاین (ALIGN_STYLE در پودمان) میآید و برنده است.
* قاعدهٔ CSS زیر فقط پایه است، برای وقتی که سبک کاربر تراز ندهد.
* ارجاعها بهصورت inline داخل متن میمانند و به آخرین واژه میچسبند،
* پس حضورشان طول متنِ کشیدهشده را کوتاه نمیکند.
*/
.poem-m {
padding-inline: 0.4em;
}
.poem-m .beyt {
display: block;
overflow-wrap: break-word;
hyphens: none;
text-align: inherit;
text-align-last: inherit;
}
.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-sizer td {
padding: 0;
border: 0;
height: 0;
line-height: 0;
font-size: 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; }
}
ppmcxnvudx8f4o9gt5jf8hkdyp8ugmw
پودمان:شعر
828
97054
303998
303748
2026-10-11T20:15:47Z
Hanooz
17889
303998
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
الگو نقطهٔ ورود در {{#invoke:شعر|…}}
{{شعر}} open آغاز شعر؛ با پارامتر «متن» کل شعر را میسازد
{{پایان شعر}} close
{{بیت}} bayt بیت دومصراعی
{{بیت۱}} stagger بیت پلکانی (ضربدری)
{{بیت نردبانی}} ladder بیت نردبانی (تورفتگی پیشفرض بیشتر)
{{مصراع}} mesra یک مصراع
{{بیت خالی}} emptyBayt
{{مصراع خالی}} emptyMesra
ساختار خروجی همان جدول ۷ستونی پیشین است (ظاهر شعرهای موجود و خروجی
کتاب الکترونیکی تغییری نمیکند). شیوهنامه: «الگو:شعر/styles.css».
چیدمان (تازه): جدول پیشفرض عرض ثابت و مستقل از محتوا دارد (عرض=، حداکثر عرض=)،
تا همهٔ ابیات شعر — حتی در جدولهای جدا (صفحههای جدا) — از یک نقطه آغاز شوند.
ترازبندی=وسط|راست|چپ|پهن|لبه برای هر نوع بیت؛ قلم=، نستعلیق خودبسنده (بدون Common.css).
در حالت «متن»، خطی که با @ آغاز شود نوع ردیفهای بعدی را عوض میکند: «@بیت نردبانی».
ترازبندی پیشفرض «پهن» (دوطرفه) است؛ اگر دو مصراع از نظر طول بسیار ناهمسان باشند
(نسبت کمتر از cfg.justifyMinRatio)، به تراز «لبه» فرو میافتد (مصراع اول راست،
دوم چپ — مانند چاپ سنگی). این رفتار با پارامتر «ترازبندی» قابل تغییر است.
شعر چندصفحهای (ایدهٔ start/end در ppoem):
• در فضای نام «صفحه» هر صفحه همیشه خودبسنده و بسته نمایش داده میشود.
• در صفحهٔ اصلی (درج با <pages/>) پارامتر «ادامه» صفحهها را به هم میدوزد:
{{شعر|ادامه}} جدول تازه باز نمیکند و {{پایان شعر|ادامه}} نمیبندد.
• ردیفِ ناتمام (مصراعی که در صفحهٔ بعد ادامه دارد) نگه داشته میشود
(ناتمام=بله) و با نخستین ردیفِ صفحهٔ بعد (دنباله=بله) یکی میشود.
این رفتار اکنون پیشفرض خاموش است (cfg.mergeRows = false) تا در صفحهٔ
اصلی هر تکه مستقل نمایش داده شود؛ درج صفحهها معمولاً خودش کافی است.
]==]
local p = {}
---------------------------------------------------------------------------
-- تنظیمات
---------------------------------------------------------------------------
local ZWNJ = '\226\128\140' -- نیمفاصله
local cfg = {
styles = 'Template:شعر/styles.css',
pageNs = 104,
defaultIndent = '20%',
ladderIndent = '45%',
mesraUnit = '1.5em',
nbsp = ' ',
-- چیدمان: 'auto' = جدول هماندازهٔ محتوا (رفتار قدیمی)؛ طول/درصد = عرض ثابت
defaultWidth = '100%',
maxWidth = '42em', -- باریکتر تا کشش واژهها در ترازبندی کم شود
-- ترازبندی پیشفرض هر نوع
-- بیت → پهن (دوطرفه) ، پلکانی → لبه
justDefault = { bayt = 'justify', stagger = 'edge' },
-- وقتی نسبت طول کوتاهترین مصراع به بلندترین از این کمتر باشد، «پهن» به
-- این تراز فرو میافتد. «لبه» شبیه چاپ سنگی است: مصراع اول به لبهٔ بیرونی راست،
-- دومی به لبهٔ بیرونی چپ؛ کنارهٔ میانی ناهموار میماند.
justFallback = 'edge',
-- آستانهٔ فروافتادن از «پهن» به justFallback
justifyMinRatio = 0.7,
-- قلم نستعلیق (همین فهرست در CSS هم آمده)
nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', "
.. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif",
-- ادغام ردیف ناتمام در صفحهٔ اصلی: پیشفرض خاموش (هر تکه مستقل نمایش داده میشود).
-- اگر واقعاً میخواهید مصراعی که در مرز صفحه قطع شده یکجا نشان داده شود،
-- آن را true کنید. برای پیشنمایش فضای نام «صفحه» تفاوتی ندارد.
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',
}
-- ترازبندی بهصورت inline روی سلول مصراع. عمداً «صریح» است تا CSS محلی ویکی
-- نتواند تراز را بازنویسی کند؛ ترتیب: ترازِ ماژول، سپس سبک کاربر.
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 = 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') }
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
-- تزریق TemplateStyles با محافظت pcall (اگر صفحهٔ CSS نبود یا افزونه خاموش بود، خطا ندهد)
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
---------------------------------------------------------------------------
-- اندازههای 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
-- ترازبندی هر مصراع. which = 1 (اول) یا 2 (دوم).
-- j = 'center' → وسط، 'edge' → مصراع اول راست (start)، دوم چپ (end)،
-- 'justify' → دوطرفه، 'right'/'left' → راست/چپ.
local function alignFor(j, which)
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 or 'center'] or ALIGN_STYLE.center
end
-- طول تقریبی متن دیدنی (نشانگر strip، تگ HTML، پیشوند پیوند، نیمفاصله حذف میشوند)
local function charLen(s)
s = s:gsub('\127[^\127]*\127', '')
:gsub('<[^>]*>', '')
:gsub('%[%[[^%]|]*|', '')
:gsub('[%[%]]', '')
:gsub('\226\128\140', '')
return mw.ustring.len(s)
end
-- اگر دو مصراع از نظر طول بسیار ناهمسان باشند، «پهن» به justFallback فرو میافتد.
local function effectiveJust(j, a, b)
if j == 'justify' then
local la, lb = charLen(a or ''), charLen(b or '')
local hi = math.max(la, lb)
if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then
return cfg.justFallback
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)
return rowOpen(d, 'poem-bayt')
.. td('poem-num poem-num-r', 1, nil, d.r)
.. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemi(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), 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 s1 = joinStyle(alignFor(j, 1), d.style)
local s2 = joinStyle(alignFor(j, 2), d.style)
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('b poem-m poem-m1', 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('b poem-m poem-m2', 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('b poem-m poem-m1', 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('b poem-m poem-m2', 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 inline = (align == 'right') and ALIGN_STYLE.right
or (align == 'left') and ALIGN_STYLE.left
or (align == 'justify') and ALIGN_STYLE.justify
or ALIGN_STYLE.center
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
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 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(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
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') 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 }
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
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,
}
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
4h2z88q4a2bz5s8zbo9cnt92vbsrgvk
304011
303998
2026-10-11T20:29:57Z
Hanooz
17889
304011
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
الگو نقطهٔ ورود در {{#invoke:شعر|…}}
{{شعر}} open آغاز شعر؛ با پارامتر «متن» کل شعر را میسازد
{{پایان شعر}} close
{{بیت}} bayt بیت دومصراعی
{{بیت۱}} stagger بیت پلکانی (ضربدری)
{{بیت نردبانی}} ladder بیت نردبانی (تورفتگی پیشفرض بیشتر)
{{مصراع}} mesra یک مصراع
{{بیت خالی}} emptyBayt
{{مصراع خالی}} emptyMesra
ساختار خروجی همان جدول ۷ستونی پیشین است (ظاهر شعرهای موجود و خروجی
کتاب الکترونیکی تغییری نمیکند). شیوهنامه: «الگو:شعر/styles.css».
چیدمان (تازه): جدول پیشفرض عرض ثابت و مستقل از محتوا دارد (عرض=، حداکثر عرض=)،
تا همهٔ ابیات شعر — حتی در جدولهای جدا (صفحههای جدا) — از یک نقطه آغاز شوند.
ترازبندی=وسط|راست|چپ|پهن|لبه برای هر نوع بیت؛ قلم=، نستعلیق خودبسنده (بدون Common.css).
در حالت «متن»، خطی که با @ آغاز شود نوع ردیفهای بعدی را عوض میکند: «@بیت نردبانی».
ترازبندی پیشفرض «پهن» (دوطرفه) است؛ اگر دو مصراع از نظر طول بسیار ناهمسان باشند
(نسبت کمتر از cfg.justifyMinRatio)، به تراز «لبه» فرو میافتد (مصراع اول راست،
دوم چپ — مانند چاپ سنگی). این رفتار با پارامتر «ترازبندی» قابل تغییر است.
شعر چندصفحهای (ایدهٔ start/end در ppoem):
• در فضای نام «صفحه» هر صفحه همیشه خودبسنده و بسته نمایش داده میشود.
• در صفحهٔ اصلی (درج با <pages/>) پارامتر «ادامه» صفحهها را به هم میدوزد:
{{شعر|ادامه}} جدول تازه باز نمیکند و {{پایان شعر|ادامه}} نمیبندد.
• ردیفِ ناتمام (مصراعی که در صفحهٔ بعد ادامه دارد) نگه داشته میشود
(ناتمام=بله) و با نخستین ردیفِ صفحهٔ بعد (دنباله=بله) یکی میشود.
این رفتار اکنون پیشفرض خاموش است (cfg.mergeRows = false) تا در صفحهٔ
اصلی هر تکه مستقل نمایش داده شود؛ درج صفحهها معمولاً خودش کافی است.
]==]
local p = {}
---------------------------------------------------------------------------
-- تنظیمات
---------------------------------------------------------------------------
local ZWNJ = '\226\128\140' -- نیمفاصله
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
defaultIndent = '20%',
ladderIndent = '45%',
mesraUnit = '1.5em',
nbsp = ' ',
-- چیدمان: 'auto' = جدول هماندازهٔ محتوا (رفتار قدیمی)؛ طول/درصد = عرض ثابت
defaultWidth = '100%',
maxWidth = '42em', -- باریکتر تا کشش واژهها در ترازبندی کم شود
-- ترازبندی پیشفرض هر نوع
-- بیت → پهن (دوطرفه) ، پلکانی → لبه
justDefault = { bayt = 'justify', stagger = 'edge' },
-- وقتی نسبت طول کوتاهترین مصراع به بلندترین از این کمتر باشد، «پهن» به
-- این تراز فرو میافتد. «لبه» شبیه چاپ سنگی است: مصراع اول به لبهٔ بیرونی راست،
-- دومی به لبهٔ بیرونی چپ؛ کنارهٔ میانی ناهموار میماند.
justFallback = 'edge',
-- آستانهٔ فروافتادن از «پهن» به justFallback
justifyMinRatio = 0.7,
-- قلم نستعلیق (همین فهرست در CSS هم آمده)
nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', "
.. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif",
-- ادغام ردیف ناتمام در صفحهٔ اصلی: پیشفرض خاموش (هر تکه مستقل نمایش داده میشود).
-- اگر واقعاً میخواهید مصراعی که در مرز صفحه قطع شده یکجا نشان داده شود،
-- آن را true کنید. برای پیشنمایش فضای نام «صفحه» تفاوتی ندارد.
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',
}
-- ترازبندی بهصورت inline روی سلول مصراع. عمداً «صریح» است تا CSS محلی ویکی
-- نتواند تراز را بازنویسی کند؛ ترتیب: ترازِ ماژول، سپس سبک کاربر.
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 = 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') }
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
-- تزریق TemplateStyles با محافظت pcall (اگر صفحهٔ CSS نبود یا افزونه خاموش بود، خطا ندهد)
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
---------------------------------------------------------------------------
-- اندازههای 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
-- ترازبندی هر مصراع. which = 1 (اول) یا 2 (دوم).
-- j = 'center' → وسط، 'edge' → مصراع اول راست (start)، دوم چپ (end)،
-- 'justify' → دوطرفه، 'right'/'left' → راست/چپ.
local function alignFor(j, which)
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 or 'center'] or ALIGN_STYLE.center
end
-- طول تقریبی متن دیدنی (نشانگر strip، تگ HTML، پیشوند پیوند، نیمفاصله حذف میشوند)
local function charLen(s)
s = s:gsub('\127[^\127]*\127', '')
:gsub('<[^>]*>', '')
:gsub('%[%[[^%]|]*|', '')
:gsub('[%[%]]', '')
:gsub('\226\128\140', '')
return mw.ustring.len(s)
end
-- اگر دو مصراع از نظر طول بسیار ناهمسان باشند، «پهن» به justFallback فرو میافتد.
local function effectiveJust(j, a, b)
if j == 'justify' then
local la, lb = charLen(a or ''), charLen(b or '')
local hi = math.max(la, lb)
if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then
return cfg.justFallback
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)
return rowOpen(d, 'poem-bayt')
.. td('poem-num poem-num-r', 1, nil, d.r)
.. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemi(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), 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 s1 = joinStyle(alignFor(j, 1), d.style)
local s2 = joinStyle(alignFor(j, 2), d.style)
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('b poem-m poem-m1', 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('b poem-m poem-m2', 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('b poem-m poem-m1', 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('b poem-m poem-m2', 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 inline = (align == 'right') and ALIGN_STYLE.right
or (align == 'left') and ALIGN_STYLE.left
or (align == 'justify') and ALIGN_STYLE.justify
or ALIGN_STYLE.center
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
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 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(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
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') 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 }
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
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,
}
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
l40i4qf781frueqkrmzlbbkyotf8ink
304022
304011
2026-10-11T21:34:07Z
Hanooz
17889
304022
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
الگو نقطهٔ ورود
{{شعر}} open
{{پایان شعر}} close
{{بیت}} bayt
{{بیت۱}} stagger
{{بیت نردبانی}} ladder
{{مصراع}} mesra
{{بیت خالی}} emptyBayt
{{مصراع خالی}} emptyMesra
▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت:
ستون ۱ : شمارهٔ ش (۶٪)
ستونهای ۲–۳: مصراع اول (۴۱٫۵٪ مجموع)
ستون ۴ : فاصلهٔ میانی شو (۵٪)
ستونهای ۵–۶: مصراع دوم (۴۱٫۵٪ مجموع)
ستون ۷ : شمارهٔ شچ (۶٪)
عرض مصراع در همهٔ انواع (بیت، بیت۱، نردبانی) یکسان است و در همهٔ
ابیات یک شعر همتراز میماند.
▸ ترازبندی پیشفرض وسط است. با ترازبندی=پهن یا لبه میتوان دوطرفه یا
لبهچین کرد.
▸ صفحهٔ باریک (در CSS زیر ۳۲em): هر مصراع یک خط، با خطچین بین بیتها.
]==]
local p = {}
local ZWNJ = '\226\128\140'
local cfg = {
styles = 'Template:شعر/styles.css',
pageNs = 104,
mesraUnit = '1.5em',
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '42em',
justDefault = { bayt = 'center', stagger = 'center' },
justFallback = 'center',
justifyMinRatio = 0.7,
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 = 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') } 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 _, 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
s = mw.text.trim(s):gsub('%s*\n%s*', ' ')
return 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 charLen(s)
s = s:gsub('\127[^\127]*\127', ''):gsub('<[^>]*>', '')
:gsub('%[%[[^%]|]*|', ''):gsub('[%[%]]', ''):gsub('\226\128\140', '')
return mw.ustring.len(s)
end
local function effectiveJust(j, a, b)
if j == 'justify' then
local la, lb = charLen(a or ''), charLen(b or '')
local hi = math.max(la, lb)
if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback 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(t) return '<span class="beyt">' .. t .. '</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)
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), hemi(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), hemi(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 = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2)
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, hemi(m1))
.. td('poem-num poem-num-c', 1, nil, '')
.. td('poem-num', 2, nil, '')
.. td('poem-num', 1, nil, '')
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num', 1, nil, '')
.. td('poem-num', 2, nil, '')
.. td('poem-num poem-num-c', 1, nil, '')
.. td('b poem-m poem-m2', 2, s2, hemi(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
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r)
.. td(cellClass, cs[2], style, content)
.. 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 }
-- سایزر: عرض ستونها را با درصد صریح تعیین میکند، مستقل از محتوا
local function sizerRow()
return '<tr class="poem-sizer">'
.. '<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(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
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') 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 }
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
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,
}
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('20%'))
p.ladder = safe(staggerFn('45%'))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
60fuv62nazs36il81q7r92p0nhbtrq5
304023
304022
2026-10-11T21:34:38Z
Hanooz
17889
304023
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
الگو نقطهٔ ورود
{{شعر}} open
{{پایان شعر}} close
{{بیت}} bayt
{{بیت۱}} stagger
{{بیت نردبانی}} ladder
{{مصراع}} mesra
{{بیت خالی}} emptyBayt
{{مصراع خالی}} emptyMesra
▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت:
ستون ۱ : شمارهٔ ش (۶٪)
ستونهای ۲–۳: مصراع اول (۴۱٫۵٪ مجموع)
ستون ۴ : فاصلهٔ میانی شو (۵٪)
ستونهای ۵–۶: مصراع دوم (۴۱٫۵٪ مجموع)
ستون ۷ : شمارهٔ شچ (۶٪)
عرض مصراع در همهٔ انواع (بیت، بیت۱، نردبانی) یکسان است و در همهٔ
ابیات یک شعر همتراز میماند.
▸ ترازبندی پیشفرض وسط است. با ترازبندی=پهن یا لبه میتوان دوطرفه یا
لبهچین کرد.
▸ صفحهٔ باریک (در CSS زیر ۳۲em): هر مصراع یک خط، با خطچین بین بیتها.
]==]
local p = {}
local ZWNJ = '\226\128\140'
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
mesraUnit = '1.5em',
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '42em',
justDefault = { bayt = 'center', stagger = 'center' },
justFallback = 'center',
justifyMinRatio = 0.7,
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 = 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') } 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 _, 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
s = mw.text.trim(s):gsub('%s*\n%s*', ' ')
return 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 charLen(s)
s = s:gsub('\127[^\127]*\127', ''):gsub('<[^>]*>', '')
:gsub('%[%[[^%]|]*|', ''):gsub('[%[%]]', ''):gsub('\226\128\140', '')
return mw.ustring.len(s)
end
local function effectiveJust(j, a, b)
if j == 'justify' then
local la, lb = charLen(a or ''), charLen(b or '')
local hi = math.max(la, lb)
if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback 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(t) return '<span class="beyt">' .. t .. '</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)
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), hemi(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), hemi(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 = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2)
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, hemi(m1))
.. td('poem-num poem-num-c', 1, nil, '')
.. td('poem-num', 2, nil, '')
.. td('poem-num', 1, nil, '')
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num', 1, nil, '')
.. td('poem-num', 2, nil, '')
.. td('poem-num poem-num-c', 1, nil, '')
.. td('b poem-m poem-m2', 2, s2, hemi(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
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r)
.. td(cellClass, cs[2], style, content)
.. 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 }
-- سایزر: عرض ستونها را با درصد صریح تعیین میکند، مستقل از محتوا
local function sizerRow()
return '<tr class="poem-sizer">'
.. '<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(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
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') 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 }
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
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,
}
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('20%'))
p.ladder = safe(staggerFn('45%'))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
o0bnt55y5ei3rau50ny9w9pq1ov29tg
304024
304023
2026-10-11T21:49:23Z
Hanooz
17889
304024
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
همهٔ مصراعهای یک شعر — در همهٔ انواع (بیت، بیت۱، نردبانی) — هماندازهاند و
اندازهٔ آنها برابر بلندترین مصراع آن شعر است. برای این کار ردیفِ سایزرِ
نامرئی در پایان جدول تولید میشود که متن بلندترین مصراع (پس از حذف
ارجاعهای پایانی) را در هر دو ستون مصراع میگذارد. با table-layout: auto
مرورگر عرض هر دو ستون مصراع را برابر همان متن میکند.
ارجاعهای پایانی (<ref> در پایان مصراع) در سنجش بلندی شمرده نمیشوند.
صفحهٔ باریک (CSS زیر ۳۲em): هر مصراع یک خط، با خطچین بین بیتها.
]==]
local p = {}
local ZWNJ = '\226\128\140'
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '42em',
justDefault = { bayt = 'center', stagger = 'center' },
justFallback = 'center',
justifyMinRatio = 0.7,
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>'
-- Helpers
local function esc(s) return (s: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
---------------------------------------------------------------------------
-- سنجش بلندی و متن سایزر
---------------------------------------------------------------------------
-- حذف <ref>...</ref> پایانی (و پیوندها در متن)
local function stripEndRefs(s)
if not s then return '' end
local prev
repeat
prev = s
s = s:gsub('%s*<ref[^>]*>.-</ref>%s*$', '')
s = s:gsub('%s*<ref[^>]*/>%s*$', '')
s = s:gsub('%s*\127[^\127]*\127%s*$', '') -- strip marker
until s == prev
return s
end
-- فقط متنِ خالص: بدون تگ HTML و ویکیمتن ساده
local function plainText(s)
if not s or s == '' then return '' end
s = s:gsub('<[^>]*>', '')
s = s:gsub('%[%[[^%]]*|([^%]]*)%]%]', '%1')
s = s:gsub('%[%[([^%]]*)%]%]', '%1')
s = s:gsub('{{[^}]*}}', '')
s = s:gsub("'''''", ''):gsub("'''", ''):gsub("''", '')
return s
end
local function findLongest()
local longest, longestLen = '', 0
for _, h in ipairs(state.hemistichs) do
local s = plainText(stripEndRefs(h))
local len = mw.ustring.len(s)
if len > longestLen then longest, longestLen = s, len end
end
return longest
end
-- ردیف سایزر: متن بلندترین مصراع در هر دو ستون مصراع، نامرئی، با ارتفاع صفر
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 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 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 charLen(s)
s = s:gsub('\127[^\127]*\127', ''):gsub('<[^>]*>', '')
:gsub('%[%[[^%]|]*|', ''):gsub('[%[%]]', ''):gsub('\226\128\140', '')
return mw.ustring.len(s)
end
local function effectiveJust(j, a, b)
if j == 'justify' then
local la, lb = charLen(a or ''), charLen(b or '')
local hi = math.max(la, lb)
if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback 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(t) return '<span class="beyt">' .. t .. '</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 register(m1, m2)
if not state.open then return end
if m1 and m1 ~= '' then state.hemistichs[#state.hemistichs + 1] = m1 end
if m2 and m2 ~= '' then state.hemistichs[#state.hemistichs + 1] = m2 end
end
-- {{بیت}} — شبکهٔ ۷ستونی: ۱ | ۲-۳ (م۱) | ۴ | ۵-۶ (م۲) | ۷
local function rowBayt(d)
local m1 = d.m1
if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end
register(d.m1, d.m2)
local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2)
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), hemi(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), hemi(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
register(d.m1, d.m2)
local j = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2)
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, hemi(m1))
.. td('poem-num poem-num-c', 1, nil, '')
.. td('b poem-m poem-m-empty', 2, nil, '')
.. td('poem-num poem-num-l', 1, 'text-align:right', '')
.. '</tr>\n'
.. rowOpen(d, 'poem-stagger poem-stagger-2', true)
.. td('poem-num poem-num-r', 1, 'text-align:left', '')
.. td('b poem-m poem-m-empty', 2, nil, '')
.. td('poem-num poem-num-c', 1, nil, '')
.. td('b poem-m poem-m2', 2, s2, hemi(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
if not d.blank then register(d.text, nil) 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
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r)
.. td(cellClass, cs[2], style, content)
.. 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 }
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">'
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') 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 '20%'
elseif c.kind == 'ladder' then indent = c.indent or '45%' 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 }
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
parts[#parts + 1] = sizerRow()
parts[#parts + 1] = CLOSE_HTML
state.open, state.ctx = false, nil
state.hemistichs = {}
end
end
if #parts == 0 then parts[1] = SKIP end
return table.concat(parts, '\n') .. categories(flags)
end
local function close(frame)
if not state.open then
-- اگر متن استفاده شده و داخل open بسته شده، اینجا چیزی نمیفرستیم
local args, flags = getArgs(frame), {}
if hasUnknown(args, 1) then flags.unknown = true end
return SKIP .. categories(flags)
end
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
local sz = sizerRow()
state.hemistichs = {}
if html ~= '' then html = html .. '\n' end
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)
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('20%'))
p.ladder = safe(staggerFn('45%'))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
a893f0u1c8yh5dqqkbh9cu3gheltzkf
304027
304024
2026-10-11T22:07:14Z
Hanooz
17889
304027
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت:
۱ ش | ۲-۳ مصراع اول | ۴ شو | ۵-۶ مصراع دوم | ۷ شچ
پس مصراعها در همهٔ ابیات و همهٔ انواع هماندازهاند.
▸ ردیف سایزرِ خالی در آغاز جدول، عرض ستونها را تعیین میکند.
▸ ترازبندی پیشفرض وسط است. «پهن» و «لبه» اختیاری.
▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت.
بیت۱ دو ردیف است، پس خطچین فقط زیر ردیف دوم میآید.
]==]
local p = {}
local ZWNJ = '\226\128\140'
local cfg = {
styles = 'Template:شعر/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '42em',
justDefault = { bayt = 'center', stagger = 'center' },
justFallback = 'center',
justifyMinRatio = 0.7,
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 function esc(s) return (s: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 charLen(s)
s = s:gsub('\127[^\127]*\127',''):gsub('<[^>]*>','')
:gsub('%[%[[^%]|]*|',''):gsub('[%[%]]',''):gsub('\226\128\140','')
return mw.ustring.len(s)
end
local function effectiveJust(j, a, b)
if j == 'justify' then
local la, lb = charLen(a or ''), charLen(b or '')
local hi = math.max(la, lb)
if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback end
end
return 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 hemi(t) return '<span class="beyt">' .. t .. '</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
-- {{بیت}} — 7 cells
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)
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), hemi(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), hemi(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 = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2)
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
-- ردیف ۱: num-r | m1 | gap | empty | empty
-- ردیف ۲: empty | empty | gap | m2 | num-l
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, hemi(m1))
.. td('poem-num poem-num-c poem-empty', 1, nil, '')
.. td('poem-empty poem-mh-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 poem-mh-empty', 2, nil, '')
.. td('poem-num poem-num-c poem-empty', 1, nil, '')
.. td('b poem-m poem-m2', 2, s2, hemi(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
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r)
.. td(cellClass, cs[2], style, content)
.. 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') 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 '20%'
elseif c.kind == 'ladder' then indent = c.indent or '45%' 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 }
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
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,
}
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('20%'))
p.ladder = safe(staggerFn('45%'))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
8trfjqv80npnu91vtm40fbb1ikdjrb6
304028
304027
2026-10-11T22:08:18Z
Hanooz
17889
304028
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت:
۱ ش | ۲-۳ مصراع اول | ۴ شو | ۵-۶ مصراع دوم | ۷ شچ
پس مصراعها در همهٔ ابیات و همهٔ انواع هماندازهاند.
▸ ردیف سایزرِ خالی در آغاز جدول، عرض ستونها را تعیین میکند.
▸ ترازبندی پیشفرض وسط است. «پهن» و «لبه» اختیاری.
▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت.
بیت۱ دو ردیف است، پس خطچین فقط زیر ردیف دوم میآید.
]==]
local p = {}
local ZWNJ = '\226\128\140'
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '42em',
justDefault = { bayt = 'center', stagger = 'center' },
justFallback = 'center',
justifyMinRatio = 0.7,
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 function esc(s) return (s: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 charLen(s)
s = s:gsub('\127[^\127]*\127',''):gsub('<[^>]*>','')
:gsub('%[%[[^%]|]*|',''):gsub('[%[%]]',''):gsub('\226\128\140','')
return mw.ustring.len(s)
end
local function effectiveJust(j, a, b)
if j == 'justify' then
local la, lb = charLen(a or ''), charLen(b or '')
local hi = math.max(la, lb)
if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback end
end
return 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 hemi(t) return '<span class="beyt">' .. t .. '</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
-- {{بیت}} — 7 cells
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)
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), hemi(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), hemi(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 = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2)
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
-- ردیف ۱: num-r | m1 | gap | empty | empty
-- ردیف ۲: empty | empty | gap | m2 | num-l
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, hemi(m1))
.. td('poem-num poem-num-c poem-empty', 1, nil, '')
.. td('poem-empty poem-mh-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 poem-mh-empty', 2, nil, '')
.. td('poem-num poem-num-c poem-empty', 1, nil, '')
.. td('b poem-m poem-m2', 2, s2, hemi(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
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r)
.. td(cellClass, cs[2], style, content)
.. 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') 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 '20%'
elseif c.kind == 'ladder' then indent = c.indent or '45%' 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 }
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
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,
}
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('20%'))
p.ladder = safe(staggerFn('45%'))
p.mesra = safe(mesra)
p.emptyBayt = safe(emptyFn('bayt'))
p.emptyMesra = safe(emptyFn('mesra'))
p.config = cfg
return p
lreao07tijsr68j7gdwrqqol4hj3gy9
304030
304028
2026-10-11T22:22:08Z
Hanooz
17889
304030
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت:
۱ ش | ۲-۳ مصراع اول | ۴ شو | ۵-۶ مصراع دوم | ۷ شچ
پس مصراعها در همهٔ ابیات و همهٔ انواع (بیت، بیت۱، نردبانی) هماندازهاند.
▸ ردیف سایزرِ خالی در آغاز جدول، عرض ستونها را تعیین میکند.
▸ ترازبندی پیشفرض وسط است. «پهن» و «لبه» اختیاری.
▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت.
بیت۱ دو ردیف است، پس خطچین فقط زیر ردیف دوم میآید.
]==]
local p = {}
local ZWNJ = '\226\128\140'
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '42em',
justDefault = { bayt = 'center', stagger = 'center' },
justFallback = 'center',
justifyMinRatio = 0.7,
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 function esc(s) return (s: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 charLen(s)
s = s:gsub('\127[^\127]*\127',''):gsub('<[^>]*>','')
:gsub('%[%[[^%]|]*|',''):gsub('[%[%]]',''):gsub('\226\128\140','')
return mw.ustring.len(s)
end
local function effectiveJust(j, a, b)
if j == 'justify' then
local la, lb = charLen(a or ''), charLen(b or '')
local hi = math.max(la, lb)
if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback end
end
return 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 hemi(t) return '<span class="beyt">' .. t .. '</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
-- {{بیت}} — 7 cells
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)
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), hemi(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), hemi(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 = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2)
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, hemi(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, hemi(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
return rowOpen(d, 'poem-mesra')
.. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r)
.. td(cellClass, cs[2], style, content)
.. 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')
local indent
if c.kind == 'stagger' then indent = c.indent
elseif c.kind == 'ladder' then indent = c.indent 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 }
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
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,
}
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
if raw then 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
oz14oetj7cnzfwt95b38bq2gcdy5z69
304032
304030
2026-10-11T22:33:40Z
Hanooz
17889
304032
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت:
۱ ش | ۲-۳ مصراع اول | ۴ شو | ۵-۶ مصراع دوم | ۷ شچ
پس مصراعها در همهٔ ابیات و همهٔ انواع هماندازهاند.
▸ ردیف سایزرِ خالی در آغاز جدول، عرض ستونها را تعیین میکند.
▸ ترازبندی پیشفرض وسط است. با ترازبندی=پهن دوطرفه و با لبه، مصراع اول
به لبهٔ راست و دوم به لبهٔ چپ میرود. «پهن» همیشه کشیده میکند و به
هیچ تراز دیگری فرو نمیافتد.
▸ ارجاعهای پایانی (<ref>) در سنجش بلندی شمرده نمیشوند و در خروجی، پس از
متن کشیدهشده و در انتهای مصراع قرار میگیرند.
▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت.
]==]
local p = {}
local ZWNJ = '\226\128\140'
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '42em',
justDefault = { bayt = 'center', stagger = 'center' },
justFallback = '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('>','>')) 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
---------------------------------------------------------------------------
-- ارجاعها: جداسازی و حذف از متن (پیش از سنجش بلندی)
---------------------------------------------------------------------------
-- حذف همهٔ <ref>...</ref>، <ref/>، و نشانگرهای \127...\127 از یک رشته
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
-- استخراج دنبالهٔ <ref>ها و نشانگرهای \127 از پایان رشته.
-- بازگردانده: (متنِ بدون ارجاعهای پایانی، رشتهٔ ارجاعها)
local function splitTrailingRefs(s)
if not s or s == '' then return '', '' end
-- ناحیهٔ ارجاعها را از انتها جدا کن
local refs = {}
local text = s
local changed = true
while changed do
changed = false
-- <ref ...>...</ref> در انتها
local before, ref, after = text:match('^(.-)(<ref[^>]*>.-</ref>)%s*$')
if before then
refs[#refs + 1] = ref
text = before
changed = true
else
-- <ref .../> در انتها
local b2, r2 = text:match('^(.-)(<ref[^>]*%s*/>)%s*$')
if b2 then
refs[#refs + 1] = r2
text = b2
changed = true
else
-- نشانگر \127...\127 در انتها
local b3, r3 = text:match('^(.-)(\127[^\127]*\127)%s*$')
if b3 then
refs[#refs + 1] = r3
text = b3
changed = true
end
end
end
end
-- ترتیب معکوس (از آخر به اول جمع شده)
local ordered = {}
for i = #refs, 1, -1 do ordered[#ordered + 1] = refs[i] end
return mw.text.trim(text), table.concat(ordered)
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 charLen(s)
s = stripRefs(s)
s = s:gsub('<[^>]*>','')
:gsub('%[%[[^%]|]*|','')
:gsub('[%[%]]','')
:gsub('\226\128\140','')
return mw.ustring.len(s)
end
-- ترازبندی «پهن» همیشه اعمال میشود؛ هیچ فروافتادی نیست.
local function effectiveJust(j)
return 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(rawText)
local body, refs = splitTrailingRefs(rawText or '')
local inner
if refs ~= '' then
inner = '<span class="beyt"><span class="beyt-text">' .. body ..
'</span><span class="beyt-refs">' .. refs .. '</span></span>'
else
inner = '<span class="beyt">' .. body .. '</span>'
end
return inner
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)
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 = effectiveJust(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 }
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
iy28ioz1pt6go0s048txpp71to2wwyr
304035
304032
2026-10-11T22:44:52Z
Hanooz
17889
304035
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت:
۱ ش | ۲-۳ مصراع اول | ۴ شو | ۵-۶ مصراع دوم | ۷ شچ
▸ ترازبندی پیشفرض «پهن» (دوطرفه) است. با ترازبندی=وسط/راست/چپ/لبه
قابل تغییر است.
▸ ارجاعهای پایانی (<ref>) از متن جدا میشوند و در ستون کوچک انتهای
مصراع مینشینند؛ متن مصراع مستقل از آنها کشیده میشود و کوتاه
بهنظر نمیرسد.
▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت.
]==]
local p = {}
local ZWNJ = '\226\128\140'
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '42em',
justDefault = { bayt = 'justify', stagger = 'center' },
justFallback = '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('>','>')) 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 splitTrailingRefs(s)
if not s or s == '' then return '', '' end
local refs = {}
local text = s
local changed = true
while changed do
changed = false
local before, ref
before, ref = text:match('^(.-)(<ref[^>]*>.-</ref>)%s*$')
if not before then
before, ref = text:match('^(.-)(<ref[^>]*%s*/>)%s*$')
end
if not before then
before, ref = text:match('^(.-)(\127[^\127]*\127)%s*$')
end
if before then
refs[#refs + 1] = ref
text = before
changed = true
end
end
local ordered = {}
for i = #refs, 1, -1 do ordered[#ordered + 1] = refs[i] end
return mw.text.trim(text), table.concat(ordered)
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 charLen(s)
s = stripRefs(s)
s = s:gsub('<[^>]*>','')
:gsub('%[%[[^%]|]*|','')
:gsub('[%[%]]','')
:gsub('\226\128\140','')
return mw.ustring.len(s)
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
-- محتوای مصراع: متن در flex:1 ، ارجاعها در flex:0
local function hemiCell(rawText)
local body, refs = splitTrailingRefs(rawText or '')
local inner = '<span class="beyt"><span class="beyt-text">' .. body .. '</span>'
if refs ~= '' then
inner = inner .. '<span class="beyt-refs">' .. refs .. '</span>'
end
return inner .. '</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 }
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
k57uxfk0yj1ebmtgys6dkahmqvbntzt
304037
304035
2026-10-12T03:59:36Z
Hanooz
17889
304037
Scribunto
text/plain
--[==[
پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی
▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت:
۱ ش | ۲-۳ مصراع اول | ۴ شو | ۵-۶ مصراع دوم | ۷ شچ
▸ ردیف سایزرِ خالی در آغاز جدول، عرض ستونها را تعیین میکند.
▸ ترازبندی پیشفرض «وسط». با ترازبندی=پهن دوطرفه و با لبه، مصراع اول
به لبهٔ راست و دوم به لبهٔ چپ میرود.
▸ ارجاعهای پایانی (<ref>) در سنجش بلندی شمرده نمیشوند اما در متن
بهصورت inline میمانند و به آخرین واژه میچسبند. بهاینترتیب حضور
ارجاع، طولِ قابل کشیدنِ متن را کوتاه نمیکند و فقط چند پیکسل
فضای طبیعی خودش را میگیرد.
▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت.
]==]
local p = {}
local ZWNJ = '\226\128\140'
local cfg = {
styles = 'Template:شعر۲/styles.css',
pageNs = 104,
nbsp = ' ',
defaultWidth = '100%',
maxWidth = '42em',
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('>','>')) 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 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 charLen(s)
s = stripRefs(s)
s = s:gsub('<[^>]*>','')
:gsub('%[%[[^%]|]*|','')
:gsub('[%[%]]','')
:gsub('\226\128\140','')
return mw.ustring.len(s)
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
-- مصراع: فقط wrap در span؛ ارجاعها inline میمانند.
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 }
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
3zjws7bplum20owc51ycqcxzk6oewrv
برگه:بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۸
104
97060
304015
302757
2026-10-11T20:31:31Z
Hanooz
17889
304015
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />{{سرصفحهمستمر|۴|بدایع شیخ سعدی|}}</noinclude>{{وسط|{{اندازه قلم|110%|'''۳'''}}}}
{{وسط|<nowiki>[---⏑|---⏑|---⏑|---⏑]</nowiki> هزج مثمّن سالم :Metre}}
{{شعر۲|نوع=ضربدری|متن=
ز حد بگذشت مشتاقی و صبر اندر غمت مارا!<ref>R; یارا, for مارا.</ref>
بوصل خود دوائی کن دل دیوانهٔ ما را
علاج درد مشتاقان طبیب عام نشناسد
مگر لیلی کند درمان غم مجنون شیدا را
گرت پروای غمگینان نخواهد بود و مسکینان
نبایستی نمود اوّل بما آن روی زیبارا
چو بنمودی و بربودی ثبات از عقل و صبر از دل
بباید چارهٔ کردن کنون این ناشکیبارا
مرا سودای بت رویان نبودی پیش ازین در سر
و لیکن تاترا دیدم گزیدم راه سودا را
مراد ما وصال تست از دنیی و از عقبی
و گرنه بی شما قدری نباشد دین و دنیارا
چنان مشتاقم ای دلبر بدیدارت که گر روزی
بر آید از دلم آهی بسوزد هفت دریارا
بیا تا یک زمان امروز خوش باشیم در خلوت
که در عالم نمیداند کسی احوال فردا را
سخن شیرین همی گوئی برغم دشمنان سعدی
ولی بیمار استسقا چه داند ذوق حلوارا
}}
{{چپ|N. B. This ode is only found in I and R.}}<noinclude>{{خطکش}}
{{پانویس|چپچین=بله}}</noinclude>
1kubmxdh1wso2w3vxpf8ihqggiegzf9
برگه:بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۹
104
97061
303991
302768
2026-10-11T18:09:50Z
Hanooz
17889
303991
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />{{سرصفحهمستمر||بدایع شیخ سعدی|۵}}</noinclude><section begin="p19-20" />
{{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}}
{{شعر|نوع=ضربدری|ادامه=بعد|متن=
با جوانی سرخوش است این پیر بی تدبیر را
جهل باشد با جوانان پنجه کردن پیر را
من که با موئی بقوّت بر نیایم ایعجب
با یکی<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>
sshnuf5hn2z3nidm60o5t4u7v6jw4tz
303992
303991
2026-10-11T18:10:24Z
Hanooz
17889
303992
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>
50ez15m3iyzsnky5pfb07770227ukph
304012
303992
2026-10-11T20:30:20Z
Hanooz
17889
304012
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
برگه:بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۲۰
104
97062
304016
302767
2026-10-11T20:31:47Z
Hanooz
17889
304016
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />{{سرصفحهمستمر|۶|بدایع شیخ سعدی|}}</noinclude>{{شعر۲|نوع=ضربدری|ادامه=قبل|متن=
زهد پیدا کفر پنهان بود چندین روزگار
پرده از سر بر گرفتم<ref>L; گرفتیم, for گرفتم.</ref> اینهمه تزویررا<ref>I, P, R; آن, for این.</ref>
سعدیا در پای جانان گر بخدمت سر نهی
همچنان عذرت بباید خواستن تقصیر را
}}
<section end="p19-20" />
<section begin="p20-21" />
{{وسط|{{اندازه قلم|110%|'''۵'''}}}}
{{وسط|<nowiki>[--⏑|⏑--⏑|⏑--⏑|⏑--]</nowiki> هزج مثمّن اخرب مکفوف محذوف :Metre}}
{{شعر۲|ادامه=بعد|متن=
ساقی بده آن کوزهٔ یاقوت روانرا
یاقوت چه باشد<ref>In R the hemistich 8(b) is substituted, for 7(b), and 7(b) & 8(a)
are omitted.</ref> بده آن قوت روانرا
اول پدر پیر خورد رطل دمادم
تا مدّعیان هیچ نگویند<ref>I(a), P, R, C; ارزد, for باشد.</ref> جوانرا
تا مست نباشی نکشی بار غم یار
آری شتر مست کشد<ref>L, Y(a), Y(b); خورده نگیرند, for هیچ نگویند: I(a); عیب نگیرند, for do.</ref> بار گرانرا
ای روی تو آرام دل خلق جهانی
بیروی تو شاید که نه بینند جهانرا
در صورت و معنی که تو داری چه توانگفت
حسن تو ز تحسین تو بسته است زبانرا
اینک عسلی دوخته دارد<ref>I; عسل اندوخت آنک, for اینک عسلی دوخته.</ref> مگس نحل
شهد لب شیرین تو زنبور میانرا
زین دست که دیدار تو دل میبرد از دست
ترسم نبرم عاقبت از دست تو جانرا
یا<ref>L, Y(b); تا, for یا.</ref> تیر هلاکم بزنی بر دل مجروح
تا<ref>R, C, Y(a), Y(b); یا for تا.</ref> جان بدهم یا بدهی تیر امانرا
وانگه که بتیرم زنی اوّل خبرم کن
تا پیشترت بوسه دهم دست و کمانرا
}}<noinclude>{{خطکش}}
{{پانویس|چپچین=بله}}</noinclude>
6ml97p02vcqh0teyf4uzq031laclnf3
برگه:بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۹
104
97124
303959
303939
2026-10-11T14:33:07Z
Hanooz
17889
303959
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" /></noinclude>{{وسط|'''مقدّمه'''}}
بدایع شیخ مصلح الدین سعدی شیرازی که خود از بدایع روزگار بود یکی از نفیسترین قسمتهای گنجینهٔ کلیات این شاعر بزرگوار را تشکیل میدهد.
در خصوص مؤلّفات و شرح حیات سعدی تحقیقات زیادی بعمل امده علمای چند عمری مصروف این خدمت نمودهاند. بهمین ملاحظه ما در اینجا از ذکر جزئیّات احوال شاعر صرف نظر نموده فقط بایراد بعضی ملاحظات میپردازیم؛ کتاب موسوم به {{w|تحقیق درباره سعدی|ESSAI SUR LE POET SAʿDI}} (تحقیقات دربارهٔ سعدی شاعر) که در سال ۱۹۱۹ مسیحی بتوسط یکنفر فرانسوی موسوم به «{{w|هانری ماسه}}» در پاریس انتشار یافته است و شاید بهترین رسالهای باشد که در این موضوع تألیف شده کتاب بسیار نفیس و جامعی است، این کتاب در ۳۴۷ صفحه شامل چهار بابست و هر باب چند فصل دارد که ما برای استحضار خوانندگان مختصراً بدانها اشاره میکنیم: باب اول در خصوص شخص سعدی، باب دوّم در تحقیق عقاید و افکار او، باب سوّم در صفت شاعری او، باب چهارم در خصوص کتب و رسالهائیست که تا کنون دربارهٔ سعدی بالسنهٔ مختلفه تألیف شده، از اینرو این کتاب جامع تمام تحقیقات راجع بسعدی است. علاوه بر این در کتاب تاریخ ادبیات ایران زبان انگلیسی A LITERARY HISTORY OF PERSIA. (جلد از فردوسی تا سعدی صفحه ۳۲۶) تألیف {{w|ادوارد براون|براون}} و در کتاب «اساس فیلولوژی ایرانی» بزبان آلمانی [[:de:Grundriss der iranischen Philologie|GRUNDRISS DER IRANISCHEN PHILOLOGIE]]. بقلم {{w|هرمان اته|اِته}} شرح مفیدی راجع بسعدی موجود است.<ref>کتاب اخیر شاید بهترین و جامعترین کتابیست که دربارهٔ زبان فارسی نوشته شده.</ref>
سعدی بعقیدهٔ مشهور در شیراز بسال ۵۸۰ متولد و ۶۹۰ یا ۶۹۱ وفات یافته است و معاصر با {{w|اتابکان فارس}} بوده نام پدرش عبدالله ولی اسم خود<noinclude>{{خطکش}}
{{پانویس-کوچک}}</noinclude>
7tb9is6cxflt9ub756je704zv162xqk
برگه:بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۰
104
97129
303960
303949
2026-10-11T14:43:22Z
Hanooz
17889
/* نمونهخوانیشده */
303960
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />
{{سرصفحهمستمر||(۴)|}}</noinclude>شاعر معلوم نیست و شیخ مصلح الدین که بسعدی خطاب میشود نام پدر اوست و لقب خودش شیخ مشرف الدین است و سعدی تخلص شاعرانهٔ اوست که بمناسبت اسم ممدوح خود اختیار کرده.
در آخر نسخهای که در این کتاب زیر صفحات جزء یادداشتها با علامت I. بدان اشاره میشود (شرح این نسخه در زیر خواهد آمد) و تاریخ تحریر ان سی و شش سال پس از وفات شاعر است عبارت عربی ذیل موجود است (نقلت من خطّ المصنّف و هو مشرف الدین بن مصلح الدین عبدالله) این عبارت دو اهمّیت دارد یکی اینکه از روی ان بخوبی واضح میشود که نام سعدی مشرف الدین است و مصلح الدین اسم پدر اوست دیگر اینکه صحّت و قدمت نسخه سابق الذکر را ثابت نموده آشکار میکند که این نسخه از روی خط خود سعدی استنساخ شده است و بهمین نظر این نسخه اهمّیت زیاد در نظر محققین دارد ولی متأسفانه ناقص است.
سعدی از دوازده سالگی شروع بمسافرت کرده سی سال از زندگانی صد و اند سالهٔ خود را در {{w|نظامیه بغداد|مدرسهٔ نظامیّهٔ بغداد}} بتحصیل علوم پرداخته سی سال دیگر همواره در سفر بوده، مسافرتهای او را اگر چه نمیتوان با ترتیب و تاریخ دقیق معین کرد ولی آنچه که از اشارات مختلفهٔ {{w|گلستان سعدی|گلستان}} و {{w|بوستان سعدی|بوستان}} او بر میآید به بین النهرین و آسیای صغیر و مصر و مراکش و شام و بیلقان (شاید از شهر تبریز) و کجرات و غیره سفر کرده و مسافرت او در مغرب مصادف با جنگهای صلیبی بوده و سعدی اسیر شده. پس از سی سال جهانگردی بشیراز آمده و در گوشهای بیرون شیراز رحل اقامت افگنده شاید اغلب آثار خود را نیز در همان زمان تألیف نموده است و اگر تاریخ تولد که ذکر کردیم صحیح باشد سعدی گلستان خود را مطابق شعری که میگوید:
{{شعر|متن=
در آن مدت که ما را وقت خوش بود
ز هجرت ششصد و پنجاه و شش بود
}}
در سنّ هفتاد و شش سالگی پس از مسافرتها و جهانگردیها تألیف نموده.
این شاعر بزرگوار در زمان حیات خود نیز مورد توجّه بوده، تربتش بیرون شهر شیراز در باغ باصفا و دلگشائیست.<noinclude></noinclude>
p36p9qu2uybfyzf1prha11vbl2pk208
304013
303960
2026-10-11T20:30:58Z
Hanooz
17889
304013
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />
{{سرصفحهمستمر||(۴)|}}</noinclude>شاعر معلوم نیست و شیخ مصلح الدین که بسعدی خطاب میشود نام پدر اوست و لقب خودش شیخ مشرف الدین است و سعدی تخلص شاعرانهٔ اوست که بمناسبت اسم ممدوح خود اختیار کرده.
در آخر نسخهای که در این کتاب زیر صفحات جزء یادداشتها با علامت I. بدان اشاره میشود (شرح این نسخه در زیر خواهد آمد) و تاریخ تحریر ان سی و شش سال پس از وفات شاعر است عبارت عربی ذیل موجود است (نقلت من خطّ المصنّف و هو مشرف الدین بن مصلح الدین عبدالله) این عبارت دو اهمّیت دارد یکی اینکه از روی ان بخوبی واضح میشود که نام سعدی مشرف الدین است و مصلح الدین اسم پدر اوست دیگر اینکه صحّت و قدمت نسخه سابق الذکر را ثابت نموده آشکار میکند که این نسخه از روی خط خود سعدی استنساخ شده است و بهمین نظر این نسخه اهمّیت زیاد در نظر محققین دارد ولی متأسفانه ناقص است.
سعدی از دوازده سالگی شروع بمسافرت کرده سی سال از زندگانی صد و اند سالهٔ خود را در {{w|نظامیه بغداد|مدرسهٔ نظامیّهٔ بغداد}} بتحصیل علوم پرداخته سی سال دیگر همواره در سفر بوده، مسافرتهای او را اگر چه نمیتوان با ترتیب و تاریخ دقیق معین کرد ولی آنچه که از اشارات مختلفهٔ {{w|گلستان سعدی|گلستان}} و {{w|بوستان سعدی|بوستان}} او بر میآید به بین النهرین و آسیای صغیر و مصر و مراکش و شام و بیلقان (شاید از شهر تبریز) و کجرات و غیره سفر کرده و مسافرت او در مغرب مصادف با جنگهای صلیبی بوده و سعدی اسیر شده. پس از سی سال جهانگردی بشیراز آمده و در گوشهای بیرون شیراز رحل اقامت افگنده شاید اغلب آثار خود را نیز در همان زمان تألیف نموده است و اگر تاریخ تولد که ذکر کردیم صحیح باشد سعدی گلستان خود را مطابق شعری که میگوید:
{{شعر۲|متن=
در آن مدت که ما را وقت خوش بود
ز هجرت ششصد و پنجاه و شش بود
}}
در سنّ هفتاد و شش سالگی پس از مسافرتها و جهانگردیها تألیف نموده.
این شاعر بزرگوار در زمان حیات خود نیز مورد توجّه بوده، تربتش بیرون شهر شیراز در باغ باصفا و دلگشائیست.<noinclude></noinclude>
cjvqx5vbkp76ypflh5x5828lef5ek1g
برگه:بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۱
104
97130
303961
303950
2026-10-11T15:14:05Z
Hanooz
17889
/* نمونهخوانیشده */
303961
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />{{سرصفحهمستمر||(۵)|}}</noinclude>تألیفات سعدی چون نتیجه و حاصل تحصیلات و تجربیات طولانیست بسیار پر معنی و در نهایت درجه اهمیت است و از لطیفترین محصولات گلستان ادبیّات فارسی میباشد.
مجموع تألیفات شاعر را تقریباً سی و پنج سال پس از وفات او شخصی موسوم به {{w|ابوبکر بیستون|علی بن احمد بن ابی بکر بیستون}} جمع کرده، نخستین بار کلیّات سعدی (شامل رسالات، گلستان، بوستان، پندنامه، قصائد عربیه، قصائد فارسی، قطعات، ملمّعات، طیّبات، بدایع، خواتیم، غزلیّات قدیمه، خبیثات، رباعیّات و مفردات) بدستیاری مولوی محمد رشید و هارینگتون ({{w|:en:John Herbert Harington|Harington}}) انگلیسی در سال ۱۲۰۹ در کلکته بخط نستعلیق چاپ شده که بهترین چاپهای کلیات است. پس از آن تا کنون قریب بیست بار دیگر کلیّات بچاپ رسیده هشت بار در بمبئی، چهار بار در طهران، سه بار در تبریز، دو بار در کلکته، یک بار در لکنهو، یک بار در دهلی و یک بار هم در خانپور. در خود ایران برای اولین دفعه در شهر تبریز بسال ۱۲۵۷ چاپ شده. گلستان برای اولین بار مدتها قبل از کلیّات بسال ۱۰۶۹ در امستردام چاپ شده ولی تا انوقت ترجمهٔ گلستان یکبار در پاریس بطبع رسیده بود بعد از آن چند بار در کلکته چاپ شد. در ایران برای نخستین مرتبه در سال ۱۲۴۰ در تبرینر چاپ شد. رویهمرفته گلستان ۷۲ مرتبه طبع شده و چاپ اخیر آن (بنظر نگارنده) چاپ برلین است که در مطبعهٔ کاویانی با اسلوب خوبی بطبع رسیده. گلستان ۵۵ مرتبه بالسنهٔ مختلفه ترجمه شده و ترجمه آلمانی ان که از «{{w|فریدریش روزن|دکتر روزن}}»<ref>سابقاً سفیر آلمان در طهران و اخیراً وزیر امور خارجهٔ آلمان بود.</ref> است ترجمه بسیار خوبیست و میتوان گفت که انسان از خواندن بعضی قسمتهای آن باندازهٔ خود اصل محظوظ میشود.
کتاب بوستان به تنهائی اولین بار بسال ۱۲۲۹ در کلکته بطبع رسیده ولی مدتی قبل از آن بالسنهٔ خارجی ترجمه شده بود. در ایران نخستین دفعه سال ۱۲۴۷ در {{کذا|تبرینر|تبریز}} چاپ شده و کلّیتهً ۳۴ مرتبه طبع و ۱۷ بار ترجمه شده است.
غزلیّات سعدی تا این اواخر جداگانه بطبع نرسیده فقط جزء کلیّات انتشار<noinclude>{{خطکش}}
{{پانویس-کوچک}}</noinclude>
g5924rhucqhzixh0ika8capl2b9xd8z
برگه:بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۲
104
97131
303962
303951
2026-10-11T15:22:17Z
Hanooz
17889
/* نمونهخوانیشده */
303962
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />{{سرصفحهمستمر||(۶)|}}</noinclude>یافته بود ولی بالاخره اهمیت و مقام ادبی این ترانهای شیرین ادبیّات شرقی نظر اهل نظر و مستشرقین را متوجه خود ساخت تا اینکه مقداری از غزلیّات جداگانه منتشر شد.
غزلیّات سعدی چهار دسته است: طیّبات، بدایع، خواتیم و غزلیّات قدیمه. عدّهٔ غزلیات طیبات بیشتر از سه قسمت دیگر یعنی ۴۱۸ است در صورتیکه مجموع غزلیات سه جزء دیگر فقط ۲۹۶ میباشد. نسخهٔ حاضره که عبارت از جزء بدایع است ۱۹۰ غزل دارد.
اگر در غزلیّات سعدی کاملاً دقت کنیم ملاحظه خواهیم نمود که شاعر بعرفان و تصوّف کاملاً آشناست و از این نقطهٔ نظر اشعار او دارای مقام مهمّی است ولی بعقیدهٔ بعضی در تغزّل بپایهٔ حافظ و در تصوّف بپایهٔ مولوی رومی و عطّار نمیرسد. اما مستشرق انگلیسی «پرفسور براون» در کتاب تاریخ ادبیّات ایران (که بدان اشاره کردیم) در این خصوص مینویسد:
{{چپ|{{سهنقطه}} in his Odes Saʿdi considered as inferior to no Persian Poet, not even Hafiz.}}
در تغزّل سعدی از هیچیک از شعرای ایران حتّی از حافظ هم کمتر نیست.
با تمام این احوال غزلیّات سعدی در ادبیات ایران پایهٔ بس بلندی دارد که هر صاحب ذوق سلیم را مجذوب خود مینماید.
زحمت انتشار یک سلسله غزلیّات را مرحوم پروفسور «لوکاس کینگ»<ref>معلّم زبان فارسی در دارالفنون دوبلن.</ref> شروع نمود. طیّبات را با اسلوبی که در همین نسخهٔ بدایع ملاحظه میشود سال ۱۹۲۰ در کلکته انتشار داد و زحمت ترتیب و مطابقهٔ نسخهٔ حاضره را متحمل شده غزلیّاترا بزبان انگلیسی نیز ترجمه نمود. متأسفانه چند ماه قبل پس از تمام شدن چاپ بدایع آخرین مراسله که از انگلیس میرسید فوت پروفسور لوکاس کینک را خبر میداد ولی زحمات او از نظر دانش پژوهان محو نشده ذکر خیرش باقی خواهد ماند.<noinclude>{{خطکش}}
{{پانویس-کوچک}}</noinclude>
6jegd544ug95chz09kyhhqba6x3hijo
303973
303962
2026-10-11T15:55:24Z
Hanooz
17889
303973
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />{{سرصفحهمستمر||(۶)|}}</noinclude>یافته بود ولی بالاخره اهمیت و مقام ادبی این ترانهای شیرین ادبیّات شرقی نظر اهل نظر و مستشرقین را متوجه خود ساخت تا اینکه مقداری از غزلیّات جداگانه منتشر شد.
غزلیّات سعدی چهار دسته است: طیّبات، بدایع، خواتیم و غزلیّات قدیمه. عدّهٔ غزلیات طیبات بیشتر از سه قسمت دیگر یعنی ۴۱۸ است در صورتیکه مجموع غزلیات سه جزء دیگر فقط ۲۹۶ میباشد. نسخهٔ حاضره که عبارت از جزء بدایع است ۱۹۰ غزل دارد.
اگر در غزلیّات سعدی کاملاً دقت کنیم ملاحظه خواهیم نمود که شاعر بعرفان و تصوّف کاملاً آشناست و از این نقطهٔ نظر اشعار او دارای مقام مهمّی است ولی بعقیدهٔ بعضی در تغزّل بپایهٔ حافظ و در تصوّف بپایهٔ مولوی رومی و عطّار نمیرسد. اما مستشرق انگلیسی «پرفسور براون» در کتاب تاریخ ادبیّات ایران (که بدان اشاره کردیم) در این خصوص مینویسد:
{{چپچین}}... in his Odes Saʿdi considered as inferior to no Persian Poet, not even Hafiz.{{پایان}}
در تغزّل سعدی از هیچیک از شعرای ایران حتّی از حافظ هم کمتر نیست.
با تمام این احوال غزلیّات سعدی در ادبیات ایران پایهٔ بس بلندی دارد که هر صاحب ذوق سلیم را مجذوب خود مینماید.
زحمت انتشار یک سلسله غزلیّات را مرحوم پروفسور «لوکاس کینگ»<ref>معلّم زبان فارسی در دارالفنون دوبلن.</ref> شروع نمود. طیّبات را با اسلوبی که در همین نسخهٔ بدایع ملاحظه میشود سال ۱۹۲۰ در کلکته انتشار داد و زحمت ترتیب و مطابقهٔ نسخهٔ حاضره را متحمل شده غزلیّاترا بزبان انگلیسی نیز ترجمه نمود. متأسفانه چند ماه قبل پس از تمام شدن چاپ بدایع آخرین مراسله که از انگلیس میرسید فوت پروفسور لوکاس کینک را خبر میداد ولی زحمات او از نظر دانش پژوهان محو نشده ذکر خیرش باقی خواهد ماند.<noinclude>{{خطکش}}
{{پانویس-کوچک}}</noinclude>
p1l7fqq12isz3ls8ozba81bndl79p7n
برگه:بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۳
104
97132
303963
303952
2026-10-11T15:30:56Z
Hanooz
17889
/* نمونهخوانیشده */
303963
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />{{سرصفحهمستمر||(۷)|}}</noinclude>در این کتاب نسخه بدلها بتوسط اشارات نسخ مختلفه در زیر صفحات معیّن شده تا خواننده در صورت داشتن آن از تمام نسخ خطّی و چاپی دیگر بی نیاز باشد و نیز وزن اشعار با تعیین نام عروضی و اشاراتیکه ما بین مستشرقین برای تعیین اوزان اشعار شرقی معمولست بالای هر غزل نوشته شده.
ما در زیر علامات و اشارات یادداشتهای زیر صفحات را شرح داده نیز ترجمهٔ بعضی کلمات انگلیسی را که اغلب تکرار میشود مینویسیم.<ref>علت وجود کلمات انگلیسی برای انتشار این کتاب در انگلیس و آمریکاست.</ref>
{{وسط|{{اندازه قلم|120%|اشارات بنسخ خطّی}}}}
(I): اشارهٔ بنسخهٔ ادارهٔ هندوستان است و ما در مشروحات گذشته از این نسخه اسم بردیم، تاریخ تحریر آن ۷۲۸ هجری (۱۳۲۷ میلادی) است و اشعار ان بترتیب حروف هجا مرتب نشده غزل اوّل طیّبات با قافیهٔ(ی) شروع میشود ولی چنانکه گفتیم متأسفانه این نسخه با وجود اهمیّت و اعتبار زیاد ناقص است و به تنهائی از روی آن نمیتوان رسالهٔ کاملی انتشار داد.
(I a): نسخهٔ دیگری از ادارهٔ هندوستان که تاریخ ندارد ولی خیلی قدیم و معتبر است.
(I b): باز نسخهٔ قدیمی است از ادارهٔ هندوستان که تاریخ ندارد ولی با وجود قدمت چندان معتبر نیست.
(B): نسخهٔ {{w|موزه بریتانیا|موزهٔ بریطانی}} (Britisch Museum) که تاریخ آن ۹۷۴ هجری (۱۵۶۶ میلادی) است و آن اعتبار زیاد ندارد.
(P): نسخهٔ {{w|کتابخانه ملی فرانسه|کتابخانهٔ ملی پاریس}} (Bibliotheque Nationale) بتاریخ ۷۷۶ هجری (۱۳۶۵ میلادی) است این نسخه غیر معتبر است و ۸۰ غزل از طیّبات را ندارد و اغلب غزلیّات قسمتهای مختلفه داخل یکدیگر شده است.
(S): نسخهٔ {{w|کتابخانه ملی روسیه|کتابخانه شاهی پطرگراد}} (Imperial Library) که نسبت بنسخ سابق الذکر خیلی معتبر است و تاریخ تحریر آن ۷۸۷ هجری (۱۳۸۵ میلادی) است.<noinclude>{{خطکش}}
{{پانویس-کوچک}}</noinclude>
2fggkgbrwjp11xxs72z64bvqhwowoip
برگه:بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۴
104
97133
303965
303953
2026-10-11T15:35:18Z
Hanooz
17889
/* نمونهخوانیشده */
303965
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />{{سرصفحهمستمر||(۸)|}}</noinclude>(R): نسخهٔ کتابخانهٔ شاهی کابل که در دست پرفسور لوکاس کینگ بوده و این نسخه بخط خوب نوشته شده و عموماً نسخهٔ معتبریست.
{{وسط|{{اندازه قلم|120%|اشارات بنسخ چاپی}}}}
(T): نسخهٔ طهران (۱۸۵۱ میلادی)
(Z): نسخهٔ تبریز (۱۸۴۱)
(C): نسخه کلکته (۱۷۹۱)
Y(a): بمبئی (۱۸۸۰)
Y(h) (۱۴۹۱):
(۱۸۹۱) در بمبئی سه نسخه انتشار یافته ولی نسخه اولی با نسخه (Y(a) شبیه است.
(D): دهلی (۱۸۵۲)
(L) لکنهو انتشار یافته سال ۱۸۷۰، ۱۸۷۶ و ۱۸۸۵. : لکنهو سه نسخه در برای اینکه ایرانیان محتر میکه انگلیسی نمیدانند نیز از مقایسه نسخ مختلفه که در زیر صفحات نوشته شده استفاده نمایند کلمات ذیل را که اغلب تکرار میشود ترجمه مینمائیم. ایضاً تمام = all, و and, هستند = are, ذیل زیر = follows برای = for,,for do =
در in, است (مخفف کلمه Manuskript) و نه = = is, سطر =۰ Line نسخه خطی، Ms No, محذوف = ommitted, نسخه چاپی = Text, Text علامت جمع است)، این. This = و در آخر کلمه Ms
چاپخانهٔ کاویانی و آفتاب که خدماتش بمعارف ایران فراموش نشدنی است با وجود گرفتاریهای زیاد نسخهٔ حاضرهٔ بدایع و ترجمهٔ انگلیسی آنرا بچاپ رسانده به گنجینهٔ ادبیات ایران میسپارد و امیدوار است که برای تکمیل خدمت بقیّهٔ غزلیّات شاعر را نیز بچاپ رساند.
{{وسط|ت. اِرانی
معلم ادبیّات شرقی در دارالفنون برلین
مهرماه ۱۳۰۴ هجری شمسی
}}<noinclude></noinclude>
ttod3vhcob3omz9gbx3ts8ia8jsdgig
303968
303965
2026-10-11T15:46:41Z
Hanooz
17889
303968
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />{{سرصفحهمستمر||(۸)|}}</noinclude>(R): نسخهٔ کتابخانهٔ شاهی کابل که در دست پرفسور لوکاس کینگ بوده و این نسخه بخط خوب نوشته شده و عموماً نسخهٔ معتبریست.
{{وسط|{{اندازه قلم|120%|اشارات بنسخ چاپی}}}}
(T): نسخهٔ طهران (۱۸۵۱ میلادی)
(Z): نسخهٔ تبریز (۱۸۴۱)
(C): نسخهٔ کلکته (۱۷۹۱)
Y(a): بمبئی (۱۸۸۰)
Y(h): {{رچ}}{{ایضاً|بمبئی|»}} (۱۸۹۱) در بمبئی سه نسخه انتشار یافته ولی نسخهٔ اولی با نسخه (Y(a) شبیه است.
(D): دهلی (۱۸۵۲)
(L): لکنهو، سه نسخه در لکنهو انتشار یافته سال ۱۸۷۰، ۱۸۷۶ و ۱۸۸۵. برای اینکه ایرانیان محترمیکه انگلیسی نمیدانند نیز از مقایسهٔ نسخ مختلفه که در زیر صفحات نوشته شده استفاده نمایند کلمات ذیل را که اغلب تکرار میشود ترجمه مینمائیم.
تمام = all, و = and, هستند = are, ذیل زیر = follows, برای = for, ایضاً = for do, در = in, است = is, سطر = Line, نسخهٔ خطی = Ms (مخفّف کلمهٔ Manuskript), نه = No, محذوف = ommitted, نسخهٔ چاپی = Text, (s در آخر کلمه Ms و Text علامت جمع است), این = This.
چاپخانهٔ کاویانی و آفتاب که خدماتش بمعارف ایران فراموش نشدنی است با وجود گرفتاریهای زیاد نسخهٔ حاضرهٔ بدایع و ترجمهٔ انگلیسی آنرا بچاپ رسانده به گنجینهٔ ادبیات ایران میسپارد و امیدوار است که برای تکمیل خدمت بقیّهٔ غزلیّات شاعر را نیز بچاپ رساند.
{{وسط|ت. اِرانی
معلم ادبیّات شرقی در دارالفنون برلین
مهرماه ۱۳۰۴ هجری شمسی
}}<noinclude></noinclude>
lfkpfc8h9xk5k4dl0kaen0iue9qxpjc
303972
303968
2026-10-11T15:51:26Z
Hanooz
17889
303972
proofread-page
text/x-wiki
<noinclude><pagequality level="3" user="Hanooz" />{{سرصفحهمستمر||(۸)|}}</noinclude>(R): نسخهٔ {{w|کتابخانه کابل|کتابخانهٔ شاهی کابل}} که در دست پرفسور لوکاس کینگ بوده و این نسخه بخط خوب نوشته شده و عموماً نسخهٔ معتبریست.
{{وسط|{{اندازه قلم|120%|اشارات بنسخ چاپی}}}}
(T): نسخهٔ طهران (۱۸۵۱ میلادی)
(Z): نسخهٔ تبریز (۱۸۴۱)
(C): نسخهٔ کلکته (۱۷۹۱)
Y(a): بمبئی (۱۸۸۰)
Y(h): {{رچ}}{{ایضاً|بمبئی|»}} (۱۸۹۱) در بمبئی سه نسخه انتشار یافته ولی نسخهٔ اولی با نسخه (Y(a) شبیه است.
(D): دهلی (۱۸۵۲)
(L): لکنهو، سه نسخه در لکنهو انتشار یافته سال ۱۸۷۰، ۱۸۷۶ و ۱۸۸۵. برای اینکه ایرانیان محترمیکه انگلیسی نمیدانند نیز از مقایسهٔ نسخ مختلفه که در زیر صفحات نوشته شده استفاده نمایند کلمات ذیل را که اغلب تکرار میشود ترجمه مینمائیم.
تمام = all, و = and, هستند = are, ذیل زیر = follows, برای = for, ایضاً = for do, در = in, است = is, سطر = Line, نسخهٔ خطی = Ms (مخفّف کلمهٔ Manuskript), نه = No, محذوف = ommitted, نسخهٔ چاپی = Text, (s در آخر کلمه Ms و Text علامت جمع است), این = This.
چاپخانهٔ کاویانی و آفتاب که خدماتش بمعارف ایران فراموش نشدنی است با وجود گرفتاریهای زیاد نسخهٔ حاضرهٔ بدایع و ترجمهٔ انگلیسی آنرا بچاپ رسانده به گنجینهٔ ادبیات ایران میسپارد و امیدوار است که برای تکمیل خدمت بقیّهٔ غزلیّات شاعر را نیز بچاپ رساند.
{{وسط|ت. اِرانی
معلم ادبیّات شرقی در دارالفنون برلین
مهرماه ۱۳۰۴ هجری شمسی
}}<noinclude></noinclude>
s2gj3p0pnabd2az53aj4qgiet0h1exv
برگه:بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۵
104
97134
303974
2026-10-11T16:32:29Z
Hanooz
17889
/* نمونهخوانینشده */ صفحهای تازه حاوی «{{وسط|'''بدایع شیخ سعدی علیه الرحمه'''}} {{وسط|بسم الله الرحمن الرحیم}} Metre: بسیط مثمن سالم {{شعر|ادامه=بعد|متن= الحمد لله رب العالمین علی ما درّ من نعمة عزّ اسمه و علا الکافل الرّزق<ref></ref> احساناً و موهبةً ان احسنوه و ان لم یحسنوا عملا سبحانه من عظیم...» ایجاد کرد
303974
proofread-page
text/x-wiki
<noinclude><pagequality level="1" user="Hanooz" />{{سرصفحهمستمر||بدایع شیخ سعدی|num}}</noinclude>{{وسط|'''بدایع شیخ سعدی علیه الرحمه'''}}
{{وسط|بسم الله الرحمن الرحیم}}
Metre: بسیط مثمن سالم
{{شعر|ادامه=بعد|متن=
الحمد لله رب العالمین علی
ما درّ من نعمة عزّ اسمه و علا
الکافل الرّزق<ref></ref> احساناً و موهبةً
ان احسنوه و ان لم یحسنوا عملا
سبحانه من عظیم قادر صمد
منشی الوری حبلاً من بعدهم حبلا
الجن و الانس و الاکوان جمهرةً
تخرّ بین یدیه<ref></ref> سجداً ذللاه<ref></ref>
طوبی لطالبه<ref></ref> تعساً<ref></ref> لتارکه
بعداً لمتخذ<ref></ref> من دونه بدلا<ref></ref>
کم فی البریة<ref></ref> من آثار قدرته
و فی السماء لآیات لمن عقلا<ref></ref>}}
C, Y(a);
. رزق for, راز)
YL; X, for
r L; e, for
: Y (a);
, for do: Y (b);, for do.
: C, Y (a), Y (b); as, for do: Z;, for do.
for, تحیّر C: تخربین یدیه for, مسخرون لدیه اکوان الوان ۱
یدیه forیدیهم (a): تخر
o In R this line is omitted.
. لطالبه for تارکه)
. تعساً for بغیاً
متخذ
AI (a);, for: C, Y(a); ja, for do.
منجزم
۹ In Z lines ۵, ۷, ۸, ۹, ۱۰, ۱۱, are omitted.
. for do, و فی البریة (Y (b: کم فی البریة من for, قد ظهرت فی الارض: ۱۰
۱۱ R; Me, for Mac: C; ie, for do.<noinclude>{{خطکش}} {{پانویس|چپچین=بله}}</noinclude>
3c6cu2hkvklhpupoiw1etkl877592t8
304014
303974
2026-10-11T20:31:20Z
Hanooz
17889
304014
proofread-page
text/x-wiki
<noinclude><pagequality level="1" user="Hanooz" />{{سرصفحهمستمر||بدایع شیخ سعدی|num}}</noinclude>{{وسط|'''بدایع شیخ سعدی علیه الرحمه'''}}
{{وسط|بسم الله الرحمن الرحیم}}
Metre: بسیط مثمن سالم
{{شعر۲|ادامه=بعد|متن=
الحمد لله رب العالمین علی
ما درّ من نعمة عزّ اسمه و علا
الکافل الرّزق<ref></ref> احساناً و موهبةً
ان احسنوه و ان لم یحسنوا عملا
سبحانه من عظیم قادر صمد
منشی الوری حبلاً من بعدهم حبلا
الجن و الانس و الاکوان جمهرةً
تخرّ بین یدیه<ref></ref> سجداً ذللاه<ref></ref>
طوبی لطالبه<ref></ref> تعساً<ref></ref> لتارکه
بعداً لمتخذ<ref></ref> من دونه بدلا<ref></ref>
کم فی البریة<ref></ref> من آثار قدرته
و فی السماء لآیات لمن عقلا<ref></ref>}}
C, Y(a);
. رزق for, راز)
YL; X, for
r L; e, for
: Y (a);
, for do: Y (b);, for do.
: C, Y (a), Y (b); as, for do: Z;, for do.
for, تحیّر C: تخربین یدیه for, مسخرون لدیه اکوان الوان ۱
یدیه forیدیهم (a): تخر
o In R this line is omitted.
. لطالبه for تارکه)
. تعساً for بغیاً
متخذ
AI (a);, for: C, Y(a); ja, for do.
منجزم
۹ In Z lines ۵, ۷, ۸, ۹, ۱۰, ۱۱, are omitted.
. for do, و فی البریة (Y (b: کم فی البریة من for, قد ظهرت فی الارض: ۱۰
۱۱ R; Me, for Mac: C; ie, for do.<noinclude>{{خطکش}} {{پانویس|چپچین=بله}}</noinclude>
2g3peqm8wybj9ul2dyt4vlt1kkznabr
بدایع سعدی/مقدمه
0
97135
303975
2026-10-11T16:37:09Z
Hanooz
17889
صفحهای تازه حاوی «{{header | title = [[../]] | author = | editor = تقی ارانی | translator = | section = مقدمه | previous = | next = | notes = }} <pages index="بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=9 to=14></pages> {{خطکش}} {{پانویس-کوچک}}» ایجاد کرد
303975
wikitext
text/x-wiki
{{header
| title = [[../]]
| author =
| editor = تقی ارانی
| translator =
| section = مقدمه
| previous =
| next =
| notes =
}}
<pages index="بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=9 to=14></pages>
{{خطکش}}
{{پانویس-کوچک}}
dd026s4u72q2g8jh7gma4x7t705ujx7
303976
303975
2026-10-11T16:37:21Z
Hanooz
17889
303976
wikitext
text/x-wiki
{{header
| title = [[../]]
| author = تقی ارانی
| editor =
| translator =
| section = مقدمه
| previous =
| next =
| notes =
}}
<pages index="بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=9 to=14></pages>
{{خطکش}}
{{پانویس-کوچک}}
qs2tkm1gm1q334sfscq707e6jahvi2i
بدایع سعدی/شعر شماره ۴
0
97136
303977
2026-10-11T16:43:56Z
Hanooz
17889
صفحهای تازه حاوی «{{header | title = [[../]] | author = سعدی | editor = تقی ارانی | translator = | section = ۴ | previous = [[../شعر شماره ۵|۵]] | next = [[../شعر شماره ۳|۳]] | notes = }} <pages index="بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=19 to=20 fromsection="p19-20...» ایجاد کرد
303977
wikitext
text/x-wiki
{{header
| title = [[../]]
| author = سعدی
| editor = تقی ارانی
| translator =
| section = ۴
| previous = [[../شعر شماره ۵|۵]]
| next = [[../شعر شماره ۳|۳]]
| notes =
}}
<pages index="بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=19 to=20 fromsection="p19-20" tosection="p19-20"></pages>
{{خطکش}}
{{پانویس|چپچین=بله|اندازه=ریز}}
f0pjifg3lnbuh1a7ppitdx7nlo21olk
303978
303977
2026-10-11T16:44:15Z
Hanooz
17889
303978
wikitext
text/x-wiki
{{header
| title = [[../]]
| author = سعدی
| editor = تقی ارانی
| translator =
| section = ۴
| previous = [[../شعر شماره ۳|۳]]
| next = [[../شعر شماره ۵|۵]]
| notes =
}}
<pages index="بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=19 to=20 fromsection="p19-20" tosection="p19-20"></pages>
{{خطکش}}
{{پانویس|چپچین=بله|اندازه=ریز}}
ghqhrvda9450vh2axauugs0ygtuwao2
پودمان:وزن بدایع سعدی
828
97137
303980
2026-10-11T16:50:13Z
Hanooz
17889
ساخته هوش مصنوعی
303980
Scribunto
text/plain
local p = {}
-- Starter lookup table for Persian metrical feet.
-- Verify patterns against the prosodic convention used on Persian Wikipedia.
local patterns = {
["مفعولن"] = "– – –",
["مفاعیلن"] = "⏑ – – –",
["مفاعلن"] = "⏑ – ⏑ –",
["فعولن"] = "⏑ – –",
["فاعلاتن"] = "– ⏑ – –",
["فاعلن"] = "– ⏑ –",
["فعلاتن"] = "⏑ ⏑ – –",
["فعلن"] = "– –",
["مستفعلن"] = "– – ⏑ –",
["مفتعلن"] = "– – ⏑ –",
["مفعول"] = "– – ⏑",
["فاعلات"] = "– ⏑ – ⏑",
["مفاعیل"] = "⏑ – – ⏑",
}
function p.main(frame)
local args = frame:getParent().args
local feet = {}
-- Collect all non-empty positional parameters.
for i = 1, #args do
local foot = mw.text.trim(args[i] or "")
if foot ~= "" then
table.insert(feet, foot)
end
end
if #feet == 0 then
return ""
end
local root = mw.html.create("span")
:attr("dir", "rtl")
:css("display", "inline-block")
:css("white-space", "nowrap")
:css("text-align", "center")
:css("font-family", "serif")
:css("font-size", "1.2em")
-- Opening bracket.
root:tag("span")
:wikitext("[")
-- Render a row of feet separated by vertical bars.
local function addRow(values)
for i, value in ipairs(values) do
if i > 1 then
root:tag("span")
:css("padding", "0 0.3em")
:wikitext("|")
end
root:tag("span")
:css("padding", "0 0.1em")
:wikitext(value)
end
end
-- First row: names of the feet.
addRow(feet)
-- Line break between names and scansion patterns.
root:tag("br")
-- Second row: corresponding syllabic patterns.
local scanned = {}
for i, foot in ipairs(feet) do
scanned[i] = patterns[foot] or "؟"
end
addRow(scanned)
-- Closing bracket.
root:tag("span")
:wikitext("]")
return tostring(root)
end
return p
oepa7m82xpg2w66yet4e9a8gm8gocl1
303983
303980
2026-10-11T17:15:30Z
Hanooz
17889
303983
Scribunto
text/plain
local p = {}
local patterns = {
["مفعولن"] = "– – –",
["مفاعیلن"] = "⏑ – – –",
["مفاعلن"] = "⏑ – ⏑ –",
["فعولن"] = "⏑ – –",
["فاعلاتن"] = "– ⏑ – –",
["فاعلن"] = "– ⏑ –",
["فعلاتن"] = "⏑ ⏑ – –",
["فعلن"] = "– –",
["مستفعلن"] = "– – ⏑ –",
["مفتعلن"] = "– – ⏑ –",
["مفعول"] = "– – ⏑",
["فاعلات"] = "– ⏑ – ⏑",
["مفاعیل"] = "⏑ – – ⏑",
}
-- Safely collect positional arguments without using the # operator,
-- which is unreliable on Scribunto frame argument tables.
local function collectPositional(args)
local values = {}
local i = 1
while true do
local v = args[i]
if v == nil then break end
v = mw.text.trim(v)
if v ~= "" then
values[#values + 1] = v
end
i = i + 1
end
return values
end
function p.main(frame)
local parent = frame:getParent()
local args = (parent and parent.args) or frame.args
local feet = collectPositional(args)
if #feet == 0 then
return ""
end
local root = mw.html.create("span")
:attr("dir", "rtl")
:css("display", "inline-block")
:css("white-space", "nowrap")
:css("text-align", "center")
:css("font-family", "serif")
:css("font-size", "1.2em")
root:tag("span"):wikitext("[")
local function addRow(values)
for i, value in ipairs(values) do
if i > 1 then
root:tag("span")
:css("padding", "0 0.3em")
:wikitext("|")
end
root:tag("span")
:css("padding", "0 0.1em")
:wikitext(value)
end
end
addRow(feet)
root:tag("br")
local scanned = {}
for i, foot in ipairs(feet) do
scanned[i] = patterns[foot] or "؟"
end
addRow(scanned)
root:tag("span"):wikitext("]")
return tostring(root)
end
return p
kckyjr48xdlijxn32gon1iwht11uzql
303984
303983
2026-10-11T17:26:48Z
Hanooz
17889
303984
Scribunto
text/plain
local p = {}
local patterns = {
["مفعولن"] = "– – –",
["مفاعیلن"] = "⏑ – – –",
["مفاعلن"] = "⏑ – ⏑ –",
["فعولن"] = "⏑ – –",
["فاعلاتن"] = "– ⏑ – –",
["فاعلن"] = "– ⏑ –",
["فعلاتن"] = "⏑ ⏑ – –",
["فعلن"] = "– –",
["مستفعلن"] = "– – ⏑ –",
["مفتعلن"] = "– – ⏑ –",
["مفعول"] = "– – ⏑",
["فاعلات"] = "– ⏑ – ⏑",
["مفاعیل"] = "⏑ – – ⏑",
}
local function collectPositional(args)
local values = {}
local i = 1
while true do
local v = args[i]
if v == nil then break end
v = mw.text.trim(v)
if v ~= "" then
values[#values + 1] = v
end
i = i + 1
end
return values
end
function p.main(frame)
local parent = frame:getParent()
local args = (parent and parent.args) or frame.args
local feet = collectPositional(args)
if #feet == 0 then
return ""
end
-- Reverse order of appearance.
local reversed = {}
for i = 1, #feet do
reversed[i] = feet[#feet - i + 1]
end
-- Scansions with internal whitespace stripped.
local scanned = {}
for i, foot in ipairs(reversed) do
local pat = patterns[foot] or "؟"
scanned[i] = pat:gsub("%s+", "")
end
local root = mw.html.create("span")
:attr("dir", "rtl")
:css("display", "inline-block")
:css("white-space", "nowrap")
:css("text-align", "center")
:css("font-family", "serif")
:css("font-size", "1.2em")
root:tag("span"):wikitext("[")
for i, value in ipairs(scanned) do
if i > 1 then
-- Bare pipe, no padding around it.
root:tag("span"):wikitext("|")
end
-- letter-spacing keeps adjacent dashes from visually merging,
-- negative margin-right cancels the trailing gap so the pipe
-- sits flush against the pattern.
root:tag("span")
:css("letter-spacing", "0.12em")
:css("margin-right", "-0.12em")
:wikitext(value)
end
root:tag("span"):wikitext("]")
return tostring(root)
end
return p
ay13lrs6n47txekz4sb2xo8hp19weps
303986
303984
2026-10-11T17:34:30Z
Hanooz
17889
303986
Scribunto
text/plain
local p = {}
local patterns = {
["مفعولن"] = "– – –",
["مفاعیلن"] = "⏑ – – –",
["مفاعلن"] = "⏑ – ⏑ –",
["فعولن"] = "⏑ – –",
["فاعلاتن"] = "– ⏑ – –",
["فاعلن"] = "– ⏑ –",
["فعلاتن"] = "⏑ ⏑ – –",
["فعلن"] = "– –",
["مستفعلن"] = "– – ⏑ –",
["مفتعلن"] = "– – ⏑ –",
["مفعول"] = "– – ⏑",
["فاعلات"] = "– ⏑ – ⏑",
["مفاعیل"] = "⏑ – – ⏑",
}
local function collectPositional(args)
local values = {}
local i = 1
while true do
local v = args[i]
if v == nil then break end
v = mw.text.trim(v)
if v ~= "" then
values[#values + 1] = v
end
i = i + 1
end
return values
end
function p.main(frame)
local parent = frame:getParent()
local args = (parent and parent.args) or frame.args
local feet = collectPositional(args)
if #feet == 0 then
return ""
end
-- Scansions with internal whitespace stripped.
local scanned = {}
for i, foot in ipairs(feet) do
local pat = patterns[foot] or "؟"
scanned[i] = pat:gsub("%s+", "")
end
local root = mw.html.create("span")
:attr("dir", "ltr")
:css("display", "inline-block")
:css("white-space", "nowrap")
:css("text-align", "center")
:css("font-family", "serif")
:css("font-size", "1.2em")
root:tag("span"):wikitext("[")
for i, value in ipairs(scanned) do
if i > 1 then
root:tag("span"):wikitext("|")
end
root:tag("span")
:css("letter-spacing", "0.12em")
:css("margin-right", "-0.12em")
:wikitext(value)
end
root:tag("span"):wikitext("]")
return tostring(root)
end
return p
hj5h9w7ncpqsikuy0fmok0fuorxietr
303987
303986
2026-10-11T17:56:40Z
Hanooz
17889
303987
Scribunto
text/plain
local p = {}
-- Scansion patterns for individual feet.
local patterns = {
["مفعولن"] = "– – –",
["مفاعیلن"] = "⏑ – – –",
["مفاعلن"] = "⏑ – ⏑ –",
["فعولن"] = "⏑ – –",
["فاعلاتن"] = "– ⏑ – –",
["فاعلن"] = "– ⏑ –",
["فعلاتن"] = "⏑ ⏑ – –",
["فعلن"] = "– –",
["مستفعلن"] = "– – ⏑ –",
["مفتعلن"] = "– – ⏑ –",
["مفعول"] = "– – ⏑",
["فاعلات"] = "– ⏑ – ⏑",
["مفاعیل"] = "⏑ – – ⏑",
}
-- Meter names for common foot sequences.
-- Key: the feet joined by a space, exactly as they appear in the template call.
local meter_names = {
-- بحر هزج
["مفاعیلن مفاعیلن مفاعیلن مفاعیلن"] = "هزج مثمن سالم",
["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "هزج مثمن محذوف",
["مفاعیلن مفاعیلن فعولن"] = "هزج مسدس محذوف",
["مفعول مفاعیلن مفعول مفاعیلن"] = "هزج مثمن اخرب",
-- بحر رمل
["فاعلاتن فاعلاتن فاعلاتن فاعلاتن"] = "رمل مثمن سالم",
["فاعلاتن فاعلاتن فاعلاتن فاعلن"] = "رمل مثمن محذوف",
["فاعلاتن فاعلاتن فاعلن"] = "رمل مسدس محذوف",
["فعلاتن فعلاتن فعلاتن فعلن"] = "رمل مثمن مخبون محذوف",
-- بحر رجز
["مستفعلن مستفعلن مستفعلن مستفعلن"] = "رجز مثمن سالم",
["مستفعلن مستفعلن مستفعلن"] = "رجز مسدس سالم",
-- بحر متقارب
["فعولن فعولن فعولن فعولن"] = "متقارب مثمن سالم",
["فعولن فعولن فعولن فعل"] = "متقارب مثمن محذوف",
-- بحر مضارع
["مفاعیلن فاعلاتن مفاعیلن فاعلاتن"] = "مضارع مثمن سالم",
["مفعول فاعلاتن مفعول فاعلاتن"] = "مضارع مثمن اخرب",
["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "مضارع مثمن اخرب مکفوف محذوف",
-- بحر مجتث
["مفاعلن فعلاتن مفاعلن فعلاتن"] = "مجتث مثمن مخبون",
["مفاعلن فعلاتن مفاعلن فعلن"] = "مجتث مثمن مخبون محذوف",
-- بحر خفیف
["فاعلاتن مفاعلن فعلاتن مفاعلن"] = "خفیف مثمن مخبون",
["فاعلاتن مفاعلن فعلن"] = "خفیف مسدس مخبون",
}
-- Safely collect positional arguments without using the # operator.
local function collectPositional(args)
local values = {}
local i = 1
while true do
local v = args[i]
if v == nil then break end
v = mw.text.trim(v)
if v ~= "" then
values[#values + 1] = v
end
i = i + 1
end
return values
end
-- Try to find a meter name for the given list of feet.
local function lookupMeterName(feet)
local key = table.concat(feet, " ")
return meter_names[key]
end
function p.main(frame)
local parent = frame:getParent()
local args = (parent and parent.args) or frame.args
local feet = collectPositional(args)
if #feet == 0 then
return ""
end
-- Scansions with internal whitespace stripped.
local scanned = {}
for i, foot in ipairs(feet) do
local pat = patterns[foot] or "؟"
scanned[i] = pat:gsub("%s+", "")
end
-- Determine the meter name:
-- 1. explicit 'وزن' or 'بحر' parameter wins,
-- 2. otherwise try the lookup table,
-- 3. otherwise leave it blank.
local meterName = args["وزن"] or args["بحر"]
if not meterName or mw.text.trim(meterName) == "" then
meterName = lookupMeterName(feet)
end
if meterName then
meterName = mw.text.trim(meterName)
else
meterName = ""
end
-- Main container: centered line, RTL direction so Metre: lands on the left.
local container = mw.html.create("div")
:css("text-align", "center")
:css("direction", "rtl")
:css("font-family", "serif")
:css("font-size", "1.2em")
:css("white-space", "nowrap")
-- 1. Notation (LTR) — pinned right in the RTL container.
local notationSpan = container:tag("span")
:attr("dir", "ltr")
:css("display", "inline-block")
notationSpan:tag("span"):wikitext("[")
for i, value in ipairs(scanned) do
if i > 1 then
-- Bare pipe, no padding around it.
notationSpan:tag("span"):wikitext("|")
end
-- letter-spacing keeps adjacent dashes from visually merging,
-- negative margin-right cancels the trailing gap so the pipe
-- sits flush against the pattern.
notationSpan:tag("span")
:css("letter-spacing", "0.12em")
:css("margin-right", "-0.12em")
:wikitext(value)
end
notationSpan:tag("span"):wikitext("]")
-- 2. Meter name (RTL) — middle, with a gap on both sides.
if meterName ~= "" then
container:tag("span")
:css("margin", "0 1em")
:wikitext(meterName)
container:tag("span")
:attr("dir", "ltr")
:wikitext("Metre:")
else
-- If no meter name, still provide a gap before Metre:.
container:tag("span")
:attr("dir", "ltr")
:css("margin", "0 1em")
:wikitext("Metre:")
end
return tostring(container)
end
return p
byiso68vtb6irexnwokpustszcgktb0
303988
303987
2026-10-11T18:00:12Z
Hanooz
17889
303988
Scribunto
text/plain
local p = {}
-- Scansion patterns for individual feet.
local patterns = {
["مفعولن"] = "– – –",
["مفاعیلن"] = "⏑ – – –",
["مفاعلن"] = "⏑ – ⏑ –",
["فعولن"] = "⏑ – –",
["فاعلاتن"] = "– ⏑ – –",
["فاعلن"] = "– ⏑ –",
["فعلاتن"] = "⏑ ⏑ – –",
["فعلن"] = "– –",
["مستفعلن"] = "– – ⏑ –",
["مفتعلن"] = "– – ⏑ –",
["مفعول"] = "– – ⏑",
["فاعلات"] = "– ⏑ – ⏑",
["مفاعیل"] = "⏑ – – ⏑",
}
-- Meter names for common foot sequences.
-- Key: the feet joined by a space, exactly as they appear in the template call.
local meter_names = {
-- بحر هزج
["مفاعیلن مفاعیلن مفاعیلن مفاعیلن"] = "هزج مثمن سالم",
["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "هزج مثمن محذوف",
["مفاعیلن مفاعیلن فعولن"] = "هزج مسدس محذوف",
["مفعول مفاعیلن مفعول مفاعیلن"] = "هزج مثمن اخرب",
-- بحر رمل
["فاعلاتن فاعلاتن فاعلاتن فاعلاتن"] = "رمل مثمن سالم",
["فاعلاتن فاعلاتن فاعلاتن فاعلن"] = "رمل مثمن محذوف",
["فاعلاتن فاعلاتن فاعلن"] = "رمل مسدس محذوف",
["فعلاتن فعلاتن فعلاتن فعلن"] = "رمل مثمن مخبون محذوف",
-- بحر رجز
["مستفعلن مستفعلن مستفعلن مستفعلن"] = "رجز مثمن سالم",
["مستفعلن مستفعلن مستفعلن"] = "رجز مسدس سالم",
-- بحر متقارب
["فعولن فعولن فعولن فعولن"] = "متقارب مثمن سالم",
["فعولن فعولن فعولن فعل"] = "متقارب مثمن محذوف",
-- بحر مضارع
["مفاعیلن فاعلاتن مفاعیلن فاعلاتن"] = "مضارع مثمن سالم",
["مفعول فاعلاتن مفعول فاعلاتن"] = "مضارع مثمن اخرب",
["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "مضارع مثمن اخرب مکفوف محذوف",
-- بحر مجتث
["مفاعلن فعلاتن مفاعلن فعلاتن"] = "مجتث مثمن مخبون",
["مفاعلن فعلاتن مفاعلن فعلن"] = "مجتث مثمن مخبون محذوف",
-- بحر خفیف
["فاعلاتن مفاعلن فعلاتن مفاعلن"] = "خفیف مثمن مخبون",
["فاعلاتن مفاعلن فعلن"] = "خفیف مسدس مخبون",
}
-- Safely collect positional arguments without using the # operator.
local function collectPositional(args)
local values = {}
local i = 1
while true do
local v = args[i]
if v == nil then break end
v = mw.text.trim(v)
if v ~= "" then
values[#values + 1] = v
end
i = i + 1
end
return values
end
-- Try to find a meter name for the given list of feet.
local function lookupMeterName(feet)
local key = table.concat(feet, " ")
return meter_names[key]
end
function p.main(frame)
local parent = frame:getParent()
local args = (parent and parent.args) or frame.args
local feet = collectPositional(args)
if #feet == 0 then
return ""
end
-- Scansions with internal whitespace stripped.
local scanned = {}
for i, foot in ipairs(feet) do
local pat = patterns[foot] or "؟"
scanned[i] = pat:gsub("%s+", "")
end
-- Determine the meter name:
-- 1. explicit 'وزن' or 'بحر' parameter wins,
-- 2. otherwise try the lookup table,
-- 3. otherwise leave it blank.
local meterName = args["وزن"] or args["بحر"]
if not meterName or mw.text.trim(meterName) == "" then
meterName = lookupMeterName(feet)
end
if meterName then
meterName = mw.text.trim(meterName)
else
meterName = ""
end
-- Main container: centered line, RTL direction so Metre: lands on the left.
-- No font-family / font-size overrides — inherits normal wiki text style.
local container = mw.html.create("div")
:css("text-align", "center")
:css("direction", "rtl")
:css("white-space", "nowrap")
-- 1. Notation (LTR) — pinned right in the RTL container.
local notationSpan = container:tag("span")
:attr("dir", "ltr")
:css("display", "inline-block")
notationSpan:tag("span"):wikitext("[")
for i, value in ipairs(scanned) do
if i > 1 then
-- Bare pipe, no padding around it.
notationSpan:tag("span"):wikitext("|")
end
-- letter-spacing keeps adjacent dashes from visually merging,
-- negative margin-right cancels the trailing gap so the pipe
-- sits flush against the pattern.
notationSpan:tag("span")
:css("letter-spacing", "0.12em")
:css("margin-right", "-0.12em")
:wikitext(value)
end
notationSpan:tag("span"):wikitext("]")
-- 2. Meter name (RTL) — middle.
-- In the RTL container, the right neighbour of this span is the
-- notation, and the left neighbour is the 'Metre:' label.
-- So: margin-right = big gap (notation), margin-left = small gap (Metre:).
if meterName ~= "" then
container:tag("span")
:css("margin-right", "2em") -- gap toward notation
:css("margin-left", "0.4em") -- gap toward Metre:
:wikitext(meterName)
container:tag("span")
:attr("dir", "ltr")
:wikitext("Metre:")
else
-- No meter name: just leave a gap before Metre:.
container:tag("span")
:attr("dir", "ltr")
:css("margin-right", "2em")
:wikitext("Metre:")
end
return tostring(container)
end
return p
bt18w7ii02kof79vfq49wl8pn58d3qp
303989
303988
2026-10-11T18:03:15Z
Hanooz
17889
303989
Scribunto
text/plain
local p = {}
-- Scansion patterns for individual feet.
local patterns = {
["مفعولن"] = "– – –",
["مفاعیلن"] = "⏑ – – –",
["مفاعلن"] = "⏑ – ⏑ –",
["فعولن"] = "⏑ – –",
["فاعلاتن"] = "– ⏑ – –",
["فاعلن"] = "– ⏑ –",
["فعلاتن"] = "⏑ ⏑ – –",
["فعلن"] = "– –",
["مستفعلن"] = "– – ⏑ –",
["مفتعلن"] = "– – ⏑ –",
["مفعول"] = "– – ⏑",
["فاعلات"] = "– ⏑ – ⏑",
["مفاعیل"] = "⏑ – – ⏑",
}
-- Meter names for common foot sequences.
-- Key: the feet joined by a space, exactly as they appear in the template call.
local meter_names = {
-- بحر هزج
["مفاعیلن مفاعیلن مفاعیلن مفاعیلن"] = "هزج مثمّن سالم",
["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "هزج مثمّن محذوف",
["مفاعیلن مفاعیلن فعولن"] = "هزج مسدّس محذوف",
["مفعول مفاعیلن مفعول مفاعیلن"] = "هزج مثمّن اخرب",
-- بحر رمل
["فاعلاتن فاعلاتن فاعلاتن فاعلاتن"] = "رمل مثمّن سالم",
["فاعلاتن فاعلاتن فاعلاتن فاعلن"] = "رمل مثمّن محذوف",
["فاعلاتن فاعلاتن فاعلن"] = "رمل مسدّس محذوف",
["فعلاتن فعلاتن فعلاتن فعلن"] = "رمل مثمّن مخبون محذوف",
-- بحر رجز
["مستفعلن مستفعلن مستفعلن مستفعلن"] = "رجز مثمّن سالم",
["مستفعلن مستفعلن مستفعلن"] = "رجز مسدّس سالم",
-- بحر متقارب
["فعولن فعولن فعولن فعولن"] = "متقارب مثمّن سالم",
["فعولن فعولن فعولن فعل"] = "متقارب مثمّن محذوف",
-- بحر مضارع
["مفاعیلن فاعلاتن مفاعیلن فاعلاتن"] = "مضارع مثمّن سالم",
["مفعول فاعلاتن مفعول فاعلاتن"] = "مضارع مثمّن اخرب",
["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "مضارع مثمّن اخرب مکفوف محذوف",
-- بحر مجتث
["مفاعلن فعلاتن مفاعلن فعلاتن"] = "مجتثّ مثمّن مخبون",
["مفاعلن فعلاتن مفاعلن فعلن"] = "مجتثّ مثمّن مخبون محذوف",
-- بحر خفیف
["فاعلاتن مفاعلن فعلاتن مفاعلن"] = "خفیف مثمّن مخبون",
["فاعلاتن مفاعلن فعلن"] = "خفیف مسدّس مخبون",
}
-- Safely collect positional arguments without using the # operator.
local function collectPositional(args)
local values = {}
local i = 1
while true do
local v = args[i]
if v == nil then break end
v = mw.text.trim(v)
if v ~= "" then
values[#values + 1] = v
end
i = i + 1
end
return values
end
-- Try to find a meter name for the given list of feet.
local function lookupMeterName(feet)
local key = table.concat(feet, " ")
return meter_names[key]
end
function p.main(frame)
local parent = frame:getParent()
local args = (parent and parent.args) or frame.args
local feet = collectPositional(args)
if #feet == 0 then
return ""
end
-- Scansions with internal whitespace stripped.
local scanned = {}
for i, foot in ipairs(feet) do
local pat = patterns[foot] or "؟"
scanned[i] = pat:gsub("%s+", "")
end
-- Determine the meter name:
-- 1. explicit 'وزن' or 'بحر' parameter wins,
-- 2. otherwise try the lookup table,
-- 3. otherwise leave it blank.
local meterName = args["وزن"] or args["بحر"]
if not meterName or mw.text.trim(meterName) == "" then
meterName = lookupMeterName(feet)
end
if meterName then
meterName = mw.text.trim(meterName)
else
meterName = ""
end
-- Main container: centered line, RTL direction so Metre: lands on the left.
-- No font-family / font-size overrides — inherits normal wiki text style.
local container = mw.html.create("div")
:css("text-align", "center")
:css("direction", "rtl")
:css("white-space", "nowrap")
-- 1. Notation (LTR) — pinned right in the RTL container.
local notationSpan = container:tag("span")
:attr("dir", "ltr")
:css("display", "inline-block")
notationSpan:tag("span"):wikitext("[")
for i, value in ipairs(scanned) do
if i > 1 then
-- Bare pipe, no padding around it.
notationSpan:tag("span"):wikitext("|")
end
-- letter-spacing keeps adjacent dashes from visually merging,
-- negative margin-right cancels the trailing gap so the pipe
-- sits flush against the pattern.
notationSpan:tag("span")
:css("letter-spacing", "0.12em")
:css("margin-right", "-0.12em")
:wikitext(value)
end
notationSpan:tag("span"):wikitext("]")
-- 2. Meter name (RTL) — middle.
-- In the RTL container, the right neighbour of this span is the
-- notation, and the left neighbour is the 'Metre:' label.
-- So: margin-right = big gap (notation), margin-left = small gap (Metre:).
if meterName ~= "" then
container:tag("span")
:css("margin-right", "2em") -- gap toward notation
:css("margin-left", "0.4em") -- gap toward Metre:
:wikitext(meterName)
container:tag("span")
:attr("dir", "ltr")
:wikitext("Metre:")
else
-- No meter name: just leave a gap before Metre:.
container:tag("span")
:attr("dir", "ltr")
:css("margin-right", "2em")
:wikitext("Metre:")
end
return tostring(container)
end
return p
oafqys95fyh52x1yqb9ht1bxm1jcel8
303990
303989
2026-10-11T18:08:34Z
Hanooz
17889
303990
Scribunto
text/plain
local p = {}
-- Scansion patterns for individual feet.
local patterns = {
["مفعولن"] = "– – –",
["مفاعیلن"] = "⏑ – – –",
["مفاعلن"] = "⏑ – ⏑ –",
["فعولن"] = "⏑ – –",
["فاعلاتن"] = "– ⏑ – –",
["فاعلن"] = "– ⏑ –",
["فعلاتن"] = "⏑ ⏑ – –",
["فعلن"] = "– –",
["مستفعلن"] = "– – ⏑ –",
["مفتعلن"] = "– – ⏑ –",
["مفعول"] = "– – ⏑",
["فاعلات"] = "– ⏑ – ⏑",
["مفاعیل"] = "⏑ – – ⏑",
}
-- Meter names for common foot sequences.
-- Key: the feet joined by a space, exactly as they appear in the template call.
local meter_names = {
-- بحر هزج
["مفاعیلن مفاعیلن مفاعیلن مفاعیلن"] = "هزج مثمّن سالم",
["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "هزج مثمّن محذوف",
["مفاعیلن مفاعیلن فعولن"] = "هزج مسدّس محذوف",
["مفعول مفاعیلن مفعول مفاعیلن"] = "هزج مثمّن اخرب",
-- بحر رمل
["فاعلاتن فاعلاتن فاعلاتن فاعلاتن"] = "رمل مثمّن سالم",
["فاعلاتن فاعلاتن فاعلاتن فاعلن"] = "رمل مثمّن محذوف",
["فاعلاتن فاعلاتن فاعلن"] = "رمل مسدّس محذوف",
["فعلاتن فعلاتن فعلاتن فعلن"] = "رمل مثمّن مخبون محذوف",
-- بحر رجز
["مستفعلن مستفعلن مستفعلن مستفعلن"] = "رجز مثمّن سالم",
["مستفعلن مستفعلن مستفعلن"] = "رجز مسدّس سالم",
-- بحر متقارب
["فعولن فعولن فعولن فعولن"] = "متقارب مثمّن سالم",
["فعولن فعولن فعولن فعل"] = "متقارب مثمّن محذوف",
-- بحر مضارع
["مفاعیلن فاعلاتن مفاعیلن فاعلاتن"] = "مضارع مثمّن سالم",
["مفعول فاعلاتن مفعول فاعلاتن"] = "مضارع مثمّن اخرب",
["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "مضارع مثمّن اخرب مکفوف محذوف",
-- بحر مجتث
["مفاعلن فعلاتن مفاعلن فعلاتن"] = "مجتثّ مثمّن مخبون",
["مفاعلن فعلاتن مفاعلن فعلن"] = "مجتثّ مثمّن مخبون محذوف",
-- بحر خفیف
["فاعلاتن مفاعلن فعلاتن مفاعلن"] = "خفیف مثمّن مخبون",
["فاعلاتن مفاعلن فعلن"] = "خفیف مسدّس مخبون",
}
-- Safely collect positional arguments without using the # operator.
local function collectPositional(args)
local values = {}
local i = 1
while true do
local v = args[i]
if v == nil then break end
v = mw.text.trim(v)
if v ~= "" then
values[#values + 1] = v
end
i = i + 1
end
return values
end
-- Try to find a meter name for the given list of feet.
local function lookupMeterName(feet)
local key = table.concat(feet, " ")
return meter_names[key]
end
function p.main(frame)
local parent = frame:getParent()
local args = (parent and parent.args) or frame.args
local feet = collectPositional(args)
if #feet == 0 then
return ""
end
-- Scansions with internal whitespace stripped.
local scanned = {}
for i, foot in ipairs(feet) do
local pat = patterns[foot] or "؟"
scanned[i] = pat:gsub("%s+", "")
end
-- Determine the meter name:
-- 1. explicit 'وزن' or 'بحر' parameter wins,
-- 2. otherwise try the lookup table,
-- 3. otherwise leave it blank.
local meterName = args["وزن"] or args["بحر"]
if not meterName or mw.text.trim(meterName) == "" then
meterName = lookupMeterName(feet)
end
if meterName then
meterName = mw.text.trim(meterName)
else
meterName = ""
end
-- Main container: centered line, RTL direction so Metre: lands on the left.
-- No font-family / font-size overrides — inherits normal wiki text style.
local container = mw.html.create("div")
:css("text-align", "center")
:css("direction", "rtl")
:css("white-space", "nowrap")
-- 1. Notation (LTR) — pinned right in the RTL container.
local notationSpan = container:tag("span")
:attr("dir", "ltr")
:css("display", "inline-block")
notationSpan:tag("span"):wikitext("[")
for i, value in ipairs(scanned) do
if i > 1 then
-- Bare pipe, no padding around it.
notationSpan:tag("span"):wikitext("|")
end
-- letter-spacing keeps adjacent dashes from visually merging,
-- negative margin-right cancels the trailing gap so the pipe
-- sits flush against the pattern.
notationSpan:tag("span")
:css("letter-spacing", "0.12em")
:css("margin-right", "-0.12em")
:wikitext(value)
end
notationSpan:tag("span"):wikitext("]")
-- 2. Meter name (RTL) — middle.
-- In the RTL container, the right neighbour of this span is the
-- notation, and the left neighbour is the 'Metre:' label.
-- So: margin-right = big gap (notation), margin-left = small gap (Metre:).
if meterName ~= "" then
container:tag("span")
:css("margin-right", "2em") -- gap toward notation
:css("margin-left", "0.4em") -- gap toward Metre:
:wikitext(meterName)
container:tag("span")
:attr("dir", "ltr")
:wikitext("Metre:")
else
-- No meter name: just leave a gap before Metre:.
container:tag("span")
:attr("dir", "ltr")
:css("margin-right", "2em")
:wikitext("Metre:")
end
local output = tostring(container)
-- Auto-categorize, but only on pages in the main namespace.
-- The category is named after the feet parameters, joined by spaces,
-- e.g. [[رده:فاعلاتن فاعلاتن فاعلاتن فاعلاتن]].
local title = mw.title.getCurrentTitle()
if title and title.namespace == 0 then
local catName = table.concat(feet, " ")
output = output .. "[[رده:" .. catName .. "]]"
end
return output
end
return p
3jehiggcv0ulr77n5pwfqwho6ur5o4z
303994
303990
2026-10-11T18:23:08Z
Hanooz
17889
303994
Scribunto
text/plain
-- نمایش ارکان و الگوی هجایی وزن شعر فارسی همراه با نام وزن و ردهٔ خودکار.
local p = {}
-- الگوی هجایی هر رکن.
local patterns = {
["مفعولن"] = "– – –",
["مفاعیلن"] = "⏑ – – –",
["مفاعلن"] = "⏑ – ⏑ –",
["فعولن"] = "⏑ – –",
["فاعلاتن"] = "– ⏑ – –",
["فاعلن"] = "– ⏑ –",
["فعلاتن"] = "⏑ ⏑ – –",
["فعلن"] = "– –",
["مستفعلن"] = "– – ⏑ –",
["مفتعلن"] = "– – ⏑ –",
["مفعول"] = "– – ⏑",
["فاعلات"] = "– ⏑ – ⏑",
["مفاعیل"] = "⏑ – – ⏑",
}
-- نام وزن بر اساس رشتهٔ ارکان.
-- کلید: ارکان با فاصله از هم جدا شده، دقیقاً همانطور که به الگو داده میشوند.
local meter_names = {
-- بحر هزج
["مفاعیلن مفاعیلن مفاعیلن مفاعیلن"] = "هزج مثمّن سالم",
["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "هزج مثمّن محذوف",
["مفاعیلن مفاعیلن فعولن"] = "هزج مسدّس محذوف",
["مفعول مفاعیلن مفعول مفاعیلن"] = "هزج مثمّن اخرب",
-- بحر رمل
["فاعلاتن فاعلاتن فاعلاتن فاعلاتن"] = "رمل مثمّن سالم",
["فاعلاتن فاعلاتن فاعلاتن فاعلن"] = "رمل مثمّن محذوف",
["فاعلاتن فاعلاتن فاعلن"] = "رمل مسدّس محذوف",
["فعلاتن فعلاتن فعلاتن فعلن"] = "رمل مثمّن مخبون محذوف",
-- بحر رجز
["مستفعلن مستفعلن مستفعلن مستفعلن"] = "رجز مثمّن سالم",
["مستفعلن مستفعلن مستفعلن"] = "رجز مسدّس سالم",
-- بحر متقارب
["فعولن فعولن فعولن فعولن"] = "متقارب مثمّن سالم",
["فعولن فعولن فعولن فعل"] = "متقارب مثمّن محذوف",
-- بحر مضارع
["مفاعیلن فاعلاتن مفاعیلن فاعلاتن"] = "مضارع مثمّن سالم",
["مفعول فاعلاتن مفعول فاعلاتن"] = "مضارع مثمّن اخرب",
["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "مضارع مثمّن اخرب مکفوف محذوف",
-- بحر مجتث
["مفاعلن فعلاتن مفاعلن فعلاتن"] = "مجتثّ مثمّن مخبون",
["مفاعلن فعلاتن مفاعلن فعلن"] = "مجتثّ مثمّن مخبون محذوف",
-- بحر خفیف
["فاعلاتن مفاعلن فعلاتن مفاعلن"] = "خفیف مثمّن مخبون",
["فاعلاتن مفاعلن فعلن"] = "خفیف مسدّس مخبون",
}
-- گردآوری پارامترهای مکانی بدون استفاده از عملگر # (که روی جدولهای
-- فریم اسکریبونتو قابل اعتماد نیست).
local function collectPositional(args)
local values = {}
local i = 1
while true do
local v = args[i]
if v == nil then break end
v = mw.text.trim(v)
if v ~= "" then
values[#values + 1] = v
end
i = i + 1
end
return values
end
-- جستجوی نام وزن بر اساس فهرست ارکان.
local function lookupMeterName(feet)
local key = table.concat(feet, " ")
return meter_names[key]
end
function p.main(frame)
local parent = frame:getParent()
local args = (parent and parent.args) or frame.args
local feet = collectPositional(args)
if #feet == 0 then
return ""
end
-- الگوهای هجایی با حذف فاصلههای داخلی.
local scanned = {}
for i, foot in ipairs(feet) do
local pat = patterns[foot] or "؟"
scanned[i] = pat:gsub("%s+", "")
end
-- تعیین نام وزن:
-- ۱. پارامتر صریح «وزن» یا «بحر» اولویت دارد،
-- ۲. در غیر این صورت جدول جستجو بررسی میشود،
-- ۳. در غیر این صورت خالی میماند.
local meterName = args["وزن"] or args["بحر"]
if not meterName or mw.text.trim(meterName) == "" then
meterName = lookupMeterName(feet)
end
if meterName then
meterName = mw.text.trim(meterName)
else
meterName = ""
end
-- بارگذاری شیوهنامه از زیرصفحهٔ styles.css.
local styles = frame:extensionTag {
name = "templatestyles",
args = { src = "Module:وزن بدایع سعدی/styles.css" },
}
-- ظرف اصلی.
local container = mw.html.create("div"):addClass("wazn-container")
-- ۱. بخش نشانهگذاری (چپبهراست) — در ظرف راستبهچپ در سمت راست مینشیند.
local notationSpan = container:tag("span"):addClass("wazn-notation")
notationSpan:tag("span"):wikitext("[")
for i, value in ipairs(scanned) do
if i > 1 then
notationSpan:tag("span"):addClass("wazn-pipe"):wikitext("|")
end
notationSpan:tag("span"):addClass("wazn-pattern"):wikitext(value)
end
notationSpan:tag("span"):wikitext("]")
-- ۲. نام وزن (راستبهچپ) و برچسب Metre: در سمت چپ.
if meterName ~= "" then
container:tag("span")
:addClass("wazn-meter")
:wikitext(meterName)
container:tag("span")
:addClass("wazn-metre-label")
:wikitext("Metre:")
else
container:tag("span")
:addClass("wazn-metre-label")
:addClass("wazn-metre-label-spaced")
:wikitext("Metre:")
end
local output = styles .. tostring(container)
-- ردهٔ خودکار فقط در فضای نام اصلی (نه صفحه: و نه هیچ فضای نام دیگری).
local title = mw.title.getCurrentTitle()
if title and title.namespace == 0 then
local catName = table.concat(feet, " ")
output = output .. "[[رده:" .. catName .. "]]"
end
return output
end
return p
mte3aattvpliru9no5mcbs9dwg9yj2m
الگو:وزن بدایع سعدی
10
97138
303981
2026-10-11T16:51:08Z
Hanooz
17889
صفحهای تازه حاوی « <includeonly>{{#invoke:وزن بدایع سعدی|main}}</includeonly><noinclude> برای نمایش ارکان و الگوی هجایی وزن شعر فارسی. </noinclude>» ایجاد کرد
303981
wikitext
text/x-wiki
<includeonly>{{#invoke:وزن بدایع سعدی|main}}</includeonly><noinclude>
برای نمایش ارکان و الگوی هجایی وزن شعر فارسی.
</noinclude>
7ifuj7ml3qenugnx2sc5t3464jz1x39
303996
303981
2026-10-11T18:26:24Z
Hanooz
17889
303996
wikitext
text/x-wiki
<includeonly>{{#invoke:وزن بدایع سعدی|main}}</includeonly><noinclude>
{{مستندات}}</noinclude>
5m8t6eowa38irlnr66mfs18klagp9a9
پودمان:وزن بدایع سعدی/styles.css
828
97139
303993
2026-10-11T18:21:02Z
Hanooz
17889
صفحهای تازه حاوی «/* ظرف اصلی: خط وسطچین، جهت راستبهچپ برای قرار گرفتن Metre: در چپ. */ .wazn-container { text-align: center; direction: rtl; white-space: nowrap; } /* بخش نشانهگذاری — جهت چپبهراست تا ترتیب نویسهها بههم نریزد. */ .wazn-notation { display: inline-block; direction: ltr; unicode-bidi: isolate; } /*...» ایجاد کرد
303993
sanitized-css
text/css
/* ظرف اصلی: خط وسطچین، جهت راستبهچپ برای قرار گرفتن Metre: در چپ. */
.wazn-container {
text-align: center;
direction: rtl;
white-space: nowrap;
}
/* بخش نشانهگذاری — جهت چپبهراست تا ترتیب نویسهها بههم نریزد. */
.wazn-notation {
display: inline-block;
direction: ltr;
unicode-bidi: isolate;
}
/* هر الگوی هجایی: فاصلهٔ نویسهای تا خطهای پیوسته بههم نچسبند. */
.wazn-pattern {
letter-spacing: 0.12em;
margin-right: -0.12em;
}
/* میلهٔ جداکننده با فاصلهٔ صفر از الگوها. */
.wazn-pipe {
/* عمداً خالی — فضای پیشفرض صفر است. */
}
/* نام وزن: فاصلهٔ بزرگ تا نشانهگذاری، فاصلهٔ کوچک تا Metre:. */
.wazn-meter {
margin-right: 2em;
margin-left: 0.4em;
}
/* برچسب Metre: */
.wazn-metre-label {
direction: ltr;
unicode-bidi: isolate;
}
/* حالت بدون نام وزن: تنها فاصلهٔ بزرگ پیش از Metre:. */
.wazn-metre-label-spaced {
margin-right: 2em;
}
iqv2264nyq6jfva4ec5a9yj6jbwt8br
304029
303993
2026-10-11T22:16:22Z
Hanooz
17889
304029
sanitized-css
text/css
/* ظرف اصلی: خط وسطچین، جهت راستبهچپ برای قرار گرفتن Metre: در چپ. */
.wazn-container {
text-align: center;
direction: rtl;
white-space: nowrap;
}
/* بخش نشانهگذاری — جهت چپبهراست تا ترتیب نویسهها بههم نریزد. */
.wazn-notation {
display: inline-block;
direction: ltr;
unicode-bidi: isolate;
}
/* هر الگوی هجایی: فاصلهٔ نویسهای تا خطهای پیوسته بههم نچسبند. */
.wazn-pattern {
letter-spacing: 0.12em;
margin-right: -0.12em;
}
/* میلهٔ جداکننده با فاصلهٔ صفر از الگوها. */
.wazn-pipe {
/* عمداً خالی. */
}
/* نام وزن: فاصلهٔ بزرگ تا نشانهگذاری، فاصلهٔ کوچک تا Metre:. */
.wazn-meter {
margin-right: 2em;
margin-left: 0.4em;
}
/* برچسب Metre: */
.wazn-metre-label {
direction: ltr;
unicode-bidi: isolate;
}
/* حالت بدون نام وزن. */
.wazn-metre-label-spaced {
margin-right: 2em;
}
/* ── صفحههای کوچک ─────────────────────────────────────────────
وقتی خط میشکند، ترتیب از چپ به راست چنین میشود:
Metre: → نام وزن → نشانهگذاری
یعنی Metre: روی خط اول میماند و نشانهگذاری در صورت
نبود جا به خط بعد میرود.
─────────────────────────────────────────────────────────── */
@media (max-width: 600px) {
.wazn-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: baseline;
/* برای ترتیب طبیعی چپبهراست، در صفحههای کوچک
ظرف را LTR میکنیم. */
direction: ltr;
white-space: normal;
row-gap: 0.25em;
}
/* ۱. Metre: — چپترین، همیشه روی خط اول. */
.wazn-metre-label,
.wazn-metre-label-spaced {
order: 1;
margin: 0;
}
/* ۲. نام وزن — کنار Metre:، در صورت طولانی بودن میشکند. */
.wazn-meter {
order: 2;
margin: 0 0 0 0.5em;
white-space: normal;
}
/* ۳. نشانهگذاری — آخرین مورد؛ اگر جا نباشد به خط بعد میرود. */
.wazn-notation {
order: 3;
}
}
kjupcwehzvarfs4xyp5efmugfnsip5f
الگو:وزن بدایع سعدی/توضیحات
10
97140
303995
2026-10-11T18:25:48Z
Hanooz
17889
صفحهای تازه حاوی «<noinclude>{{توضیحات بالای صفحه}}</noinclude> این ماژول وزن عروضی شعر فارسی را بهصورت یک خط وسطچین نشان میدهد که سه بخش دارد: <div dir="ltr" style="text-align:center; font-family:monospace; margin:1em 0;"> [–⏑––|–⏑––|–⏑––|–⏑––] مضارع مثمّن اخرب مکفوف محذوف &nb...» ایجاد کرد
303995
wikitext
text/x-wiki
<noinclude>{{توضیحات بالای صفحه}}</noinclude>
این ماژول وزن عروضی شعر فارسی را بهصورت یک خط وسطچین نشان میدهد که سه بخش دارد:
<div dir="ltr" style="text-align:center; font-family:monospace; margin:1em 0;">
[–⏑––|–⏑––|–⏑––|–⏑––] مضارع مثمّن اخرب مکفوف محذوف Metre:
</div>
از راست به چپ: '''نشانهگذاری هجایی''' — '''نام وزن''' — '''برچسب Metre:'''.
== نحوهٔ فراخوانی ==
این ماژول از طریق الگوی {{الگو|وزن بدایع سعدی}} فراخوانی میشود. الگو تنها یک خط دارد:
<pre>
<includeonly>{{#invoke:وزن بدایع سعدی|main}}</includeonly><noinclude>
{{توضیحات|الگو:وزن بدایع سعدی/توضیحات}}
</noinclude>
</pre>
سپس در صفحههای محتوا:
<pre>
{{وزن بدایع سعدی|مفاعیلن|مفاعیلن|مفاعیلن|فعولن}}
</pre>
== پارامترها ==
{| class="wikitable"
! پارامتر !! نوع !! توضیح
|-
| <code>1</code>, <code>2</code>, <code>3</code>, … || مکانی || نام ارکان وزن به ترتیب ظهور در مصراع.
|-
| <code>وزن</code> یا <code>بحر</code> || نامدار (اختیاری) || نام وزن. در صورت نبود، ماژول خودش از جدول جستجو پیدا میکند.
|}
هر پارامتر مکانی خالی نادیده گرفته میشود؛ میتوانید فاصله یا خط جدید هم اضافه کنید.
== ارکان شناختهشده ==
{| class="wikitable" style="text-align:center;"
! رکن !! الگوی هجایی
|-
| مفعولن || <code>–––</code>
|-
| مفاعیلن || <code>⏑–––</code>
|-
| مفاعلن || <code>⏑–⏑–</code>
|-
| فعولن || <code>⏑––</code>
|-
| فاعلاتن || <code>–⏑––</code>
|-
| فاعلن || <code>–⏑–</code>
|-
| فعلاتن || <code>⏑⏑––</code>
|-
| فعلن || <code>––</code>
|-
| مستفعلن || <code>––⏑–</code>
|-
| مفتعلن || <code>––⏑–</code>
|-
| مفعول || <code>––⏑</code>
|-
| فاعلات || <code>–⏑–⏑</code>
|-
| مفاعیل || <code>⏑––⏑</code>
|}
اگر رکنی در جدول نباشد، بهجای الگوی هجایی <code>؟</code> چاپ میشود.
== وزنهای شناختهشده ==
ماژول این ترکیبهای ارکان را بهطور خودکار میشناسد و نام وزن را نمایش میدهد:
=== بحر هزج ===
* <code>مفاعیلن مفاعیلن مفاعیلن مفاعیلن</code> ← هزج مثمّن سالم
* <code>مفاعیلن مفاعیلن مفاعیلن فعولن</code> ← هزج مثمّن محذوف
* <code>مفاعیلن مفاعیلن فعولن</code> ← هزج مسدّس محذوف
* <code>مفعول مفاعیلن مفعول مفاعیلن</code> ← هزج مثمّن اخرب
=== بحر رمل ===
* <code>فاعلاتن فاعلاتن فاعلاتن فاعلاتن</code> ← رمل مثمّن سالم
* <code>فاعلاتن فاعلاتن فاعلاتن فاعلن</code> ← رمل مثمّن محذوف
* <code>فاعلاتن فاعلاتن فاعلن</code> ← رمل مسدّس محذوف
* <code>فعلاتن فعلاتن فعلاتن فعلن</code> ← رمل مثمّن مخبون محذوف
=== بحر رجز ===
* <code>مستفعلن مستفعلن مستفعلن مستفعلن</code> ← رجز مثمّن سالم
* <code>مستفعلن مستفعلن مستفعلن</code> ← رجز مسدّس سالم
=== بحر متقارب ===
* <code>فعولن فعولن فعولن فعولن</code> ← متقارب مثمّن سالم
* <code>فعولن فعولن فعولن فعل</code> ← متقارب مثمّن محذوف
=== بحر مضارع ===
* <code>مفاعیلن فاعلاتن مفاعیلن فاعلاتن</code> ← مضارع مثمّن سالم
* <code>مفعول فاعلاتن مفعول فاعلاتن</code> ← مضارع مثمّن اخرب
* <code>مفاعیلن مفاعیلن مفاعیلن فعولن</code> ← مضارع مثمّن اخرب مکفوف محذوف
=== بحر مجتث ===
* <code>مفاعلن فعلاتن مفاعلن فعلاتن</code> ← مجتثّ مثمّن مخبون
* <code>مفاعلن فعلاتن مفاعلن فعلن</code> ← مجتثّ مثمّن مخبون محذوف
=== بحر خفیف ===
* <code>فاعلاتن مفاعلن فعلاتن مفاعلن</code> ← خفیف مثمّن مخبون
* <code>فاعلاتن مفاعلن فعلن</code> ← خفیف مسدّس مخبون
== مثالهای واقعی ==
=== ۱. حافظ — «دلا بسوز که سوز تو کارها بکند» ===
وزن: '''هزج مسدّس محذوف'''
<pre>
{{وزن بدایع سعدی|مفاعیلن|مفاعیلن|فعولن}}
</pre>
خروجی:
<div dir="ltr" style="text-align:center; font-family:monospace; margin:0.5em 0;">
[⏑–––|⏑–––|⏑––] هزج مسدّس محذوف Metre:
</div>
=== ۲. سعدی — «بنی آدم اعضای یک پیکرند» ===
وزن: '''رمل مسدّس محذوف'''
<pre>
{{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلن}}
</pre>
خروجی:
<div dir="ltr" style="text-align:center; font-family:monospace; margin:0.5em 0;">
[–⏑––|–⏑––|–⏑–] رمل مسدّس محذوف Metre:
</div>
=== ۳. مولوی — «بشنو از نی چون حکایت میکند» ===
همان وزن بالا:
<pre>
{{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلن}}
</pre>
=== ۴. حافظ — «دوش دیدم که ملائک در میخانه زدند» ===
وزن: '''رمل مثمّن مخبون محذوف'''
<pre>
{{وزن بدایع سعدی|فعلاتن|فعلاتن|فعلاتن|فعلن}}
</pre>
خروجی:
<div dir="ltr" style="text-align:center; font-family:monospace; margin:0.5em 0;">
[⏑⏑––|⏑⏑––|⏑⏑––|––] رمل مثمّن مخبون محذوف Metre:
</div>
=== ۵. حافظ — «در خرابات مغان نور خدا میبینم» ===
وزن: '''مضارع مثمّن اخرب مکفوف محذوف'''
<pre>
{{وزن بدایع سعدی|مفاعیلن|مفاعیلن|مفاعیلن|فعولن}}
</pre>
خروجی:
<div dir="ltr" style="text-align:center; font-family:monospace; margin:0.5em 0;">
[⏑–––|⏑–––|⏑–––|⏑––] مضارع مثمّن اخرب مکفوف محذوف Metre:
</div>
=== ۶. تعیین دستی نام وزن ===
اگر میخواهید نام وزن با آنچه در جدول است متفاوت باشد یا رکنها در جدول نباشند:
<pre>
{{وزن بدایع سعدی|مفاعیلن|مفاعیلن|فعولن|وزن=هزج مسدّس محذوف}}
</pre>
یا با <code>بحر</code>:
<pre>
{{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلن|بحر=رمل مسدّس محذوف}}
</pre>
== ردهٔ خودکار ==
ماژول در '''فضای نام اصلی (مقاله)''' بهطور خودکار یک رده بر اساس ارکان میسازد. مثلاً:
<pre>
{{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلاتن}}
</pre>
صفحه را در <code>رده:فاعلاتن فاعلاتن فاعلاتن فاعلاتن</code> قرار میدهد.
در '''فضای نام صفحه (Page:)''' و سایر فضاهای نام (الگو، رده، کاربر، …) رده اضافه نمیشود.
'''نکتهٔ ProofreadPage:''' وقتی صفحهای در فضای نام صفحه از طریق <code><pages></code> در یک مقاله تراگنجانده میشود، ماژول در بستر مقاله ارزیابی میشود، یعنی <code>mw.title.getCurrentTitle()</code> مقاله را برمیگرداند و رده به مقاله اضافه میشود — نه به صفحهٔ اثبات. این همان رفتار مطلوب است.
== نکات فنی ==
* '''جهت متن:''' ظرف اصلی <code>direction: rtl</code> دارد تا ترتیب «نشانهگذاری → وزن → Metre:» از راست به چپ باشد. اما بخش نشانهگذاری <code>direction: ltr</code> دارد چون نویسههای <code>–</code>، <code>⏑</code>، <code>|</code>، <code>[</code>، <code>]</code> همه از ردهٔ bidi «خنثی» هستند و در بستر راستبهچپ وارونه نمایش داده میشوند.
* '''فاصلهٔ نویسهای:''' <code>letter-spacing: 0.12em</code> روی هر الگو و <code>margin-right: -0.12em</code> متناظر با آن باعث میشود خطهای پیوسته (مثل <code>––</code>) بهصورت دو خط جدا دیده شوند، ولی میلهٔ <code>|</code> به الگو بچسبد.
* '''شیوهنامه:''' تمام استایلها در <code>Module:وزن بدایع سعدی/styles.css</code> قرار دارد و از طریق افزونهٔ TemplateStyles بارگذاری میشود. برای اعمال تغییرات ظاهری کافی است همان صفحه را ویرایش کنید.
* '''بدون بازگردانی:''' ترتیب ارکان دقیقاً همان است که به الگو داده میشود.
== گسترش جدول وزنها ==
برای افزودن وزن تازه، یک سطر به جدول <code>meter_names</code> در ماژول بیفزایید:
<pre>
["فاعلاتن فاعلاتن فاعلاتن فاعلان"] = "رمل مثمّن مقصور",
</pre>
کلید همیشه: رشتهٔ ارکان با فاصله، دقیقاً مطابق ترتیبی که به الگو داده میشود.
'''یادآوری شَدّه:''' در نام وزنها شَدّه روی حرف میانی باب تفعیل (مثمّن، مسدّس) و حرف آخر باب افتعال با ریشهٔ مضاعف (مجتثّ) میآید. سایر واژهها (سالم، محذوف، مخبون، مکفوف، اخرب، و نام بحرها) شَدّه ندارند.
<includeonly>[[رده:الگوهای وزن عروضی]]</includeonly>
<noinclude>[[رده:صفحههای توضیحات الگو]]</noinclude>
2fbhn2ilezlzo6ljwtllqutwwg7la0r
الگو:شعر۲
10
97141
304010
2026-10-11T20:28:17Z
Hanooz
17889
صفحهای تازه حاوی «<includeonly>{{#invoke:شعر|open}}</includeonly><noinclude>{{توضیحات}}[[رده:الگوهای شعر]]</noinclude>» ایجاد کرد
304010
wikitext
text/x-wiki
<includeonly>{{#invoke:شعر|open}}</includeonly><noinclude>{{توضیحات}}[[رده:الگوهای شعر]]</noinclude>
9vl5o1b9he7bu1qhewafzbe5oimjak7
بحث:دیوان حافظ/سحرم دولت بیدار به بالین آمد
1
97142
304017
2026-10-11T20:35:28Z
~2026-54745-61
40107
/* هویت ایرانی */ بخش جدید
304017
wikitext
text/x-wiki
== هویت ایرانی ==
استادان گرامی، از بکار گیری واژه های انگلیسی و فرانسوی خو داری کنید. واژه ای برای استاندار پیدا کنید. واژهایی در ردیف «معمول» ویا «رایج» میتواند گویای منظور باشد.
چندش آور بود که روزی از زبان سخنگوی آموزش و پرورش واژه « اکسپرت» را شنیدم ویا دهقانی که اژه «تایم» را به زبان آورد. این پدیده مثل بیماری واگیری به زبان ایرانیان افتاده. فرهنگ ایرانی بر پایه زبان فارسی استوارست. تیشه به ریشه خود نزنیم! [[ویژه:مشارکتها/~2026-54745-61|~2026-54745-61]] ([[بحث کاربر:~2026-54745-61|بحث]]) ۱۱ اکتبر ۲۰۲۶، ساعت ۲۰:۳۵ (UTC)
a5jbijv9i4ssz43u35fhp4xkf3iffdc
بدایع سعدی/شعر شماره ۵
0
97143
304018
2026-10-11T20:43:37Z
Hanooz
17889
صفحهای تازه حاوی «{{header | title = [[../]] | author = سعدی | editor = تقی ارانی | translator = | section = ۵ | previous = [[../شعر شماره ۴|۴]] | next = [[../شعر شماره ۶|۶]] | notes = }} <pages index="بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=20 to=21 fromsection="p20-21...» ایجاد کرد
304018
wikitext
text/x-wiki
{{header
| title = [[../]]
| author = سعدی
| editor = تقی ارانی
| translator =
| section = ۵
| previous = [[../شعر شماره ۴|۴]]
| next = [[../شعر شماره ۶|۶]]
| notes =
}}
<pages index="بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=20 to=21 fromsection="p20-21" tosection="p20-21"></pages>
{{خطکش}}
{{پانویس|چپچین=بله|اندازه=ریز}}
03jenfeypd1z0q632gia4pd3x8c4tea
برگه:بدایع غزلیات افصح المتکلمین شیخ مصلحالدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۲۱
104
97144
304019
2026-10-11T20:49:03Z
Hanooz
17889
/* نمونهخوانینشده */ صفحهای تازه حاوی «{{شعر۲|متن= سعدی ز فراق تو نه آن رنج کشیده است کز شادی وصل تو فرامش کند آنرا گر تیر جراحت زند و <ref name=p7-n8>I(a); مگیر, for مده.</ref> باز هم آید از جای جراحت نتوان برد نشانرا<ref name=p7-n8/> }} <section end="p20-21" />» ایجاد کرد
304019
proofread-page
text/x-wiki
<noinclude><pagequality level="1" user="Hanooz" />{{سرصفحهمستمر||بدایع شیخ سعدی|num}}</noinclude>{{شعر۲|متن=
سعدی ز فراق تو نه آن رنج کشیده است
کز شادی وصل تو فرامش کند آنرا
گر تیر جراحت زند و <ref name=p7-n8>I(a); مگیر, for مده.</ref> باز هم آید
از جای جراحت نتوان برد نشانرا<ref name=p7-n8/>
}}
<section end="p20-21" /><noinclude>{{خطکش}} {{پانویس|چپچین=بله}}</noinclude>
kci1m6x73dcp5nniyg22xzxmjui5d33