ویکی‌نبشته fawikisource https://fa.wikisource.org/wiki/%D8%B5%D9%81%D8%AD%D9%87%D9%94_%D8%A7%D8%B5%D9%84%DB%8C MediaWiki 1.47.0-wmf.23 first-letter مدیا ویژه بحث کاربر بحث کاربر ویکی‌نبشته بحث ویکی‌نبشته پرونده بحث پرونده مدیاویکی بحث مدیاویکی الگو بحث الگو راهنما بحث راهنما رده بحث رده درگاه بحث درگاه پدیدآورنده بحث پدیدآورنده برگه گفتگوی برگه فهرست گفتگوی فهرست TimedText TimedText talk پودمان بحث پودمان Event Event talk الگو:بدون منبع 10 22527 304040 303997 2026-10-12T14:05:27Z Hanooz 17889 304040 wikitext text/x-wiki {{ambox |type=content |text='''سند منبع این متن مشخص نیست.'''<br /> {{smaller block|لطفاً برای کسب اطلاعات بیشتر و بررسی صحت منبع، [[{{TALKPAGENAME}}|صفحهٔ بحث]] را ببینید. منظور از «منبع»، مکانی است که کاربران دیگر بتوانند نسخه‌ای از این اثر را در آن بیابند. ترجیحاً باید نسخه‌ای اسکن‌شده از اصل اثر باشد که بتوان آن را در [[commons:Main page|ویکی‌انبار]] بارگذاری و در [[راهنما:نمونه‌خوانی|نمونه‌خوانی]] کرد. در غیر این صورت، بهتر است نشانی اینترنتی (URL) ارائه شود. اگر هیچ‌یک از این موارد در دسترس نیست، لطفاً دلیل آن را در صفحهٔ بحث توضیح دهید.}} }} <includeonly>{{category handler | all = [[رده:آثار بدون منبع|{{PAGENAME}}]] | nocat = {{{nocat|}}} }}</includeonly><noinclude> {{documentation}} </noinclude> bpwbyme0tpu1cwq7x9ro221gx4qvzoi مدیاویکی:Gadgets-definition 8 26276 304045 299729 2026-10-12T17:12:04Z NguoiDungKhongDinhDanh 30809 Remove duplicate definition (no functional changes) ([[:m:GS|global sysop]] action) 304045 wikitext text/x-wiki == browsing-gadgets == * popups|popups.js == LanguageSupport == * BiDiEditing[default]|BiDiEditing.js * ShortLink[default]|ShortLink.js * GoogleTranslator|GoogleTranslator.js == Interface == * ocr|ocr.js * editzero[dependencies=mediawiki.util]|editzero.js * lastdiff[dependencies=mediawiki.util]|lastdiff.js * purgetab[dependencies=mediawiki.util]|purgetab.js <!-- * contribsrange[default]|contribsrange.js --> <!-- * signit[default]|signit.js --> <!-- * diffswitchdir[default]|diffswitchdir.js --> * defaultsummaries|defaultsummaries.js == Extra-Editbuttons == * addon-buttonsformove|addon-buttonsformove.js * CustomizedEditTool|CustomizedEditTool.js * matchsplit|matchsplit.js * AHK[dependencies=mediawiki.util]|AHK.js * intropreload|intropreload.js * Extra-Editbuttons[default|rights=move|dependencies=mediawiki.util]|Extra-Editbuttons.js|Extra-Editbuttons-persiantools.js|Extra-Editbuttons-persianwikitools.js|Extra-Editbuttons-dictionary.js|Extra-Editbuttons-autoed.js|Extra-Editbuttons-botworks.js * UserMessages[dependencies=jquery.ui,mediawiki.util,ext.gadget.morebits|type=general]|UserMessages.js|UserMessages.css * dropdown-menus[dependencies=mediawiki.api,mediawiki.util,mediawiki.Title,user.options|peers=dropdown-menus-pagestyles]|dropdown-menus.js * dropdown-menus-pagestyles[hidden]|dropdown-menus-pagestyles.css * Edittools[default|type=general]|Edittools.js|Edittools.css * BotTools[dependencies=mediawiki.util]|BotTools.js == VisualEditor == * VeSuperTool|VeSuperTool.js|Extra-Editbuttons-persiantools.js|Extra-Editbuttons-persianwikitools.js|Extra-Editbuttons-dictionary.js * VeExtendedBar[dependencies=ext.visualEditor.desktopArticleTarget.init]|VeExtendedBar.js * VeDirectionMarkTool[hidden|dependencies=ext.visualEditor.core|type=general]|VeDirectionMarkTool.js|VeDirectionMarkTool.css == Categories == * HotCat|HotCat.js == admin-gadgets == * CleanDeleteReasons|CleanDeleteReasons.js == othermod == * InterwikiTranslate|InterwikiTranslate.js * GreenRedirect[dependencies=mediawiki.util]|GreenRedirect.js * ImageAnnotator|ImageAnnotator.js * yekewarning|yekewarning.js onrnj0695924vk3z4dr8gkl4piblkym برگه:The Blind Owl.pdf/۵۸ 104 46698 304046 166960 2026-10-12T18:03:55Z Kaur.gurmel 39954 /* هم‌سنجی‌شده */ 304046 proofread-page text/x-wiki <noinclude><pagequality level="4" user="Kaur.gurmel" />{{سصم|۵۸||}}</noinclude>و روشن همین الان هم شک دارم– نمیدانم اگر انگشتهایم را به هاون سنگی گوشه حیاطمان بزنم و از او بپرسم آیا ثابت و محکم هستی در صورت جواب مثبت باید حرف او را باور بکنم یا نه.– «آیا من یک موجود مجزا و مشخص هستم؟ نمیدانم– ولی حالا که در آینه نگاه کردم خودم را نشناختم، نه، آن «من» سابق مرده است، تجزیه شده ولی هیچ سد و مانعی بین ما وجود ندارد– باید حکایت خودم را نقل بکنم ولی نمیدانم باید از کجا شروع کرد– سرتاسر زندگی قصه و حکایت است. باید خوشه انگور را بفشارم و شیره آنرا قاشق قاشق در گلوی خشک این سایه پیر بریزم. «آیا از کجا باید شروع کرد؟ چون همه فکرهائی که عجالتاً در کله‌ام میجوشد، مال همین الان است، ساعت و دقیقه و تاریخ ندارد– یک اتفاق دیروز ممکن است برای من کهنه‌تر و بی تاثیرتر از یک اتفاق هزار سال پیش باشد. «شاید از آنجائیکه همه روابط من با دنیای زنده‌ها بریده شده یادگارهای گذشته جلوم نقش می‌بندد– گذشته، آینده، ساعت، روز، ماه و<noinclude></noinclude> 0w0l07pwps87vcc8cqljp8bm0kwknth برگه:The Blind Owl.pdf/۵۹ 104 46699 304060 124159 2026-10-12T23:06:20Z Kaur.gurmel 39954 /* هم‌سنجی‌شده */ 304060 proofread-page text/x-wiki <noinclude><pagequality level="4" user="Kaur.gurmel" />{{سصم|۵۹||}}</noinclude>سال همه برایم یکسان است. مراحل مختلف بچگی و پیری برای من جز حرفهای پوچ چیزدیگری نیست، فقط برای مردمان معمولی، برای رجاله‌ها– رجاله با تشدید– همین لغت را می‌جستم، برای رجاله‌ها که زندگی آنها موسم و حد معینی دارد، مثل فصلهای سال و در منطقه معتدل زندگی واقع شده است صدق می‌کند. ولی زندگی من همه‌اش یک فصل و یک حالت داشته– مثل اینست که در یک منطقه سردسیر و در تاریکی جاودانی گذشته است در صورتیکه میان تنم همیشه یک شعله میسوزد و مرا مثل شمع آب میکند. «میان چهاردیواری که اطاق مرا تشکیل میدهد و حصاری که دور زندگی و افکار من کشیده، زندگی من مثل شمع خرده خرده آب میشود، نه، اشتباه میکنم– مثل یک کنده هیزم تر است که گوشه دیگدان افتاده و به آتش هیزم‌های دیگر برشته و ذغال شده، ولی نه سوخته است و نه تر و تازه مانده فقط از دود و دم دیگران خفه شده.– اطاقم مثل همه اطاقها با خشت و آجر روی خرابه هزاران خانه‌های قدیمی ساخته شده، بدنه سفید کرده و یک حاشیه کتیبه دارد– درست شبیه مقبره است– کمترین حالات و جزئیات اطاقم کافی است که ساعتهای<noinclude></noinclude> 0c19225ycozdhhru6g3vhvk44pok965 304062 304060 2026-10-13T11:46:56Z Kaur.gurmel 39954 304062 proofread-page text/x-wiki <noinclude><pagequality level="4" user="Kaur.gurmel" />{{سصم|۵۹||}}</noinclude>سال همه برایم یکسان است. مراحل مختلف بچگی و پیری برای من جز حرفهای پوچ چیزدیگری نیست، فقط برای مردمان معمولی، برای رجاله‌ها– رجاله با تشدید– همین لغت را می‌جستم، برای رجاله‌ها که زندگی آنها موسم و حد معینی دارد، مثل فصلهای سال و در منطقه معتدل زندگی واقع شده است صدق می‌کند. ولی زندگی من همه‌اش یک فصل و یک حالت داشته– مثل اینست که در یک منطقه سردسیر و در تاریکی جاودانی گذشته است در صورتیکه میان تنم همیشه یک شعله میسوزد و مرا مثل شمع آب میکند. «میان چهاردیواری که اطاق مرا تشکیل میدهد و حصاری که دور زندگی و افکار من کشیده، زندگی من مثل شمع خرده خرده آب میشود، نه، اشتباه میکنم– مثل یک کنده هیزم تر است که گوشه دیگران افتاده و به آتش هیزم‌های دیگر برشته و ذغال شده، ولی نه سوخته است و نه تر و تازه مانده فقط از دود و دم دیگران خفه شده.– اطاقم مثل همه اطاقها با خشت و آجر روی خرابه هزاران خانه‌های قدیمی ساخته شده، بدنه سفید کرده و یک حاشیه کتیبه دارد– درست شبیه مقبره است– کمترین حالات و جزئیات اطاقم کافی است که ساعتهای<noinclude></noinclude> c2ar28i7b5vgummrm548uui1bmkh2np الگو:صفحه‌های بی‌سند 10 65931 304041 199113 2026-10-12T14:14:34Z Hanooz 17889 304041 wikitext text/x-wiki {{ambox |type=content |text=این صفحه حاوی متنی است که با روش ویکی‌نبشته از روی یک سند اسکن‌شده پیاده‌سازی نشده است و احتمالاً برای ویکی‌نبشته مناسب نیست. متن جاری به شکل فعلی مستند نیست و امکان عدم تطابق با متن اصلی در آن زیاد است. {{سخ}} لطفاً سند اسکن‌شده مربوط به این متن را روی ویکی‌انبار بارگذاری کرده و به پیاده‌سازی و نمونه‌خوانی متن بر اساس آنچه در [[راهنما:راهنمای کوتاه|صفحه راهنما]] آمده بپردازید.}} }} <includeonly>{{category handler | all = [[رده:متن‌های بدون نسخهٔ اسکن‌شده|{{PAGENAME}}]] | nocat = {{{nocat|}}} }}</includeonly><noinclude> {{documentation}} </noinclude> ci26bulk7pvxxwgt3tf0jy8nvwdwmqh 304042 304041 2026-10-12T14:14:52Z Hanooz 17889 304042 wikitext text/x-wiki {{ambox |type=content |text=این صفحه حاوی متنی است که با روش ویکی‌نبشته از روی یک سند اسکن‌شده پیاده‌سازی نشده است و احتمالاً برای ویکی‌نبشته مناسب نیست. متن جاری به شکل فعلی مستند نیست و امکان عدم تطابق با متن اصلی در آن زیاد است. {{سخ}} لطفاً سند اسکن‌شده مربوط به این متن را روی ویکی‌انبار بارگذاری کرده و به پیاده‌سازی و نمونه‌خوانی متن بر اساس آنچه در [[راهنما:راهنمای کوتاه|صفحه راهنما]] آمده بپردازید. }} <includeonly>{{category handler | all = [[رده:متن‌های بدون نسخهٔ اسکن‌شده|{{PAGENAME}}]] | nocat = {{{nocat|}}} }}</includeonly><noinclude> {{documentation}} </noinclude> ilctk91o8le8gsa106w006k8hch4aql الگو:شعر۲/styles.css 10 97053 304039 304036 2026-10-12T13:44:48Z Hanooz 17889 304039 sanitized-css text/css /* * الگو:شعر/styles.css — TemplateStyles برای «پودمان:شعر» * * table-layout: auto ⇒ عرض ستون مصراع از متنِ ردیف سایزر میآید. * ردیف سایزر بلندترین مصراعِ شعر را در هر دو ستون مصراع میگذارد، پس * هر دو ستون هماندازه و برابر با بلندترین مصراعاند. */ /* ═══ جدول ═══ */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: auto; /* ← کلید کار */ text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ ردیف سایزر: نامرئی، ولی در تعیین عرض شرکت میکند ═══ * visibility: hidden ⇒ متن رندر نمیشود ولی برای اندازهگیری عرض * همچنان شمرده میشود. height: 0 و line-height: 0 ⇒ ارتفاع صفر. */ .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; overflow: hidden; white-space: nowrap; } .poem-sz { visibility: hidden; white-space: nowrap; } /* ═══ عرض ستونها ═══ * .poem-mh عرضی ندارد ⇒ از محتوای سایزر (بلندترین مصراع) میگیرد. * .poem-g عرض ثابت ۲em ⇒ همان فاصلهٔ نسخهٔ اصلی. * .poem-s حداقل ۲em ⇒ جا برای شماره. */ .poem-g { width: 2em; min-width: 2em; } .poem-s { width: 2em; min-width: 2em; } /* ═══ سلولهای خالی ساختاری ═══ */ .poem-empty { padding: 0 !important; width: auto !important; min-width: 0; border: 0; line-height: 0; font-size: 0; } /* ═══ مصراع ═══ */ .poem-m { padding-inline: 0.4em; white-space: nowrap; /* مصراع در یک خط؛ هماندازه با سایزر */ } .poem-m .beyt { display: inline; } /* ═══ شمارهٔ ابیات ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.5em; } .poem-num-l { text-align: right; padding-right: 0.5em; } /* ═══ مصراع راست/چپ ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ فاصلهگذارها ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ نستعلیق ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ چندصفحهای ═══ */ .poem-skip { display: none; } .poem-row.poem-final-marker { display: none !important; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ صفحهٔ متوسط ═══ */ @media (max-width: 45em) { .poem-g { width: 1.2em; min-width: 1.2em; } .poem-s { width: 1.6em; min-width: 1.6em; } .poem-m { padding-inline: 0.3em; } .poem-num-r { padding-left: 0.3em; } .poem-num-l { padding-right: 0.3em; } } /* ═══ صفحهٔ باریک: هر مصراع یک خط ═══ */ @media (max-width: 32em) { .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; table-layout: auto; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } .poem-row.poem-stagger-1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0; } .poem-row.poem-stagger-2 { border-bottom: 1px dotted #c8c8c8; margin-bottom: 0.6em; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } .poem-empty, .poem-num-c, .poem-g, .poem-gapcell { display: none !important; } .poem-num-r, .poem-num-l { display: block; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; white-space: normal; } } c3hjw0qicx3007crzbtnm9l3926y8fb 304043 304039 2026-10-12T15:04:55Z Hanooz 17889 304043 sanitized-css text/css /* * Template:شعر۲/styles.css * Shared seven-column layout for Module:شعر */ /* ═══ Table ═══ */ .poem-table { width: 100%; table-layout: fixed; border-collapse: collapse; border-spacing: 0; text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ Seven-column grid ═══ * 5% | 21% | 21% | 6% | 21% | 21% | 5% * Each hemistich spans two columns: 42%. */ .poem-s { width: 5%; } .poem-mh { width: 21%; } .poem-g { width: 6%; } /* ═══ Structural empty cells ═══ */ .poem-empty { padding: 0 !important; width: auto !important; min-width: 0; border: 0; line-height: 0; font-size: 0; } /* ═══ Hemistichs ═══ */ .poem-m { padding-inline: 0.4em; } .poem-m .beyt { display: block; width: 100%; min-width: 0; overflow-wrap: break-word; hyphens: none; text-align: inherit; text-align-last: inherit; } .poem-m .beyt sup.reference { white-space: nowrap; } /* ═══ Verse numbering ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.5em; } .poem-num-l { text-align: right; padding-right: 0.5em; } /* ═══ Left/right hemistichs ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ Spacers ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ Sizing row ═══ */ .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; font-size: 0; } /* ═══ Nastaliq ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ Multi-page continuation ═══ */ .poem-skip { display: none; } .poem-row.poem-final-marker { display: none !important; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ Medium screens ═══ */ @media (max-width: 45em) { .poem-m { padding-inline: 0.3em; } .poem-num-r { padding-left: 0.3em; } .poem-num-l { padding-right: 0.3em; } } /* ═══ Narrow screens ═══ */ @media (max-width: 32em) { .poem, .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; table-layout: fixed; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } .poem-row.poem-stagger-1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0; } .poem-row.poem-stagger-2 { border-bottom: 1px dotted #c8c8c8; margin-bottom: 0.6em; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } .poem-empty, .poem-num-c, .poem-g, .poem-gapcell { display: none !important; } .poem-num-r, .poem-num-l { display: block; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; } .poem-m .beyt { width: 100%; white-space: normal; } } e01utxchevuc7mxs6l6cuyvizzjlan6 304048 304043 2026-10-12T18:56:39Z Hanooz 17889 304048 sanitized-css text/css /* * الگو:شعر۲/styles.css — TemplateStyles برای «پودمان:شعر» * * چیدمان ثابت: عرض ستونها با درصد تعیین میشود. جدول از table-layout: * fixed استفاده میکند و ردیف سایزرِ خالی در آغاز، عرضها را تثبیت میکند. * همهٔ مصراعها در همهٔ ابیات و انواع، ۴۲٪ جدولاند. */ /* ═══ جدول ═══ */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: fixed; text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ عرض ستونها (روی ردیف سایزر) ═══ * ۵ + ۲۱ + ۲۱ + ۶ + ۲۱ + ۲۱ + ۵ = ۱۰۰ */ .poem-s { width: 5%; } .poem-mh { width: 21%; } .poem-g { width: 6%; } /* ═══ ردیف سایزر: نامرئی ═══ */ .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; font-size: 0; } /* ═══ سلولهای خالی ساختاری (بیت۱/نردبانی) ═══ */ .poem-empty { padding: 0 !important; border: 0; line-height: 0; font-size: 0; } /* ═══ مصراع ═══ * ترازبندی از style اینلاین (ALIGN_STYLE در پودمان) میآید. * اگر ندهد، از .poem-table به ارث میرسد (وسط). * ارجاعها inline میمانند و به آخرین واژه میچسبند. */ .poem-m { padding-inline: 0.4em; } .poem-m .beyt { overflow-wrap: break-word; hyphens: none; } .poem-m .beyt sup.reference { white-space: nowrap; } /* ═══ شمارهٔ ابیات ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.5em; } .poem-num-l { text-align: right; padding-right: 0.5em; } /* ═══ مصراع راست/چپ (شعر نو) ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ فاصلهگذارها ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ نستعلیق ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ چندصفحهای ═══ */ .poem-skip { display: none; } .poem-row.poem-final-marker { display: none !important; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ صفحهٔ متوسط ═══ */ @media (max-width: 45em) { .poem-s { width: 4%; } .poem-mh { width: 22%; } .poem-g { width: 4%; } .poem-m { padding-inline: 0.3em; } .poem-num-r { padding-left: 0.3em; } .poem-num-l { padding-right: 0.3em; } } /* ═══ صفحهٔ باریک: هر مصراع یک خط ═══ */ @media (max-width: 32em) { .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; table-layout: auto; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } /* بیت۱ دو ردیف است: خطچین فقط پس از ردیف دوم */ .poem-row.poem-stagger-1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0; } .poem-row.poem-stagger-2 { border-bottom: 1px dotted #c8c8c8; margin-bottom: 0.6em; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } .poem-empty, .poem-num-c, .poem-g, .poem-gapcell { display: none !important; } .poem-num-r, .poem-num-l { display: block; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; } .poem-m .beyt { white-space: normal; } } o6c2d1inu0i6tfgqfkf1cdiww26fvkh 304051 304048 2026-10-12T19:22:35Z Hanooz 17889 304051 sanitized-css text/css /* * الگو:شعر۲/styles.css — TemplateStyles برای «پودمان:شعر» * عرض ستونها بهصورت inline در پودمان تعیین شده؛ این فایل فقط ظاهر * (فاصله، قلم، واکنشگرا) را مدیریت میکند. */ /* ═══ جدول ═══ */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: fixed; text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ عرض ستونها (پشتیبان؛ در پودمان هم inline هست) ═══ */ .poem-s { width: 5%; } .poem-mh { width: 21%; } .poem-g { width: 6%; } /* ═══ سایزر ═══ */ .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; font-size: 0; } /* ═══ سلولهای خالی ساختاری ═══ */ .poem-empty { padding: 0 !important; border: 0; line-height: 0; font-size: 0; } /* ═══ مصراع ═══ */ .poem-m { padding-inline: 0.4em; } .poem-m .beyt { overflow-wrap: break-word; hyphens: none; } .poem-m .beyt sup.reference { white-space: nowrap; } /* ═══ شمارهها ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.5em; } .poem-num-l { text-align: right; padding-right: 0.5em; } /* ═══ مصراع راست/چپ ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ فاصلهگذارها ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ نستعلیق ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ چندصفحهای ═══ */ .poem-skip { display: none; } .poem-row.poem-final-marker { display: none !important; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ صفحهٔ متوسط ═══ */ @media (max-width: 45em) { .poem-m { padding-inline: 0.3em; } .poem-num-r { padding-left: 0.3em; } .poem-num-l { padding-right: 0.3em; } } /* ═══ صفحهٔ باریک ═══ */ @media (max-width: 32em) { .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; table-layout: auto; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } .poem-row.poem-stagger-1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0; } .poem-row.poem-stagger-2 { border-bottom: 1px dotted #c8c8c8; margin-bottom: 0.6em; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } .poem-empty, .poem-num-c, .poem-g, .poem-gapcell { display: none !important; } .poem-num-r, .poem-num-l { display: block; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; } .poem-m .beyt { white-space: normal; } } gz1erx2bef8p81wgkxf44euewj5igb4 304055 304051 2026-10-12T19:59:02Z Hanooz 17889 304055 sanitized-css text/css /* * الگو:شعر/styles.css — TemplateStyles برای «پودمان:شعر» */ /* ═══ جدول ═══ */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: fixed; text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ عرض ستونها ═══ * پیشفرض = با شماره: * ۵٪ | ۲۱٪×۲ | ۶٪ | ۲۱٪×۲ | ۵٪ ⇒ مصراع ۴۲٪ * اگر هیچ سلول شمارهای در جدول محتوا نداشته باشد (نه خودکار، نه دستی)، * فضا به مصراعها برمیگردد: * ۲٪ | ۲۲.۵٪×۲ | ۶٪ | ۲۲.۵٪×۲ | ۲٪ ⇒ مصراع ۴۵٪ * سلولهای شماره همیشه یا خالیاند یا شماره دارند؛ پس :empty کار میکند. */ /* پشتیبان. عرضهای واقعی inline روی هر سلول نوشته میشوند. */ .poem-s { width: 5%; } .poem-mh { width: 21%; } .poem-g { width: 6%; } /* ═══ سایزر ═══ */ .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; font-size: 0; } /* ═══ سلولهای خالی ساختاری ═══ */ .poem-empty { padding: 0 !important; border: 0; line-height: 0; font-size: 0; } /* ═══ مصراع ═══ */ .poem-m { padding-inline: 0.4em; } .poem-m .beyt { overflow-wrap: break-word; hyphens: none; } .poem-m .beyt sup.reference { white-space: nowrap; } /* ═══ شمارهها ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.5em; } .poem-num-l { text-align: right; padding-right: 0.5em; } /* ═══ مصراع راست/چپ (شعر نو) ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ فاصلهگذارها ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ نستعلیق ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ چندصفحهای ═══ */ .poem-skip { display: none; } .poem-row.poem-final-marker { display: none !important; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ صفحهٔ متوسط ═══ */ @media (max-width: 45em) { .poem-m { padding-inline: 0.3em; } .poem-num-r { padding-left: 0.3em; } .poem-num-l { padding-right: 0.3em; } } /* ═══ صفحهٔ باریک ═══ */ @media (max-width: 32em) { .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; table-layout: auto; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } .poem-row.poem-stagger-1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0; } .poem-row.poem-stagger-2 { border-bottom: 1px dotted #c8c8c8; margin-bottom: 0.6em; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } .poem-empty, .poem-num-c, .poem-g, .poem-gapcell { display: none !important; } .poem-num-r, .poem-num-l { display: block; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; } .poem-m .beyt { white-space: normal; } } tqqtcca1uvbz72bx9qpn2sn9sr31dar 304056 304055 2026-10-12T20:01:15Z Hanooz 17889 نسخهٔ [[Special:Diff/304055|304055]] از [[Special:Contributions/Hanooz|Hanooz]] ([[User talk:Hanooz|بحث]]) خنثی شد 304056 sanitized-css text/css /* * الگو:شعر۲/styles.css — TemplateStyles برای «پودمان:شعر» * عرض ستونها بهصورت inline در پودمان تعیین شده؛ این فایل فقط ظاهر * (فاصله، قلم، واکنشگرا) را مدیریت میکند. */ /* ═══ جدول ═══ */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: fixed; text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ عرض ستونها (پشتیبان؛ در پودمان هم inline هست) ═══ */ .poem-s { width: 5%; } .poem-mh { width: 21%; } .poem-g { width: 6%; } /* ═══ سایزر ═══ */ .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; font-size: 0; } /* ═══ سلولهای خالی ساختاری ═══ */ .poem-empty { padding: 0 !important; border: 0; line-height: 0; font-size: 0; } /* ═══ مصراع ═══ */ .poem-m { padding-inline: 0.4em; } .poem-m .beyt { overflow-wrap: break-word; hyphens: none; } .poem-m .beyt sup.reference { white-space: nowrap; } /* ═══ شمارهها ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.5em; } .poem-num-l { text-align: right; padding-right: 0.5em; } /* ═══ مصراع راست/چپ ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ فاصلهگذارها ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ نستعلیق ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ چندصفحهای ═══ */ .poem-skip { display: none; } .poem-row.poem-final-marker { display: none !important; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ صفحهٔ متوسط ═══ */ @media (max-width: 45em) { .poem-m { padding-inline: 0.3em; } .poem-num-r { padding-left: 0.3em; } .poem-num-l { padding-right: 0.3em; } } /* ═══ صفحهٔ باریک ═══ */ @media (max-width: 32em) { .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; table-layout: auto; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } .poem-row.poem-stagger-1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0; } .poem-row.poem-stagger-2 { border-bottom: 1px dotted #c8c8c8; margin-bottom: 0.6em; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } .poem-empty, .poem-num-c, .poem-g, .poem-gapcell { display: none !important; } .poem-num-r, .poem-num-l { display: block; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; } .poem-m .beyt { white-space: normal; } } gz1erx2bef8p81wgkxf44euewj5igb4 304059 304056 2026-10-12T22:47:30Z Hanooz 17889 304059 sanitized-css text/css /* * الگو:شعر/styles.css — شیوه‌نامهٔ TemplateStyles برای «پودمان:شعر» * محتوای این صفحه باید از نوع «Sanitized CSS» باشد؛ پیشوند .mw-parser-output * را خود افزونه اضافه می‌کند. */ /* ─── فاصله‌ها (همه یک‌جا) ────────────────────────────────────────────────── * فاصلهٔ دو مصراع در هر بیت = ستون میانی (۴em) + حاشیهٔ درونی دو مصراع (۲×۰٫۹em) * فاصلهٔ شماره تا مصراع = حاشیهٔ شماره (۱٫۴em) + حاشیهٔ درونی مصراع (۰٫۹em) * (.poem-num-c و .poem-num کوچک‌تر (۸۵٪) نوشته می‌شوند، پس عددهایشان ×۰٫۸۵ است.) */ .poem-m { padding-left: 0.9em; padding-right: 0.9em; } .poem-num-c, .poem-g { width: 4.7em; } .poem-s { width: 3.4em; } /* ─── نستعلیق ─────────────────────────────────────────────────────────────── * قلم و اندازه از خودِ الگوی «نستعلیق» می‌آید: پودمان {{نستعلیق|…}} را صدا می‌زند و * کلاس/قلمی را که آن الگو (و «الگو:نستعلیق/styles.css») می‌گذارد روی شعر می‌نشاند؛ پس هر * تغییر در آن صفحه‌ها خودکار به شعرها می‌رسد. اینجا فقط می‌گوییم سلول‌ها و مصراع‌ها از * شعر ارث ببرند تا قاعده‌های سایت برای td/.b قلم را عوض نکنند. */ .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: inherit; font-size: inherit; line-height: inherit; } /* مصراع‌ها وسط‌چین (پایه) */ .poem-row { text-align: center; } /* ─── ترازبندی مصراع‌ها (کلاس‌هایی که پودمان می‌گذارد) ───────────────────────── * start/end بسته به جهت متن‌اند (در فارسی: start = راست، end = چپ). * «میانه» (پیش‌فرض بیت): مصراع ۱ end، مصراع ۲ start ← هر دو به محور میانی می‌چسبند. * «لبه» (پیش‌فرض بیت۱ و نردبانی): مصراع ۱ start، مصراع ۲ end. */ .poem-a-start { text-align: start; } .poem-a-end { text-align: end; } .poem-a-right { text-align: right; } .poem-a-left { text-align: left; } /* نمادِ میان دو مصراع (جداکننده=…) */ .poem-sep { color: #b3362b; font-size: 90%; } /* شمارهٔ ابیات: کوچک‌تر و بدون شکستن خط */ .poem-num { font-size: 85%; white-space: nowrap; } .poem-num-r { text-align: left; padding-left: 1.4em; } .poem-num-l { text-align: right; padding-right: 1.4em; } /* مصراع تنها با تراز راست یا چپ (شعر نو) */ .poem-m-right { text-align: right; } .poem-m-left { text-align: left; } /* سلول‌های فاصله‌گذار در بیت‌های پلکانی و ردیف فاصله */ .poem-indent, .poem-gapcell { padding: 0; } /* ─── چیدمان ثابت (پیش‌فرض) ────────────────────────────────────────────────── * ستون‌ها را ردیف نامرئی .poem-sizer تعیین می‌کند (.poem-s کنار، .poem-g میان)؛ * عرض و حداکثر عرض روی جعبهٔ .poem-fixed می‌نشیند، پس همهٔ جدول‌های یک شعر * هم‌اندازه‌اند. (اینجا font-size صفر نمی‌شود: عرض‌ها با em نوشته شده‌اند.) */ .poem-fixed .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; } .poem-fixed .b { overflow-wrap: break-word; } /* شعر چندصفحه‌ای: ردیف نامرئی که جای «ادامه» می‌نشیند (خط خالی پدید نیاید) */ .poem-skip { display: none; } /* پیش‌نمایش در فضای نام «صفحه»: ردیفی که از صفحهٔ قبل می‌آید یا به صفحهٔ بعد می‌رود */ .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ─── صفحهٔ باریک (تلفن همراه): هر بیت زیر هم، مانند گنجور ─────────────────────── * شماره بالا، دو مصراع وسط‌چین زیر هم، و خط‌چینِ جداکننده میان ابیات. نماد حذف می‌شود. * با تلفن=دوستونه (یا ترازبندی=پهن) کلاس .poem-stack نیست و جدول دوستونه می‌ماند. */ @media (max-width: 36em) { .poem-stack .poem-table, .poem-stack .poem-table tbody, .poem-stack .poem-row, .poem-stack .poem-row td { display: block; } .poem-stack .poem-sizer { display: none; } .poem-stack .poem-row td { width: auto; text-align: center; } .poem-stack .poem-bayt { display: flex; flex-direction: column; align-items: center; padding: 0.4em 0; border-bottom: 1px dashed rgba(128, 128, 128, 0.55); } .poem-stack .poem-bayt .poem-num-r, .poem-stack .poem-bayt .poem-num-l { order: -1; } .poem-stack .poem-sep { display: none; } } /* صفحهٔ باریک با جدولِ دوستونه: فاصله‌ها کمتر تا جا برای متن بماند */ @media (max-width: 30em) { .poem-m { padding-left: 0.4em; padding-right: 0.4em; } .poem-num-r { padding-left: 0.5em; } .poem-num-l { padding-right: 0.5em; } .poem-num-c, .poem-g { width: 2.4em; } .poem-s { width: 2em; } } 2zezziqlkc4yco6q3guhaid8jtb9z49 پودمان:شعر 828 97054 304038 304037 2026-10-12T13:44:45Z Hanooz 17889 304038 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی عرض ستونها با محتوا تعیین میشود (table-layout: auto): بلندترین مصراعِ شعر — هر جا که باشد — عرض هر دو ستون مصراع را مشخص میکند، پس همهٔ مصراعهای شعر هماندازهاند. ردیف سایزرِ نامرئی، متنِ بلندترین مصراع را در هر دو ستون مصراع تکرار میکند؛ مرورگر با table-layout: auto این متن را برای تعیین عرض میشمارد ولی بهخاطر visibility: hidden نمایش نمیدهد. ارجاعهای پایانی (<ref>) پیش از سنجش بلندی حذف میشوند. ]==] local p = {} local ZWNJ = '\226\128\140' local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = 'none', justDefault = { bayt = 'center', stagger = 'center' }, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil, hemistichs = {} } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local FINAL_MARKER = '<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>' local function esc(s) return (s:gsub('&','&amp;'):gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end --------------------------------------------------------------------------- -- ارجاعها --------------------------------------------------------------------------- local function stripRefs(s) if not s or s == '' then return '' end local prev repeat prev = s s = s:gsub('<ref[^>]*%s*/>', '') s = s:gsub('<ref[^>]*>.-</ref>', '') s = s:gsub('\127[^\127]*\127', '') until s == prev return s end -- متنِ فقط-دیدنی برای سنجش و سایزر local function plainText(s) if not s or s == '' then return '' end s = stripRefs(s) s = s:gsub('<[^>]*>', '') s = s:gsub('%[%[[^%]|]*|', ''):gsub('%[%[', ''):gsub('%]%]', '') s = s:gsub("'''''", ''):gsub("'''", ''):gsub("''", '') return mw.text.trim(s) end -- طول متن دیدنی local function charLen(s) s = plainText(s) s = s:gsub('\226\128\140', '') return mw.ustring.len(s) end -- بلندترین مصراع در state.hemistichs (بر اساس طول متن دیدنی) local function findLongest() local longest, longestLen = '', 0 for _, h in ipairs(state.hemistichs) do local t = plainText(h) local n = mw.ustring.len(t) if n > longestLen then longest, longestLen = t, n end end return longest end -- ثبت مصراع برای سایزر local function register(kind, d) if not state.open then return end if kind == 'bayt' or kind == 'stagger' then if d.m1 and d.m1 ~= '' then state.hemistichs[#state.hemistichs + 1] = d.m1 end if d.m2 and d.m2 ~= '' then state.hemistichs[#state.hemistichs + 1] = d.m2 end elseif kind == 'mesra' then if d.text and d.text ~= '' and not d.blank then state.hemistichs[#state.hemistichs + 1] = d.text end end end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end local function td(class, colspan, style, content) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemiCell(text) return '<span class="beyt">' .. (text or '') .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.bayt return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.stagger local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, s1, hemiCell(m1)) .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-l poem-empty', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end local cell if d.blank and content == cfg.nbsp then cell = content else cell = hemiCell(content) end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r) .. td(cellClass, cs[2], style, cell) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- ردیف سایزر: متن بلندترین مصراع در هر دو ستون مصراع. -- visibility: hidden ⇒ برای اندازهگیری عرض شمرده میشود، دیده نمیشود. local function sizerRow() local longest = findLongest() if longest == '' then return '' end local cell = '<span class="poem-sz">' .. esc(longest) .. '</span>' return '<tr class="poem-sizer" aria-hidden="true">' .. '<td class="poem-s"></td>' .. '<td colspan="2" class="poem-mh">' .. cell .. '</td>' .. '<td class="poem-g"></td>' .. '<td colspan="2" class="poem-mh">' .. cell .. '</td>' .. '<td class="poem-s"></td>' .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="table-layout:auto">' end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} register(kind, d) if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') return twoH, rowKind, c.indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil state.hemistichs = {} end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end local sz = sizerRow() if sz ~= '' then parts[#parts + 1] = sz end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx, state.hemistichs = false, nil, {} else parts[#parts + 1] = FINAL_MARKER end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end local html = flushHtml() if html ~= '' then html = html .. '\n' end if cont and not inPageNs() then return html .. FINAL_MARKER .. categories(flags) end state.open, state.ctx = false, nil local sz = sizerRow() state.hemistichs = {} return html .. sz .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) if raw then d.indent = parseLength(raw) if not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent else d.indent = defaultIndent end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(nil)) p.ladder = safe(staggerFn(nil)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p pqdveyht8v0d34reosfiy99s2w8p3n8 304044 304038 2026-10-12T15:05:36Z Hanooz 17889 304044 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی عرض ستونها با محتوا تعیین میشود (table-layout: auto): بلندترین مصراعِ شعر — هر جا که باشد — عرض هر دو ستون مصراع را مشخص میکند، پس همهٔ مصراعهای شعر هماندازهاند. ردیف سایزرِ نامرئی، متنِ بلندترین مصراع را در هر دو ستون مصراع تکرار میکند؛ مرورگر با table-layout: auto این متن را برای تعیین عرض میشمارد ولی بهخاطر visibility: hidden نمایش نمیدهد. ارجاعهای پایانی (<ref>) پیش از سنجش بلندی حذف میشوند. ]==] local p = {} local ZWNJ = '\226\128\140' local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = 'none', justDefault = { bayt = 'center', stagger = 'center' }, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil, hemistichs = {} } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local FINAL_MARKER = '<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>' local function esc(s) return (s:gsub('&','&amp;'):gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end --------------------------------------------------------------------------- -- ارجاعها --------------------------------------------------------------------------- local function stripRefs(s) if not s or s == '' then return '' end local prev repeat prev = s s = s:gsub('<ref[^>]*%s*/>', '') s = s:gsub('<ref[^>]*>.-</ref>', '') s = s:gsub('\127[^\127]*\127', '') until s == prev return s end -- متنِ فقط-دیدنی برای سنجش و سایزر local function plainText(s) if not s or s == '' then return '' end s = stripRefs(s) s = s:gsub('<[^>]*>', '') s = s:gsub('%[%[[^%]|]*|', ''):gsub('%[%[', ''):gsub('%]%]', '') s = s:gsub("'''''", ''):gsub("'''", ''):gsub("''", '') return mw.text.trim(s) end -- طول متن دیدنی local function charLen(s) s = plainText(s) s = s:gsub('\226\128\140', '') return mw.ustring.len(s) end -- بلندترین مصراع در state.hemistichs (بر اساس طول متن دیدنی) local function findLongest() local longest, longestLen = '', 0 for _, h in ipairs(state.hemistichs) do local t = plainText(h) local n = mw.ustring.len(t) if n > longestLen then longest, longestLen = t, n end end return longest end -- ثبت مصراع برای سایزر local function register(kind, d) if not state.open then return end if kind == 'bayt' or kind == 'stagger' then if d.m1 and d.m1 ~= '' then state.hemistichs[#state.hemistichs + 1] = d.m1 end if d.m2 and d.m2 ~= '' then state.hemistichs[#state.hemistichs + 1] = d.m2 end elseif kind == 'mesra' then if d.text and d.text ~= '' and not d.blank then state.hemistichs[#state.hemistichs + 1] = d.text end end end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end local function td(class, colspan, style, content) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemiCell(text) return '<span class="beyt">' .. (text or '') .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.bayt return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.stagger local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, s1, hemiCell(m1)) .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-l poem-empty', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end local cell if d.blank and content == cfg.nbsp then cell = content else cell = hemiCell(content) end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r) .. td(cellClass, cs[2], style, cell) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- ردیف سایزر: متن بلندترین مصراع در هر دو ستون مصراع. -- visibility: hidden ⇒ برای اندازهگیری عرض شمرده میشود، دیده نمیشود. local function sizerRow() local longest = findLongest() if longest == '' then return '' end local cell = '<span class="poem-sz">' .. esc(longest) .. '</span>' return '<tr class="poem-sizer" aria-hidden="true">' .. '<td class="poem-s"></td>' .. '<td colspan="2" class="poem-mh">' .. cell .. '</td>' .. '<td class="poem-g"></td>' .. '<td colspan="2" class="poem-mh">' .. cell .. '</td>' .. '<td class="poem-s"></td>' .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') -- Use the same width constraints in every namespace and mode. local width = o.width or cfg.defaultWidth local maxWidth = o.maxWidth or cfg.maxWidth local wrap = joinStyle( 'width:' .. width, 'max-width:' .. maxWidth, 'margin:0 auto', font, o.style ) -- Always use a fixed seven-column grid and its sizing row. return div .. ' style="' .. esc(wrap) .. '">' .. '<table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end --------------------------------------------------------------------------- -- چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} register(kind, d) if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') return twoH, rowKind, c.indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil state.hemistichs = {} end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end local sz = sizerRow() if sz ~= '' then parts[#parts + 1] = sz end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx, state.hemistichs = false, nil, {} else parts[#parts + 1] = FINAL_MARKER end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end local html = flushHtml() if html ~= '' then html = html .. '\n' end if cont and not inPageNs() then return html .. FINAL_MARKER .. categories(flags) end state.open, state.ctx = false, nil local sz = sizerRow() state.hemistichs = {} return html .. sz .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) if raw then d.indent = parseLength(raw) if not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent else d.indent = defaultIndent end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(nil)) p.ladder = safe(staggerFn(nil)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p ea4feyi4uc69givu77ar7iq2ole7r6z 304047 304044 2026-10-12T18:56:37Z Hanooz 17889 304047 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی ▸ شبکهٔ ثابت ۷ستونی با درصدهای قطعی: ۵٪ ش | ۲۱٪ | ۲۱٪ | ۶٪ شو | ۲۱٪ | ۲۱٪ | ۵٪ شچ هر مصراع (colspan=2) دقیقاً ۴۲٪ جدول است؛ در همهٔ ابیات و همهٔ انواع (بیت، بیت۱، نردبانی) هماندازه. ▸ ردیف سایزرِ خالی در آغاز جدول، عرض ستونها را تثبیت میکند (table-layout: fixed). ▸ ترازبندی پیشفرض «وسط». ترازبندی=پهن یا لبه اختیاری. ▸ ارجاعهای <ref> در متن inline میمانند (به آخرین واژه میچسبند). ▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت. ]==] local p = {} local ZWNJ = '\226\128\140' local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = '48em', justDefault = { bayt = 'center', stagger = 'center' }, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local FINAL_MARKER = '<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>' local function esc(s) return (s:gsub('&','&amp;'):gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end local function td(class, colspan, style, content) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemiCell(text) return '<span class="beyt">' .. (text or '') .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.bayt return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.stagger local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, s1, hemiCell(m1)) .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-l poem-empty', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end local cell if d.blank and content == cfg.nbsp then cell = content else cell = hemiCell(content) end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r) .. td(cellClass, cs[2], style, cell) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- سایزرِ خالی: ۷ ستون با عرضهای ثابت درصدی. -- با table-layout: fixed، این ردیف عرض ستونها را برای همهٔ ردیفهای -- بعدی تثبیت میکند. local function sizerRow() return '<tr class="poem-sizer" aria-hidden="true">' .. '<td class="poem-s"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-g"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-s"></td>' .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') return twoH, rowKind, c.indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil else parts[#parts + 1] = FINAL_MARKER end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end local html = flushHtml() if html ~= '' then html = html .. '\n' end if cont and not inPageNs() then return html .. FINAL_MARKER .. categories(flags) end state.open, state.ctx = false, nil return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) if raw then d.indent = parseLength(raw) if not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent else d.indent = defaultIndent end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(nil)) p.ladder = safe(staggerFn(nil)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p 9ja5iu2kjk4w3mhe1h96in6ql1l7bgx 304050 304047 2026-10-12T19:22:25Z Hanooz 17889 304050 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی ▸ عرض ستونها بهصورت inline روی هر سلول نوشته میشود، نه در CSS. شبکهٔ ثابت ۷ستونی: ۵٪ | ۲۱٪ | ۲۱٪ | ۶٪ | ۲۱٪ | ۲۱٪ | ۵٪ هر مصراع (colspan=2) دقیقاً ۴۲٪ جدول است. ▸ بهاینترتیب حتی اگر شیوهنامه در فضای نام صفحه بارگذاری نشود یا ProofreadPage تگ templatestyles را حذف کند، عرضها حفظ میشوند. ▸ ترازبندی پیشفرض «وسط». ترازبندی=پهن / لبه اختیاری. ▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت. ]==] local p = {} local ZWNJ = '\226\128\140' -- عرضهای قطعی (درصد). مجموع = ۱۰۰. local W_SIDE = '5%' local W_MH = '21%' local W_GAP = '6%' local W_MISRA = '42%' -- 21 + 21 local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = '48em', justDefault = { bayt = 'center', stagger = 'center' }, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local FINAL_MARKER = '<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>' local function esc(s) return (s:gsub('&','&amp;'):gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end -- td با عرض قطعی: width همیشه در style اینلاین مینشیند. local function td(class, colspan, style, content, width) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end local full = style if width then if full and full ~= '' then full = 'width:' .. width .. ';' .. full else full = 'width:' .. width end end if full and full ~= '' then s = s .. ' style="' .. esc(full) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemiCell(text) return '<span class="beyt">' .. (text or '') .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.bayt return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1), W_MISRA) .. td('poem-num poem-num-c', 1, d.style, d.c, W_GAP) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2), W_MISRA) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE) .. '</tr>' end local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.stagger local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end -- ردیف ۱: side | misra1 | gap | empty | empty -- ردیف ۲: empty | empty | gap | misra2 | side return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE) .. td('b poem-m poem-m1', 2, s1, hemiCell(m1), W_MISRA) .. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP) .. td('poem-empty', 2, nil, '', W_MISRA) .. td('poem-num poem-num-l poem-empty', 1, nil, '', W_SIDE) .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '', W_SIDE) .. td('poem-empty', 2, nil, '', W_MISRA) .. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP) .. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2), W_MISRA) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end local cell if d.blank and content == cfg.nbsp then cell = content else cell = hemiCell(content) end local w1 = (cs[1] == 1) and W_SIDE or W_MISRA local w2 = (cs[2] == 3) and W_MISRA or (W_SIDE .. '+') -- never used; simplified below -- ساده: اگر cs = {2,3,2} باشد ⇒ side=42%? نه، side=5%+? مجموع = 7 ستون -- cs={2,3,2}: 5+21 / 21+6+21 / 21+5 ⇒ 26% / 68% / 26% -- cs={1,5,1}: 5 / 5+21+21+6+21+21 / 5 ⇒ 5% / 95% / 5% local wLeft = (cs[1] == 1) and W_SIDE or '26%' local wMid = (cs[2] == 3) and '48%' or '90%' -- approximation; browser resolves under fixed local wRight = (cs[3] == 1) and W_SIDE or '26%' return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r, wLeft) .. td(cellClass, cs[2], style, cell, wMid) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l, wRight) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content, '100%') .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- سایزر با عرضهای inline روی هر سلول. local function sizerRow() return '<tr class="poem-sizer" aria-hidden="true">' .. td('poem-s', 1, nil, '', W_SIDE) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-g', 1, nil, '', W_GAP) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-s', 1, nil, '', W_SIDE) .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') return twoH, rowKind, c.indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil else parts[#parts + 1] = FINAL_MARKER end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end local html = flushHtml() if html ~= '' then html = html .. '\n' end if cont and not inPageNs() then return html .. FINAL_MARKER .. categories(flags) end state.open, state.ctx = false, nil return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) if raw then d.indent = parseLength(raw) if not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent else d.indent = defaultIndent end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(nil)) p.ladder = safe(staggerFn(nil)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p ihe5rqn9bnxuu6wzjes4y19ov8umbwt 304053 304050 2026-10-12T19:29:46Z Hanooz 17889 304053 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی ▸ عرض ستونها بهصورت inline روی هر سلول نوشته میشود، نه در CSS. شبکهٔ ثابت ۷ستونی: ۵٪ | ۲۱٪ | ۲۱٪ | ۶٪ | ۲۱٪ | ۲۱٪ | ۵٪ هر مصراع (colspan=2) دقیقاً ۴۲٪ جدول است. ▸ بهاینترتیب حتی اگر شیوهنامه در فضای نام صفحه بارگذاری نشود یا ProofreadPage تگ templatestyles را حذف کند، عرضها حفظ میشوند. ▸ ترازبندی پیشفرض «وسط». ترازبندی=پهن / لبه اختیاری. ▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت. ]==] local p = {} local ZWNJ = '\226\128\140' -- عرضهای قطعی (درصد). مجموع = ۱۰۰. local W_SIDE = '5%' local W_MH = '21%' local W_GAP = '6%' local W_MISRA = '42%' -- 21 + 21 local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = '48em', justDefault = { bayt = 'center', stagger = 'center' }, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local FINAL_MARKER = '<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>' local function esc(s) return (s:gsub('&','&amp;'):gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end -- td با عرض قطعی: width همیشه در style اینلاین مینشیند. local function td(class, colspan, style, content, width) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end local full = style if width then if full and full ~= '' then full = 'width:' .. width .. ';' .. full else full = 'width:' .. width end end if full and full ~= '' then s = s .. ' style="' .. esc(full) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemiCell(text) return '<span class="beyt">' .. (text or '') .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.bayt return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1), W_MISRA) .. td('poem-num poem-num-c', 1, d.style, d.c, W_GAP) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2), W_MISRA) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE) .. '</tr>' end local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.stagger local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end -- ردیف ۱: side | misra1 | gap | empty | empty -- ردیف ۲: empty | empty | gap | misra2 | side return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE) .. td('b poem-m poem-m1', 2, s1, hemiCell(m1), W_MISRA) .. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP) .. td('poem-empty', 2, nil, '', W_MISRA) .. td('poem-num poem-num-l poem-empty', 1, nil, '', W_SIDE) .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '', W_SIDE) .. td('poem-empty', 2, nil, '', W_MISRA) .. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP) .. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2), W_MISRA) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end local cell if d.blank and content == cfg.nbsp then cell = content else cell = hemiCell(content) end local w1 = (cs[1] == 1) and W_SIDE or W_MISRA local w2 = (cs[2] == 3) and W_MISRA or (W_SIDE .. '+') -- never used; simplified below -- ساده: اگر cs = {2,3,2} باشد ⇒ side=42%? نه، side=5%+? مجموع = 7 ستون -- cs={2,3,2}: 5+21 / 21+6+21 / 21+5 ⇒ 26% / 68% / 26% -- cs={1,5,1}: 5 / 5+21+21+6+21+21 / 5 ⇒ 5% / 95% / 5% local wLeft = (cs[1] == 1) and W_SIDE or '26%' local wMid = (cs[2] == 3) and '48%' or '90%' -- approximation; browser resolves under fixed local wRight = (cs[3] == 1) and W_SIDE or '26%' return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r, wLeft) .. td(cellClass, cs[2], style, cell, wMid) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l, wRight) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content, '100%') .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- سایزر با عرضهای inline روی هر سلول. local function sizerRow() return '<tr class="poem-sizer" aria-hidden="true">' .. td('poem-s', 1, nil, '', W_SIDE) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-g', 1, nil, '', W_GAP) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-s', 1, nil, '', W_SIDE) .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then -- عرض قطعی روی خود جدول، به em، نه درصد. جدول مستقل از والد -- دارای عرض معیّن میشود و table-layout: fixed روی عدد ثابت کار میکند. -- max-width: 100% برای صفحهٔ باریک نگه داشته میشود. local absWidth = o.absWidth or '42em' local tblStyle = 'width:' .. absWidth .. ';max-width:100%;table-layout:fixed;margin:0 auto;border-collapse:collapse' if font then tblStyle = tblStyle .. ';' .. font end if o.style then tblStyle = tblStyle .. ';' .. o.style end local wrapperStyle = joinStyle( 'width:100%', o.maxWidth and o.maxWidth ~= 'none' and ('max-width:' .. o.maxWidth) or nil, 'margin:0 auto', font, o.style ) -- هر دو لایه: بیرونی برای مرکزکردن و سقف عرض، داخلی با عرض em return div .. ' style="' .. esc(wrapperStyle) .. '">' .. '<table class="' .. tcls .. '" role="presentation" style="' .. esc(tblStyle) .. '">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') return twoH, rowKind, c.indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil else parts[#parts + 1] = FINAL_MARKER end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end local html = flushHtml() if html ~= '' then html = html .. '\n' end if cont and not inPageNs() then return html .. FINAL_MARKER .. categories(flags) end state.open, state.ctx = false, nil return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) if raw then d.indent = parseLength(raw) if not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent else d.indent = defaultIndent end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(nil)) p.ladder = safe(staggerFn(nil)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p p1mmb8g2rb5ijgk92jk7kd476crpa9f 304054 304053 2026-10-12T19:54:48Z Hanooz 17889 304054 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی ▸ جدول با عرض قطعی em؛ مصون از والدِ shrink-to-fit در فضای نام صفحه. ▸ عرض ستونها با CSS variable: پیشفرض «با شماره» = مصراع ۴۲٪. اگر جدول هیچ شمارهای نداشته باشد، CSS بهکمک :has() متغیرها را بازنویسی میکند و مصراع به ۴۵٪ میرسد. تمام CSS، بدون state. ▸ شمارههای خودکار با شمارهگذاری=…؛ شمارههای دستی با ش=، شو=، شچ=. ▸ ارجاعهای <ref> در متن inline میمانند. ▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت. ]==] local p = {} local ZWNJ = '\226\128\140' -- عرضها با CSS variable. مقدار پیشفرض = «با شماره». -- مقدار «بدون شماره» در styles.css تعیین میشود. local W_SIDE = 'var(--poem-side, 5%)' local W_MH = 'var(--poem-mh, 21%)' local W_GAP = 'var(--poem-gap, 6%)' local W_MISRA = 'var(--poem-misra, 42%)' local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '42em', -- عرض قطعی جدول maxWidth = '48em', justDefault = { bayt = 'center', stagger = 'center' }, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = true } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local FINAL_MARKER = '<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>' local function esc(s) return (s:gsub('&','&amp;'):gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end local function td(class, colspan, style, content, width) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end local full = style if width then if full and full ~= '' then full = 'width:' .. width .. ';' .. full else full = 'width:' .. width end end if full and full ~= '' then s = s .. ' style="' .. esc(full) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemiCell(text) return '<span class="beyt">' .. (text or '') .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.bayt return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1), W_MISRA) .. td('poem-num poem-num-c', 1, d.style, d.c, W_GAP) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2), W_MISRA) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE) .. '</tr>' end local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.stagger local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE) .. td('b poem-m poem-m1', 2, s1, hemiCell(m1), W_MISRA) .. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP) .. td('poem-empty', 2, nil, '', W_MISRA) .. td('poem-num poem-num-l poem-empty', 1, nil, '', W_SIDE) .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '', W_SIDE) .. td('poem-empty', 2, nil, '', W_MISRA) .. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP) .. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2), W_MISRA) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end local cell if d.blank and content == cfg.nbsp then cell = content else cell = hemiCell(content) end -- عرضهای تقریبی روی colspan ها (در table-layout: fixed فقط ردیف -- سایزر تعیینکننده است؛ اینها فقط جهت سازگاری بیشترند) local wLeft = (cs[1] == 1) and W_SIDE or '26%' local wMid = (cs[2] == 3) and '48%' or '90%' local wRight = (cs[3] == 1) and W_SIDE or '26%' return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r, wLeft) .. td(cellClass, cs[2], style, cell, wMid) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l, wRight) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content, '100%') .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- ردیف سایزر با عرضهای متغیر. local function sizerRow() return '<tr class="poem-sizer" aria-hidden="true">' .. td('poem-s', 1, nil, '', W_SIDE) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-g', 1, nil, '', W_GAP) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-s', 1, nil, '', W_SIDE) .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then -- عرض قطعی em روی خود جدول؛ مصون از والدِ shrink-to-fit. local absWidth = o.width or cfg.defaultWidth local tblStyle = 'width:' .. absWidth .. ';max-width:100%;table-layout:fixed;margin:0 auto;border-collapse:collapse' if font then tblStyle = tblStyle .. ';' .. font end if o.style then tblStyle = tblStyle .. ';' .. o.style end local wrapperStyle = joinStyle( 'width:100%', o.maxWidth and o.maxWidth ~= 'none' and ('max-width:' .. o.maxWidth) or nil, 'margin:0 auto' ) return div .. ' style="' .. esc(wrapperStyle) .. '">' .. '<table class="' .. tcls .. '" role="presentation" style="' .. esc(tblStyle) .. '">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = true end if o.fixed then o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or cfg.maxWidth end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') return twoH, rowKind, c.indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil else parts[#parts + 1] = FINAL_MARKER end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end local html = flushHtml() if html ~= '' then html = html .. '\n' end if cont and not inPageNs() then return html .. FINAL_MARKER .. categories(flags) end state.open, state.ctx = false, nil return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) if raw then d.indent = parseLength(raw) if not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent else d.indent = defaultIndent end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(nil)) p.ladder = safe(staggerFn(nil)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p 3ifycz6aje4lb6nd1wi8a9ggnzuslx1 304057 304054 2026-10-12T20:01:20Z Hanooz 17889 نسخهٔ [[Special:Diff/304054|304054]] از [[Special:Contributions/Hanooz|Hanooz]] ([[User talk:Hanooz|بحث]]) خنثی شد 304057 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی ▸ عرض ستونها بهصورت inline روی هر سلول نوشته میشود، نه در CSS. شبکهٔ ثابت ۷ستونی: ۵٪ | ۲۱٪ | ۲۱٪ | ۶٪ | ۲۱٪ | ۲۱٪ | ۵٪ هر مصراع (colspan=2) دقیقاً ۴۲٪ جدول است. ▸ بهاینترتیب حتی اگر شیوهنامه در فضای نام صفحه بارگذاری نشود یا ProofreadPage تگ templatestyles را حذف کند، عرضها حفظ میشوند. ▸ ترازبندی پیشفرض «وسط». ترازبندی=پهن / لبه اختیاری. ▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت. ]==] local p = {} local ZWNJ = '\226\128\140' -- عرضهای قطعی (درصد). مجموع = ۱۰۰. local W_SIDE = '5%' local W_MH = '21%' local W_GAP = '6%' local W_MISRA = '42%' -- 21 + 21 local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = '48em', justDefault = { bayt = 'center', stagger = 'center' }, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local FINAL_MARKER = '<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>' local function esc(s) return (s:gsub('&','&amp;'):gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end -- td با عرض قطعی: width همیشه در style اینلاین مینشیند. local function td(class, colspan, style, content, width) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end local full = style if width then if full and full ~= '' then full = 'width:' .. width .. ';' .. full else full = 'width:' .. width end end if full and full ~= '' then s = s .. ' style="' .. esc(full) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemiCell(text) return '<span class="beyt">' .. (text or '') .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.bayt return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1), W_MISRA) .. td('poem-num poem-num-c', 1, d.style, d.c, W_GAP) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2), W_MISRA) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE) .. '</tr>' end local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.stagger local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end -- ردیف ۱: side | misra1 | gap | empty | empty -- ردیف ۲: empty | empty | gap | misra2 | side return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r, W_SIDE) .. td('b poem-m poem-m1', 2, s1, hemiCell(m1), W_MISRA) .. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP) .. td('poem-empty', 2, nil, '', W_MISRA) .. td('poem-num poem-num-l poem-empty', 1, nil, '', W_SIDE) .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '', W_SIDE) .. td('poem-empty', 2, nil, '', W_MISRA) .. td('poem-num poem-num-c poem-empty', 1, nil, '', W_GAP) .. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2), W_MISRA) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l, W_SIDE) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end local cell if d.blank and content == cfg.nbsp then cell = content else cell = hemiCell(content) end local w1 = (cs[1] == 1) and W_SIDE or W_MISRA local w2 = (cs[2] == 3) and W_MISRA or (W_SIDE .. '+') -- never used; simplified below -- ساده: اگر cs = {2,3,2} باشد ⇒ side=42%? نه، side=5%+? مجموع = 7 ستون -- cs={2,3,2}: 5+21 / 21+6+21 / 21+5 ⇒ 26% / 68% / 26% -- cs={1,5,1}: 5 / 5+21+21+6+21+21 / 5 ⇒ 5% / 95% / 5% local wLeft = (cs[1] == 1) and W_SIDE or '26%' local wMid = (cs[2] == 3) and '48%' or '90%' -- approximation; browser resolves under fixed local wRight = (cs[3] == 1) and W_SIDE or '26%' return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r, wLeft) .. td(cellClass, cs[2], style, cell, wMid) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l, wRight) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content, '100%') .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- سایزر با عرضهای inline روی هر سلول. local function sizerRow() return '<tr class="poem-sizer" aria-hidden="true">' .. td('poem-s', 1, nil, '', W_SIDE) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-g', 1, nil, '', W_GAP) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-mh', 1, nil, '', W_MH) .. td('poem-s', 1, nil, '', W_SIDE) .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then -- عرض قطعی روی خود جدول، به em، نه درصد. جدول مستقل از والد -- دارای عرض معیّن میشود و table-layout: fixed روی عدد ثابت کار میکند. -- max-width: 100% برای صفحهٔ باریک نگه داشته میشود. local absWidth = o.absWidth or '42em' local tblStyle = 'width:' .. absWidth .. ';max-width:100%;table-layout:fixed;margin:0 auto;border-collapse:collapse' if font then tblStyle = tblStyle .. ';' .. font end if o.style then tblStyle = tblStyle .. ';' .. o.style end local wrapperStyle = joinStyle( 'width:100%', o.maxWidth and o.maxWidth ~= 'none' and ('max-width:' .. o.maxWidth) or nil, 'margin:0 auto', font, o.style ) -- هر دو لایه: بیرونی برای مرکزکردن و سقف عرض، داخلی با عرض em return div .. ' style="' .. esc(wrapperStyle) .. '">' .. '<table class="' .. tcls .. '" role="presentation" style="' .. esc(tblStyle) .. '">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') return twoH, rowKind, c.indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil else parts[#parts + 1] = FINAL_MARKER end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end local html = flushHtml() if html ~= '' then html = html .. '\n' end if cont and not inPageNs() then return html .. FINAL_MARKER .. categories(flags) end state.open, state.ctx = false, nil return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) if raw then d.indent = parseLength(raw) if not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent else d.indent = defaultIndent end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(nil)) p.ladder = safe(staggerFn(nil)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p p1mmb8g2rb5ijgk92jk7kd476crpa9f 304058 304057 2026-10-12T22:47:19Z Hanooz 17889 304058 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکی‌نبشتهٔ فارسی الگو نقطهٔ ورود در {{#invoke:شعر|…}} {{شعر}} open آغاز شعر؛ با پارامتر «متن» کل شعر را می‌سازد {{پایان شعر}} close {{بیت}} bayt بیت دومصراعی {{بیت۱}} stagger بیت پلکانی (ضربدری) {{بیت نردبانی}} ladder بیت نردبانی (تورفتگی پیش‌فرض بیشتر) {{مصراع}} mesra یک مصراع {{بیت خالی}} emptyBayt {{مصراع خالی}} emptyMesra ساختار خروجی همان جدول ۷ستونی پیشین است (ظاهر شعرهای موجود و خروجی کتاب الکترونیکی تغییری نمی‌کند). شیوه‌نامه: «الگو:شعر/styles.css». چیدمان (تازه): جدول پیش‌فرض عرض ثابت و مستقل از محتوا دارد (عرض=، حداکثر عرض=)، تا همهٔ ابیات شعر — حتی در جدول‌های جدا (صفحه‌های جدا) — از یک نقطه آغاز شوند. ترازبندی=وسط|راست|چپ|پهن|لبه برای هر نوع بیت؛ قلم=، نستعلیق خودبسنده (بدون Common.css). در حالت «متن»، خطی که با @ آغاز شود نوع ردیف‌های بعدی را عوض می‌کند: «@بیت نردبانی». شعر چندصفحه‌ای (ایدهٔ start/end در ppoem): • در فضای نام «صفحه» هر صفحه همیشه خودبسنده و بسته نمایش داده می‌شود. • در صفحهٔ اصلی (درج با <pages/>) پارامتر «ادامه» صفحه‌ها را به هم می‌دوزد: {{شعر|ادامه}} جدول تازه باز نمی‌کند و {{پایان شعر|ادامه}} نمی‌بندد. • ردیفِ ناتمام (مصراعی که در صفحهٔ بعد ادامه دارد) نگه داشته می‌شود (ناتمام=بله) و با نخستین ردیفِ صفحهٔ بعد (دنباله=بله) یکی می‌شود. ]==] local p = {} --------------------------------------------------------------------------- -- تنظیمات --------------------------------------------------------------------------- local ZWNJ = '\226\128\140' -- نیم‌فاصله local cfg = { styles = 'Template:شعر۲/styles.css', -- صفحهٔ TemplateStyles pageNs = 104, -- شمارهٔ فضای نام «صفحه» (ProofreadPage) defaultIndent = '20%', -- تورفتگی پیش‌فرض {{بیت۱}} ladderIndent = '45%', -- تورفتگی پیش‌فرض {{بیت نردبانی}} mesraUnit = '1.5em', -- تورفتگی هر «:» در حالت متن با نوع=مصراع nbsp = '&#160;', -- چیدمان: 'auto' = جدولِ هم‌اندازهٔ محتوا (رفتار قدیمی)؛ طول/درصد = عرض ثابت defaultWidth = '100%', maxWidth = '50em', -- سقف عرض (وقتی عرض داده نشده) -- فاصلهٔ دو مصراع، ستون شمارهٔ کناری و فاصلهٔ شماره تا مصراع در الگو:شعر/styles.css است -- ترازبندی پیش‌فرض هر نوع: inner | center | right | left | justify | edge -- inner (میانه): مصراع‌ها به محور میانی می‌چسبند — شیوهٔ گنجور و کتاب‌های کلاسیک -- edge (لبه) : مصراع ۱ به راست، مصراع ۲ به چپ justDefault = { bayt = 'inner', stagger = 'edge' }, -- نستعلیق: خودِ الگوی ویکی ({{نستعلیق|…}}) صدا زده می‌شود و کلاس/قلمی که می‌گذارد روی شعر -- می‌نشیند؛ پس هر تغییر در «الگو:نستعلیق» یا «الگو:نستعلیق/styles.css» خودکار به شعرها می‌رسد. nastaliqTemplate = 'نستعلیق', -- آخرین چاره، فقط اگر آن الگو در دسترس نبود nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", -- نمادِ میان دو مصراع: separator برای همهٔ شعرها ('' = بدون نماد، یا مثلاً '❖')؛ -- ornament نمادِ «جداکننده=بله» است separator = '', ornament = '❖', -- ترازبندی «پهن»: اگر کوتاه‌ترین مصراعِ بیت کمتر از این نسبتِ طولِ بلندترین باشد، آن بیت -- کشیده نمی‌شود (وسط‌چین می‌ماند) تا فاصلهٔ واژه‌ها بی‌جهت باز نشود justifyMinRatio = 0.7, -- true: ردیف ناتمامِ آخر صفحه نگه داشته و با دنباله‌اش ادغام می‌شود (نیازمند اجرای ترتیبی -- الگوها در یک رندر). اگر متنی در مرز صفحه گم شد، false کنید: هر تکه فوراً و جدا نمایش داده می‌شود. mergeRows = true, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } -- نام پارامترها (فارسی + معادل انگلیسی) local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, -- شعر از/به صفحهٔ دیگر ادامه دارد unfinished = { 'ناتمام', 'unfinished' }, -- این ردیف در صفحهٔ بعد ادامه دارد continued = { 'دنباله', 'continued' }, -- این ردیف ادامهٔ ردیف ناتمامِ صفحهٔ قبل است joiner = { 'اتصال', 'join' }, -- چسباندن دو تکه: فاصله | چسبیده | نیم‌فاصله stanza = { 'بند', 'stanza' }, -- بند=جدید width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, sep = { 'جداکننده', 'separator' }, phone = { 'تلفن', 'phone' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep', } local YES = { ['بله'] = true, ['بلی'] = true, ['آری'] = true, ['خودکار'] = true, yes = true, y = true, auto = true, ['true'] = true, ['1'] = true, } local NO = { ['نه'] = true, ['خیر'] = true, no = true, n = true, ['false'] = true, ['0'] = true, } local UNITS = { [''] = 'px', ['%'] = '%', px = 'px', em = 'em', ex = 'ex', ch = 'ch', rem = 'rem', vw = 'vw', vh = 'vh', pt = 'pt', cm = 'cm', mm = 'mm', ['in'] = 'in', } local CONT = { ['قبل'] = 'prev', ['پیشین'] = 'prev', prev = 'prev', ['بعد'] = 'next', ['پسین'] = 'next', next = 'next', ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both', } local JOIN = { ['فاصله'] = ' ', space = ' ', ['چسبیده'] = '', ['بدون فاصله'] = '', tight = '', ['نیم' .. ZWNJ .. 'فاصله'] = ZWNJ, ['نیمفاصله'] = ZWNJ, ['نیم فاصله'] = ZWNJ, zwnj = ZWNJ, } local WHICH = { ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both', ['اول'] = 'first', first = 'first', ['دوم'] = 'second', second = 'second', } local STANZA = { ['جدید'] = true, new = true } local JUST = { ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', ['راست'] = 'right', right = 'right', ['چپ'] = 'left', left = 'left', ['پهن'] = 'justify', ['دوطرفه'] = 'justify', ['دو' .. ZWNJ .. 'طرفه'] = 'justify', justify = 'justify', ['لبه'] = 'edge', edge = 'edge', ['میانه'] = 'inner', ['درون'] = 'inner', inner = 'inner', } local ALIGN_CLASS = { right = 'poem-a-right', left = 'poem-a-left' } local JUSTIFY_STYLE = 'text-align:justify;text-align-last:justify' local PHONE = { ['ستونی'] = 'stack', stack = 'stack', ['دوستونه'] = 'two', ['دو ستونه'] = 'two', ['دو' .. ZWNJ .. 'ستونه'] = 'two', two = 'two', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar = true, arz = true, azb = true, bgn = true, ckb = true, dv = true, fa = true, glk = true, he = true, ks = true, lrc = true, mzn = true, pnb = true, prs = true, ps = true, sd = true, ug = true, ur = true, yi = true, } -- وضعیتِ میان فراخوانی‌های یک صفحه (هر تجزیه موتور Lua تازه دارد) local state = { ctx = nil, -- شماره‌گذاری و ارقام شعرِ باز open = false, -- آیا شعری باز است؟ (برای دوختن صفحه‌ها در صفحهٔ اصلی) carry = nil, -- ردیف ناتمام در انتظار دنباله: { kind = , d = } } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto'), sep = cfg.separator } end -- ردیف نامرئی: جای خروجی «تهی» تا خط خالی (و در نتیجه <p><br>) پدید نیاید local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' --------------------------------------------------------------------------- -- ابزارهای عمومی --------------------------------------------------------------------------- local function esc(s) s = s:gsub('"', '&quot;'):gsub('<', '&lt;'):gsub('>', '&gt;') return s end local function pick(args, names) for _, name in ipairs(names) do local v = args[name] if v and v ~= '' then return v end end return nil end -- آیا یکی از نام‌ها داده شده است (حتی با مقدار تهی)؟ local function present(args, names) for _, name in ipairs(names) do if args[name] ~= nil then return true end end return false end local function bool(v, default) if v == nil then return default end v = v:lower() if YES[v] then return true end if NO[v] then return false end return default end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end -- مصراع: یک‌خطی‌کردن و حذف فاصلهٔ اضافه local function clean(s) if not s then return '' end s = mw.text.trim(s) s = s:gsub('%s*\n%s*', ' ') return s end -- آرگومان‌های الگوی فراخوان‌کننده (در صورت نبود، آرگومان‌های #invoke) local function getArgs(frame) local args = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then args[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return args end -- آیا پارامتر ناشناخته (یا مصراعِ اضافه) وجود دارد؟ -- اشتباه رایج: وجود «=» در مصراع که آن را به پارامتر نام‌دار تبدیل می‌کند. local function hasUnknown(args, positional) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > positional or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end -- فضای نام «صفحه»: همیشه خودبسنده نمایش می‌دهیم local function inPageNs() return nsId() == cfg.pageNs end -- ردیابی فقط در اسم‌فضای اصلی و اسم‌فضاهای محتوایی سفارشی (مثل صفحه) local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn', 1, true) or rest:find('cyrl', 1, true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab', 1, true) or rest:find('hebr', 1, true) then return 'rtl' end return 'ltr' end --------------------------------------------------------------------------- -- ارقام --------------------------------------------------------------------------- local FA = { [0] = '۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹' } -- mode: 'fa' (پیش‌فرض) | 'latin' | 'keep' -- اگر مقدار شبیه نشانه‌گذاری (HTML/ویکی) باشد دست نمی‌زنیم. local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end -- \127 = نشانگر strip (مثل <ref>) که خودش رقم دارد و نباید خراب شود if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names) if not v then return '' end return convertDigits(v, digits) end -- عدد صحیح در بازهٔ [lo, hi] یا nil (از inf و nan و اعداد غول‌آسا جلوگیری می‌کند) local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end --------------------------------------------------------------------------- -- اندازه‌های CSS --------------------------------------------------------------------------- local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin') v = v:gsub('٪', '%%'):gsub('٫', '.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local num, unit = splitLength(v) if not num then return nil end return string.format('%g', num) .. unit end --------------------------------------------------------------------------- -- شماره‌گذاری خودکار --------------------------------------------------------------------------- local function newCtx(args, kind, flags) local ctx = { digits = 'fa', count = 0, start = 1, side = 'l', mesra = (kind == 'mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()] if not ctx.digits then ctx.digits = 'fa'; flags.unknown = true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9) if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()] if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end -- ۰ یعنی خاموش else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end -- شمارهٔ بعدی (یا nil اگر شماره‌گذاری خاموش است یا نوبت نمایش نیست) local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end -- counted: آیا این ردیف در شماره‌گذاری شمرده شود؟ noCenter: ردیف «وسط» ندارد local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end -- شمارهٔ دستی مقدم است local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end --------------------------------------------------------------------------- -- ساخت HTML --------------------------------------------------------------------------- -- چند تکه استایل را با «;» به هم می‌چسباند local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end -- کلاس ترازِ مصراعِ which (۱ یا ۲) برای ترازبندی j. وسط = پیش‌فرض شیوه‌نامه؛ پهن = استایل -- درون‌خطی (JUSTIFY_STYLE). ترازها کلاس‌اند تا در صفحهٔ باریک (تلفن) بتوان آن‌ها را وسط‌چین کرد. local function alignClass(j, which) if not j or j == 'center' or j == 'justify' then return nil end if j == 'edge' then return (which == 1) and 'poem-a-start' or 'poem-a-end' end if j == 'inner' then return (which == 1) and 'poem-a-end' or 'poem-a-start' end return ALIGN_CLASS[j] end local function withClass(base, extra) if extra then return base .. ' ' .. extra end return base end -- شمار تقریبی نویسه‌های دیدنیِ یک مصراع (بدون نشانه‌گذاری و نشانگرهای strip) local function charLen(s) s = s:gsub('\127[^\127]*\127', ''):gsub('<[^>]*>', ''):gsub('%[%[[^%]|]*|', ''):gsub('[%[%]]', '') return #(s:gsub('[\128-\191]', '')) end -- «پهن» فقط وقتی که دو مصراع هم‌اندازه‌اند؛ وگرنه کشش زیاد می‌شود ← وسط‌چین local function effectiveJust(j, a, b) if j == 'justify' then local la, lb = charLen(a), charLen(b) local hi = math.max(la, lb) if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return 'center' end end return j end local function td(class, colspan, style, content) local s = '<td class="' .. class .. '"' if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemi(text) return '<span class="beyt">' .. text .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end -- {{بیت}} local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end -- ردیف خالی ارتفاع خط را حفظ کند local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2) local js = (j == 'justify') and JUSTIFY_STYLE or nil local mid = d.c if mid == '' and d.sep and d.sep ~= '' then mid = '<span class="poem-sep">' .. d.sep .. '</span>' -- نمادِ میان دو مصراع (اگر شمارهٔ میانی نیست) end return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, nil, d.r) .. td(withClass('b poem-m poem-m1', alignClass(j, 1)), 2, joinStyle(js, d.style), hemi(m1)) .. td('poem-num poem-num-c', 1, d.style, mid) .. td(withClass('b poem-m poem-m2', alignClass(j, 2)), 2, joinStyle(js, d.style), hemi(d.m2)) .. td('poem-num poem-num-l', 1, nil, d.l) .. '</tr>' end -- پهنای جعبهٔ هر خط در بیت پلکانی با چیدمان ثابت: ۱۰۰٪ منهای تورفتگی local function boxWidth(indent) local num, unit = indent:match('^([%d%.]+)(.*)$') if unit == '%' then return string.format('%g%%', math.max(100 - tonumber(num), 10)) end return 'calc(100% - ' .. indent .. ')' end -- {{بیت۱}} و {{بیت نردبانی}} local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2) local js = (j == 'justify') and JUSTIFY_STYLE or nil local s1, s2 = joinStyle(js, d.style), joinStyle(js, d.style) local c1, c2 = withClass('b poem-m poem-m1', alignClass(j, 1)), withClass('b poem-m poem-m2', alignClass(j, 2)) if d.fixed then -- چیدمان ثابت: هر خط جعبه‌ای با پهنای درصدی در سلولِ ۵ستونه؛ جعبهٔ اول به آغاز، دوم به پایان می‌چسبد local bw = boxWidth(d.indent) return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, nil, d.r) .. td(c1, 5, s1, '<div class="poem-st" style="width:' .. bw .. ';margin-inline-end:auto">' .. hemi(m1) .. '</div>') .. td('poem-num', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num', 1, nil, '') .. td(c2, 5, s2, '<div class="poem-st" style="width:' .. bw .. ';margin-inline-start:auto">' .. hemi(d.m2) .. '</div>') .. td('poem-num poem-num-l', 1, nil, d.l) .. '</tr>' end local sp = 'width:' .. d.indent return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, nil, d.r) .. td(c1, 3, s1, hemi(m1)) .. td('poem-indent', 2, sp, '') .. td('poem-num', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num', 1, nil, '') .. td('poem-indent', 2, sp, '') .. td(c2, 3, s2, hemi(d.m2)) .. td('poem-num poem-num-l', 1, nil, d.l) .. '</tr>' end -- {{مصراع}} local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local style = d.style or '' local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], nil, d.r) .. td(cellClass, cs[2], style, content) .. td('poem-num poem-num-l', cs[3], nil, d.l) .. '</tr>' end -- ردیف فاصله با ارتفاع دلخواه (یا یک خط خالی) local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- ردیف نامرئی نخست در چیدمان ثابت: پهنای ستون‌ها را مستقل از محتوا تعیین می‌کند local function sizerRow() return '<tr class="poem-sizer">' .. '<td class="poem-s"></td><td></td><td></td>' .. '<td class="poem-g"></td><td></td><td></td>' .. '<td class="poem-s"></td></tr>' end local function openHtml(o) local look = o.look -- نمای نستعلیق (کلاس و قلمِ الگوی ویکی) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') .. (o.stack and ' poem-stack' or '') if look and look.class then cls = cls .. ' ' .. look.class end if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = look and look.style or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then -- عرض و قلم و سبک کاربر روی جعبهٔ بیرونی؛ جدول همیشه ۱۰۰٪ آن و با ستون‌های ثابت local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end -- چیدمان خودکار (قدیمی): همان بایت‌به‌بایتِ پیشین وقتی قلم/سبک نیست local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- شعر چندصفحه‌ای: نگه‌داشتن ردیف ناتمام و یکی‌کردن آن با دنباله‌اش --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end -- tail را در head (ردیف ناتمام) ادغام می‌کند: متن مصراع‌ها پشت هم، شماره‌ها از tail local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end -- ردیفِ نگه‌داشته‌شده را (اگر باشد) به‌صورت HTML بیرون می‌دهد local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end -- ردیف‌های بدون منطق ادامه (فاصله، خالی): ردیف ناتمامِ جاافتاده را هم بیرون می‌ریزد local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end --[[ یک ردیف را خروجی می‌دهد. فضای نام «صفحه»: همان‌جا و کامل (فقط کلاس‌های ناتمام/دنباله برای نشان‌دادن). جاهای دیگر (صفحهٔ اصلی، درج صفحه‌ها): ردیف ناتمام نگه داشته می‌شود و با نخستین ردیفِ دارای «دنباله» یکی می‌شود. opt: unfinished, continued, joiner ]] local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end -- بدون ادغام: هر تکه فوراً و جدا local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) -- دنباله‌ای نیامد: همان‌طور که هست if opt.continued then flags.unknown = true end -- نوع ردیف‌ها نمی‌خواند end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end --------------------------------------------------------------------------- -- خواندن پارامترها --------------------------------------------------------------------------- -- ناتمام / دنباله / اتصال local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil) if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil) if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()] if opt.joiner == nil then flags.unknown = true end end return opt end -- جداکننده: نه | بله (نماد پیش‌فرض) | یک نماد کوتاه (مثل ✤) local function parseSep(v, flags) local lv = v:lower() if lv == 'نه' or lv == 'خیر' or lv == 'بدون' or lv == 'ندارد' or lv == 'no' then return '' end if lv == 'بله' or lv == 'خودکار' or lv == 'yes' or lv == 'auto' then return cfg.ornament end if #v <= 24 and not v:find('[<>{}|%[%]"\n]') then return v end flags.unknown = true return cfg.separator end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) -- پارامترهای بدون نام: نستعلیق، ادامه for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end -- ادامه از/به صفحهٔ دیگر local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end -- «به بعد» را در {{پایان شعر}} بنویسید else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style) o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()] if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()] if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g) if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind) if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength(cfg.mesraUnit) if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end -- ترازبندی (همهٔ ابیات) local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()] if not o.just then flags.unknown = true end end -- جداکننده (نماد میان دو مصراع) local sv = pick(args, N.sep) if sv then o.sep = parseSep(sv, flags) else o.sep = cfg.separator end -- تلفن (صفحهٔ باریک): ستونی (پیش‌فرض: عدد، دو مصراع زیر هم) یا دوستونه local stack = true local ph = pick(args, N.phone) if ph then local m = PHONE[ph:lower()] if m == 'two' then stack = false elseif not m then flags.unknown = true end end o.stack = stack and o.just ~= 'justify' -- شعر دوطرفه همیشه دوستونه می‌ماند -- چیدمان: عرض ثابت (پیش‌فرض) یا «خودکار» = هم‌اندازهٔ محتوا local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w) if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx) if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then -- «پهن» (دوطرفه): ستون‌ها به اندازهٔ بلندترین مصراع می‌شوند تا کمترین کشش لازم باشد؛ -- با دادن «عرض» همان عرضِ ثابت اعمال می‌شود o.fixed = (cfg.defaultWidth ~= 'auto') and (o.just ~= 'justify') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end -- ادامهٔ شعر در صفحهٔ بعد؟ {{پایان شعر|ادامه}} یا ادامه=بعد local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true -- «از قبل» را در {{شعر}} بنویسید else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end --------------------------------------------------------------------------- -- حالت «متن»: ساخت کل شعر از خط‌های ساده --------------------------------------------------------------------------- --[[ نشانه‌های درون‌خطی (به الگوی ppoem): ‎>> متن‎ مصراع راست‌چین ‎<> متن‎ مصراع وسط‌چین ‎۱۲ <<< متن‎ شمارهٔ سمت راست (ش) ‎متن >>> ۱۲‎ شمارهٔ سمت چپ (شچ) ‎:: متن‎ (نوع=مصراع) تورفتگی ]] local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*') s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center' s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right' s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post s = body end t.text = s return t end -- «@بیت نردبانی تورفتگی=30%» ← { kind = 'ladder', indent = '30%' }؛ پارامتر دوم: گزینهٔ نامعتبر داشت local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v) if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv] if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv] if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) -- وضعیت جاری: نوع ردیف و گزینه‌هایش؛ خط «@…» آن را عوض می‌کند local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') local indent if c.kind == 'stagger' then indent = c.indent or cfg.defaultIndent elseif c.kind == 'ladder' then indent = c.indent or cfg.ladderIndent end return twoH, rowKind, indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just, sep = ctx.sep } numbers(d, t1) numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false) -- مصراع تنها: وسط‌چین، بی‌شمارش pending = nil end end -- ۱) خط به خط local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) -- مصراع‌های نوع «مصراع» فقط در شعرِ نوع=مصراع شمرده می‌شوند else local handled = false if ropt.continued and isFirst then -- ادامهٔ ردیفِ ناتمام: کدام مصراع(ها) در این صفحه ادامه دارند؟ local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true) pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) -- بیتِ ناتمام: مصراع دوم در صفحهٔ بعد else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end -- ۲) شماره‌گذاری (ردیفِ ادامه را دوباره نمی‌شماریم) for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end -- ۳) خروجی local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end --------------------------------------------------------------------------- -- نقطه‌های ورود --------------------------------------------------------------------------- -- نمای نستعلیق از روی خودِ الگوی ویکی: {{نستعلیق|x}} را بسط می‌دهد و از خروجی‌اش نشانگر -- شیوه‌نامه، کلاس و (فقط) استایل‌های قلم را برمی‌دارد. اگر الگو نبود، فهرست قلمِ cfg. local FONT_PROPS = { ['font-family'] = true, ['font-size'] = true, ['font-style'] = true, ['font-weight'] = true, ['font-variant'] = true, ['font'] = true, ['line-height'] = true, ['letter-spacing'] = true, ['word-spacing'] = true, ['text-rendering'] = true, ['font-feature-settings'] = true, } local function nastaliqLook(frame) local ok, res = pcall(function() return frame:expandTemplate{ title = cfg.nastaliqTemplate, args = { 'x' } } end) if ok and type(res) == 'string' then local prefix = mw.text.trim(res:match('^(.-)<%a') or '') local attrs = res:match('<%a+([^>]*)>') if attrs then local cls = attrs:match('class%s*=%s*"([^"]*)"') or attrs:match("class%s*=%s*'([^']*)'") local sty = attrs:match('style%s*=%s*"([^"]*)"') or attrs:match("style%s*=%s*'([^']*)'") if cls then cls = mw.text.trim((cls:gsub('[^%w%s_%-\128-\255]', ''))) end local keep = {} for decl in (sty or ''):gmatch('[^;]+') do local prop = mw.text.trim(decl:match('^([^:]+):') or ''):lower() if FONT_PROPS[prop] then keep[#keep + 1] = mw.text.trim(decl) end end if (cls and cls ~= '') or #keep > 0 or prefix ~= '' then return { prefix = prefix, class = (cls and cls ~= '') and cls or 'nastaliq', style = (#keep > 0) and table.concat(keep, ';') or nil, } end end end return { prefix = '', class = 'nastaliq', style = 'font-family:' .. cfg.nastaliqFonts } end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end -- صفحهٔ اصلی: اگر شعری باز است و این صفحه ادامهٔ آن است، جدول تازه باز نمی‌کنیم local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just, ctx.sep = o.fixed, o.just, o.sep end local parts = {} if not joins then if o.nastaliq then o.look = nastaliqLook(frame) end parts[#parts + 1] = frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } .. ((o.look and o.look.prefix) or '') .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then -- کل شعر در یک فراخوانی؛ نیازی به {{پایان شعر}} نیست local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end if cont and not inPageNs() then -- شعر در صفحهٔ بعد ادامه دارد: نبندیم (ردیف ناتمام هم نگه داشته می‌ماند) return SKIP .. categories(flags) end local html = flushHtml() state.open, state.ctx = false, nil if html ~= '' then html = html .. '\n' end return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, sep = ctx.sep, } local sv = pick(args, N.sep) if sv then d.sep = parseSep(sv, flags) end local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()] if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', '' html = rowBayt(d) else d.text, d.blank = '', true html = rowMesra(d) end return plain(html) .. categories(flags) end end -- هر خطای پیش‌بینی‌نشده به‌جای شکستن صفحه، پیامی قرمز + رده می‌دهد local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(cfg.defaultIndent)) p.ladder = safe(staggerFn(cfg.ladderIndent)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg -- (برای آزمون و سفارشی‌سازی) return p 9ikh7e97d9cpcetu2hzogkzw6dcfkrz 304061 304058 2026-10-13T03:40:15Z Hanooz 17889 304061 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکی‌نبشتهٔ فارسی الگو نقطهٔ ورود در {{#invoke:شعر|…}} {{شعر}} open آغاز شعر؛ با پارامتر «متن» کل شعر را می‌سازد {{پایان شعر}} close {{بیت}} bayt بیت دومصراعی {{بیت۱}} stagger بیت پلکانی (ضربدری) {{بیت نردبانی}} ladder بیت نردبانی (تورفتگی پیش‌فرض بیشتر) {{مصراع}} mesra یک مصراع {{بیت خالی}} emptyBayt {{مصراع خالی}} emptyMesra ساختار خروجی همان جدول ۷ستونی پیشین است (ظاهر شعرهای موجود و خروجی کتاب الکترونیکی تغییری نمی‌کند). شیوه‌نامه: «الگو:شعر/styles.css». چیدمان (تازه): جدول پیش‌فرض عرض ثابت و مستقل از محتوا دارد (عرض=، حداکثر عرض=)، تا همهٔ ابیات شعر — حتی در جدول‌های جدا (صفحه‌های جدا) — از یک نقطه آغاز شوند. ترازبندی=وسط|راست|چپ|پهن|لبه|میانه برای هر نوع بیت؛ قلم=، نستعلیق خودبسنده. در حالت «متن»، خطی که با @ آغاز شود نوع ردیف‌های بعدی را عوض می‌کند: «@بیت نردبانی». ▸ پیش‌فرض بیت: «پهن» (دوطرفه). بهاین‌ترتیب در فضای نام «صفحه» هم که هر صفحهٔ تنها باز می‌شود، ابیات کشیده می‌شوند و به «میانه»/«وسط» فرو نمی‌افتند. ▸ پیش‌فرض بیت پلکانی: «لبه» (مصراع ۱ راست، مصراع ۲ چپ). «پهن» برای بیت پلکانی به «لبه» فرو می‌افتد چون کشیده‌کردن واژه‌ها بی‌جهت فاصله می‌سازد. شعر چندصفحه‌ای (ایدهٔ start/end در ppoem): • در فضای نام «صفحه» هر صفحه همیشه خودبسنده و بسته نمایش داده می‌شود. • در صفحهٔ اصلی (درج با <pages/>) پارامتر «ادامه» صفحه‌ها را به هم می‌دوزد: {{شعر|ادامه}} جدول تازه باز نمی‌کند و {{پایان شعر|ادامه}} نمی‌بندد. • ردیفِ ناتمام (مصراعی که در صفحهٔ بعد ادامه دارد) نگه داشته می‌شود (ناتمام=بله) و با نخستین ردیفِ صفحهٔ بعد (دنباله=بله) یکی می‌شود. ]==] local p = {} --------------------------------------------------------------------------- -- تنظیمات --------------------------------------------------------------------------- local ZWNJ = '\226\128\140' -- نیم‌فاصله local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, defaultIndent = '20%', ladderIndent = '45%', mesraUnit = '1.5em', nbsp = '&#160;', defaultWidth = '100%', maxWidth = '50em', -- ترازبندی پیش‌فرض هر نوع: inner | center | right | left | justify | edge -- justify (پهن) : پیش‌فرض بیت — دوطرفه. در صفحهٔ تنها هم کشیده می‌شود. -- edge (لبه) : پیش‌فرض بیت۱ و نردبانی — مصراع ۱ راست، مصراع ۲ چپ. -- inner (میانه) : اختیاری — مصراع‌ها به محور میانی می‌چسبند (گنجور). justDefault = { bayt = 'justify', stagger = 'edge' }, nastaliqTemplate = 'نستعلیق', nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", separator = '', ornament = '❖', justifyMinRatio = 0.7, mergeRows = true, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, sep = { 'جداکننده', 'separator' }, phone = { 'تلفن', 'phone' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep', } local YES = { ['بله'] = true, ['بلی'] = true, ['آری'] = true, ['خودکار'] = true, yes = true, y = true, auto = true, ['true'] = true, ['1'] = true, } local NO = { ['نه'] = true, ['خیر'] = true, no = true, n = true, ['false'] = true, ['0'] = true, } local UNITS = { [''] = 'px', ['%'] = '%', px = 'px', em = 'em', ex = 'ex', ch = 'ch', rem = 'rem', vw = 'vw', vh = 'vh', pt = 'pt', cm = 'cm', mm = 'mm', ['in'] = 'in', } local CONT = { ['قبل'] = 'prev', ['پیشین'] = 'prev', prev = 'prev', ['بعد'] = 'next', ['پسین'] = 'next', next = 'next', ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both', } local JOIN = { ['فاصله'] = ' ', space = ' ', ['چسبیده'] = '', ['بدون فاصله'] = '', tight = '', ['نیم' .. ZWNJ .. 'فاصله'] = ZWNJ, ['نیمفاصله'] = ZWNJ, ['نیم فاصله'] = ZWNJ, zwnj = ZWNJ, } local WHICH = { ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both', ['اول'] = 'first', first = 'first', ['دوم'] = 'second', second = 'second', } local STANZA = { ['جدید'] = true, new = true } local JUST = { ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', ['راست'] = 'right', right = 'right', ['چپ'] = 'left', left = 'left', ['پهن'] = 'justify', ['دوطرفه'] = 'justify', ['دو' .. ZWNJ .. 'طرفه'] = 'justify', justify = 'justify', ['لبه'] = 'edge', edge = 'edge', ['میانه'] = 'inner', ['درون'] = 'inner', inner = 'inner', } local ALIGN_CLASS = { right = 'poem-a-right', left = 'poem-a-left' } local JUSTIFY_STYLE = 'text-align:justify;text-align-last:justify' local PHONE = { ['ستونی'] = 'stack', stack = 'stack', ['دوستونه'] = 'two', ['دو ستونه'] = 'two', ['دو' .. ZWNJ .. 'ستونه'] = 'two', two = 'two', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar = true, arz = true, azb = true, bgn = true, ckb = true, dv = true, fa = true, glk = true, he = true, ks = true, lrc = true, mzn = true, pnb = true, prs = true, ps = true, sd = true, ug = true, ur = true, yi = true, } local state = { ctx = nil, open = false, carry = nil, } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto'), sep = cfg.separator } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' --------------------------------------------------------------------------- -- ابزارهای عمومی --------------------------------------------------------------------------- local function esc(s) s = s:gsub('"', '&quot;'):gsub('<', '&lt;'):gsub('>', '&gt;') return s end local function pick(args, names) for _, name in ipairs(names) do local v = args[name] if v and v ~= '' then return v end end return nil end local function present(args, names) for _, name in ipairs(names) do if args[name] ~= nil then return true end end return false end local function bool(v, default) if v == nil then return default end v = v:lower() if YES[v] then return true end if NO[v] then return false end return default end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end s = mw.text.trim(s) s = s:gsub('%s*\n%s*', ' ') return s end local function getArgs(frame) local args = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then args[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return args end local function hasUnknown(args, positional) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > positional or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn', 1, true) or rest:find('cyrl', 1, true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab', 1, true) or rest:find('hebr', 1, true) then return 'rtl' end return 'ltr' end --------------------------------------------------------------------------- -- ارقام --------------------------------------------------------------------------- local FA = { [0] = '۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names) if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end --------------------------------------------------------------------------- -- اندازه‌های CSS --------------------------------------------------------------------------- local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin') v = v:gsub('٪', '%%'):gsub('٫', '.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local num, unit = splitLength(v) if not num then return nil end return string.format('%g', num) .. unit end --------------------------------------------------------------------------- -- شماره‌گذاری خودکار --------------------------------------------------------------------------- local function newCtx(args, kind, flags) local ctx = { digits = 'fa', count = 0, start = 1, side = 'l', mesra = (kind == 'mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()] if not ctx.digits then ctx.digits = 'fa'; flags.unknown = true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9) if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()] if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end --------------------------------------------------------------------------- -- ساخت HTML --------------------------------------------------------------------------- local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignClass(j, which) if not j or j == 'center' or j == 'justify' then return nil end if j == 'edge' then return (which == 1) and 'poem-a-start' or 'poem-a-end' end if j == 'inner' then return (which == 1) and 'poem-a-end' or 'poem-a-start' end return ALIGN_CLASS[j] end local function withClass(base, extra) if extra then return base .. ' ' .. extra end return base end local function charLen(s) s = s:gsub('\127[^\127]*\127', ''):gsub('<[^>]*>', ''):gsub('%[%[[^%]|]*|', ''):gsub('[%[%]]', '') return #(s:gsub('[\128-\191]', '')) end local function effectiveJust(j, a, b) if j == 'justify' then local la, lb = charLen(a), charLen(b) local hi = math.max(la, lb) if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return 'center' end end return j end local function td(class, colspan, style, content) local s = '<td class="' .. class .. '"' if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemi(text) return '<span class="beyt">' .. text .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end -- {{بیت}} local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2) local js = (j == 'justify') and JUSTIFY_STYLE or nil local mid = d.c if mid == '' and d.sep and d.sep ~= '' then mid = '<span class="poem-sep">' .. d.sep .. '</span>' end return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, nil, d.r) .. td(withClass('b poem-m poem-m1', alignClass(j, 1)), 2, joinStyle(js, d.style), hemi(m1)) .. td('poem-num poem-num-c', 1, d.style, mid) .. td(withClass('b poem-m poem-m2', alignClass(j, 2)), 2, joinStyle(js, d.style), hemi(d.m2)) .. td('poem-num poem-num-l', 1, nil, d.l) .. '</tr>' end local function boxWidth(indent) local num, unit = indent:match('^([%d%.]+)(.*)$') if unit == '%' then return string.format('%g%%', math.max(100 - tonumber(num), 10)) end return 'calc(100% - ' .. indent .. ')' end -- {{بیت۱}} و {{بیت نردبانی}} local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end -- ترازبندی: پیش‌فرض stagger = edge. اگر کاربر «پهن» خواسته، آن را به -- «لبه» فرو می‌افکنیم چون کشیده‌کردن واژه‌ها در بیت پلکانی فاصله‌های -- بی‌جهت می‌سازد. local j = d.just or cfg.justDefault.stagger if j == 'justify' then j = 'edge' end j = effectiveJust(j, d.m1, d.m2) local js = (j == 'justify') and JUSTIFY_STYLE or nil local s1, s2 = joinStyle(js, d.style), joinStyle(js, d.style) local c1 = withClass('b poem-m poem-m1', alignClass(j, 1)) local c2 = withClass('b poem-m poem-m2', alignClass(j, 2)) if d.fixed then local bw = boxWidth(d.indent) return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, nil, d.r) .. td(c1, 5, s1, '<div class="poem-st" style="width:' .. bw .. ';margin-inline-end:auto">' .. hemi(m1) .. '</div>') .. td('poem-num', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num', 1, nil, '') .. td(c2, 5, s2, '<div class="poem-st" style="width:' .. bw .. ';margin-inline-start:auto">' .. hemi(d.m2) .. '</div>') .. td('poem-num poem-num-l', 1, nil, d.l) .. '</tr>' end local sp = 'width:' .. d.indent return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, nil, d.r) .. td(c1, 3, s1, hemi(m1)) .. td('poem-indent', 2, sp, '') .. td('poem-num', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num', 1, nil, '') .. td('poem-indent', 2, sp, '') .. td(c2, 3, s2, hemi(d.m2)) .. td('poem-num poem-num-l', 1, nil, d.l) .. '</tr>' end -- {{مصراع}} local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local style = d.style or '' local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], nil, d.r) .. td(cellClass, cs[2], style, content) .. td('poem-num poem-num-l', cs[3], nil, d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } local function sizerRow() return '<tr class="poem-sizer">' .. '<td class="poem-s"></td><td></td><td></td>' .. '<td class="poem-g"></td><td></td><td></td>' .. '<td class="poem-s"></td></tr>' end local function openHtml(o) local look = o.look local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') .. (o.stack and ' poem-stack' or '') if look and look.class then cls = cls .. ' ' .. look.class end if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = look and look.style or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- شعر چندصفحه‌ای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end --------------------------------------------------------------------------- -- خواندن پارامترها --------------------------------------------------------------------------- local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil) if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil) if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()] if opt.joiner == nil then flags.unknown = true end end return opt end local function parseSep(v, flags) local lv = v:lower() if lv == 'نه' or lv == 'خیر' or lv == 'بدون' or lv == 'ندارد' or lv == 'no' then return '' end if lv == 'بله' or lv == 'خودکار' or lv == 'yes' or lv == 'auto' then return cfg.ornament end if #v <= 24 and not v:find('[<>{}|%[%]"\n]') then return v end flags.unknown = true return cfg.separator end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style) o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()] if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()] if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g) if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind) if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength(cfg.mesraUnit) if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()] if not o.just then flags.unknown = true end end local sv = pick(args, N.sep) if sv then o.sep = parseSep(sv, flags) else o.sep = cfg.separator end local stack = true local ph = pick(args, N.phone) if ph then local m = PHONE[ph:lower()] if m == 'two' then stack = false elseif not m then flags.unknown = true end end o.stack = stack and o.just ~= 'justify' local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w) if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx) if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') and (o.just ~= 'justify') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end --------------------------------------------------------------------------- -- حالت «متن» --------------------------------------------------------------------------- local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*') s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center' s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right' s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v) if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv] if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv] if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') local indent if c.kind == 'stagger' then indent = c.indent or cfg.defaultIndent elseif c.kind == 'ladder' then indent = c.indent or cfg.ladderIndent end return twoH, rowKind, indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just, sep = ctx.sep } numbers(d, t1) numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false) pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true) pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end --------------------------------------------------------------------------- -- نقطه‌های ورود --------------------------------------------------------------------------- local FONT_PROPS = { ['font-family'] = true, ['font-size'] = true, ['font-style'] = true, ['font-weight'] = true, ['font-variant'] = true, ['font'] = true, ['line-height'] = true, ['letter-spacing'] = true, ['word-spacing'] = true, ['text-rendering'] = true, ['font-feature-settings'] = true, } local function nastaliqLook(frame) local ok, res = pcall(function() return frame:expandTemplate{ title = cfg.nastaliqTemplate, args = { 'x' } } end) if ok and type(res) == 'string' then local prefix = mw.text.trim(res:match('^(.-)<%a') or '') local attrs = res:match('<%a+([^>]*)>') if attrs then local cls = attrs:match('class%s*=%s*"([^"]*)"') or attrs:match("class%s*=%s*'([^']*)'") local sty = attrs:match('style%s*=%s*"([^"]*)"') or attrs:match("style%s*=%s*'([^']*)'") if cls then cls = mw.text.trim((cls:gsub('[^%w%s_%-\128-\255]', ''))) end local keep = {} for decl in (sty or ''):gmatch('[^;]+') do local prop = mw.text.trim(decl:match('^([^:]+):') or ''):lower() if FONT_PROPS[prop] then keep[#keep + 1] = mw.text.trim(decl) end end if (cls and cls ~= '') or #keep > 0 or prefix ~= '' then return { prefix = prefix, class = (cls and cls ~= '') and cls or 'nastaliq', style = (#keep > 0) and table.concat(keep, ';') or nil, } end end end return { prefix = '', class = 'nastaliq', style = 'font-family:' .. cfg.nastaliqFonts } end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just, ctx.sep = o.fixed, o.just, o.sep end local parts = {} if not joins then if o.nastaliq then o.look = nastaliqLook(frame) end parts[#parts + 1] = frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } .. ((o.look and o.look.prefix) or '') .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end if cont and not inPageNs() then return SKIP .. categories(flags) end local html = flushHtml() state.open, state.ctx = false, nil if html ~= '' then html = html .. '\n' end return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, sep = ctx.sep, } local sv = pick(args, N.sep) if sv then d.sep = parseSep(sv, flags) end local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()] if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', '' html = rowBayt(d) else d.text, d.blank = '', true html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(cfg.defaultIndent)) p.ladder = safe(staggerFn(cfg.ladderIndent)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p hx4zveqnbl0cfc9939epo0hmr0cuj3g برگه:بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۹ 104 97061 304049 304012 2026-10-12T18:58:42Z Hanooz 17889 تست 304049 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر||بدایع شیخ سعدی|۵}}</noinclude><section begin="p19-20" /> {{وسط|{{اندازه قلم|110%|'''۴'''}}}} {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}} {{شعر۲|نوع=ضربدری|ادامه=بعد|ترازبندی=پهن|متن= با جوانی سرخوش است این پیر بی تدبیر را جهل باشد با جوانان پنجه کردن پیر را من که با موئی بقوّت بر نیایم ایعجب با یکی<ref>I(a); کسی, for یکی.</ref> افتاده ام کو بگسلد زنجیر را چون کمان در بازو آرد سروقدّ سیمتن آرزویم میکند<ref>L, Z; کشد, for کند.</ref> کآماج باشم تیر<ref>This line is omitted in R.</ref> را میرود تا در کمند افتد بپای خویشتن گر بر آن دست<ref>L, Z; تیر, for دست.</ref> و کمان چشم اوفتد نخجیر را<ref>In R the second hemistich runs as follows:— <div dir="rtl">«چون تو چوگان میزنی جرمی نباشد تیر را».</div></ref> کس ندید است آدمیزاد<ref>R; را, for زاد.</ref> از تو شیرین تر سخن شکر از پستان مادر خوردهٔ یا شیر را روز بازار جوانی پنج<ref>C, Y(a), Y(b); چند, for پنج.</ref> روزی بیش نیست نقد را باش ای پسر کآفت بود تأخیر را ایکه گفتی دیده از دیدار بت رویان<ref>I(a), R, L, C, Y(a), Y(b); مه, for بت.</ref> بدوز هر چه گوئی چاره دانم کرد جز تقدیر را }}<noinclude>{{خطکش}} {{پانویس|چپ‌چین=بله}}</noinclude> pkzrbtbmhkdxkzv79hrdnwanxecajiu 304052 304049 2026-10-12T19:23:52Z Hanooz 17889 نسخهٔ [[Special:Diff/304049|304049]] از [[Special:Contributions/Hanooz|Hanooz]] ([[User talk:Hanooz|بحث]]) خنثی شد 304052 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر||بدایع شیخ سعدی|۵}}</noinclude><section begin="p19-20" /> {{وسط|{{اندازه قلم|110%|'''۴'''}}}} {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}} {{شعر۲|نوع=ضربدری|ادامه=بعد|متن= با جوانی سرخوش است این پیر بی تدبیر را جهل باشد با جوانان پنجه کردن پیر را من که با موئی بقوّت بر نیایم ایعجب با یکی<ref>I(a); کسی, for یکی.</ref> افتاده ام کو بگسلد زنجیر را چون کمان در بازو آرد سروقدّ سیمتن آرزویم میکند<ref>L, Z; کشد, for کند.</ref> کآماج باشم تیر<ref>This line is omitted in R.</ref> را میرود تا در کمند افتد بپای خویشتن گر بر آن دست<ref>L, Z; تیر, for دست.</ref> و کمان چشم اوفتد نخجیر را<ref>In R the second hemistich runs as follows:— <div dir="rtl">«چون تو چوگان میزنی جرمی نباشد تیر را».</div></ref> کس ندید است آدمیزاد<ref>R; را, for زاد.</ref> از تو شیرین تر سخن شکر از پستان مادر خوردهٔ یا شیر را روز بازار جوانی پنج<ref>C, Y(a), Y(b); چند, for پنج.</ref> روزی بیش نیست نقد را باش ای پسر کآفت بود تأخیر را ایکه گفتی دیده از دیدار بت رویان<ref>I(a), R, L, C, Y(a), Y(b); مه, for بت.</ref> بدوز هر چه گوئی چاره دانم کرد جز تقدیر را }}<noinclude>{{خطکش}} {{پانویس|چپ‌چین=بله}}</noinclude> 6d0tvt4f1u7t27mgi88ra3bbhmm9bm0